Consolidation branch for U7, per the new one-branch working mode. **Supersedes #2607, #2635, #2640** — the three of my PRs that were stuck on review threads. My other seven (#2602, #2605, #2606, #2611, #2621, #2628, #2633) are green with **zero unresolved threads** and are deliberately left alone for the merge sweep. ## What is in here, file by file | file | change | guards before → after | |---|---|---| | `plugins/…/glasses/src/agent-actions.ts` | gates, destinations and degraded-resolution refusal all resolve from the task's own workflow | 2 → 0 | | `plugins/…/glasses/src/quick-capture.ts` | accepted capture columns come from the board; default no longer names the deleted column | 1 → 0 | | `plugins/…/glasses/src/settings.ts` | quick-capture default was `triage`, the column #2515 removed | (assignment, uncounted) | | `plugins/…/dependency-graph/src/GraphTaskNode.tsx` | redundant column condition deleted | 1 → 0 | | `packages/engine/src/executor.ts` | 8 rebound guards compare the resolved column; 4 resume-eligibility literals share one resolver | 151 → 143 (+4 off-bar) | | `packages/engine/src/__tests__/` | 4 new suites, 26 cases | — | `plugins/` reaches **zero** column guards with this branch. ## The three threads it closes **#2607 — five findings, all mine, all the same rule.** I kept *qualifying* a legacy-id fallback instead of removing it: | attempt | rule | hole review found | |---|---|---| | 1 | fall back to `todo` when the role is missing | moved cards to phantom columns | | 2 | …only if the workflow **declares** `todo` | aliased **review** lane named `todo` | | 3 | …and only if no other role is assigned to it | **traitless** parking column named `todo` | The qualifications were the mistake. Once `resolveLanes` returns a lane set the workflow *has* a column vocabulary, so "no column carries the hold trait" is a complete answer — refuse. `destination()` is two lines now, with no aliasing surface left to qualify. Plus a sixth, which is a genuinely different state: **degraded resolution is indistinguishable from the default board.** `resolveWorkflowIrForTask` is total by design — a missing definition silently returns the *default* coding IR — so a card on a custom board whose definition could not be read resolved to `todo`/`in-progress`. `undefined` lanes cannot express that (it means "no workflow at all", where the legacy ids *are* the answer). The actions now refuse with 409. #2618 would replace this check with resolver provenance; it is not merged, so this does not depend on it. **#2635 — "seven rebound sites remain untested."** Fair; my "same shape" note was an assertion, not coverage. Seven of the eight need a live graph run to reach, so the *shape* is pinned instead: a static check that no guard in front of a rebound move compares against a column literal, with a vacuity case (the same detection run against the original shape) and a match-count floor (≥8), because a guard reporting success on zero matches is worse than no guard. **#2640 — duplicate workflow resolution.** Framed as I/O; it is also a correctness bug. Eligibility and re-entry are two halves of one decision and resolved the workflow separately, so a workflow edit landing between them has the halves reading *different boards*. Now one caller-owned memo per decision — caller-owned because a process-lifetime cache would have to guess when a mid-flight workflow edit invalidates it. ## Behavioural findings, not tidying - **The last-resort recovery for completed-but-stranded work did not exist off the default lineage.** `promotedFromPlannerColumn` was false on a renamed board, so finished work resting in planning was never promoted; the code fell through to a review handoff that role adjacency rejects, and the card stayed stuck with its work complete. - **Rebound guards could not see the column their own move targeted.** U5b converted the move target; the eight `column !== "todo"` checks in front of it were left literal, so on a renamed board the engine moved a card into the column it was already in — and `moveTaskInternal` runs reset-on-entry on every real move, so at the `preserveProgress: false` site it reset step progress a second time. - **The FN-1404 `task:move` audit row was lying**, recording `to: "todo"` while the move target was resolved. A run-audit trail that disagrees with the move it describes is worse than none. Not a comparison, so no census counts it. - **A task interrupted by an engine pause never resumed on a renamed board** (off-bar, `in-review`/`in-progress` literals): four comparisons decided one question and had to agree; two of them disagreed on a renamed board, so re-entry silently never fired. ## Revert proofs, isolated per site | reverted | result | |---|---| | `destination()` back to attempt 3 | 3 of 38 fail | | degraded-resolution refusals removed | 2 of 42 fail | | capture set back to the legacy five | 2 of 3 fail (renamed-board suite) | | forward exclusions → literals | 1 of 14 fails | | missing-wip refusal removed | 2 of 14 fail | | `promotedFromPlannerColumn` → literals | 3 of 7 fail | | promotion target → `"in-progress"` | 3 of 7 fail | | one rebound guard → `!== "todo"` | 1 of 3 fails (static shape) | | resume lanes → legacy trio | 1 of 5 fails | Every conversion is paired with a negative — a forward move, a not-a-planner-lane card, a default-lineage card, an unresolvable workflow — so neither "always fire" nor "never fire" can pass for "resolve the role". ## Commit discipline Twelve commits, each one thing: the code move (`resolvePlannerLanes` out of `triage.ts`) is separate from every behavior change, and each review fix is its own commit with its own revert proof. ## Verification - `pnpm test:gate` **71/71** - 162/162 across the glasses plugin's 19 files; 26/26 across the four new engine suites - engine + glasses typecheck clean; `pnpm lint` clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Engine recovery and retries now work correctly with renamed or customized workflow columns. * Tasks in manual-intake columns are no longer automatically planned. * Agent actions and quick capture now respect each board’s declared columns and lifecycle stages. * Awaiting-approval tasks are recognized regardless of their current column. * Command Center SDLC funnel stages now accurately reflect customized workflows. * **Documentation** * Added guidance for safely changing workflow-column logic and interpreting lifecycle-column checks. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
fusion-plugin-dependency-graph
Plugin-provided top-level Graph dashboard view for Fusion.
Host registration and bundled loading
The dependency graph view is registered as a bundled plugin view in the dashboard. This means:
registerBundledPluginViews()(called at dashboard startup) registers the view component in the static plugin view registry under keyplugin:fusion-plugin-dependency-graph:graph.- The view component is resolved via a literal-specifier lazy import (
import("@fusion-plugin-examples/dependency-graph/dashboard-view")) — this is critical for Vite/esbuild to emit a production chunk. - In Vite dev/build and Vitest,
@fusion-plugin-examples/dependency-graph/dashboard-viewis aliased toplugins/fusion-plugin-dependency-graph/src/dashboard-view.tsx(source, notdist/). This avoids stale dist artifacts breaking the dashboard view. - The App.tsx graph route resolves to the bundled view via
isPluginViewRegisteredfallback, so the graph view works even when the plugin is not installed/loaded through the PluginLoader API (e.g. fresh DB). - The canonical route ID is
plugin:fusion-plugin-dependency-graph:graph.
Rendering approach
- Filtering: includes
triage,todo,in-progress,in-review; excludesdone,archived - Graph build: edges are resolved only from
task.dependenciesassource=dependent,target=dependency - Orphan dependency handling: if a visible task depends on an excluded/missing dependency (for example
done/archivedafter filtering), the missing edge is silently dropped and graph rendering continues without broken connectors - Auto-layout: Sugiyama-style layered layout (
computeAutoLayout) groups nodes by dependency depth; both vertical and horizontal orientations use measured per-node card heights to compute stacked offsets (preventing card overlap), and orientation still flips responsively (height > widthorwidth < 768) so depth flows left-to-right on tall/narrow viewports - Edge drawing: SVG bezier curves from source bottom-center to target top-center, with vertical mode anchoring source tails to each node's measured rendered height and arrowheads showing dependent → dependency direction
- Interaction: drag-to-pan canvas background, scroll/two-finger pan, Ctrl/Cmd-wheel zoom, pinch-to-zoom with stationary midpoint, drag-to-reposition nodes, keyboard shortcuts, zoom toolbar, reset, and fit-to-graph
- Zoomed navigation reachability: pan clamping now scales with the full rendered graph bounds (min/max node extents) at the active zoom level, so zooming in no longer traps off-screen content behind fixed viewport-only limits
- Fit-to-graph: computes node bounding box from both minimum and maximum node coordinates (including negative auto-layout origins) with layout node dimensions and applies zoom/pan so the graph fits in viewport with padding
- Initial auto-fit: when no saved scoped positions exist, the first non-empty render auto-fits once; subsequent updates preserve user navigation state
- Position persistence: dragged node positions are stored per project in browser localStorage and restored on reload
- Animated transitions: fit/reset operations animate
transform(var(--transition-normal)), while continuous drag/wheel/pinch stays transition-free for responsiveness - Node rendering: each graph node renders the real dashboard
TaskCardviaGraphTaskNode(no duplicated card markup) - Task detail integration: a primary non-drag click on a graph node surface opens the native dashboard task detail modal exactly once through host context (
openTaskDetail), matching board/list behavior - In-progress behavior: steps are visible by default and active-task glow (
agent-active) is preserved because node cards reuse TaskCard directly - Active-state indicator bar: active nodes render a compact top bar (
.graph-task-active-indicator) with the current execution status label (for exampleExecuting,Planning) and pulsing--in-progressemphasis - Current-step highlighting: active nodes set
data-current-stepfor valid native step indices so CSS selectors highlight the currently executing.card-step-itemand pulse its step dot - Zoom-out differentiation:
.graph-task-node--activeadds amplified glow and subtle scale/border tint so active nodes remain distinguishable at reduced zoom levels - In-review visual treatment:
in-reviewnodes get a static.graph-task-node--in-reviewleft accent in--in-reviewto distinguish waiting-review work from active execution nodes - Graph node classes:
.graph-task-node,.graph-task-node--active,.graph-task-node--in-review,.graph-task-node--highlighted,.graph-task-node--dimmed,.graph-node--highlighted, and.graph-node--dimmedare available for graph-specific layering/highlight states while card internals remain owned byTaskCard.css - Graph edge classes:
.graph-edge--highlightedand.graph-edge--dimmedare applied during dependency-chain emphasis states - Drag behavior: graph nodes pass
disableDrag={true}toTaskCardso card-level HTML5 drag does not conflict with canvas pan/zoom
Position persistence
- Canonical base key:
fusion-plugin-dependency-graph:positions - Storage key format:
kb:${projectId}:fusion-plugin-dependency-graph:positions(falls back tofusion-plugin-dependency-graph:positionswhen no project is selected) - Read path: positions load on graph mount and whenever
projectIdchanges, then merge with fresh auto-layout so new tasks still receive layout defaults - Write path: positions persist on drag end only (not on every drag frame), filtered to currently visible tasks for stale cleanup
- Reset behavior: Fit to graph / Reset view clear persisted positions and re-apply auto-layout
- Implementation detail: the plugin now reuses dashboard
projectStoragehelpers (getScopedItem/setScopedItem/removeScopedItem) instead of duplicating scoped localStorage logic
Dependency chain highlighting
- Hover a node to highlight the full transitive upstream + downstream chain for that task.
- Click a node to persist selection highlighting until the same node is clicked again or the canvas pane is clicked; this same click also opens task detail once through the host detail callback.
- Priority: hover state overrides selected state; when hover leaves, selected highlighting reappears.
- Dimming: when a chain is active, unrelated nodes and edges are dimmed.
- Neutral state: when nothing is hovered/selected, no highlight/dim classes are applied.
- Drag suppression: drag movements above the node drag threshold suppress the post-drag click, preventing accidental detail opens on pointer release.
- Edge rule: an edge is highlighted only when both its source and target nodes are in the active chain.
Controls
Toolbar (bottom-right)
- Zoom in (
ZoomIn) — button +Ctrl+=/Cmd+= - Zoom out (
ZoomOut) — button +Ctrl+-/Cmd+- - Zoom percent label — live readout (for example
100%,75%,250%) - Fit to graph (
Maximize) — button +Ctrl+Shift+F/Cmd+Shift+F - Reset view (
RotateCcw) — button +Ctrl+0/Cmd+0
Additional keyboard behavior
- Escape resets to default view (
zoom=1,pan=0,0) - Shortcuts are suppressed when focus is inside
input,textarea,select, orcontentEditableelements
All controls are rendered as floating .btn-icon actions in the bottom-right corner, with mobile-friendly 44px touch targets in the @media (max-width: 768px) override.