feat: declarative pane pinning via RunPane CLI#359
Conversation
QA evidence — automated pass (zone 2, command-shaped)All acceptance criteria were proven by independent verifier runs during the pipeline and re-run once more against the final commit ( Passed automatedAC1–AC4 — handler integration tests (
AC5 — restart persistence (real temp-file SQLite): the test pins via D1 — declarative, not toggle (code inspection, verifier-quoted): D3 — contract parity: The parity harness asserts npm and Python send exactly Repo gates: Remaining for the humanThe six Manual-test checkboxes in the body all require a daemon running this build (the installed daemon 2.4.29 predates
Suggested setup: run the dev instance ( |
type: wrap-up-report
|
Postmortem — 357 (ops-only)type: postmortem
|
| Phase | Span | Notes |
|---|---|---|
| Step 0 preflight + load | 05:53–05:58 (~5m) | issue fetch, artifact harvest, branch ff, install refresh |
| Step 1 research + plan | 05:58–06:10 (~12m) | direct research (zone 2, no dossier); plan-reviewer dispatch 1:43 |
| Plan fixes | 06:10–06:15 (~5m) | 3 MF + 2 SF folded in |
| Step 2 implement | 06:15–06:24 (~9m dispatch) | Codex implementer, 202k tokens |
| Step 3 verify + fix + re-verify | 06:24–06:44 (~20m) | verifier fail (dry-run rubric) → resume fix (6:42) → scoped re-verify pass |
| Step 4 commit + PR | 06:44–06:47 (~3m) | PR #359 opened, closing line verified |
| Step 5 review ∥ diagram | 06:34–06:47 (overlapped) | code-reviewer ran parallel to diagram authoring — good overlap |
| Stall | 06:47–07:12 (25:36) | turn ended after the QA-drive command returned; no ScheduleWakeup |
| Step 5 QA publish + Step 6 | 07:12–07:16+ | body update, QA comment, wrap-up, label, notify |
Ranked stalls:
- 25:36 human-idle at the QA-drive boundary. The QA command-shaped run
(vitest + contract checks) returned green and the turn ended without
publishing the results or scheduling a wakeup — exactly the "idle-waiting
on a human nudge is a pipeline bug" case the /do charter names. The
fix that removes it: treat the QA drive's own command results like a
verifier report — the same turn must publish (body edit + evidence
comment) before ending, and any turn that ends mid-run must carry a
ScheduleWakeup. - ~2:00 wasted on a hung compound Bash: a stray
sedwith no input file
read stdin until the 2-minute tool timeout. Self-inflicted; the dispatch
itself was unaffected. - ~1:30 wasted on a code-reviewer launch whose role-instructions path
(.claude/agents/code-reviewer.md) doesn't exist in this repo — the
existence check and the launch ran in the same shell command, so the
check couldn't prevent the launch. Killed and relaunched against
~/.claude/agents/code-reviewer.md.
Blocker inventory: no AskUserQuestion gates, no rate limits, no red-tier
gates. Legitimate background waits: 6 Codex dispatches totaling ~24 min,
all polled in-turn.
Dispatch health: 6/6 dispatches completed exit 0; one classified failure
loop (verify → fix → re-verify) worked as designed and caught a real
contract gap (mutating commands shipped without functional --dry-run) —
that loop's ~20 min bought a genuine fix, not churn.
Dial record (from wrapup.md)
zone: 2, lanes: single-codex, passes: {plan: 1/1, post_pr: 1/1}
findings: {plan_pass1_codex: 5 (3 MF), post_pr_pass1_codex: 0}
verifiers: {frontend: skipped, qa_pass: trimmed}, qa_findings: 0
wall_clock: "1:22", pr_size: {files: 18, +1466, -24}
tokens: {codex: 643916, overseer: 338751, claude_subagents: 0, total: 982667}
spend_ratio: 659.5Judgment: right-sized. The plan pass was high-yield (3 Must Fix, one of
which — the atomic-write race — materially changed the design). The post-PR
pass found nothing, but at 85k tokens it was cheap assurance on a 1.5k-line
diff; no dial change is supported by one data point. The verify-stage rubric
(cli-script) earned its keep — it caught the only real defect. Zone 2's
trimmed QA (no app driving) was correct: no UI ACs existed, and the live-app
checks are honestly parked on the human's checklist.
Outcome
Deferred — ops-only run at wrap-up. On the automated evidence the run is
on-target (all 5 ACs proven, review Approve with zero findings); the outcome
half runs after the human's PR review and live-app manual pass.
What to change so it doesn't recur
claude/skills/do/SKILL.md(dcouple/orchestra) — Step 5's publishing
rule says "The turn in which a reviewer or verifier report arrives
publishes its results (body edit, evidence comment) before ending."
The 25-minute stall slipped through because the QA drive's results came
from the Overseer's own command run, not a sub-agent report. Proposed
edit — extend the sentence: "The turn in which a reviewer or verifier
report arrives — or in which the QA drive's own checks complete —
publishes its results (body edit, evidence comment) before ending."claude/skills/codex/SKILL.md(dcouple/orchestra) — Step 1 says to
confirm the instruction/format files exist before dispatching. The wasted
launch happened because the check and the launch shared one shell command,
so a failed check couldn't stop the launch. Proposed edit — add to the
path-resolution note: "Run the existence check as its own command and read
its result before writing the launcher; a check-and-launch combined in
one shell invocation cannot prevent a bad dispatch."- Consumer-repo sync (dcouple/Pane) —
.claude/agents/in this repo
carries only 6 of the orchestra agent charters;code-reviewer.md(and
others, e.g.discussant.md,frontend-verifier.md) are missing, forcing
home-directory fallback paths. Not an orchestra edit — re-run the
orchestra→Pane sync so repo-relative charter paths resolve.
System changes recorded here for /postmortem-loop; nothing applied.
Published as comments on issue #357 and PR #359 (URLs added post-publish).
Summary
What — RunPane can now declare a pane's pinned state:
--pinnedonpanes create, newpanes pin/panes unpincommands, andpinnedinpanes listJSON, across the daemon, the npm CLI, and the Python wrapper.Why — an orchestrator (Pane Chat) could already create background panes but not arrange them: in the four-workstream dogfood run (#353), all four panes were created correctly in the background and the user still had to find and pin each one by hand in the UI. Now the orchestrator declares the layout (pinned, unfocused) at creation time and reconciles it later without stealing focus.
How — the persistence already existed (
sessions.is_favorite/favorite_pinned_at;startPinnedon session creation), so this is contract plumbing plus one new atomic mutation: arunpane:panes:pindaemon command whose written value comes only from the request (one SQLUPDATEwith aCASEthat preservesfavorite_pinned_aton repeat pins — declarative set, never a toggle, so a retry after a transport failure can't flip state). Pinning never touches focus/activation. Everything is additive and contract-generated:contracts/runpane/contract.json→ regenerated npm/shared/Python modules, fixture, and docs; both wrappers move together with parity assertions.Done means: an orchestrator can run
runpane panes create … --pinned --no-focus --jsonandrunpane panes pin|unpin --pane <id> --yes --json(idempotently, with--dry-runpreview), readpinnedback frompanes list --json, and the state matches the Pane UI's pin star including after app restart.Visual overview
Before: layout intent dies at the CLI boundary and the user hand-pins every pane. After:
--pinned --no-focusdeclares the layout at create time, and apanes list→panes pin|unpinreconcile loop keeps it, as an idempotent set.User journeys
Primary journey — orchestrator arranges a workspace: Pane Chat creates four worktree panes with
--pinned --no-focus→ each pane appears in the sidebar already starred/pinned, none steals focus → the user opens Pane to a pre-arranged workspace instead of hand-pinning four panes.Reconciliation journey: the orchestrator reads
panes list --json, diffs each pane'spinnedagainst the desired layout, and issuespanes pin/panes unpinfor the drift — repeat requests are no-ops, so a retried command never un-pins what it just pinned.Verification
Independently proven by a fresh verifier pass (quoted evidence in the run's verification reports; commands reproducible locally):
panes createwithpinned: true+noFocus: truereturnspinned: true, focused: falseand the session row hasis_favoriteset with non-nullfavorite_pinned_at→pnpm --filter main exec vitest run src/ipc/runpane.test.ts(45 tests pass; assertions at the new pinned-create case).pinned: trueand updates the DB row → same suite.favorite_pinned_at) byte-identical; unpin mirror case → same suite; the atomicsetSessionFavoriteUPDATE preserves the timestamp viaCOALESCE.panes listJSON carriespinnedfor both states → same suite.DatabaseServiceis closed and a new instance re-initialized on the same file, pin state and timestamp read back →pnpm --filter main exec vitest run src/database/database.favorite-pinning.test.ts.node scripts/generate-runpane-contract.js --check(generated files current) andnode scripts/test-runpane-contract.js(npm and Python send identical{paneId, pinned}/{paneId, pinned, dryRun}payloads,--pinnedcreate payloads,--yesgating, clean--jsonstdout).pnpm typecheckclean;pnpm lint0 errors (165 pre-existing warnings); plan reviewed (1 Codex pass, 3 Must Fix findings folded in pre-implementation); verifier rubric blocker (missing functional--dry-runon the new mutating commands) found and fixed, re-verified pass.Manual tests
All ACs are covered by the automated suites above; these human-runnable items exercise the live app + CLI end-to-end against a daemon running this build.
Important (user-facing behavior):
runpane panes create --repo <repo> --name pin-test --agent claude --pinned --no-focus --yes --json→ response has"pinned": true, "focused": false; the pane appears in the Pane sidebar already pinned and the app window does not steal focus — left to human: needs a daemon running this build (installed daemon is 2.4.29, pre-feat: declarative pane pinning via RunPane CLI #359)runpane panes pin --pane <id> --yes --jsonon an unpinned pane → UI pin star turns on immediately (no restart);panes unpinturns it off — left to human: needs a daemon running this build (installed daemon is 2.4.29, pre-feat: declarative pane pinning via RunPane CLI #359)panes pincommand twice → second run reports the same state, UI unchanged — left to human: needs a daemon running this build (installed daemon is 2.4.29, pre-feat: declarative pane pinning via RunPane CLI #359)runpane panes pin --pane <id> --dry-run --json→ previews without changing the UI star — left to human: needs a daemon running this build (installed daemon is 2.4.29, pre-feat: declarative pane pinning via RunPane CLI #359)Nice (cosmetic):
runpane panes list(text mode) shows the pinned marker on pinned panes — left to human: needs a daemon running this build (installed daemon is 2.4.29, pre-feat: declarative pane pinning via RunPane CLI #359)Areas not affected: Pane UI code (unchanged — it already renders favorite/pinned state), panel-level pinning, archive semantics, existing RunPane commands/JSON fields,
sessions:toggle-favorite(UI keeps using it).QA results
Automated QA executed 0 of 6 Manual-test items (all six need a live daemon running this build; the installed daemon is 2.4.29, pre-#359) and instead re-proved the full command-shaped verification against the final commit: 46/46 vitest tests (AC1–AC5), generated-contract freshness, and npm/Python parity including the new pin/unpin/dry-run assertions — all pass, no bugs found. Post-PR code review: Approve, 0 Must Fix / 0 Should Fix (single Codex lane, zone 2). Evidence in the QA comment below.
Residual risks
runpane:panes:pin; the CLI fails fast withNo Pane daemon command registered for channel "runpane:panes:pin"rather than misbehaving. CLI and app ship on the same release train.is_favorite/favorite_pinned_atwhile the public contract sayspinned; the mapping is stated in the command help text (deliberate, per the item's capture gate).Closes #357