v7.0.0 — Forge

[Forge](/docs/glossary#forge) brings manual task reordering across the full stack — a new `TaskService.ReorderTasks` RPC backs `Shift+↑/↓` in the TUI and `@dnd-kit`-powered drag-and-drop in the GUI, replacing the silent task-number-descending sort with the spec'd `(position ASC, task_number ASC)` order, and a new-task-defaults-to-bottom rule so manual orderings survive task creation. The GUI chat terminal stops snapping to byte 0 mid-scroll thanks to a daemon-side cursor on `SubscribeRawOutput` and an idempotent client-side subscription effect. The Open-in-IDE menu now finds CLIs installed outside the GUI's stripped-down PATH (`code` at `/usr/local/bin/code`, Homebrew shims at `/opt/homebrew/bin`) so VS Code, Cursor, Windsurf, Zed, Sublime, and the JetBrains shims launch from the GUI even when started from Finder or the Dock.

### Added

- **TUI manual task reorder via `Shift+↑/↓`.** `Shift+↑` and `Shift+↓` on a focused active row swap the selected task with its in-bounds same-status neighbour and fire `TaskService.ReorderTasks` with the project's full new active task-number ordering. The optimistic flow snapshots the pre-move state, replaces the active list immediately, and either accepts the server response on success or restores the snapshot and surfaces `Reorder failed — reverted` on failure. Cross-section moves (Draft↔Ready, Ready↔Done) and top/bottom boundaries are silent no-ops. A race guard on `TasksLoadedMsg` drops poll-driven refreshes that arrive between the optimistic swap and the RPC response when the task set is unchanged, so the moved row no longer snaps back. Help overlay gains `Shift+↑` / `Shift+↓` "Move selected task up/down" entries under the Task List section.
- **GUI drag-to-reorder for active tasks.** The Tasks tab reuses the same `@dnd-kit` pieces already proven out by the Sidebar — `DndContext` + `SortableContext` + `useSortable` + `arrayMove` from `@dnd-kit/sortable` — so there's no new dependency. Each active status group ("In Development" / "Todo") owns its own `DndContext` with an 8 px `PointerSensor` activation distance so a stray pixel of pointer drift on a click never turns into a reorder. A `GripVertical` icon at the left of each row is the only target wired with drag listeners; the row body keeps its click-to-open-modal behaviour. The "Failed" and "Done" groups render non-sortable rows so historical groups stay click-to-open with zero drag affordance. Cross-group drags are a structural impossibility (each group is its own DndContext). The tasks store applies the new order optimistically, calls the `ReorderTasks` RPC, and either commits the server response or restores the snapshot + toasts `Reorder failed: …` on rejection.
- **`TaskService.ReorderTasks` server handler + manager method.** The proto declared `TaskService.ReorderTasks(ReorderTasksRequest) returns (TaskList)` but no server handler existed, so any call hit `UnimplementedTaskServiceServer.ReorderTasks` and returned `codes.Unimplemented` — blocking the drag-to-reorder UI. New `task.Manager.ReorderTasks(projectPath, taskNumbers)` loads the active set, validates each number (unknown → `task not found: `; duplicate → `duplicate task in reorder request: `), appends any unmentioned active tasks in canonical Position-then-TaskNumber order so a partial-list request silently parks the leftovers at the end of the queue, then rewrites positions densely 1..N and persists each via `config.SaveTask`. The handler maps validation errors to `codes.InvalidArgument` and any other error to `codes.Internal`.

### Fixed

- **GUI Open-in-IDE — finds CLIs installed outside the GUI's stripped-down PATH.** Launching the GUI from Finder / Dock on macOS inherits a minimal PATH (`/usr/bin:/bin:/usr/sbin:/sbin`) — `path_helper` only runs from login shells, and `spawn(..., {shell: true})` uses non-login `/bin/sh -c`, so the user's profile PATH is never sourced. `code` at `/usr/local/bin/code` (and Homebrew shims at `/opt/homebrew/bin`) failed to resolve and every IDE pick errored out. New `spawnEnv()` helper in `gui/src/main/ipc.ts` prepends the well-known macOS install locations (`/usr/local/bin`, `/usr/local/sbin`, `/opt/homebrew/bin`, `/opt/homebrew/sbin`, `~/.local/bin`, `~/bin`) to PATH for the spawned process; Linux gets `~/.local/bin` + `~/bin`. Fixes `vscode`, `cursor`, `windsurf`, `zed`, `subl`, `webstorm`, `idea`, and `fleet`; `xcode` (uses macOS `open -a`) and `finder` (uses `shell.openPath`) were never affected. Bug only surfaced when the GUI was launched from Finder/Dock — `npm run dev` in a terminal inherited the shell PATH and masked the issue.
- **Task work order — oldest-first by (position ASC, task_number ASC); new tasks default to bottom.** `internal/daemon/task/manager.go::ListTasks` was sorting strictly descending by `task_number`, contradicting the spec's "Task Work Order" rule. Every consumer reads `tasks[0]` — the `start-all` chain and both wildfire pickers — so `watchfire start-all` and `watchfire wildfire` walked a 4-task queue backwards (4 → 3 → 2 → 1) instead of forward (1 → 2 → 3 → 4). The `Position` field was already on `models.Task` and surfaced through proto + converters but never read by the sort, so the dead-data path silently stripped the manual-override knob the spec relied on. Replaced the sort with the spec'd compound ascending order: `position ASC` primary, `task_number ASC` tiebreaker. `CreateTask` also changed: the default position is now `max(active.position) + 1` (or `1` if zero active tasks), appending to the bottom of the work queue so a new task after a manual reorder no longer jumps ahead. An explicit `opts.Position` still wins.
- **GUI Chat terminal — viewport no longer snaps to byte 0 on scroll.** Two stacked causes both fixed. **Client side:** the subscribe effect previously ran `term.clear()` + abort + re-subscribe on every dep change, and `ChatTab.tsx`'s 2 s status poll routinely flipped `active` `true → false → true` whenever `getAgentStatus` hit a transient error, spuriously replaying the full daemon raw buffer from byte 0 mid-scroll. The effect is now idempotent — when `active=true` and the previous `AbortController` is still unaborted, the effect bails out (no clear, no resubscribe, no daemon round-trip). The unsubscribe path is debounced 3 s so a single poll flicker can't tear down a live subscription. `term.clear()` is no longer called anywhere. **Server side:** new catch-up cursor — `Process` tracks `rawTotalBytes` (monotonic count of broadcast bytes), and the new `SubscribeRawFrom(id, bytesReceived)` slices the late-join snapshot so only bytes past the client's offset are sent. `proto/watchfire.proto` `SubscribeRawOutputRequest` gains an `int64 bytes_received` field. Negative / past-end cursors are clamped; cursors before the 1 MiB rolling buffer's floor return the full buffer. Also: xterm `scrollback` raised from 1 000 to **10 000** lines, and the `ResizeObserver` now records the last `(rows, cols)` it sent and bails when the fitted dims are unchanged so scrollbar-appearance nudges no longer spam the daemon with no-op resize RPCs.

### Migration

- All Forge changes are additive on the wire — existing clients keep working. The new `SubscribeRawOutputRequest.bytes_received` field is optional and defaults to `0` (full snapshot), so v6 clients see no behaviour change.
- Task ordering: any project that relied on the v3.0.0 Blaze descending-by-task_number sort will now see tasks listed oldest-first (`position ASC`, `task_number ASC`). New tasks now default to the bottom of the work queue so manual reorderings survive task creation.
- `task.Manager.ReorderTasks` is new but no schema or YAML field changed. Existing `.watchfire/tasks/.yaml` files load identically — the `position` field has been on the model since the start; Forge just finally honours it.