Browse Source

chore: remove stale docs/superpowers plan

Deletes the tracked-but-ignored AI-generated implementation plan
(docs/superpowers/plans/2026-07-08-share-closepane-shutdown.md) for the
closePane graceful-shutdown refactor, which is already complete. The
.gitignore entry for docs/superpowers is retained so the path stays
ignored going forward.
Michael Henke 3 weeks ago
parent
commit
26200507a2
1 changed files with 0 additions and 351 deletions
  1. 0 351
      docs/superpowers/plans/2026-07-08-share-closepane-shutdown.md

+ 0 - 351
docs/superpowers/plans/2026-07-08-share-closepane-shutdown.md

@@ -1,351 +0,0 @@
-# Share closePane graceful-shutdown across multiplexer backends
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-
-**Goal:** Extract the duplicated Ctrl+C → 250ms → kill/close pane lifecycle from tmux/zellij/herdr `closePane` into one `gracefulClosePane` helper in `shared.ts`, and force the hidden exit-code/guard inconsistencies into the open via an `options` param.
-
-**Architecture:** One shared helper takes the binary, paneId, and the two backend-specific command arrays (`ctrlC`, `close`) plus an `options` object for the divergent bits (accept exit code 1, empty-paneId returns true). Each backend's `closePane` shrinks to: guard → `getBinary()` → `return gracefulClosePane(...)`. No base class, no interface change. `session-manager.ts` is untouched (it already uses the `Multiplexer` interface).
-
-**Tech Stack:** TypeScript, Bun test, existing `crossSpawn` from `../utils/compat`, existing `log` from `../utils/logger`.
-
----
-
-### Task 1: Add `gracefulClosePane` helper + tests
-
-**Files:**
-- Modify: `src/multiplexer/shared.ts`
-- Create: `src/multiplexer/shared.test.ts`
-
-- [ ] **Step 1: Write the failing test**
-
-```typescript
-import { describe, it, expect, mock, beforeEach, afterEach } from 'bun:test';
-import { gracefulClosePane } from './shared';
-import { crossSpawn } from '../utils/compat';
-
-const DELAY_MS = 250;
-
-function fakeProc(exitCode: number, stderr = '') {
-  return {
-    exited: Promise.resolve(exitCode),
-    stdout: () => Promise.resolve(''),
-    stderr: () => Promise.resolve(stderr),
-  } as unknown as ReturnType<typeof crossSpawn>;
-}
-
-describe('gracefulClosePane', () => {
-  beforeEach(() => mock.module('../utils/compat', () => ({ crossSpawn: mock() })));
-  afterEach(() => mock.restore());
-
-  it('sends Ctrl+C, waits 250ms, then closes, returning true on exit 0', async () => {
-    const calls: string[][] = [];
-    let ctrlCTime = 0;
-    let closeTime = 0;
-    (crossSpawn as unknown as ReturnType<typeof mock>).mockImplementation((args: string[]) => {
-      if (args.includes('C-c') || args.includes('\u0003') || args.includes('ctrl+c')) {
-        ctrlCTime = Date.now();
-      } else {
-        closeTime = Date.now();
-      }
-      calls.push(args);
-      return fakeProc(0);
-    });
-
-    const ok = await gracefulClosePane('tmux', '%1', {
-      ctrlC: ['send-keys', '-t', '%1', 'C-c'],
-      close: ['kill-pane', '-t', '%1'],
-    });
-
-    expect(ok).toBe(true);
-    expect(calls).toHaveLength(2);
-    expect(closeTime - ctrlCTime).toBeGreaterThanOrEqual(DELAY_MS - 20);
-  });
-
-  it('returns true when acceptExitCode1 and exit code is 1', async () => {
-    (crossSpawn as unknown as ReturnType<typeof mock>).mockImplementation(() => fakeProc(1));
-    const ok = await gracefulClosePane('zellij', 'terminal_1', {
-      ctrlC: ['action', 'write', '--pane-id', 'terminal_1', '\u0003'],
-      close: ['action', 'close-pane', '--pane-id', 'terminal_1'],
-      acceptExitCode1: true,
-    });
-    expect(ok).toBe(true);
-  });
-
-  it('returns false on exit 1 when acceptExitCode1 is false', async () => {
-    (crossSpawn as unknown as ReturnType<typeof mock>).mockImplementation(() => fakeProc(1));
-    const ok = await gracefulClosePane('tmux', '%1', {
-      ctrlC: ['send-keys', '-t', '%1', 'C-c'],
-      close: ['kill-pane', '-t', '%1'],
-    });
-    expect(ok).toBe(false);
-  });
-
-  it('returns emptyPaneReturnsTrue when paneId is empty', async () => {
-    (crossSpawn as unknown as ReturnType<typeof mock>).mockImplementation(() => fakeProc(0));
-    const ok = await gracefulClosePane('zellij', '', {
-      ctrlC: ['action', 'write', '--pane-id', '', '\u0003'],
-      close: ['action', 'close-pane', '--pane-id', ''],
-      emptyPaneReturnsTrue: true,
-    });
-    expect(ok).toBe(true);
-    expect((crossSpawn as unknown as ReturnType<typeof mock>).mock.calls).toHaveLength(0);
-  });
-
-  it('returns false when binary is null', async () => {
-    const ok = await gracefulClosePane(null, '%1', {
-      ctrlC: ['x'],
-      close: ['y'],
-    });
-    expect(ok).toBe(false);
-  });
-});
-```
-
-- [ ] **Step 2: Run test to verify it fails**
-
-Run: `bun test src/multiplexer/shared.test.ts`
-Expected: FAIL — `gracefulClosePane` is not exported.
-
-- [ ] **Step 3: Write minimal implementation**
-
-Append to `src/multiplexer/shared.ts`:
-
-```typescript
-const GRACEFUL_SHUTDOWN_DELAY_MS = 250;
-
-export interface GracefulClosePaneOptions {
-  /** Backend-specific Ctrl+C command args (binary prepended by caller). */
-  ctrlC: string[];
-  /** Backend-specific close/kill command args (binary prepended by caller). */
-  close: string[];
-  /** Accept exit code 1 as success (zellij/herdr treat "already closed" as 1). */
-  acceptExitCode1?: boolean;
-  /** Return true for empty/unknown paneId instead of false (zellij/herdr behavior). */
-  emptyPaneReturnsTrue?: boolean;
-}
-
-export async function gracefulClosePane(
-  binary: string | null,
-  paneId: string,
-  options: GracefulClosePaneOptions,
-): Promise<boolean> {
-  if (!binary) return false;
-
-  const isEmpty = !paneId || paneId === 'unknown';
-  if (isEmpty) return options.emptyPaneReturnsTrue ?? false;
-
-  try {
-    const ctrlCProc = crossSpawn([binary, ...options.ctrlC], {
-      stdout: 'ignore',
-      stderr: 'ignore',
-    });
-    await ctrlCProc.exited;
-
-    await new Promise((r) => setTimeout(r, GRACEFUL_SHUTDOWN_DELAY_MS));
-
-    const proc = crossSpawn([binary, ...options.close], {
-      stdout: 'pipe',
-      stderr: 'pipe',
-    });
-    const exitCode = await proc.exited;
-
-    if (exitCode === 0) return true;
-    if (options.acceptExitCode1 && exitCode === 1) return true;
-    return false;
-  } catch {
-    return false;
-  }
-}
-```
-
-- [ ] **Step 4: Run test to verify it passes**
-
-Run: `bun test src/multiplexer/shared.test.ts`
-Expected: PASS (5 tests)
-
-- [ ] **Step 5: Commit**
-
-```bash
-git add src/multiplexer/shared.ts src/multiplexer/shared.test.ts
-git commit -m "feat(multiplexer): add gracefulClosePane helper with tests"
-```
-
----
-
-### Task 2: Rewrite tmux `closePane` to use the helper
-
-**Files:**
-- Modify: `src/multiplexer/tmux/index.ts:115-164`
-
-- [ ] **Step 1: Replace the closePane body**
-
-Replace lines 115-164 with:
-
-```typescript
-  async closePane(paneId: string): Promise<boolean> {
-    const tmux = await this.getBinary();
-    return gracefulClosePane(tmux, paneId, {
-      ctrlC: ['send-keys', '-t', paneId, 'C-c'],
-      close: ['kill-pane', '-t', paneId],
-      // tmux: empty paneId is a real error, exit 0 only.
-      emptyPaneReturnsTrue: false,
-    });
-  }
-```
-
-Note: tmux used to call `this.scheduleLayout()` on success. That rebalance is a tmux-specific concern, not part of the shared shutdown. Preserve it by wrapping:
-
-```typescript
-  async closePane(paneId: string): Promise<boolean> {
-    const tmux = await this.getBinary();
-    const closed = await gracefulClosePane(tmux, paneId, {
-      ctrlC: ['send-keys', '-t', paneId, 'C-c'],
-      close: ['kill-pane', '-t', paneId],
-    });
-    if (closed) this.scheduleLayout();
-    return closed;
-  }
-```
-
-- [ ] **Step 2: Add the import**
-
-At top of `src/multiplexer/tmux/index.ts`, add to the shared import (or new line):
-
-```typescript
-import { gracefulClosePane } from '../shared';
-```
-
-- [ ] **Step 3: Verify typecheck + existing tmux tests**
-
-Run: `bun run typecheck && bun test src/multiplexer/`
-Expected: typecheck clean, all multiplexer tests pass.
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add src/multiplexer/tmux/index.ts
-git commit -m "refactor(multiplexer): use gracefulClosePane in tmux"
-```
-
----
-
-### Task 3: Rewrite zellij `closePane` to use the helper
-
-**Files:**
-- Modify: `src/multiplexer/zellij/index.ts:496-525`
-
-- [ ] **Step 1: Replace the closePane body**
-
-Replace lines 496-525 with:
-
-```typescript
-  async closePane(paneId: string): Promise<boolean> {
-    const zellij = await this.getBinary();
-    return gracefulClosePane(zellij, paneId, {
-      ctrlC: ['action', 'write', '--pane-id', paneId, '\u0003'],
-      close: ['action', 'close-pane', '--pane-id', paneId],
-      acceptExitCode1: true,
-      emptyPaneReturnsTrue: true,
-    });
-  }
-```
-
-- [ ] **Step 2: Add the import**
-
-At top of `src/multiplexer/zellij/index.ts`:
-
-```typescript
-import { gracefulClosePane } from '../shared';
-```
-
-- [ ] **Step 3: Verify typecheck + tests**
-
-Run: `bun run typecheck && bun test src/multiplexer/`
-Expected: clean + pass.
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add src/multiplexer/zellij/index.ts
-git commit -m "refactor(multiplexer): use gracefulClosePane in zellij"
-```
-
----
-
-### Task 4: Rewrite herdr `closePane` to use the helper
-
-**Files:**
-- Modify: `src/multiplexer/herdr/index.ts:155-200`
-
-- [ ] **Step 1: Replace the closePane body**
-
-Replace lines 155-200 with:
-
-```typescript
-  async closePane(paneId: string): Promise<boolean> {
-    const herdr = await this.getBinary();
-    return gracefulClosePane(herdr, paneId, {
-      ctrlC: ['pane', 'send-keys', paneId, 'ctrl+c'],
-      close: ['pane', 'close', paneId],
-      acceptExitCode1: true,
-      emptyPaneReturnsTrue: true,
-    });
-  }
-```
-
-- [ ] **Step 2: Add the import**
-
-At top of `src/multiplexer/herdr/index.ts`:
-
-```typescript
-import { gracefulClosePane } from '../shared';
-```
-
-- [ ] **Step 3: Verify typecheck + tests**
-
-Run: `bun run typecheck && bun test src/multiplexer/`
-Expected: clean + pass.
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add src/multiplexer/herdr/index.ts
-git commit -m "refactor(multiplexer): use gracefulClosePane in herdr"
-```
-
----
-
-### Task 5: Final verification + lint
-
-**Files:**
-- None new
-
-- [ ] **Step 1: Run full check + test suite**
-
-Run: `bun run check:ci && bun run typecheck && bun test`
-Expected: Biome clean, types clean, all tests pass.
-
-- [ ] **Step 2: Confirm no orphaned duplicate logic**
-
-Run: `grep -rn "setTimeout(r, 250)" src/multiplexer/`
-Expected: only the `GRACEFUL_SHUTDOWN_DELAY_MS` usage inside `shared.ts`. No per-backend 250ms copies remain.
-
-- [ ] **Step 3: Commit (if any lint auto-fix applied) and push branch**
-
-```bash
-git add -A
-git commit -m "chore(multiplexer): lint fixes for closePane refactor" || echo "nothing to commit"
-git push -u origin fix/703-share-closepane-shutdown
-```
-
----
-
-## Self-Review
-
-**1. Spec coverage:** Issue #703 asks for one `gracefulClosePane(binary, paneId, { ctrlC, close }, options?)` helper in `shared.ts`. Task 1 delivers exactly that signature + tests. Tasks 2-4 wire all three backends. Task 5 verifies. The "inconsistencies forced into the open" (exit 0 vs 0||1, empty guard false vs true) are handled by `acceptExitCode1` / `emptyPaneReturnsTrue` options, preserving each backend's real behavior rather than silently unifying it. Covered.
-
-**2. Placeholder scan:** No TBD/TODO. Every step has code or exact command. The tmux `scheduleLayout` nuance is explicitly handled, not deferred.
-
-**3. Type consistency:** `gracefulClosePane(binary: string | null, paneId: string, options: GracefulClosePaneOptions)` is defined in Task 1 and called identically in Tasks 2-4. `ctrlC`/`close` are `string[]`. `acceptExitCode1`/`emptyPaneReturnsTrue` are optional booleans. Consistent.
-
-**Ponytail note:** Deliberately NOT extracting `spawnPane` (diverges too much per @oracle) and NOT building a `MultiplexerBase` class (over-engineering for this scope). The single helper is the smallest change that removes the 3-place shutdown hazard. `session-manager.ts` is correctly left alone.