diff --git a/apps/streamdeck/README.md b/apps/streamdeck/README.md index fc2aa85b8..4f89ea234 100644 --- a/apps/streamdeck/README.md +++ b/apps/streamdeck/README.md @@ -266,16 +266,20 @@ builder (+ its phase and position); PR / Spawn show the item + `i/N`. - **Spawn from Backlog** — rotate the backlog; **push** spawns a builder for the selected issue (VSCode prompts for the protocol). - **Review: Files / Headings** — phase-aware coarse review dial. The selected - builder's phase picks the mode. *Diff phase* (implement / review, or blocked at - dev-approval / pr): rotate = next / previous file, **push** = submit the current - file as feedback, **tap** = jump to the first file. *Spec/plan phase* (specify / - plan, or blocked at spec-approval / plan-approval): rotate = next / previous heading - in the artifact canvas, **push** = open the composer at the focused block, **tap** = - jump to the document top. The touch strip names the live mode and, in diff phase, - the delivery mode (`Files · send` vs `Files · queue`). -- **Review: Changes / Blocks** — phase-aware fine review dial, same mode split. - *Diff phase*: rotate = next / previous change, **push** = submit the current change - as feedback, **tap** = jump to the first change. *Spec/plan phase*: rotate = next / + builder's phase picks the mode. *Diff phase* (implement / review / verify, the + BUGFIX/AIR pr phase, or blocked at dev-approval / pr / verify-approval): rotate = + next / previous file, **push** = submit the current file as feedback, **tap** = jump + to the first file. *Spec/plan phase* (specify / plan, or blocked at spec-approval / + plan-approval): rotate = next / previous heading in the artifact canvas, **push** = + open the composer at the focused block, **tap** = jump to the document top. When the + selected builder has no reviewable artifact (an `init` / no-live-status builder, or a + work-in-progress BUGFIX investigate / fix phase), both dials go inert and the strip + reads `No review target` rather than a mode label. The touch strip names the live mode + and, in diff phase, the delivery mode (`Files · send` vs `Files · queue`). +- **Review: Changes / Blocks** — phase-aware fine review dial, same mode split + (including the inert `No review target` state on a builder with no reviewable + artifact). *Diff phase*: rotate = next / previous change, **push** = submit the current + change as feedback, **tap** = jump to the first change. *Spec/plan phase*: rotate = next / previous block, **push** = open the composer, **tap** = walk forward to the next commented block. The touch strip names the live mode (`Changes · send` / `Changes · queue`, or `Blocks`). diff --git a/apps/streamdeck/src/__tests__/actions.test.ts b/apps/streamdeck/src/__tests__/actions.test.ts index 7e9bbfc83..fa2f5a6bc 100644 --- a/apps/streamdeck/src/__tests__/actions.test.ts +++ b/apps/streamdeck/src/__tests__/actions.test.ts @@ -635,6 +635,53 @@ describe('encoders', () => { expect(ctx.canvasSent).toHaveLength(0); }); + it('BUGFIX pr-phase builder: the review dials drive the diff (dead before the pr gate fired, #1606)', async () => { + const ctx = makeStore(); + // A BUGFIX/AIR builder in the pr *phase*, not yet blocked at the pr *gate* (blockedGate null). + // Before #1606 this resolved to reviewMode 'none' and every gesture no-opped until the gate. + ctx.store.overview = { + builders: [{ id: 'bugfix-1', roleId: null, issueId: '1606', issueTitle: 'Review dials', blocked: null, blockedGate: null, protocolPhase: 'pr', progress: 95, worktreePath: '/w' }], + pendingPRs: [], backlog: [], recentlyClosed: [], + } as never; + const nav = new DiffFileNav(ctx.store); + await nav.onDialRotate(dial(1) as never); // next file + await nav.onDialDown(); // submit feedback + await nav.onTouchTap(); // first file + expect(ctx.sent.map((s) => s.verb)).toEqual(['diff-next-file', 'feedback-file', 'diff-first-file']); + expect(ctx.canvasSent).toHaveLength(0); // diff mode never touches the canvas channel + }); + + it('BUGFIX investigate builder: the review dials stay inert (WIP is not a review target, #1606)', async () => { + const ctx = makeStore(); + // Owner ruling: investigate/fix are work-in-progress, so the dials are none-mode and no-op. + ctx.store.overview = { + builders: [{ id: 'bugfix-1', roleId: null, issueId: '1606', issueTitle: 'Review dials', blocked: null, blockedGate: null, protocolPhase: 'investigate', progress: 10, worktreePath: '/w' }], + pendingPRs: [], backlog: [], recentlyClosed: [], + } as never; + const nav = new DiffFileNav(ctx.store); + await nav.onDialRotate(dial(1) as never); + await nav.onDialDown(); + await nav.onTouchTap(); + expect(ctx.sent).toHaveLength(0); // no diff verbs + expect(ctx.canvasSent).toHaveLength(0); // no canvas commands + }); + + it('legibility: a none-mode builder\'s strip says "No review target", not the diff label (#1606)', () => { + const ctx = makeStore(); + // A porch `init`-phase (or soft/shell/task) builder: genuinely nothing to review. The strip + // must NOT borrow the diff label (`Files · send`) while every gesture is inert — that lie is + // the compounding half of #1606. + ctx.store.overview = { + builders: [{ id: 'task-x', roleId: null, issueId: null, issueTitle: null, blocked: null, blockedGate: null, protocolPhase: 'init', progress: 0, worktreePath: '/w' }], + pendingPRs: [], backlog: [], recentlyClosed: [], + } as never; + const fileAction = { isDial: () => true, setFeedback: vi.fn() }; + new DiffFileNav(ctx.store).onWillAppear({ action: fileAction, payload: {} } as never); + const fb = fileAction.setFeedback.mock.calls.at(-1)?.[0] as Record; + expect(fb.title).toBe('No review target'); + expect(fb.title).not.toBe('Files · send'); // the old lie + }); + it('ScrollNav diff mode: rotate relays byte-for-byte editorScroll, press submits the selection as feedback', async () => { const ctx = makeStore(); // State the mode the assertion depends on: pir-2 (implement, no gate) is the DIFF-mode builder. @@ -939,6 +986,17 @@ describe('phaseArtifactVerb (shared resolver — recognised verb or undefined)', // The two callers diverge exactly here: dial → view-diff, Builder Action → open-terminal. expect(zoomInVerb(b({}))).toBe('view-diff'); }); + it('leaves BUGFIX investigate/fix and the pr PHASE unrecognised — the review dials own that vocabulary (#1606)', () => { + // Deliberate split: `phaseArtifactVerb` stays SPIR/PIR-scoped so the auto-open action key + // keeps opening a terminal and Zoom falls back to view-diff. Only `reviewMode` maps the pr + // phase to diff (see DIFF_REVIEW_PHASES); investigate/fix stay none as work-in-progress. If + // this ever changes, reconsider the Builder Action key's open-terminal→open-diff-first side + // effect first. + expect(phaseArtifactVerb(b({ protocolPhase: 'investigate' }))).toBeUndefined(); + expect(phaseArtifactVerb(b({ protocolPhase: 'fix' }))).toBeUndefined(); + expect(phaseArtifactVerb(b({ protocolPhase: 'pr' }))).toBeUndefined(); + expect(zoomInVerb(b({ protocolPhase: 'pr' }))).toBe('view-diff'); // Zoom already correct for the pr phase + }); }); describe('reviewMode (dial mode from the shared resolver)', () => { @@ -956,9 +1014,23 @@ describe('reviewMode (dial mode from the shared resolver)', () => { expect(reviewMode(b({ blockedGate: 'pr' }))).toBe('diff'); expect(reviewMode(b({ blockedGate: 'verify-approval' }))).toBe('diff'); // #1431: dials navigate the diff while the human reviews finished work }); - it('an unknown phase, no live status, or no builder → none', () => { + it('the BUGFIX+AIR pr PHASE → diff (#1606: dead between entering the pr phase and the gate firing)', () => { + // The pr *phase* (BUGFIX `investigate/fix/pr`, AIR `implement/pr`) holds a finished diff, so + // the dials go live before the pr *gate* is requested. The pr *gate* already resolved to diff + // via phaseArtifactVerb (blockedGate 'pr', line above) — this covers the phase before it. + expect(reviewMode(b({ protocolPhase: 'pr' }))).toBe('diff'); + }); + it('BUGFIX work-in-progress phases investigate/fix → none (owner ruling: WIP is not a review target)', () => { + // #1606 owner ruling 2026-09-04: investigate/fix are work-in-progress (partial/no diff yet), + // so the dials stay inert with the honest "No review target" label rather than driving a + // half-written diff. Only the pr phase (finished work) is diffable. + expect(reviewMode(b({ protocolPhase: 'investigate' }))).toBe('none'); + expect(reviewMode(b({ protocolPhase: 'fix' }))).toBe('none'); + }); + it('an unknown phase, no live status, or no builder → none (init/soft/shell/task have nothing to review)', () => { expect(reviewMode(b({}))).toBe('none'); expect(reviewMode(b({ protocolPhase: 'mystery' }))).toBe('none'); + expect(reviewMode(b({ protocolPhase: 'init' }))).toBe('none'); // #1606: porch init phase stays none expect(reviewMode(undefined)).toBe('none'); }); }); diff --git a/apps/streamdeck/src/actions.ts b/apps/streamdeck/src/actions.ts index b662c337e..7d9504c3d 100644 --- a/apps/streamdeck/src/actions.ts +++ b/apps/streamdeck/src/actions.ts @@ -548,18 +548,44 @@ export function zoomInVerb(b: OverviewBuilder): string { /** Which artifact form the selected builder's phase implies for the review dials. */ export type ReviewMode = 'diff' | 'canvas' | 'none'; +/** + * Phase ids whose review dials drive a DIFF, but that `phaseArtifactVerb` deliberately does + * NOT auto-open (so the Builder Action key and Zoom keep their behavior — see the guard test): + * + * - `pr` (BUGFIX + AIR): by the pr *phase* the work already exists as a finished diff + * (owner ruling 2026-09-04), even before the pr *gate* is requested (`blockedGate === 'pr'` + * already resolves to diff via `phaseArtifactVerb`). This closes the window where an AIR / + * BUGFIX builder's dials were dead between entering the pr phase and the gate firing. + * + * Deliberately EXCLUDED, do not re-add without an owner ruling: BUGFIX `investigate` / `fix` + * are work-in-progress (owner ruling 2026-09-04). Their diff is partial or not yet written, so + * they are not a review target; the dials stay `none` (inert, with an honest `No review target` + * label, not the old lie), matching `phaseArtifactVerb`, which opens a terminal there. + * + * REGISTRATION NOTE (mirrors Tower's GATE_LABELS pattern, face.ts): `protocol.json` phase + * metadata carries no field expressing review mode (only id/name/type/steps/transition/ + * gate), and the SDK's `PHASE_TO_STAGE` drives the VSCode Builders tree with different + * semantics, so neither can be reused verbatim. A new protocol whose phases review as a + * finished diff registers its phase id here (or maps onto `phaseArtifactVerb`'s SPIR/PIR + * vocabulary). An unregistered phase resolves to `none`, visibly inert with an honest label, + * never a silent lie. + */ +const DIFF_REVIEW_PHASES = new Set(['pr']); + /** * The review mode for a builder: a builder still writing its spec/plan reviews as a - * canvas (`open-spec` / `open-plan`), one with a diff reviews as a diff (`view-diff`), - * and an unknown/no-status builder has neither. Derived from the shared phase/gate - * resolver so the wire source stays single (`blockedGate` beats `protocolPhase`; never - * guessed) — this is the same resolver family #1404's press keys off. + * canvas (`open-spec` / `open-plan`), one with a diff reviews as a diff (`view-diff`, or a + * `DIFF_REVIEW_PHASES` phase), and an unknown/no-status builder has neither. The + * canvas/diff-verb split derives from the shared phase/gate resolver (`blockedGate` beats + * `protocolPhase`; never guessed) — the same resolver family #1404's press keys off; the + * extra diff phases are the review-dial-only vocabulary `phaseArtifactVerb` omits. */ export function reviewMode(b: OverviewBuilder | undefined): ReviewMode { if (!b) return 'none'; const verb = phaseArtifactVerb(b); if (verb === 'open-spec' || verb === 'open-plan') return 'canvas'; if (verb === 'view-diff') return 'diff'; + if (DIFF_REVIEW_PHASES.has(b.protocolPhase ?? '')) return 'diff'; return 'none'; } @@ -902,11 +928,16 @@ abstract class ReviewNav extends SingletonAction { private renderTo(action: DialAction): void { // Canvas line 1 pairs the rotate axis with the press meaning (`Blocks · Open/Submit`, // `Headings · Cancel`); diff mode pairs its axis with the feedback delivery mode - // (`Files · queue` vs `Files · send`, #1410) so a press is never a surprise. - const label = - this.mode() === 'canvas' - ? `${this.canvas.label} · ${this.canvas.pressLabel}` - : `${this.diff.label} · ${this.store.feedbackMode() === 'queue' ? 'queue' : 'send'}`; + // (`Files · queue` vs `Files · send`, #1410) so a press is never a surprise. `none` + // states its own emptiness (`No review target`) rather than borrowing the diff label — + // every gesture in `none` mode is inert, so the strip must not imply otherwise (#1606). + const mode = this.mode(); + let label = 'No review target'; + if (mode === 'canvas') { + label = `${this.canvas.label} · ${this.canvas.pressLabel}`; + } else if (mode === 'diff') { + label = `${this.diff.label} · ${this.store.feedbackMode() === 'queue' ? 'queue' : 'send'}`; + } const b = this.store.selectedBuilder(); void action.setFeedback({ title: label, diff --git a/codev/projects/bugfix-1606-streamdeck-review-dials-silent/status.yaml b/codev/projects/bugfix-1606-streamdeck-review-dials-silent/status.yaml new file mode 100644 index 000000000..544d684c6 --- /dev/null +++ b/codev/projects/bugfix-1606-streamdeck-review-dials-silent/status.yaml @@ -0,0 +1,17 @@ +id: bugfix-1606 +title: streamdeck-review-dials-silent +protocol: bugfix +phase: verified +plan_phases: [] +current_plan_phase: null +gates: + pr: + status: approved + requested_at: '2026-09-04T01:53:53.303Z' + approved_at: '2026-09-04T09:18:48.942Z' +iteration: 1 +build_complete: false +history: [] +started_at: '2026-09-04T01:36:08.893Z' +updated_at: '2026-09-04T09:19:35.190Z' +pr_ready_for_human: false diff --git a/codev/state/bugfix-1606_thread.md b/codev/state/bugfix-1606_thread.md new file mode 100644 index 000000000..d02739cef --- /dev/null +++ b/codev/state/bugfix-1606_thread.md @@ -0,0 +1,172 @@ +# bugfix-1606 — streamdeck: Review dials silently dead for BUGFIX/AIR phases + +Issue #1606. Protocol: BUGFIX (strict). Branch: builder/bugfix-1606. + +## INVESTIGATE — findings (root cause confirmed, no code written) + +### Bug +Stream Deck **Review dials** (Files/Headings, Changes/Blocks) silently no-op for a +BUGFIX builder's whole working life (`investigate`, `fix`) and for AIR during the `pr` +phase (until the pr *gate* is requested). Reads as "diff dials sometimes work, sometimes +not" — the intermittency is protocol identity, not flakiness. + +### Root cause +`phaseArtifactVerb` (`apps/streamdeck/src/actions.ts:527`) has a hardcoded phase/gate +vocabulary written against SPIR/PIR: phases `specify|plan|implement|review|verify`, gates +`spec-approval|plan-approval|dev-approval|pr|verify-approval`. `reviewMode` +(`actions.ts:558`) derives from it: `open-spec/open-plan → canvas`, `view-diff → diff`, +`undefined → none`. + +Checked each bundled `protocol.json` (`phases[].id`): +- SPIR `specify/plan/implement/review/verify`, PIR `plan/implement/review` — covered. +- AIR `implement/pr` — the `pr` **phase** id is unrecognized → `none` until `blockedGate==='pr'`. +- BUGFIX `investigate/fix/pr` — `investigate`/`fix` unrecognized → `reviewMode==='none'` + for the builder's whole working life; dials only revive when blocked at the pr gate. +- `init`/no-status (soft/shell/task) → `none` (genuinely nothing to review). + +In `none` mode `ReviewNav.onDialRotate/onDialDown/onTouchTap` all no-op (actions.ts:918-944). + +### Compounding "lie" +`ReviewNav.renderTo` label ternary (actions.ts:906-909) has only canvas vs diff branches; +`none` falls into the diff branch and renders `Files · send` while every gesture no-ops — +violates the dial's "a gesture is never a surprise" contract. + +### Metadata-vs-list decision (issue's stated preference) +Issue prefers deriving mode from `protocol.json` phase metadata over extending a name list. +Verified: `protocol.json` phases carry only `id/name/type/steps/transition/gate` — **no field +expresses review mode**. The SDK's `packages/sdk/src/phase-grouping.ts` `PHASE_TO_STAGE` +folds `investigate → plan`, which for review-mode purposes would wrongly imply **canvas** +(BUGFIX has no spec/plan doc), and it's used by the VSCode Builders tree — can't repurpose +without blast radius. So per the issue's documented fallback ("if metadata can't express it, +extend the list and leave a registration note — the GATE_LABELS pattern"), I'll extend the +recognized diff-phase vocabulary with a registration note. + +### Fix surface (well under 300 LOC; FIX phase) +1. Make the review dials resolve **diff** for phases `investigate`, `fix`, `pr`. Do it in + `reviewMode` (the dial-specific resolver) — **not** in `phaseArtifactVerb** — so + `zoomInVerb` (already `?? 'view-diff'`, so BUGFIX zoom already works) and `BuilderAction` + auto-press (`?? 'open-terminal'`) are untouched. Keeping `phaseArtifactVerb` stable avoids + changing the Builder Action key from open-terminal→open-diff-first during `investigate` + (no diff yet) — out-of-scope blast radius the issue doesn't ask for. Add a registration note. +2. Honest `none` label: add a `none` branch in `renderTo` → `No review target` instead of + the diff label. +3. Keep `init`/no-status → `none` (with the honest label). + +Consumers verified: `reviewMode` also feeds `ScrollNav` (actions.ts:1080/1150) but those +only branch on `=== 'canvas'`; none→diff doesn't change ScrollNav behavior (both non-canvas). + +### Regression test +`apps/streamdeck/src/__tests__/actions.test.ts`: BUGFIX `investigate` builder → +`reviewMode==='diff'` and dial gestures emit diff verbs (fails today: 'none', no verbs); +`fix`/`pr` phases → 'diff'; `none`-mode label renders `No review target` not `Files · send`. + +### Shipping note +`apps/streamdeck` change → plugin version bump + Elgato resubmission (~4-10 working days). +Flagged in issue; batching decision is the architect's. + +Signal: PHASE_COMPLETE. + +## FIX — implemented + +Architect approved the scoped approach (registered diff-phase set in `reviewMode`, +`phaseArtifactVerb` untouched, trade-off flagged in PR body). Release is HELD — no version +bump (fix merges to main, ships later in a batched Elgato resubmission). + +Changes (2 files, ~89 lines incl. tests): +- `apps/streamdeck/src/actions.ts`: + - Added `DIFF_REVIEW_PHASES = {investigate, fix, pr}` with a GATE_LABELS-style registration + note; `reviewMode` now returns `diff` for these phases (after the existing verb-based + canvas/diff resolution, so gates still win). `phaseArtifactVerb` untouched → Builder Action + key (open-terminal) and Zoom (view-diff) behavior unchanged. + - `ReviewNav.renderTo`: `none` mode now renders `No review target` instead of borrowing the + diff label (`Files · send`). Converted the label ternary to if/else. +- `apps/streamdeck/src/__tests__/actions.test.ts`: added reviewMode coverage for + investigate/fix/pr → diff and init/'' → none; a phaseArtifactVerb guard documenting the + deliberate split; a behavioral test (BUGFIX investigate dials emit diff verbs); a legibility + test (none-mode strip says `No review target`, not the lie). + +Verification (from the worktree, apps/streamdeck): +- Full suite: 246 passed (10 files). +- Regression check: reverted both source edits → the 3 behavioral/mode/label tests FAIL + (`none`→ expected `diff`; strip `Files · send`→ expected `No review target`); restored → pass. +- `tsc --noEmit` exit 0 (after building the codev-sdk workspace dep, which had no dist here), + `npm run build` exit 0. + +No skeleton mirror needed (apps/ is app code, single copy; not a codev/↔codev-skeleton framework file). + +## PR — up + +PR #1607 opened: https://github.com/cluesmith/codev/pull/1607 (Fixes #1606). Body carries the +metadata-vs-registered-list trade-off note per the architect's call. + +CMAP dispatched (gemini/codex/claude, `--type pr`). Note: consult auto-detect failed with +"Multiple projects found" from this worktree — had to pass `--project-id bugfix-1606` explicitly. +Awaiting three verdicts before notifying the architect + running `porch done` (fires the pr gate). + +**@architect: hardware check** — once you approve, this is ready for your streamdeck-link +verification: select a BUGFIX builder (investigate/fix) and confirm the Files/Changes dials now +step the diff and the strip no longer reads `Files · send` when there's nothing to review. + +## CMAP verdicts (3/3) + +- **gemini: APPROVE** (HIGH) — no issues. +- **codex: APPROVE** (HIGH) — no issues. +- **claude: COMMENT** (HIGH) — three points, all addressed or triaged: + 1. Plugin README stale on the dial vocabulary → **fixed** (commit f73e3f8): README now lists + the BUGFIX/AIR investigate/fix/pr phases and the honest `No review target` state. + 2. `DIFF_REVIEW_PHASES` comment attributed `investigate` to BUGFIX alone (RESEARCH uses it + too) → **fixed** (same commit): registered by name, note corrected. + 3. MAINTAIN `maintain` / EXPERIMENT `execute`,`analyze` / SPIKE `spike` still resolve to + `none` (dead dials on a diff, though the honest label now stops them lying). Claude flags + this as an **architect scope call, not a blocker** — out of #1606's investigate/fix/pr + scope. **Raised to the architect** in the gate notification; recommend a follow-up issue + rather than expanding this PR. + +Non-substantial changes (docs + comment, zero behavior change), so CMAP was not re-run. +Sent the single gate notification with all three verdicts; ran `porch done` to fire the pr gate. + +## At the pr gate — holding (2026-09-04) + +Architect review of PR #1607: **APPROVE**. Rulings: +- **Scope call decided**: PR stays as-is. The MAINTAIN/EXPERIMENT/SPIKE phase registrations + (Claude's point 3) are now **follow-up issue #1609** (architect's lane) — do NOT expand this PR. +- The `investigate`-by-id RESEARCH-nuance handling and the `phaseArtifactVerb` split note both + confirmed correct. +- The **pr gate word rests with the owner** (a CMAP/architect APPROVE is not gate authorization). + The owner's decision and the physical-deck hardware check (once he links the deck to this + worktree build) are both in motion. Architect: "hold at the gate." + +**Status: holding at the pr gate. Not running `porch approve` — awaiting the owner's explicit +gate decision.** Nothing actionable for me until then. + +## Owner scope correction: pr-only (2026-09-04, at the gate) + +Owner ruled directly: **only the `pr` phase should be diffable, NOT `investigate`/`fix`** — those +are work-in-progress (partial/no diff yet), so they are not a review target. This supersedes the +issue #1606 Fix text (which had listed investigate/fix) and the architect's earlier "as-is" ruling. + +Narrowed the fix: +- `DIFF_REVIEW_PHASES` = `{pr}` only; rewrote the note to record investigate/fix as deliberately + EXCLUDED (owner ruling), so no future reader re-adds them. +- investigate/fix now resolve to `none` → inert dials with the honest `No review target` label. + The value of the fix there is the honest label, not live dials. +- Tests: `pr` phase → diff (behavioral + unit); investigate/fix → none (behavioral + unit); + guard + none-label tests updated. 248 pass; tsc + build clean. +- README + PR #1607 body updated to pr-only. + +This is a real behavior change from the CMAP'd version (investigate/fix diff→none), but it's a +scope REDUCTION on the same mechanism the reviewers blessed. Notifying the architect; will offer +to re-run CMAP if they want. Awaiting the owner's gate word on the corrected PR. + +## pr gate APPROVED + protocol advanced (2026-09-04 09:18) + +Amr approved the pr gate via VS Code at 09:18 on the pr-only code head **dbc1fcc87** (architect +relayed it as `porch approve`, human-approved). The approval landing on the pr-only head also +authenticates the pr-only scope ruling. + +Ran `porch done` → **PROTOCOL COMPLETE** (advanced to the verified/terminal state). Branch in sync +with origin. + +**MERGE IS NOT MINE.** Architect instruction: the merge still needs Amr's explicit per-PR word +plus his admin bypass (branch protection); the architect handles it when Amr's words land. So I am +NOT running `gh pr merge` and NOT running `porch done` again (the post-merge step). **Holding.**