Skip to content

feat(timeboxing): one path builds every session surface — root header + threaded card - #244

Merged
hugolytics merged 9 commits into
mainfrom
feat/timebox-session-surface-convergence
Sep 1, 2026
Merged

feat(timeboxing): one path builds every session surface — root header + threaded card#244
hugolytics merged 9 commits into
mainfrom
feat/timebox-session-surface-convergence

Conversation

@hugolytics

Copy link
Copy Markdown
Collaborator

Implements docs/superpowers/specs/2026-09-01-timebox-session-surface-convergence-design.md.

Stacked on #242 — merge that first; this PR retargets to main automatically when it does. #242's reselect guard is deliberately kept here, now scoped to legacy in-flight cards.

Why

On 2026-08-31 22:57 a live /timebox session posted its Stage-0 date card, the user picked a different day from the dropdown, and the card lost every button — the session then waited forever for a Confirm click on a control that no longer existed. Root cause: in the slash route one Slack message served as ack, progress card, outcome card and session-thread root, so a text-only "relabel the thread root" update wiped the card's blocks.

#242 guarded the one writer that did it. This removes the layout that armed it.

What changed

_begin_timeboxing_session_surface (nested in route_slack_event) is now the single builder of every timeboxing session surface: a dedicated root header plus the working/card message threaded under it. Both doors call it — the slash/direct fresh start and the receptionist handoff — so the two paths can no longer drift apart. The root is only ever a header, so a relabel cannot erase a control again.

A surface that half-builds relabels its root canceled rather than leaving a dead header that looks live.

Two bugs the reviews caught, both fixed here

  • A DM-origin handoff lost its card. The extracted builder gated DM delivery on not is_dm, so a session started in a DM opened correctly in the channel and left the DM showing a spinner forever — the same dead end this branch exists to remove, on the other door. Verified by probe against pre-refactor code, fixed, and covered by a test.
  • A guard test had gone vacuous. test_both_entry_points_reach_the_harness counted source lines; a total regression of the handoff to AutoGen left it green. Rewritten AST-based so it fails loudly. A second guard (test_the_handoff_interception_uses_the_redirected_thread) broke mid-branch for the same class of reason — a positional rindex probe that followed the wrong call site after the refactor — and is likewise now AST-based.

Tests

111 passing across the timeboxing, routing, replay and handoff suites. Every new test was broken on purpose and observed failing before being trusted (CLAUDE.md rule); assertions run over system-minted identifiers only — Slack ts, action ids, channel ids — never prose.

Two pre-existing order-dependent failures in very large full-suite runs (test_slack_handoff_flow / test_slack_tasks_handoff_flow ::test_slack_handoff_sets_focus_and_forwards*) were confirmed failing identically at the branch base; they pass in isolation and are untouched here.

Follow-up filed

#243 — text-only root relabels strip the root's constraint Review buttons. Pre-existing on the handoff route; this branch generalises the exposure, so it is recorded rather than widened into this diff.

🤖 Generated with Claude Code

hugocool and others added 9 commits September 1, 2026 02:24
…sions

Approved design from the 2026-08-31 date-card clobber incident: extract the
handoff branch's root+thread surface block into one function, have the slash
fresh start delegate to it, and delete the aliased one-message branch. The
PR #242 guard stays for legacy in-flight cards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d threaded under it

The would-alias fresh start now delegates to _begin_timeboxing_session_surface:
root header (the repurposed origin ack) plus the card as its first thread
reply. The one-message layout that let a relabel erase the Stage-0 card is no
longer reachable on the harness path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…h the one function

Same surface, one implementation. The inline timeboxing arms of the redirect
block are gone; the generic agent redirect stays as it was.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The delegation call to _begin_timeboxing_session_surface sat as a sibling of
the try block instead of its first statement, so a failure inside the surface
builder raised past route_slack_event instead of falling through to the
existing in-thread fallback. Moves it back inside try, matching the brief and
pre-refactor behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A root whose card never arrived used to sit there pending forever, which is
the same silence the incident produced by other means. Now it says so.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The guard found its call site with rindex, so it asserted on whichever
_run_adaptive_timebox_turn came last in the source. When the handoff path moved
into _begin_timeboxing_session_surface, last became the direct route's call and
the guard failed on a key that was correct for that path. It now checks both
sites by name: the surface builder keys off the redirected thread, the direct
route off its own recipient.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… the whole surface

The extracted surface builder gated the card's DM delivery on `not is_dm`, so a
handoff that began in a DM opened the session correctly and then left the DM
showing a spinner forever — no card, no controls, no link. The same dead end the
convergence exists to remove, on the other door.

The canceled-root guard also opened four statements after the root was posted,
so a refused focus binding or a dead origin ack left an orphan pending root
behind. It now covers everything the root's existence outlives.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hugolytics
hugolytics changed the base branch from fix/timebox-date-reselect-keeps-card to main September 1, 2026 20:55
@hugolytics
hugolytics merged commit c2f8ee1 into main Sep 1, 2026
hugolytics pushed a commit that referenced this pull request Sep 3, 2026
The working copy of these two was the pre-refactor version: 451 deletions
against 174 insertions when diffed against origin/main, missing the
session-surface work and the AST-based handoff test that landed in #244.

They looked like work in progress and were not. Local main sat several merges
behind while #242, #244 and #246 landed, so 41 of the 43 files that read as
modified were byte-identical to origin — dirty against a stale HEAD, not
against the branch. These two were the only real difference, and they were
behind rather than ahead.

Checked before restoring rather than after: `_post_pending_harness_approval`,
`PLANNING_MODEL` and the approve-commits path appear identically in both
copies, and the session-id, `committed_tx_id` and `NEEDS_APPROVAL` fixes are
already on origin in `harness_bridge.py` and `dsh_commit_gate_hook.py`. The
working copy held nothing unique. Both files are backed up with a patch against
origin in case that judgement was wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

2 participants