Problem
The Review: Files / Headings and Review: Changes / Blocks dials silently no-op for BUGFIX builders during their entire working life (investigate, fix phases) and for AIR builders during the pr phase (until the pr gate is requested). The owner hit this in live use as "the file diff dials sometimes work and sometimes not" — the intermittency is protocol identity, not flakiness.
Diagnosis
phaseArtifactVerb (apps/streamdeck/src/actions.ts:527) resolves dial mode from a hardcoded vocabulary — gates spec-approval | plan-approval | dev-approval | pr | verify-approval, phases specify | plan | implement | review | verify. That list was written against SPIR/PIR's lifecycle. Checked against each bundled protocol's protocol.json phase ids:
- SPIR (
specify/plan/implement/review/verify) and PIR (plan/implement/review): fully covered. - AIR (
implement/pr): the prphase id is unrecognized, so the dials die between entering the phase and the pr gate being requested (blockedGate === 'pr' revives them). - BUGFIX (
investigate/fix/pr): investigate and fix are unrecognized — review mode resolves to none for the builder's whole working life; dials only come alive blocked at the pr gate. - Porch
init phase and no-status (soft/shell/task) builders: also none.
Two compounding effects make it read as intermittent rather than broken:
- The Zoom dial's tap falls back to
view-diff for unknown phases (zoomInVerb), so a BUGFIX builder's diff opens normally — then the Review dials won't step it. - The
none-mode strip label lies: ReviewNav.renderTo has only a canvas/diff branch for line 1, so none renders the diff label (Files · send) while every gesture is a no-op — violating the dial's own "a gesture is never a surprise" contract and the instrument principle (an instrument answering a narrower question than its face implies must say so).
Owner ruling (2026-09-04)
The pr phase should be diffable as well — by that point (BUGFIX and AIR both) the work exists as a diff.
Fix
- Map
investigate, fix, and phase pr to diff mode. Prefer deriving the mode from the protocol's own protocol.json phase metadata over extending the hardcoded name list, so new/custom protocols never silently fall out of the vocabulary; if metadata can't express it, extend the list and leave a registration note (the same pattern Tower's GATE_LABELS documents). - Make the
none-mode strip label honest (e.g. No review target) instead of rendering the diff label. - Keep
init/no-status as none (there is genuinely nothing to review), but with the honest label from (2).
Shipping note
apps/streamdeck change → requires a plugin version bump and an Elgato Marketplace resubmission (review lag ~4–10 working days). Consider batching with other queued plugin work rather than shipping alone.
Problem
The Review: Files / Headings and Review: Changes / Blocks dials silently no-op for BUGFIX builders during their entire working life (
investigate,fixphases) and for AIR builders during theprphase (until the pr gate is requested). The owner hit this in live use as "the file diff dials sometimes work and sometimes not" — the intermittency is protocol identity, not flakiness.Diagnosis
phaseArtifactVerb(apps/streamdeck/src/actions.ts:527) resolves dial mode from a hardcoded vocabulary — gatesspec-approval | plan-approval | dev-approval | pr | verify-approval, phasesspecify | plan | implement | review | verify. That list was written against SPIR/PIR's lifecycle. Checked against each bundled protocol'sprotocol.jsonphase ids:specify/plan/implement/review/verify) and PIR (plan/implement/review): fully covered.implement/pr): theprphase id is unrecognized, so the dials die between entering the phase and the pr gate being requested (blockedGate === 'pr'revives them).investigate/fix/pr):investigateandfixare unrecognized — review mode resolves tononefor the builder's whole working life; dials only come alive blocked at the pr gate.initphase and no-status (soft/shell/task) builders: alsonone.Two compounding effects make it read as intermittent rather than broken:
view-difffor unknown phases (zoomInVerb), so a BUGFIX builder's diff opens normally — then the Review dials won't step it.none-mode strip label lies:ReviewNav.renderTohas only a canvas/diff branch for line 1, sononerenders the diff label (Files · send) while every gesture is a no-op — violating the dial's own "a gesture is never a surprise" contract and the instrument principle (an instrument answering a narrower question than its face implies must say so).Owner ruling (2026-09-04)
The
prphase should be diffable as well — by that point (BUGFIX and AIR both) the work exists as a diff.Fix
investigate,fix, and phaseprto diff mode. Prefer deriving the mode from the protocol's ownprotocol.jsonphase metadata over extending the hardcoded name list, so new/custom protocols never silently fall out of the vocabulary; if metadata can't express it, extend the list and leave a registration note (the same pattern Tower's GATE_LABELS documents).none-mode strip label honest (e.g.No review target) instead of rendering the diff label.init/no-status asnone(there is genuinely nothing to review), but with the honest label from (2).Shipping note
apps/streamdeckchange → requires a plugin version bump and an Elgato Marketplace resubmission (review lag ~4–10 working days). Consider batching with other queued plugin work rather than shipping alone.