Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-09-15
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
## Context

See `proposal.md` for motivation. Current state that shapes the approach:

- The launcher's contract picker is inline in `src/launch-tui.ts` (`handleContractKey`, `acceptContract`, `contractDetail`) and pins exactly one change into `selectedChangeIds`.
- The archive picker is a standalone component, `src/change-picker-tui.ts`, returning `{ kind: "select", changeId }`; `src/cli.ts` wraps it as `[choice.changeId]`.
- Selection downstream already accepts ordered lists: `enabledFlags` emits one `--change` per id, `runSelection.changes` feeds the run plan, `resolveChange`/`loadOpenSpecBundle` preserve order and dedupe, and `archiveSelectedChanges` journals one step per change and commits the batch.
- `src/change-selection.ts` already models `SelectedChangeInput[]`, validation, and freezing, but is only exercised by tests; the interactive surfaces never produce more than one entry.

## Goals / Non-Goals

**Goals:**

- Make the existing 0..N ordered selection reachable from both TUI pickers (all / several / one / none).
- Open the launcher prompt step focused on the first active change, consistently on first open and re-entry.
- Preserve the explicit-acceptance invariant: nothing attaches without an explicit confirm.

**Non-Goals:**

- No downstream run-plan, bundle-resolution, archive-journal, or headless `--change` changes.
- No arbitrary reorder affordance in the TUI (order is the picker's listing order).
- No new "auto-attach" mode: the change is attached on confirm, never on open.
- No unification of the two pickers into one shared component (they keep separate rendering).

## Decisions

### D1: Keys — `space` toggles, `a` selects all, `enter` confirms, the no-change row is "none"

Both pickers gain `space` to toggle the highlighted row and `a` to mark every active change. The launcher keeps its `Manual prompt` row as the explicit no-change gesture rather than adding a hidden "none" key, so the manual/no-change mode stays a visible, deliberate row. Alternatives: a separate `n` key for none (rejected: hides the explicit decision); `enter`-to-toggle (rejected: breaks the existing confirm gesture).

### D2: Multiple selection is ordered by the picker listing

A confirmed multi-selection is ordered by the active-change listing (alphabetical, as `listOpenSpecChanges` returns it), not by toggle order. This is deterministic, matches select-all, and keeps the visible list order equal to the reviewed order. Alternative: toggle order with numeric badges (rejected by the operator as irrelevant). The headless CLI still preserves a verbatim `--change` sequence; that path is unchanged.

### D3: `enter` semantics — confirm marks, else pin the highlighted row

If any row is marked, `enter` confirms the marked set. If nothing is marked, `enter` pins the highlighted row only: the no-change row selects manual/no-change mode, a spec row attaches just that change. This keeps today's single-pick behavior (and makes the focused-first-change flow attach with one `enter`) while adding multi-select on top.

### D4: One focus rule across every entry path

`openPrompt`, the options-step `p`/`escape` return, and the prompt-editor `escape` return all compute the picker highlight with the same rule: if a selection exists, first selected change; else if active changes exist, the first active change; else the no-change row. The rule is centralized so the three paths cannot drift.

### D5: Shape changes are minimal and local

The launcher keeps `selectedChangeIds: string[]`. The archive picker's result becomes `{ kind: "select", changeIds: string[] } | { kind: "cancel" }`, and the `src/cli.ts` call site passes that batch to `runWorktreeArchive`. Downstream signatures already accept a list.

### D6: Pure selection helpers, unit-tested

Toggle, select-all, and "confirm in listing order" are extracted as pure functions so the multi-select behavior is testable without the renderer; the TUI layers call them. This mirrors the existing pure/it's-I/O-free split used by `resolveChange`.

## Risks / Trade-offs

- [The default `enter` now attaches the first change instead of opening the manual editor] → Accepted per the request; attachment still requires an explicit `enter`, the highlight is visible, and the resulting prompt/notice names the change.
- [A marked set can look reordered relative to toggle order] → Ordering is always the visible listing order, so the marks and the reviewed order agree.
- [Footer counters and picker notices still say "pick one"] → Update the picker intro, footer counter, and the OpenSpec notice alongside the behavior; cover with TUI tests.
- [Archive confirms with nothing marked] → Keep the confirm a no-op that does not report success, and test it.

## Migration Plan

No data or config migration. Rollback is reverting the UI changes; the downstream ordered-selection support predates this change and stays.

## Open Questions

None.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## Why

The run launcher and the archive picker only let the operator select exactly one active OpenSpec change, but the specs already require an ordered list of zero or more (`run-launcher`: "an operator-selected ordered list of zero or more active local OpenSpec changes"; `worktree-operations`: "an explicitly reviewed ordered batch"). The selection plumbing downstream (`selectedChangeIds: string[]`, `--change` repeated, `resolveChange`, `loadOpenSpecBundle`, `archiveSelectedChanges`) already handles N, so today the capability is reachable only from the headless CLI, never from the TUI. In addition, opening the launcher prompt step lands the cursor on "Manual prompt", forcing an operator who wants the active change as the contract to move down first.

## What Changes

- The launcher's OpenSpec contract picker supports selecting all, several, one, or none of the checkout's active changes: `space` toggles the highlighted row, `a` selects every active change, and `enter` confirms the marked set. `Manual prompt` remains the explicit no-change mode.
- The archive picker supports the same multi-select, returning the ordered batch instead of a single change id.
- Both pickers order a multiple selection by the change list (alphabetical), independent of the order in which rows were toggled; `a` uses the same order.
- When the launcher opens the prompt step in a checkout with active changes and no preset selection, the cursor lands on the first active change instead of `Manual prompt`; the same rule applies on every re-entry into the picker.
- Selecting a change still requires explicit confirmation: no change is attached without `enter`, and the no-change mode stays explicit.

## Capabilities

### New Capabilities

<!-- none -->

### Modified Capabilities

- `run-launcher`: the explicit checkout-local change selection is reachable from the launcher UI as an ordered list of zero or more (all/several/one/none), and the picker opens focused on the first active change.
- `worktree-operations`: the reviewed archive batch is reachable from the archive picker UI (all/several/one), not only from repeated `--change`.

## Impact

- `src/launch-tui.ts` — contract picker key handling, state, and detail/footer rendering; initial and re-entry focus.
- `src/change-picker-tui.ts` — multi-select result and rendering; `src/cli.ts` archive call site passes the ordered batch to `runWorktreeArchive`.
- Selection helpers (toggle/all/order) and their unit tests, plus launcher and picker TUI tests.
- No changes to the downstream run-plan, bundle resolution, archive journal, or headless `--change` behavior; explicit-acceptance and no-silent-attach invariants are preserved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
## ADDED Requirements

### Requirement: Launcher change picker offers ordered multi-selection

When the launcher prompt step opens for a checkout with active local OpenSpec changes, the picker SHALL let the operator select zero, one, or several of them before confirming: toggling the highlighted row marks or unmarks that change, a select-all action marks every active change, and the explicit no-change row remains available as the manual/no-change mode. Confirming SHALL attach exactly the marked changes, ordered by the picker's active-change listing, independent of the order in which the rows were toggled; confirming the no-change row SHALL select nothing. No change SHALL be attached without an explicit confirmation, and a sole active change SHALL remain unselected until confirmed.

#### Scenario: Several changes are marked and confirmed

- **WHEN** the operator marks the second and then the first active change and confirms
- **THEN** review and the run plan carry both changes ordered by the picker's listing, with the no-change mode not set

#### Scenario: Select-all marks every active change

- **WHEN** the operator invokes select-all and confirms
- **THEN** every active change is attached in the picker's listing order

#### Scenario: No-change row selects nothing

- **WHEN** the operator confirms the explicit no-change row
- **THEN** the run proceeds in manual/no-change mode with zero attached changes

#### Scenario: A sole active change still needs confirmation

- **WHEN** the checkout has exactly one active change and the picker opens on it
- **THEN** the change is attached only when the operator confirms, never on open alone

### Requirement: Launcher change picker opens focused on the first active change

When the launcher prompt step opens, or is re-entered, for a checkout with active local OpenSpec changes and no already-preset selection, the picker SHALL place its highlight on the first active change rather than on the no-change row. When a selection was already made, the highlight SHALL return to the first selected change. The no-change row SHALL remain reachable.

#### Scenario: Fresh worktree with active changes

- **WHEN** the prompt step opens in a checkout with active changes and no preset selection
- **THEN** the highlight sits on the first active change, not on the no-change row

#### Scenario: Re-entry returns to the selection

- **WHEN** the operator returns to the picker after a selection was made
- **THEN** the highlight sits on the first selected change

#### Scenario: Manual/no-change remains reachable

- **WHEN** the picker opens focused on the first active change
- **THEN** the operator can still move to the no-change row and choose it explicitly
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## ADDED Requirements

### Requirement: Archive picker offers ordered batch selection

The interactive archive picker SHALL let the operator select one or more of the checkout's active changes before confirming: toggling the highlighted row marks or unmarks that change, a select-all action marks every active change, and the confirmed batch is ordered by the picker's active-change listing. Confirming with no change marked SHALL NOT archive anything. The confirmed batch SHALL be handed to the archive command as the reviewed ordered batch and journaled per change.

#### Scenario: Several changes archived as one batch

- **WHEN** the operator marks two active changes and confirms
- **THEN** both are archived in the picker's listing order and committed as one verified archive commit

#### Scenario: Select-all archives every active change

- **WHEN** the operator invokes select-all and confirms
- **THEN** every active change in the checkout is archived in listing order

#### Scenario: Nothing marked does not archive

- **WHEN** the operator confirms with no change marked
- **THEN** no archive runs and the picker does not report a success
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## 1. Pure selection helpers

- [x] 1.1 Add pure helpers for marking/unmarking a change, selecting all active changes, and producing the confirmed selection in picker-listing order; verify with unit tests covering toggle, select-all, dedupe, and empty selection
- [x] 1.2 Add unit tests asserting a confirmed multi-selection is ordered by the listing regardless of toggle order; verify `bun test test/change-selection.test.ts` passes

## 2. Launcher change picker (surface A)

- [x] 2.1 Add `space` to toggle the highlighted row and `a` to select all in the launcher contract picker, keeping the `Manual prompt` row as the explicit no-change gesture; verify with a TUI test that marks two rows and confirms
- [x] 2.2 Make `enter` confirm the marked set when any row is marked, and otherwise pin the highlighted row (no-change row → manual mode, spec row → that change only); verify the existing single-pick and manual-mode launcher tests still pass
- [x] 2.3 Render mark state and selection count in the picker detail and footer (replace the `specIndex+1/N+1`-only counter); verify a TUI frame shows the marks and the count
- [x] 2.4 Update the picker intro and the OpenSpec notice copy so they describe picking one or more (no "pick one" wording); verify the notice tests assert the new copy

## 3. Launcher focus rule (request D)

- [x] 3.1 Centralize one focus rule — first selected change, else first active change, else the no-change row — across `openPrompt`, the options-step return, and the prompt-editor escape return; verify a TUI test that opens the prompt step in a worktree with active changes lands the highlight on the first active change
- [x] 3.2 Verify re-entry restores the highlight to the first selected change and the no-change row stays reachable with unit/TUI tests

## 4. Archive picker (surface B)

- [x] 4.1 Change `showChangePickerTui` to return the ordered batch (`{ kind: "select", changeIds }`) with `space` toggle and `a` select-all; verify `bun test test/change-picker-tui.test.ts` covers selecting several and selecting all
- [x] 4.2 Make confirming with nothing marked archive nothing and not report success; verify with a picker test
- [x] 4.3 Pass the confirmed batch from the `src/cli.ts` archive call site to `runWorktreeArchive`; verify the existing archive command tests still pass

## 5. Integration verification

- [x] 5.1 Verify `bun run typecheck` passes
- [x] 5.2 Verify the full suite passes with `bun test`, including the launcher, picker, and archive tests updated above
43 changes: 43 additions & 0 deletions openspec/specs/run-launcher/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,46 @@ Every new run SHALL review an explicit execution checkout and an operator-select

- **WHEN** the operator explicitly selects manual/no-change execution in a spec-less checkout
- **THEN** the plan contains zero local changes and records the absence of local specs without consulting another checkout

### Requirement: Launcher change picker offers ordered multi-selection

When the launcher prompt step opens for a checkout with active local OpenSpec changes, the picker SHALL let the operator select zero, one, or several of them before confirming: toggling the highlighted row marks or unmarks that change, a select-all action marks every active change, and the explicit no-change row remains available as the manual/no-change mode. Confirming SHALL attach exactly the marked changes, ordered by the picker's active-change listing, independent of the order in which the rows were toggled; confirming the no-change row SHALL select nothing. No change SHALL be attached without an explicit confirmation, and a sole active change SHALL remain unselected until confirmed.

#### Scenario: Several changes are marked and confirmed

- **WHEN** the operator marks the second and then the first active change and confirms
- **THEN** review and the run plan carry both changes ordered by the picker's listing, with the no-change mode not set

#### Scenario: Select-all marks every active change

- **WHEN** the operator invokes select-all and confirms
- **THEN** every active change is attached in the picker's listing order

#### Scenario: No-change row selects nothing

- **WHEN** the operator confirms the explicit no-change row
- **THEN** the run proceeds in manual/no-change mode with zero attached changes

#### Scenario: A sole active change still needs confirmation

- **WHEN** the checkout has exactly one active change and the picker opens on it
- **THEN** the change is attached only when the operator confirms, never on open alone

### Requirement: Launcher change picker opens focused on the first active change

When the launcher prompt step opens, or is re-entered, for a checkout with active local OpenSpec changes and no already-preset selection, the picker SHALL place its highlight on the first active change rather than on the no-change row. When a selection was already made, the highlight SHALL return to the first selected change. The no-change row SHALL remain reachable.

#### Scenario: Fresh worktree with active changes

- **WHEN** the prompt step opens in a checkout with active changes and no preset selection
- **THEN** the highlight sits on the first active change, not on the no-change row

#### Scenario: Re-entry returns to the selection

- **WHEN** the operator returns to the picker after a selection was made
- **THEN** the highlight sits on the first selected change

#### Scenario: Manual/no-change remains reachable

- **WHEN** the picker opens focused on the first active change
- **THEN** the operator can still move to the no-change row and choose it explicitly
21 changes: 20 additions & 1 deletion openspec/specs/worktree-operations/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,30 @@ Archive SHALL invoke the supported OpenSpec workflow in the explicitly selected
#### Scenario: Archive was performed outside Convoy
- **WHEN** OpenSpec moves a change into a dated archive directory outside Convoy
- **THEN** refresh shows the local archive and no association repair, branch rename, or lifecycle update is required

#### Scenario: Archive modifies unrelated files

- **WHEN** an archive attempt leaves changes outside its verified output
- **THEN** Convoy preserves the files, stops before an automatic commit, and explains the recovery requirement

### Requirement: Archive picker offers ordered batch selection

The interactive archive picker SHALL let the operator select one or more of the checkout's active changes before confirming: toggling the highlighted row marks or unmarks that change, a select-all action marks every active change, and the confirmed batch is ordered by the picker's active-change listing. Confirming with no change marked SHALL NOT archive anything. The confirmed batch SHALL be handed to the archive command as the reviewed ordered batch and journaled per change.

#### Scenario: Several changes archived as one batch

- **WHEN** the operator marks two active changes and confirms
- **THEN** both are archived in the picker's listing order and committed as one verified archive commit

#### Scenario: Select-all archives every active change

- **WHEN** the operator invokes select-all and confirms
- **THEN** every active change in the checkout is archived in listing order

#### Scenario: Nothing marked does not archive

- **WHEN** the operator confirms with no change marked
- **THEN** no archive runs and the picker does not report a success

### Requirement: Squash integration is whole-branch and does not rewrite its source
Squash-to-base SHALL review the entire source/base difference, require the pinned base to be contained in the clean source (or explicitly perform sync first), and create exactly one operator-authored conventional candidate with that base as its only parent. Signing, hooks, secret protections, and existing run-recovery refs SHALL remain effective. The source's history SHALL NOT be rewritten. The base checkout SHALL be validated clean and on the intended branch before landing; movement of source/base or unknown state SHALL stop for renewed review. Empty aggregate content SHALL produce no commit and no historical integration claim. Successful integration SHALL report the actual base and commit, not create a permanent receipt or mark a domain entity completed.

Expand Down
Loading