Files
fusion/packages/cli
gsxdsm 4158cf1ab7 Phase A: workflow-owned lifecycle foundation (U1, U2, U3) (#2467)
Phase A (Foundation) of
`docs/plans/2026-07-26-001-refactor-workflow-owned-lifecycle-plan.md`.
Three units, one commit each. No operator-visible behavior change.

## U1 — Lifecycle-column resolution seam

`resolveLifecycleColumns(ir)` returns `{ intake, hold, wip, review,
complete, archived }` — the first column carrying each trait,
`undefined` for a role no column carries.
`resolveTaskLifecycleColumns(store, taskId, cache?)` is the store-aware
form; the cache is caller-owned so a sweep reads one IR per workflow
rather than one per card.

A v1/column-less IR resolves to `undefined` for the **whole struct**
rather than a struct of undefined roles. A caller must be able to
distinguish "this workflow declares no hold column" (a real shape to
honor) from "no column vocabulary at all" (skip and log) — only the
second licenses conservative fallback.

Nothing consumes the seam yet; Phases B–D convert the ~207 hardcoded
column literals onto it.

## U2 — Delete the pre-cutover parity machinery (delete-only)

**`workflow-columns-settings.ts`** — `isWorkflowColumnsEnabled` had the
body `return true`. Six live call sites branched on it, so every
flag-OFF arm was dead code that read as a supported configuration.
Deleted; surviving side inlined at self-healing's transitionPending
sweep, the scheduler's per-column capacity diagnostic, merge-trait's
policy resolver, the board-workflows payload, two task-workflow routes,
and the CLI TUI's column enrichment.

**`workflow-parity.ts`** — asserted the default workflow's adjacency
*equals* the legacy `VALID_TRANSITIONS`. U11 deliberately breaks that
equality by merging Todo into Planning, so this is not a stale assertion
to update; it is a contract against the target state. Its emitter
(`workflow-parity-observer.ts`) is already a tombstone, so
`getWorkflowParitySummary` and `computeWorkflowColumnsGraduationReport`
aggregated run-audit rows nothing writes and had no caller outside
`TaskStore`. Both store methods go with it.

`flagEnabled` stays on the board-workflows **wire** as a constant `true`
— shipped dashboard clients still branch on it, and changing the
response shape is not a deletion. U10 retires the field once no client
reads it.

The `legacy-tombstones` ratchet is extended to both files plus seven
symbols, each with the reason it is gone.

### ⚠️ Finding: the third listed deletion was NOT dead

The plan also lists "the flag-off inline move path" in
`task-store/moves.ts`. It is **not** deleted, per U2's execution note
("any behavior change found while removing a branch means the branch was
not dead").

That path is gated on `isWorkflowColumnsCompatibilityFlagEnabled`
(`store.ts:38`) — a **different** function from the always-true public
helper. It reads the raw `experimentalFeatures.workflowColumns` setting,
which nothing in production sets (`settings-schema.ts:396` — "no default
flags are emitted"; zero non-test writers; the operator's own
`~/.fusion/settings.json` has no such key). So `useWorkflow` is false
for effectively every real project: the flag-OFF inline side effects are
the **live** default move path and the flag-ON `default-workflow-hooks`
path is the dead one. The code says so itself at `moves.ts:638`.

Deleting that branch would swap every project onto an untravelled code
path — a behavior change, not a deletion.

**Carry this into Phases B and C, stated plainly so the plan's error is
not repeated:**

> **The inline move path in `moves.ts` is LIVE.
`default-workflow-hooks.ts` (the trait-hook path) is DEAD.** KTD-6
asserted the inverse. Until the convergence unit lands, **nothing may
assume trait hooks run** — a guard, sweep, or subscriber written against
`applyDefaultWorkflowMoveEffects` would never fire in production and
would still pass its tests.

Convergence is **not** attempted here. It is its own unit (Phase A2)
with a proper equivalence proof, per operator decision.

### U3's emit point is on the LIVE path — the seam is not born dead

Worth stating explicitly because it is the failure mode that would make
every later subscriber silently never fire: the `TaskTransitioned` emit
is **not** inside the `if (useWorkflow)` branch. That block closes at
`moves.ts:1212`; the emit sits at `:1214`, beside the existing
`store.emit("task:moved", …)`, on the unconditional post-commit path. It
therefore fires on **both** the live inline path and the dead hooks
path, and the convergence unit inherits the obligation to keep it firing
on whichever path survives — same events, same order, same payloads.

The graph-side emitters (`NodeEntered`, `RunSuspended`) carry the same
risk from a different direction: the bus refuses an invalid payload
*silently* by design, so an emitter regression would stop the event with
no test failure. They are asserted end-to-end through the real bus —
"did a subscriber actually receive it", not "was emit called" — because
a spy passes on a refused payload. The `moveTaskInternalImpl` emit does
**not** yet have that end-to-end assertion against a real store move;
that proof belongs to the convergence unit, which has to build the
both-paths fixture anyway.

## U3 — Post-commit event seam with a transactional outbox

**The bus is not a queue, not a transaction participant, and not a
delivery guarantee.** Durable follow-on work uses the transactional
outbox — a `workflow_work_items` row written *inside* the transition
transaction (the shape `createCompletionHandoffWorkflowWork` already
uses). "Emit after commit, let a subscriber enqueue the work" has a
crash window where a process dies between commit and subscriber, leaving
no event *and* no work-item row, so required work is skipped permanently
with nothing to recover from. Post-commit subscribers therefore carry
only losable reactions.

Emission is consequently lossy and isolated by design: a throwing or
rejecting subscriber is caught and logged, cannot roll back the
transition, and cannot stop the others. Deliveries append to one serial
chain, so two transitions on a task deliver in commit order.

The ids/outcomes-only rule is **mechanised, not documented** —
run-audit's equivalent lives only in prose and has been violated
repeatedly. A payload carrying an object body or a prose string is
refused at the emit boundary and never reaches a subscriber or log sink.
It degrades rather than throws: the emitter is post-commit, so a shape
bug must not become a lifecycle failure.

Emit points: `TaskTransitioned` from the single post-commit point in
`moveTaskInternalImpl`; `NodeEntered` and `RunSuspended` from the graph
column boundary, the latter *after* the durable continuation is
persisted so an observed suspension implies a resumable run.

`registerWorkflowEventSubscribers` (engine) is empty on purpose —
U7/U8/U10 move real reactions onto it, each with the characterization
test proving the reaction was non-authoritative first.

## Verification

- `pnpm test:gate` — green (2/10, 16/299, 1/71).
- `pnpm lint`, `pnpm build`, `tsc --noEmit` on core and engine — green.
- U1: 20 tests in `workflow-lifecycle-traits.test.ts`, including the
fully-renamed-workflow case (fails if the resolver falls back to a
literal) and a shared-cache read-count assertion.
- U2: `legacy-tombstones.test.ts` green with the extended ratchet;
`board-workflows`, `merge-trait`, `workflow-graph-executor-parity`, and
move-hook suites green with no expectation edits.
- U3: 20 bus-invariant unit tests (isolation, ordering, the allowed-key
and required-key halves of the ids-only rule, lossiness) plus 3
end-to-end emitter-delivery tests; 5 outbox tests against a **real
PostgreSQL** work-item table (crash survival, rollback, at-least-once
redelivery on lease expiry, idempotent handler → one effect,
dropped-subscriber vs. durable work). A hand-written fake of the lease
predicate would only prove the fake redelivers.

**Not verified:** the `moveTaskInternalImpl` emit is confirmed on the
unconditional post-commit path by structure and by the surrounding
tests, but is *not* yet asserted end-to-end against a real store move on
both flag settings — that is Phase A2's fixture. The engine subscriber
registry ships empty by design, so no production subscriber exercises
the bus end-to-end yet. `settings-defaults.test.ts` has one pre-existing
failure on `main` (a logger-prefix mismatch in the
`mergeIntegrationWorktree=cwd-main` warning) — confirmed present on a
clean tree, unrelated to this branch.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Workflow lifecycle columns are now derived from workflow definitions,
supporting renamed and custom workflows.
* Added post-commit lifecycle events for task transitions, node entry,
and run suspend/resume with validated payloads.
* Follow-on processing for lifecycle emissions is now more robust
(rollback-safe, at-least-once delivery, idempotent handling).
* **Bug Fixes**
* Workflow board responses, task enrichment, and promotion no longer
depend on workflow-columns feature-flag gating.
  * Subscriber failures no longer impact committed workflow transitions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-27 13:30:13 -07:00
..
2026-06-10 18:06:51 -07:00
2026-07-26 18:11:47 -07:00

Fusion

@runfusion/fusion

From rough idea to production code — automatically.

Multi-node agent orchestrator — tasks, agents, missions, git, files, and worktrees, with any model, local or cloud.

runfusion.ai → · GitHub · Docs


Fusion reel: from rough idea to production code

Install

Zero install, straight from npm:

npx runfusion.ai

Boots the dashboard. Subcommands forward through (npx runfusion.ai task list, etc). Long form: npx @runfusion/fusion dashboard.

One-line installer (macOS & Linux — auto-picks Homebrew, falls back to npm):

curl -fsSL https://runfusion.ai/install.sh | sh

Homebrew (macOS & Linux):

brew install runfusion/fusion/fusion

Fully-qualified install auto-taps and, on Homebrew 6.0+, trusts only this formula. If short-name install fails with “untrusted tap”, run brew trust --formula runfusion/fusion/fusion then brew install fusion.

npm global:

npm install -g @runfusion/fusion
fn dashboard              # or: fusion dashboard

Launch the dashboard

From a shell:

fn dashboard                 # or: fusion dashboard / npx @runfusion/fusion dashboard
fn dashboard --paused        # start with automation paused
fn dashboard --dev           # development-mode dashboard + AI engine
fn dashboard --no-engine     # web UI only, no AI engine

The dashboard gives you:

  • A live kanban board — tasks move through columns automatically as AI works on them
  • Task detail view — generated spec, step-by-step progress, reviewer verdicts, full execution log
  • Dependency-aware scheduling — declare task dependencies or let the engine infer them
  • Auto-merge — on by default; reviewed work squash-merges without you lifting a finger
  • Parallel execution — independent tasks run simultaneously in isolated git worktrees
  • Self-sustaining board — agents may spawn follow-up tasks; the board feeds itself

Your entire dev environment. On a single pane of glass.

Describe a task in plain language. A triage agent reads your project, understands context, and writes a full PROMPT.md spec — steps, file scope, acceptance criteria. Then Fusion plans, reviews, executes, and reviews again, in an isolated git worktree, with a human approval gate wherever you want one.

One board. Controlled from anywhere. Laptop, Mac mini, Linux server, cloud VM, phone — all connected.

Fusion mesh: laptop, Mac mini, Linux server, cloud VM, phone — all synced

Run an agent company

Import a team. Run it autonomously for weeks. 440+ agents across 16 companies, wired for missions, mailboxes, and inter-agent delegation.

npx companies.sh add paperclipai/companies/gstack
Fusion agent company: import a team, run it autonomously for weeks

How it works

You create a task with a rough description. A pipeline of specialized agents takes over.

Specification. A triage agent reads your codebase — file structure, existing patterns, related code — and turns your rough idea into a detailed spec. It breaks the work into discrete steps, identifies which files are in scope, writes acceptance criteria, and assigns a complexity rating that determines how aggressively the work gets reviewed.

Scheduling. Tasks declare dependencies on each other. The scheduler builds a dependency graph and starts work only when upstream tasks are done. Independent tasks run in parallel — each in its own isolated git worktree, so there are no conflicts during execution.

Execution & review. An executor agent works through the spec step by step in the worktree. At each step boundary, a separate reviewer agent, with read-only access, independently evaluates the work. The reviewer can approve (continue), request revisions (fix specific issues), or force a rethink (change the approach entirely). Review depth scales with the task's complexity rating: trivial tasks get light checks, complex tasks get thorough multi-pass review.

Merge. When execution finishes and the reviewer signs off, the task moves to In Review:

  • Direct merge (default) — automatically squash-merges the completed task branch into your current branch with a clean commit.
  • Pull request — automatically creates or links a GitHub PR, waits for reviews/checks, then merges once policy conditions are satisfied.

autoMerge controls whether Fusion performs completion automatically. If disabled, tasks stay in In Review until you finish the merge yourself. For PR-first mode, authenticate GitHub with gh auth login.

Tasks flow through: Triage → Todo → In Progress → In Review → Done.

This execution model is heavily based on Taskplane.


What makes it different

🧠 AI specification Rough idea in, detailed PROMPT.md out — steps, file scope, acceptance criteria.
🔁 Workflow gates Plan → Review → Execute → Review on every step. Block or pass automatically.
🌳 Worktree isolation Each task runs in its own branch and worktree. Parallel tasks. Zero conflicts.
⚡ Smart merge Passing every gate? Fusion squash-merges and moves on.
🛰️ Multi-node mesh Laptop, server, cloud, phone — all synced. Desktop, mobile, web.
🧩 Any model Anthropic, OpenAI, Ollama, and more.
🏢 Agent companies Import pre-built teams — 440+ agents across 16 companies.
📬 Inter-agent messaging Built-in mailbox between agents. Delegate, clarify, coordinate.
🗺️ Missions Hierarchical planning with autopilot and validation contracts.
🔓 Open source. MIT. No vendor lock-in. Run it on your own hardware.

Working from chat

Manage tasks without leaving the conversation:

"Every ten minutes, analyze the server code for logic the client hasn't implemented yet and create tasks. Tasks may spawn additional tasks, so just add enough to keep the board saturated."

"Create a Fusion task to fix the login redirect bug"

"Add a task for dark mode support, it depends on FN-003"

"What's the status of FN-042"

"Attach screenshot.png to FN-007"

"Pause FN-012 — I want to add more context first"

The Fusion extension exposes tools to create tasks, check progress, attach files, and pause or resume automation.


Standalone CLI

See STANDALONE.md for additional installation and usage options.

Optional provider: Factory AI via Droid CLI

@runfusion/fusion now ships a vendored @fusion/droid-cli extension in the published CLI bundle.

To use it:

  1. Install the droid binary and ensure it is on your PATH
  2. Authenticate with Droid CLI (droid auth login)
  3. In Fusion dashboard, go to Settings → Authentication and enable Factory AI — via Droid CLI
  4. Restart Fusion when prompted so the extension is loaded into the runtime

Once enabled, droid-cli models appear in Fusion model selection.

Maintainer note: workspace plugins in published CLI bundles

When CLI or dashboard runtime code imports workspace plugin packages (for example @fusion-plugin-examples/roadmap), those imports must stay statically analyzable and covered by packages/cli/tsup.config.ts noExternal rules so plugin runtime code is inlined into dist/bin.js.

Do not introduce dynamic or variable module specifiers for workspace plugin runtime paths in the published execution path. If a workspace plugin is needed for bundled auto-install, stage a bundled plugin entry (dist/plugins/<id>/bundled.js) rather than copying raw TypeScript source into dist/.

Full documentation

Architecture details, development setup, and contributor info live in the project README.

License

MIT — see LICENSE.