Skip to content

Migrating from a legacy install

If you’ve been running FlowForge as a per-project install (a .flowforge/ directory inside each customer repo), v1.0 moves your FlowForge state to the standalone architecture in one step — with a dry run, an automatic backup, and a rollback path if anything looks wrong.

This page is for existing FlowForge users. New users should start with the installation guide instead — there is nothing to migrate.

Before starting: you need to have completed flowforge install --global. The migration script writes to ~/.flowforge/repos/<repo-id>/ and that directory only exists after the global install. If you haven’t installed yet, start there.

The migration tool is designed around three guardrails:

  1. --dry-run is mandatory first. Every migration starts with a no-write dry run that reports what would happen.
  2. Active timer = refuse. The migration script refuses to proceed while a session timer is running on the repo. billing/time-tracking.json is the active write target during a session, and a concurrent write during the migration’s mv corrupts it unrecoverably. Stop the session first — your billing data is worth the 30 seconds.
  3. Pre-migration backup is automatic. Before any move, a tarball of .flowforge/ is written to ~/.flowforge/.legacy-archives/<repo-id>/<timestamp>.tar.gz. This archive is not auto-deleted; it stays until you explicitly clean it up (a --finalize command is planned for v1.1, 90 days post-migration; for v1.0, the archive simply persists).
Terminal window
flowforge tui
# in the cockpit: end your session, OR
/flowforge:session:pause

Or from the CLI:

Terminal window
flowforge version # confirm install
# pause/end via your usual session command

If you skip this step, the migration script catches it in pre-flight and refuses with a clear error message — you do not get to corrupt your billing data by accident.

Terminal window
cd /path/to/your/project
flowforge migrate-from-legacy --dry-run

--dry-run is mandatory before any live migration. The dry run:

  • Computes the repo ID from your remote URL.
  • Walks every entry in your .flowforge/ directory and reports its disposition: GLOBAL (already at ~/.flowforge/), PER-REPO (will move under ~/.flowforge/repos/<repo-id>/), SHIM (will become .git/hooks/ shim + .claude/ symlinks), DROP (test residue / stale backups, archived then removed), or REGEN (caches, regenerated on first use).
  • Confirms the timer-in-flight check passes.
  • Exits with code 0 and no writes.

Read the dry-run output carefully. If you see anything you don’t expect — particularly under PER-REPO entries for billing/, audit/, or developers/ — stop and investigate before proceeding.

Terminal window
flowforge migrate-from-legacy

The migration runs in this order:

  1. Pre-flight timer check. Refuses if a timer is running unless you pass --force, in which case you also get an interactive y/N prompt warning that billing data may be inconsistent. --force is intentionally hard to use. Stop your session first — your billing data is worth the 30 seconds.
  2. Idempotency guard. If the migration has already run for this repo (a .migrated-from-legacy marker exists at ~/.flowforge/repos/<repo-id>/), the script exits cleanly without re-running.
  3. Pre-migration backup. Full tarball of .flowforge/ to ~/.flowforge/.legacy-archives/<repo-id>/<timestamp>.tar.gz.
  4. Pre-create state dirs. ~/.flowforge/repos/<repo-id>/{billing,developers,sessions,logs,audit,...} are created.
  5. Move PER-REPO state. billing/, developers/<dev-id>/ (preserving the per-developer partition exactly), sessions/, audit/, and the rest of the per-repo state move under ~/.flowforge/repos/<repo-id>/. audit/ is the highest-stakes move — the script verifies byte-equivalence after the move.
  6. Drop test residue. Drift directories from older FF versions are merged into the canonical developers/<dev-id>/ partition or archived. Stale backups (e.g., RULES.md.backup-20250821-173309) are archived.
  7. Render hook shims and .claude/ symlinks at the structural floor. Five Git hook shims at <repo>/.git/hooks/; .claude/agents and .claude/commands symlinks; .claude/settings.json rendered.
  8. Mark migrated with a .migrated-from-legacy file at the new state root, then rename the legacy directory: <repo>/.flowforge/<repo>/.flowforge.legacy/. The legacy directory is preserved (renamed, not deleted) so a rollback is possible.

When the migration completes, your customer repo’s working tree contains:

  • .git/hooks/<5 shims> (new)
  • .claude/{agents, commands, settings.json} (new symlinks + rendered settings)
  • .flowforge.legacy/ (your old state, renamed; preserved for rollback)

That’s it. No .flowforge/ directory anymore. Your billing history, your audit trail, your sessions, your tasks cache — all of that is now under ~/.flowforge/repos/<repo-id>/, byte-equivalent to where it was before.

Terminal window
flowforge tui

Should land on the Repo Switcher with your repo listed. Press Enter to activate it, then check that:

  • The header shows the correct project name.
  • Your timer state is visible (idle, since you stopped your session in step 1).
  • Your session history is intact (the workers panel and logs panel scope to your repo).
  • The audit log queries work (audit/ was the highest-stakes move, and you want to confirm it’s queryable).

If anything is off, do not start a new session yet. Instead, see the rollback path below.

Terminal window
flowforge migrate-from-legacy --rollback

This reverses the migration:

  1. Removes the new state at ~/.flowforge/repos/<repo-id>/ (your billing data is preserved in the pre-migration tarball under ~/.flowforge/.legacy-archives/).
  2. Renames <repo>/.flowforge.legacy/ back to <repo>/.flowforge/.
  3. Removes the new hook shims and .claude/ shim from the customer repo.

The rollback path is byte-for-byte equivalent to the pre-migration state. If anything looks wrong after rollback, the pre-migration tarball at ~/.flowforge/.legacy-archives/<repo-id>/<timestamp>.tar.gz is your final safety net.

After rolling back, you’re on the legacy .flowforge/ install. Your session timer, billing state, and audit log are intact. If you want to try the migration again, re-run from Step 1.

The migration does not delete your old .flowforge/ directory — it renames it to .flowforge.legacy/. This is intentional:

The .legacy/ archive is NEVER deleted by migration; only by explicit --finalize.

For v1.0, .flowforge.legacy/ simply persists in your customer repo’s working tree. You can choose to delete it manually after a few weeks, once you’re confident the migration was successful — but FlowForge will not delete it for you. The --finalize cleanup command is on the v1.1 follow-up roadmap and is intended to run on a 90-day post-migration window.

If .flowforge.legacy/ is bloating your repo and you’re past the comfort window, a manual rm -rf .flowforge.legacy/ is safe — your real billing history and audit trail are at ~/.flowforge/repos/<repo-id>/, not in the legacy directory.

Yes. Three independent safety nets:

  1. The pre-migration tarball at ~/.flowforge/.legacy-archives/<repo-id>/<timestamp>.tar.gz is a complete snapshot of your .flowforge/ directory captured before any moves.
  2. The legacy directory is renamed (not deleted) to .flowforge.legacy/, preserved in your customer repo until you choose to delete it.
  3. The migrated state under ~/.flowforge/repos/<repo-id>/billing/ is the live copy. The per-developer partition shape is preserved exactly, so the aggregator continues to work without changes.

The timer-in-flight gate (step 1 above) prevents the one scenario in which billing data could be corrupted: a concurrent write from task-time.sh during the migration’s mv. Stop your session, then migrate.

The audit log at audit/ is the migration’s highest-stakes move. The script verifies byte-equivalence after moving audit/ from <repo>/.flowforge/audit/ to ~/.flowforge/repos/<repo-id>/audit/. If verification fails, the migration aborts and you can rollback.

The append-only JSONL format and the per-instance per-day rotation are unaffected by the directory move. Queries that worked before the migration work after it, against the new path.

No. The v1.0 migration is a hard cutover; soft-cutover options would have provided dual-mode runtime windows but were not accepted.

The hard cutover is the right trade because we ran this migration on FlowForge’s own repo first — the safety net was tested against real billing data before this guide was written. The --dry-run, the pre-migration tarball, the .flowforge.legacy/ rename, and the --rollback path all combine to make the cutover safe; a soft-cutover would have added 90 days of dual-mode complexity for marginal additional safety.

Migrate them one at a time. Each repo’s migration is independent — different repo IDs, different state directories under ~/.flowforge/repos/<repo-id>/. There’s no global lock, no cross-repo coupling. Stop the session on the repo you’re about to migrate, run --dry-run, run the live migration, verify, then move to the next repo.

If you have many repos and want to script the migration, you can. The dry run and live migration are both idempotent; running the migration on an already-migrated repo exits cleanly via the idempotency guard.

What if ~/.flowforge/repos/<repo-id>/ already exists from a previous attempt?

Section titled “What if ~/.flowforge/repos/<repo-id>/ already exists from a previous attempt?”

The idempotency guard at step 2 catches this. If the migration completed previously, it exits cleanly without re-running. If you need to start over (e.g., after a --rollback), the rollback removes the new state directory, so a subsequent flowforge migrate-from-legacy will run from scratch.

My CI broke after migrating — what do I do?

Section titled “My CI broke after migrating — what do I do?”

CI runners typically don’t have FlowForge installed, and the v1.0 hook shims fail-open in that case — the shim hits the [[ ! -x "$FF_BIN" ]] branch and exits 0, allowing CI’s own validators to run unimpeded.

If your CI has FF_HOOK_FAIL_CLOSED=1 set in its environment (e.g., inherited from a ~/.zshrc or ~/.bashrc on a self-hosted runner), the shims fail-closed instead. This is a known footgun — the env var is meant for one-shot per-invocation overrides, not persistent shell-rc inheritance. For persistent fail-closed configuration on machines that should fail-closed, use ~/.flowforge/config.json instead:

{
"hooks": {
"fail_closed": true
}
}

Do not set FF_HOOK_FAIL_CLOSED=1 in shell rc files.