Skip to content

fix(daemon): drop superseded review cycles from the attention list - #377

Merged
jeremymcs merged 3 commits into
mainfrom
fix/issue-356-attention-superseded-cycles
Oct 11, 2026
Merged

jeremymcs merged 3 commits into
mainfrom
fix/issue-356-attention-superseded-cycles

Conversation

@jeremymcs

@jeremymcs jeremymcs commented Oct 10, 2026 •

Copy link
Copy Markdown
Member

Part of #356 (item 2).

Intent

Fix item 2 of GitHub issue #356 in open-gsd/gsd-path (item 1 was already fixed by merged PR #355). The monitor daemon's compute_attention() added a 'failed' Needs-attention row for every .project/review wave file whose verdict is blocked. The runtime requires earlier review cycles to stay on disk, so a wave that blocked at cycle 1 and passed at cycle 2 kept a permanent failed row until the milestone was archived. Decisions made on purpose: only the highest cycle of each wave and lens drives the attention row; status.reviews still lists every cycle as history, and final, gap and other review files are unchanged. Follow-up after an independent review: a newer cycle file supersedes an older one only when it has a valid wave verdict (pass or blocked, the two values the runtime accepts; a 'Wave verdict:' line with no value does not count), so a review that is still being written cannot hide a blocked cycle and turn the project health green; cycle numbers compare as numbers. The daemon is a separate package and cannot import scripts/, so it reuses its own WAVE_REVIEW_FILE_RE with capture groups; the name rule matches the runtime's wave-N.cycleC[.lens].md pattern and no new naming rule was added.

What Changed

  • compute_attention() in daemon/gsd_daemon/probe.py now skips a wave review file when a later cycle of the same wave and lens replaces it, so a wave that blocked at cycle 1 and passed at cycle 2 no longer keeps a failed row. status.reviews still lists every cycle, and final, gap and other review files are not filtered.
  • A newer cycle replaces an older one only when its verdict is pass or blocked (new WAVE_VERDICTS set). A cycle file with no verdict, or with an empty Wave verdict: line, does not hide a blocked cycle, and the project health stays red. Wave and cycle numbers compare as integers.
  • WAVE_REVIEW_FILE_RE gains capture groups for wave, cycle and lens; the file name rule itself is unchanged. tests/test_daemon_attention.py adds six tests: superseded blocked cycle, blocked newest cycle per wave, per-lens supersession, missing verdict, empty verdict line, and numeric cycle order (9 against 10).

Risk Assessment

✅ Low: The change is one small pure helper and one skip in compute_attention(); traced cases (blocked then pass, blocked then blocked, per-lens deep review, newer cycle with no verdict or an empty verdict line, cycle 9 against 10, panel files with no Wave verdict) all give the correct rows, and the runtime fixes review depth per wave, so the lens set is the same in every cycle and per-lens grouping cannot leave a permanent failed row.

Testing

I installed the daemon from the target commit and from the base commit into two disposable venvs, and ran gsd-path-daemon serve for each with an isolated HOME, a temporary daemon.json and nine throwaway fixture projects. I read /status from both daemons, drove a full review-cycle lifecycle on disk against the running target daemon, and captured headless Chrome screenshots of both dashboards. The focused tests/test_daemon_attention.py file also passed. No baseline commands ran before this step. All scenarios passed; the temporary setup was removed and the worktree is clean. One note: the first lifecycle run used a fixed 4-second wait that was shorter than the daemon's 3 to 4 second change-detection time, so I re-drove it with a wait-until-seen check.

  • Live validation: ✅ go - 9 of 9 scenarios driven live against the product
Scenario Result Live Evidence
A wave blocked at cycle 1 and passed at cycle 2 shows no failed row, and both cycles stay in the review history ✅ pass live status-target.txt, project a-blocked-then-pass: failed_rows=[] and status.reviews lists cycle1=blocked and cycle2=pass. status-base.txt shows the bug: failed_rows=['wave-1.cycle1.md'], health red. Als…
A wave whose newest cycle is still blocked shows one failed row per wave, for the newest cycle only ✅ pass live status-target.txt, project b-still-blocked: failed_rows=['wave-1.cycle2.md', 'wave-2.cycle1.md'], health red. Base showed three rows, with wave-1.cycle1.md included.
Deep review lenses are superseded separately: a passed adversarial cycle 2 does not hide a blocked contract cycle 1 ✅ pass live status-target.txt, project c-deep-lenses: failed_rows=['wave-1.cycle1.contract.md']. Base showed the adversarial cycle 1 row too.
Adversarial: a newer cycle file with no verdict line does not hide the blocked cycle, and health stays red ✅ pass live status-target.txt, project d-newer-no-verdict: failed_rows=['wave-1.cycle1.md'], health red. live-transition-target.txt step 2 shows the same on a file written while the daemon ran.
Adversarial: a newer cycle with a 'Wave verdict:' line and no value does not hide the blocked cycle ✅ pass live status-target.txt, project e-newer-empty-verdict: the parser reads verdict 'cycle' from the next line, failed_rows=['wave-1.cycle1.md'], health red. live-transition-target.txt step 3 shows the same.
Adversarial: a newer cycle with a verdict the runtime does not accept ('approved') does not hide the blocked cycle ✅ pass live status-target.txt, project h-newer-invalid-verdict: failed_rows=['wave-1.cycle1.md'], health red.
Cycle numbers compare as numbers: a passed cycle 10 supersedes a blocked cycle 9 ✅ pass live status-target.txt, project f-cycle-9-then-10: failed_rows=[]. Base showed failed_rows=['wave-1.cycle9.md'].
A blocked FINAL.md still gives a failed row when a wave in the same project has a superseded blocked cycle ✅ pass live status-target.txt, project g-final-blocked: failed_rows=['FINAL.md'], health red. Base showed FINAL.md and wave-1.cycle1.md.
Running daemon follows a review lifecycle: the failed row stays while cycle 2 is being written, goes away when cycle 2 passes, and returns for a blocked cycle 3 ✅ pass live live-transition-target.txt steps 1 to 5: failed row is wave-1.cycle1.md in steps 1 to 3, empty in step 4, wave-1.cycle3.md in step 5.

Dashboard on the target commit (fixed)
Dashboard on the base commit (bug: superseded blocked cycles listed)

Evidence: /status attention rows per project, target commit
a-blocked-then-pass      health=amber  failed_rows=[]
                         other_attention=['unverified:Runtime validation is unavailable.']
                         status.reviews=['wave-1.cycle1.md=blocked', 'wave-1.cycle2.md=pass']
b-still-blocked          health=red    failed_rows=['wave-1.cycle2.md', 'wave-2.cycle1.md']
                         other_attention=['unverified:Runtime validation is unavailable.']
                         status.reviews=['wave-1.cycle1.md=blocked', 'wave-1.cycle2.md=blocked', 'wave-2.cycle1.md=blocked', 'wave-10.cycle1.md=pass']
c-deep-lenses            health=red    failed_rows=['wave-1.cycle1.contract.md']
                         other_attention=['unverified:Runtime validation is unavailable.']
                         status.reviews=['wave-1.cycle1.adversarial.md=blocked', 'wave-1.cycle1.contract.md=blocked', 'wave-1.cycle2.adversarial.md=pass']
d-newer-no-verdict       health=red    failed_rows=['wave-1.cycle1.md']
                         other_attention=['unverified:Runtime validation is unavailable.']
                         status.reviews=['wave-1.cycle1.md=blocked', 'wave-1.cycle2.md=None']
e-newer-empty-verdict    health=red    failed_rows=['wave-1.cycle1.md']
                         other_attention=['unverified:Runtime validation is unavailable.']
                         status.reviews=['wave-1.cycle1.md=blocked', 'wave-1.cycle2.md=cycle']
f-cycle-9-then-10        health=amber  failed_rows=[]
                         other_attention=['unverified:Runtime validation is unavailable.']
                         status.reviews=['wave-1.cycle9.md=blocked', 'wave-1.cycle10.md=pass']
g-final-blocked          health=red    failed_rows=['FINAL.md']
                         other_attention=['unverified:Runtime validation is unavailable.']
                         status.reviews=['FINAL.md=blocked', 'wave-1.cycle1.md=blocked', 'wave-1.cycle2.md=pass']
h-newer-invalid-verdict  health=red    failed_rows=['wave-1.cycle1.md']
                         other_attention=['unverified:Runtime validation is unavailable.']
                         status.reviews=['wave-1.cycle1.md=blocked', 'wave-1.cycle2.md=approved']
i-live-transition        health=red    failed_rows=['wave-1.cycle1.md']
                         other_attention=['unverified:Runtime validation is unavailable.']
                         status.reviews=['wave-1.cycle1.md=blocked']
Evidence: /status attention rows per project, base commit
a-blocked-then-pass      health=red    failed_rows=['wave-1.cycle1.md']
                         other_attention=['unverified:Runtime validation is unavailable.']
                         status.reviews=['wave-1.cycle1.md=blocked', 'wave-1.cycle2.md=pass']
b-still-blocked          health=red    failed_rows=['wave-1.cycle1.md', 'wave-1.cycle2.md', 'wave-2.cycle1.md']
                         other_attention=['unverified:Runtime validation is unavailable.']
                         status.reviews=['wave-1.cycle1.md=blocked', 'wave-1.cycle2.md=blocked', 'wave-2.cycle1.md=blocked', 'wave-10.cycle1.md=pass']
c-deep-lenses            health=red    failed_rows=['wave-1.cycle1.adversarial.md', 'wave-1.cycle1.contract.md']
                         other_attention=['unverified:Runtime validation is unavailable.']
                         status.reviews=['wave-1.cycle1.adversarial.md=blocked', 'wave-1.cycle1.contract.md=blocked', 'wave-1.cycle2.adversarial.md=pass']
d-newer-no-verdict       health=red    failed_rows=['wave-1.cycle1.md']
                         other_attention=['unverified:Runtime validation is unavailable.']
                         status.reviews=['wave-1.cycle1.md=blocked', 'wave-1.cycle2.md=None']
e-newer-empty-verdict    health=red    failed_rows=['wave-1.cycle1.md']
                         other_attention=['unverified:Runtime validation is unavailable.']
                         status.reviews=['wave-1.cycle1.md=blocked', 'wave-1.cycle2.md=cycle']
f-cycle-9-then-10        health=red    failed_rows=['wave-1.cycle9.md']
                         other_attention=['unverified:Runtime validation is unavailable.']
                         status.reviews=['wave-1.cycle9.md=blocked', 'wave-1.cycle10.md=pass']
g-final-blocked          health=red    failed_rows=['FINAL.md', 'wave-1.cycle1.md']
                         other_attention=['unverified:Runtime validation is unavailable.']
                         status.reviews=['FINAL.md=blocked', 'wave-1.cycle1.md=blocked', 'wave-1.cycle2.md=pass']
h-newer-invalid-verdict  health=red    failed_rows=['wave-1.cycle1.md']
                         other_attention=['unverified:Runtime validation is unavailable.']
                         status.reviews=['wave-1.cycle1.md=blocked', 'wave-1.cycle2.md=approved']
i-live-transition        health=red    failed_rows=['wave-1.cycle1.md']
                         other_attention=['unverified:Runtime validation is unavailable.']
                         status.reviews=['wave-1.cycle1.md=blocked']
Evidence: Live review-cycle lifecycle against the running target daemon

step 1: only wave-1.cycle1.md (blocked) health=red failed_rows=['wave-1.cycle1.md'] step 2: cycle2 started, no verdict line health=red failed_rows=['wave-1.cycle1.md'] step 3: cycle2 has 'Wave verdict:' with no value health=red failed_rows=['wave-1.cycle1.md'] step 4: cycle2 gets 'Wave verdict: pass' health=amber failed_rows=[] (history keeps cycle1=blocked, cycle2=pass) step 5: cycle3 is blocked health=red failed_rows=['wave-1.cycle3.md'] The amber health in step 4 comes from the separate 'Runtime validation is unavailable.' row: the fixture projects have no installed runtime.

## step 1: only wave-1.cycle1.md (blocked) is on disk
i-live-transition        health=red    failed_rows=['wave-1.cycle1.md']
                         other_attention=['unverified:Runtime validation is unavailable.']
                         status.reviews=['wave-1.cycle1.md=blocked']
## step 2: reviewer starts wave-1.cycle2.md, no verdict line yet
i-live-transition        health=red    failed_rows=['wave-1.cycle1.md']
                         other_attention=['unverified:Runtime validation is unavailable.']
                         status.reviews=['wave-1.cycle1.md=blocked', 'wave-1.cycle2.md=None']
## step 3: wave-1.cycle2.md has a 'Wave verdict:' line with no value
i-live-transition        health=red    failed_rows=['wave-1.cycle1.md']
                         other_attention=['unverified:Runtime validation is unavailable.']
                         status.reviews=['wave-1.cycle1.md=blocked', 'wave-1.cycle2.md=cycle']
## step 4: wave-1.cycle2.md gets 'Wave verdict: pass'
i-live-transition        health=amber  failed_rows=[]
                         other_attention=['unverified:Runtime validation is unavailable.']
                         status.reviews=['wave-1.cycle1.md=blocked', 'wave-1.cycle2.md=pass']
## step 5: wave-1.cycle3.md is blocked again
i-live-transition        health=red    failed_rows=['wave-1.cycle3.md']
                         other_attention=['unverified:Runtime validation is unavailable.']
                         status.reviews=['wave-1.cycle1.md=blocked', 'wave-1.cycle2.md=pass', 'wave-1.cycle3.md=blocked']

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 9 of 9 scenarios driven live against the product
Scenario Result Live Evidence
A wave blocked at cycle 1 and passed at cycle 2 shows no failed row, and both cycles stay in the review history ✅ pass live status-target.txt, project a-blocked-then-pass: failed_rows=[] and status.reviews lists cycle1=blocked and cycle2=pass. status-base.txt shows the bug: failed_rows=['wave-1.cycle1.md'], health red. Als…
A wave whose newest cycle is still blocked shows one failed row per wave, for the newest cycle only ✅ pass live status-target.txt, project b-still-blocked: failed_rows=['wave-1.cycle2.md', 'wave-2.cycle1.md'], health red. Base showed three rows, with wave-1.cycle1.md included.
Deep review lenses are superseded separately: a passed adversarial cycle 2 does not hide a blocked contract cycle 1 ✅ pass live status-target.txt, project c-deep-lenses: failed_rows=['wave-1.cycle1.contract.md']. Base showed the adversarial cycle 1 row too.
Adversarial: a newer cycle file with no verdict line does not hide the blocked cycle, and health stays red ✅ pass live status-target.txt, project d-newer-no-verdict: failed_rows=['wave-1.cycle1.md'], health red. live-transition-target.txt step 2 shows the same on a file written while the daemon ran.
Adversarial: a newer cycle with a 'Wave verdict:' line and no value does not hide the blocked cycle ✅ pass live status-target.txt, project e-newer-empty-verdict: the parser reads verdict 'cycle' from the next line, failed_rows=['wave-1.cycle1.md'], health red. live-transition-target.txt step 3 shows the same.
Adversarial: a newer cycle with a verdict the runtime does not accept ('approved') does not hide the blocked cycle ✅ pass live status-target.txt, project h-newer-invalid-verdict: failed_rows=['wave-1.cycle1.md'], health red.
Cycle numbers compare as numbers: a passed cycle 10 supersedes a blocked cycle 9 ✅ pass live status-target.txt, project f-cycle-9-then-10: failed_rows=[]. Base showed failed_rows=['wave-1.cycle9.md'].
A blocked FINAL.md still gives a failed row when a wave in the same project has a superseded blocked cycle ✅ pass live status-target.txt, project g-final-blocked: failed_rows=['FINAL.md'], health red. Base showed FINAL.md and wave-1.cycle1.md.
Running daemon follows a review lifecycle: the failed row stays while cycle 2 is being written, goes away when cycle 2 passes, and returns for a blocked cycle 3 ✅ pass live live-transition-target.txt steps 1 to 5: failed row is wave-1.cycle1.md in steps 1 to 3, empty in step 4, wave-1.cycle3.md in step 5.
  • gsd-path-daemon --config <tmp>/daemon.json serve --port 47811 (target 0e75e8ac, isolated HOME) then GET /status for 9 fixture projects
  • gsd-path-daemon --config <tmp>/daemon.json serve --port 47812 (base a09754ff, same fixtures) then GET /status to reproduce the bug
  • Live lifecycle on project i-live-transition: wrote wave-1.cycle2.md with no verdict, then an empty Wave verdict: line, then Wave verdict: pass, then a blocked wave-1.cycle3.md, and read /status after the daemon saw each change
  • Headless Chrome screenshot of http://127.0.0.1:47811/ (target) and http://127.0.0.1:47812/ (base)
  • python3 -m unittest tests.test_daemon_attention
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

jeremymcs and others added 3 commits October 10, 2026 18:36
`compute_attention()` added a "failed" row for each `.project/review/*.md`
file with a negative verdict. The runtime keeps earlier review cycles on
disk, so a wave that blocked at cycle 1 and passed at cycle 2 kept a
permanent "failed" row and a red project until the milestone was archived.

For wave review files (`wave-N.cycleC.md` and the deep-review lens files
`wave-N.cycleC.<lens>.md`), only the highest cycle of each wave and lens now
drives attention. The Reviews table still lists every cycle. Final, gap and
other review files are unchanged.

Item 1 of the issue (runtime validation on the pinned-runtime layout) was
fixed by 84b97f4.

Fixes #356

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rdict

A newer wave review file with no verdict line (a review that is still
being written) replaced an older blocked cycle of the same wave and
lens. The failed row disappeared and health could turn green.

Only a review with a verdict now replaces an older cycle. A cycle with
no verdict replaces nothing and adds no row.

Tests: a newer cycle with no verdict keeps the blocked row and red
health; cycle 10 is newer than cycle 9.

Refs #356

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ave verdict

A review that is still being written can have `Wave verdict:` with no value.
The verdict pattern then reads the first word of the next line (`Cycle`), so
the unfinished cycle counted as a verdict, replaced the blocked cycle before
it, and the failed row disappeared.

A cycle now replaces an older one only when its verdict is `pass` or
`blocked`, the two values the runtime accepts for a wave review.

Refs #356

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jeremymcs
jeremymcs merged commit ca73537 into main Oct 11, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant