FN-9063: document multi-repository workspace mode
Document workspace setup, operation, recovery, and lifecycle guidance for operators. - Add a dedicated multi-repository workspace guide. - Link workspace mode from onboarding, settings, task management, and project documentation. - Cover the documentation contract with focused dashboard tests. Files changed: docs/README.md | 1 + docs/getting-started.md | 2 +- docs/multi-project.md | 2 +- docs/settings-reference.md | 1 + docs/task-management.md | 4 +- docs/workspaces.md | 129 +++++++++++++++++++++ packages/dashboard/src/__tests__/workspace-documentation.test.ts | 74 ++++++++++++ 7 files changed, 209 insertions(+), 4 deletions(-) Fusion-Task-Id: FN-9063 Fusion-Task-Lineage: b79baa34-fcd7-47ed-89de-b4637160c305 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
@@ -42,6 +42,7 @@ For a full walkthrough (installation, onboarding, first task, and daily workflow
|
||||
| [Custom Non-Coding Workflows MVP Spec](./custom-workflows-mvp-spec.md) | MVP framing for user-authored non-coding workflows, lifecycle mapping, metrics, and risk checklist |
|
||||
| [Task Evaluations](./evals.md) | Eval scoring contract, evidence persistence, score categories, and evaluation pipeline |
|
||||
| [Multi-Project](./multi-project.md) | Central registry architecture, project management, isolation modes, and migration paths |
|
||||
| [Workspaces (Multi-Repository)](./workspaces.md) | Workspace setup, per-repository execution and land, recovery, revert, and archive cleanup |
|
||||
|
||||
### Configuration & Agents
|
||||
<!--
|
||||
|
||||
@@ -101,7 +101,7 @@ On first launch, Fusion opens an onboarding wizard with guided setup steps:
|
||||
1. **AI Setup** — choose a provider and authenticate (you only need one to start). Anthropic/Claude and OpenAI Codex use a pasted authorization-code OAuth flow in onboarding and Settings (sign in, then paste the final redirect URL or code back into Fusion), and Fusion warns before login so you remember to copy the browser address bar URL before the redirect tab appears to fail. After the initial Claude OAuth login, Fusion normally refreshes the OAuth credential automatically with the stored refresh token when the access token expires, so repeated manual re-login is not usually required. **Anthropic — via Claude CLI** remains available as a separate optional path. Deprecated Google Gemini CLI / Antigravity entries are hidden; Google/Gemini API key, Google Generative AI, Vertex, and Cloud Code options remain available.
|
||||
2. **GitHub (Optional)** — connect GitHub for issue import and PR workflows. When dashboard OAuth is configured, this step includes an in-flow **Connect GitHub OAuth** action. It also shows whether the Fusion host has GitHub CLI (`gh`) available: run `gh auth login` on the host when `gh` is installed but unauthenticated, or use the GitHub CLI releases/install guidance when `gh` is missing. The step also checks whether the Fusion host can run `git`; if Git is missing, onboarding shows platform install guidance before you reach clone, init, or repository registration flows. Install Git and GitHub CLI on the machine or service container running Fusion, not just on the browser/client device. See [Git downloads](https://git-scm.com/downloads) and [GitHub CLI releases](https://github.com/cli/cli/releases/latest) for macOS, Windows, and Linux options.
|
||||
3. **Project Setup** — choose how Fusion should prepare a repository:
|
||||
- **Use Existing Directory** registers a folder that is already a git repository or a workspace root with detected sub-repositories.
|
||||
- **Use Existing Directory** registers a folder that is already a git repository or a workspace root with detected sub-repositories. See [Workspaces](./workspaces.md) for multi-repository setup and operation.
|
||||
- **Initialize New Repository** registers an existing local folder and lets the server run `git init` during registration when the folder is not already a git repository.
|
||||
- **Clone Git Repository** runs `git clone` from a remote URL into an empty or absent destination directory, then registers the cloned folder. Fusion rejects blank clone URLs and populated destinations.
|
||||
- Creating a folder from the project directory picker automatically selects that new folder for registration.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[← Docs index](./README.md)
|
||||
|
||||
Fusion can coordinate multiple repositories from one installation, with shared visibility and global concurrency control.
|
||||
Fusion can coordinate multiple repositories from one installation, with shared visibility and global concurrency control. Multi-project mode means many registered projects; [workspace mode](./workspaces.md) means one registered project containing multiple Git sub-repositories.
|
||||
|
||||
The [2026-07-14 PostgreSQL runtime cutover review](./postgres-migration-review-2026-07-14.md) is the current authority for legacy-reader and deployment boundaries.
|
||||
|
||||
|
||||
@@ -500,6 +500,7 @@ Security-sensitive file-browser escape hatches are project-only. `allowAbsoluteF
|
||||
| `ignoreHiddenOverlapPaths` | `boolean` | `true` | Exclude hidden dot paths from overlap serialization by default. A hidden path is any normalized project-relative path with a segment beginning with `.`, such as `.fusion/tasks/FN-1/PROMPT.md`, `.changeset/fix.md`, `.github/workflows/ci.yml`, `.env`, or `packages/.cache/out.js`. Set to `false` to restore legacy strict counting of dot paths. Explicit `overlapIgnorePaths` entries still apply in addition to this default filter, and still apply when hidden-path filtering is disabled. |
|
||||
| `overlapIgnorePaths` | `string[]` | `[]` | Optional project-relative file or directory paths to exclude from overlap blocking (for example `docs` or `generated/openapi.json`). Entries are trimmed, deduplicated, and must not be absolute or contain `..` traversal. |
|
||||
| `allowAbsoluteFileBrowserPaths` | `boolean` | `false` | Project-scoped Settings → General toggle for the workspace file browser. When enabled, slash-prefixed paths such as `/tmp` can be listed/read/written/downloaded through workspace file-browser routes while keeping existing file-size, binary, type, null-byte, traversal, and permission checks. Windows drive-letter paths remain blocked, and task-local file routes, memory APIs, worktree-copy validation, plugin bundle paths, and other validators are unchanged. |
|
||||
| `workspaceMode` | `boolean` | `undefined` (disabled) | When enabled, treats the project root as a workspace containing multiple Git sub-repositories. Tasks run per sub-repository and no Git repository is created at the root. Disable for single-repository projects. See [Workspaces](./workspaces.md). |
|
||||
| `autoMerge` | `boolean` | `true` | Auto-finalize tasks from `in-review`. Tasks can override this per-task (including at create time in New Task modal via **Auto-merge** = Default/Enabled/Disabled); explicit overrides are tagged with `autoMergeProvenance: "user"`, while tasks left at **Default** keep following the live global setting and do not snapshot it when entering review. Legacy pre-FN-6245 in-review rows that were stamped `autoMerge: true` are marked `autoMergeProvenance: "legacy-stamp"` on startup and can be inspected/cleared with Settings → Merge → **Legacy auto-merge stamp cleanup**, `fn pr automerge-cleanup [--apply] [--json]`, or `reconcileLegacyAutoMergeStamps({ apply: true })` after operator review. For grouped branch flows, per-task `autoMerge` governs member→group-integration landing while group `autoMerge` governs group→default-branch promotion eligibility. |
|
||||
| `planApprovalMode` | `"workflow" \| "auto-approve-all" \| "require-all"` | `"auto-approve-all"` | Project-scoped override for the manual planning approval gate. Defaults to auto-approve-all (FN-7557) so new/unset projects skip the manual gate; `"workflow"` instead preserves the workflow-resolved `requirePlanApproval`; `"auto-approve-all"` moves every successfully specified task to `todo` without manual plan approval even when the selected workflow or stored workflow setting has `requirePlanApproval: true`; `"require-all"` parks every specified task at `status: "awaiting-approval"` regardless of workflow settings. Settings → Merge remains the full three-state editor; the Board Triage/intake **Auto-approve plan** switch is a binary shortcut for `"auto-approve-all"` vs `"workflow"`. This does not disable Workflow Plan Review or other non-plan safety gates. (A separate triage release-authorization gate that used to also park tasks at `status: "awaiting-approval"` was removed — see `b5b0458`, FN-7732; releases are now kept out of Fusion by agent instruction, AGENTS.md → "Releasing". The `awaitingApprovalReason: "release-authorization"` field value is retained only so legacy rows deserialize and now renders as an ordinary manual approval hold.) **FN-7569:** approving a plan under `"workflow"`/`"require-all"` manual approval records a fingerprint (hash) of the exact approved `PROMPT.md`. If the task is later re-specified (a replan, a plan-review reviewer-outage retry, or a self-healing rebound back to triage) and produces the identical plan content, the manual gate is idempotent: it skips re-parking at `status: "awaiting-approval"` and proceeds straight to `todo`, so the operator is never asked to re-approve a plan they already approved. A genuinely changed `PROMPT.md` still re-asks, and rejecting a plan (Reject Plan) clears the fingerprint so the regenerated plan is treated as new. This idempotency check lives strictly inside the manual gate, after Workflow Plan Review has already decided, and has no effect under `"auto-approve-all"` (which never reaches the manual gate). |
|
||||
| `maxAutoMergeRetries` | `number` | `3` | Project-scoped positive-integer cap for auto-merge conflict-resolution retries before Fusion parks or bounces a task for human/recovery handling. Unset, non-finite, zero, or negative values fall back to `3` to preserve historical behavior. |
|
||||
|
||||
@@ -108,7 +108,7 @@ This layer complements, rather than replaces, FN-4829 similarity detection, FN-4
|
||||
|
||||
### Workspace worktree cleanup on archive
|
||||
|
||||
Archiving a workspace (multi-repository) task now synchronously removes every recorded per-sub-repository worktree, including archives initiated by `fn_task_archive` and CLI paths that do not construct an executor. Each path is protected by a per-repository cross-process reservation until backend removal and branch cleanup finish. If one removal fails, its reservation is quarantined and the next acquisition reconciles that orphan; successful sibling repositories are still released. `archiveTask(..., { cleanup: false })` intentionally retains worktrees, and the self-healing workspace sweep remains an idempotent backstop.
|
||||
Archiving a workspace (multi-repository) task now synchronously removes every recorded per-sub-repository worktree, including archives initiated by `fn_task_archive` and CLI paths that do not construct an executor. Each path is protected by a per-repository cross-process reservation until backend removal and branch cleanup finish. If one removal fails, its reservation is quarantined and the next acquisition reconciles that orphan; successful sibling repositories are still released. `archiveTask(..., { cleanup: false })` intentionally retains worktrees, and the self-healing workspace sweep remains an idempotent backstop. See [Workspaces](./workspaces.md#archiving-and-cleanup) for the workspace operator lifecycle.
|
||||
|
||||
### Task-pinned orphan recovery
|
||||
|
||||
@@ -721,7 +721,7 @@ Recovery/backfill guidance:
|
||||
- Also accepts an optional `{ granularity?: "squash" | "per-sha" }` field (FN-7548) that selects the git-path commit granularity: `"squash"` (default, unchanged) accumulates all attributable commits into one revert commit; `"per-sha"` creates one attributed revert commit per original sha (each with its own `Fusion-Task-Id` trailer and audit line), skipping no-op shas without empty commits. A mid-batch conflict in either mode rolls back the whole batch — no partially-landed per-sha commits. This field only affects the single-repo git path and is ignored when `mode` resolves to `"ai"` or the task is a workspace task.
|
||||
- Git-path response contract (additive only): `{ mode: "git", clean, revertCommitSha?, revertCommitShas?, conflicts?, alreadyReverted?, unsupported?, needsHuman?, reason? }`. A clean revert lands a `revert(FN-xxxx): ...` commit carrying a `Fusion-Task-Id` trailer on the resolved base branch; `revertCommitShas` reports every commit created (all of them for `per-sha`, the single one for `squash`) alongside the existing `revertCommitSha`.
|
||||
- AI-undo response contract: `{ mode: "ai", createdTaskId: "FN-YYYY", alreadyOpen?: true }`. The created task is an ordinary `triage`-column board task (via the normal `store.createTask` path) that references the source task's id, mission, and landed files, and instructs undoing the source task's behavior while preserving unrelated later changes to the same files, using a `revert(FN-xxxx): ...` commit convention. It carries NO dependency on the (already done/archived) source task. A `sourceMetadata.revertOf` marker makes repeated fallback calls idempotent — while an AI-undo task for that source is still open, a further call returns the same `createdTaskId` with `alreadyOpen: true` instead of creating a duplicate; a prior undo task that itself reached `done`/`archived` does not suppress a fresh one.
|
||||
- **Workspace (multi-repo) tasks (FN-7547):** tasks with `workspaceWorktrees` populated (`isWorkspaceTask`) are revertable too — the route dispatches to a dedicated workspace path that reasons about every sub-repo's integration branch as ONE all-or-nothing unit. It resolves each sub-repo's attributable commit(s), dry-run classifies every sub-repo first, and only commits a `revert(FN-xxxx): ...` commit on EACH sub-repo when every sub-repo classifies clean/already-reverted; if any sub-repo conflicts, no sub-repo is committed and every touched sub-repo worktree is rolled back to its pre-call state. Response contract for workspace tasks: `{ mode: "git", clean, workspace: { repos: [{ repo, classification, revertCommitSha?, conflicts?, alreadyReverted? }] }, conflicts?: {repo, file, ...}[] }`. A conflicting workspace result still falls back to the AI-undo task under `"auto"` mode, same as a single-repo conflicting result.
|
||||
- **Workspace (multi-repo) tasks (FN-7547):** tasks with `workspaceWorktrees` populated (`isWorkspaceTask`) are revertable too — the route dispatches to a dedicated workspace path that reasons about every sub-repo's integration branch as ONE all-or-nothing unit. It resolves each sub-repo's attributable commit(s), dry-run classifies every sub-repo first, and only commits a `revert(FN-xxxx): ...` commit on EACH sub-repo when every sub-repo classifies clean/already-reverted; if any sub-repo conflicts, no sub-repo is committed and every touched sub-repo worktree is rolled back to its pre-call state. Response contract for workspace tasks: `{ mode: "git", clean, workspace: { repos: [{ repo, classification, revertCommitSha?, conflicts?, alreadyReverted? }] }, conflicts?: {repo, file, ...}[] }`. A conflicting workspace result still falls back to the AI-undo task under `"auto"` mode, same as a single-repo conflicting result. See [Workspaces](./workspaces.md#reverting-a-workspace-task) for the operator lifecycle.
|
||||
- **`autoMerge:false` PR-based revert (FN-7554):** for a single-repo task whose git revert classifies **clean**, `autoMerge:false` no longer dead-ends at `needsHuman`. The route prepares a dedicated `fusion/revert-<id>` branch off the resolved base branch (via the engine's `prepareRevertPrBranch`, which NEVER writes to the base branch itself), pushes it, and opens a GitHub PR through the same owner/repo resolution, `githubRateLimiter` gate, `findPrForBranch` idempotency, and `manual: true` handoff as `POST /tasks/:id/pr/create`. Response: `{ mode: "pr", clean: true, prUrl, prNumber, revertBranch, existingPr? }` — a second call while the PR is still open links the existing PR (`existingPr: true`) instead of re-pushing. GitHub unconfigured or rate-limited still degrades gracefully to `{ mode: "git", needsHuman: true, reason }`, and a conflicting/unsupported/already-reverted classification is unaffected (no PR is opened; `"auto"` mode still falls back to the AI-undo task on conflict/unsupported).
|
||||
- **`autoMerge:false` PR-based revert extended to workspace tasks (FN-7577):** a workspace task whose git revert classifies **clean across every sub-repo** also opens PRs instead of dead-ending at `needsHuman` under `autoMerge:false`. The engine's `prepareWorkspaceRevertPrBranches` mirrors the workspace all-or-nothing classify-all contract: it dry-run classifies EVERY sub-repo first, and only prepares one `fusion/revert-<id>` branch per sub-repo (never writing any sub-repo's integration branch) when every sub-repo classifies clean/already-reverted — a single conflicting sub-repo aborts the WHOLE preparation with no branch created anywhere. The route then resolves owner/repo and checks the rate limiter for EVERY sub-repo before pushing/creating any PR (so a GitHub-unconfigured or rate-limited sub-repo degrades the whole task to `needsHuman` rather than opening a partial subset), then opens one PR per sub-repo reusing FN-7554's per-sub-repo `findPrForBranch` idempotency and `manual: true` handoff. Response: `{ mode: "pr", clean: true, workspace: { repos: [{ repo, revertBranch, prUrl, prNumber, existingPr? }] } }`. Existing `{ mode: "git" | "ai" | "pr" }` shapes, the `autoMerge:true` workspace path, and FN-7554's single-repo path are unchanged.
|
||||
- **Dashboard auto-linking (FN-7555):** the AI-undo task's card shows an "Undo of FN-xxxx" chip and its detail view shows a clickable "Created to undo FN-xxxx" link back to the source task. The source task's detail view shows an "Undo task: FN-YYYY" link whenever an OPEN undo task referencing it exists in the loaded tasks (matching `TaskStore.findOpenRevertTaskForSource`'s open-only semantics — a `done`/`archived`/soft-deleted undo task is never surfaced as active). Both directions are derived client-side from `sourceMetadata.revertOf`; no new API. A dedicated Done/Archived card revert-trigger action is still a separate follow-up (see FN-7525).
|
||||
|
||||
129
docs/workspaces.md
Normal file
129
docs/workspaces.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# Workspaces (Multi-Repository Projects)
|
||||
|
||||
## Overview
|
||||
|
||||
A workspace is one Fusion project whose root is **not** a Git repository and whose direct child directories are Git repositories. Use it when one task regularly changes several repositories that must be reviewed and landed together. Use separate Fusion projects when the repositories have independent task queues, settings, or lifecycle ownership.
|
||||
|
||||
| Concern | Single-repository project | Workspace project |
|
||||
| --- | --- | --- |
|
||||
| Project root | Git repository | Browse-only non-Git parent directory |
|
||||
| Task checkout | One root worktree | One on-demand worktree per configured sub-repository |
|
||||
| Landing | One merge | Per-repository, non-atomic land loop |
|
||||
| Recovery | Single merge recovery | Per-repository landing proof and partial-land recovery |
|
||||
|
||||
## Setup and detection
|
||||
|
||||
You can register a workspace from three surfaces:
|
||||
|
||||
- In the **Setup Wizard**, choose **Use Existing Directory**. Fusion calls `POST /api/projects/detect-workspace` while you select the directory and pre-checks **Workspace mode (multi-repo)** when it finds candidates. The checkbox applies only to an existing directory.
|
||||
- The project registration API accepts `workspaceMode`. An explicit `true` requests detection; an omitted value also permits automatic detection. The detection endpoint returns `{ repos, isWorkspace }`.
|
||||
- The interactive CLI project resolver detects candidates, asks you to confirm workspace mode, initializes the store, then writes the workspace configuration.
|
||||
|
||||
`detectWorkspaceRepos` scans only direct children of the selected root. It excludes `node_modules`, `.fusion`, `.git`, and `.pi`; a child must have a `.git` marker and pass a real Git work-tree probe. Nested repositories are not discovered. When workspace configuration is present, `ensureGitRepositoryForProjectPath` intentionally skips `git init` at the root: do not create a root repository just to make workspace mode work.
|
||||
|
||||
## The workspace config file
|
||||
|
||||
Fusion records members in:
|
||||
|
||||
```text
|
||||
<workspace-root>/.fusion/workspace.json
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```json
|
||||
{
|
||||
"repos": ["api", "web"]
|
||||
}
|
||||
```
|
||||
|
||||
Each `repos` entry is relative to the workspace root and must stay inside it. Absolute paths, `..` escapes, empty values, and non-string values are rejected or filtered when `loadWorkspaceConfig` reads the file. Keep member repositories as direct children so they remain discoverable and easy to operate.
|
||||
|
||||
The configuration file makes the root a workspace at repository-initialization time. The automatic path writes the `workspaceMode` setting before `workspace.json`, preventing a partially written configuration from making the next registration incorrectly treat the root as a workspace.
|
||||
|
||||
## The workspaceMode setting
|
||||
|
||||
`workspaceMode` is a project-scoped boolean. Its default is unset, which is disabled: when enabled, the project root is treated as a workspace containing multiple Git sub-repositories; tasks run per sub-repository and Fusion does not create a root Git repository. You can set it during existing-directory registration through the Setup Wizard.
|
||||
|
||||
An explicit `workspaceMode: false` in `.fusion/config.json` prevents `ensureGitRepositoryForProjectPath` from automatically detecting and re-enabling workspace mode. The setting is not itself the member list: the workspace-config writers are registration, repository initialization, and the interactive CLI flow. If you change the setting and need to create, remove, or refresh `.fusion/workspace.json`, re-register the project or manage that file deliberately; toggling alone may not create or remove it.
|
||||
|
||||
## How a workspace task executes
|
||||
|
||||
A workspace task starts with its session current directory at the browse-only, non-Git root. Fusion does not acquire a root worktree and leaves the task's singular `worktree` field unset. Before editing a member repository, the agent calls:
|
||||
|
||||
```text
|
||||
fn_acquire_repo_worktree
|
||||
```
|
||||
|
||||
The tool accepts only a configured repository name and returns an isolated, task-specific worktree path in that sub-repository. Work only in that returned path. Each member uses its own branch and its own repository branch resolution; Fusion will not commit using a non-task branch.
|
||||
|
||||
Fusion adds acquired member paths to the task's active-worktree set, so liveness and ownership checks see the root plus every active member worktree. A live remembered worktree is reused across a resumed task or executor restart. If another task is acquiring the same member, the tool returns a temporary busy error asking the agent to retry `fn_acquire_repo_worktree` shortly; acquire a different member or retry rather than editing the original repository checkout.
|
||||
|
||||
## Review and verification
|
||||
|
||||
Fusion captures changes per acquired sub-repository, not from the non-Git root. Modified file paths are repository-prefixed, such as `api/src/server.ts`, and each member is diffed against its own base. Per-repository branch attribution, contamination, and worktree-invariant checks apply to those member worktrees. Review and verification should therefore identify the member repository alongside every changed path and command result.
|
||||
|
||||
## Merging: the per-repo land loop
|
||||
|
||||
`landWorkspaceTask` processes configured/acquired repositories in a deterministic per-repository loop. Each repository lands on **its own local integration ref**; a shared workspace integration branch is not used. This means the operation is non-atomic: an earlier repository can land before a later repository fails.
|
||||
|
||||
The CLI command `fn task merge` reports each repository as `landed`, `empty`, or `failed`, and exits non-zero for a partial land. Fusion finalizes the task to `done` only after every member has either landed or has no changes to land. A partial result remains recoverable and must be treated as an operator-visible state, not as one atomic merge.
|
||||
|
||||
## landedSha idempotency
|
||||
|
||||
After a repository's integration ref advances, Fusion persists that repository's `landedSha`. `isRepoLanded` first proves that recorded SHA is an ancestor of the repository integration ref. If the ref advanced but persistence was lost in that narrow window, `findProvenLandedCommit` can instead prove the task's `Fusion-Task-Id` trailer on the integration history.
|
||||
|
||||
On a re-run, a proven landed repository is skipped and its exact proven SHA is retained. This prevents a partial-land retry from creating a second squash commit for a repository that already landed.
|
||||
|
||||
## Partial-land recovery and self-healing
|
||||
|
||||
The non-atomic land loop has a partial-land window. The `task:reconcile-workspace-partial-land` self-healing sweep re-enqueues an eligible task so it can retry unlanded repositories while skipping proven ones. It takes no action when auto-merge is off, the user paused the task, or a live member worktree/merge owner proves work is still active.
|
||||
|
||||
If a member task branch is gone and Fusion has no recorded or otherwise proven `landedSha`, the sweep parks the task as failed with a manual-intervention-required error. Inspect the per-repository integration history and task logs, establish whether the missing work landed or must be recovered, then repair/retry the task only after the workspace is safe. Do not assume a partial land rolled back repositories that already landed.
|
||||
|
||||
Additional sweeps emit `task:reconcile-orphaned-workspace-worktree` when they remove a recorded dead member worktree and `task:reclaim-phantom-workspace-land-lease` when they reclaim a leaked member landing lease. Search run-audit records for these event IDs and `task:reconcile-workspace-partial-land` when diagnosing recovery.
|
||||
|
||||
## Reverting a workspace task
|
||||
|
||||
Workspace Git revert is all-or-nothing across member repositories: Fusion classifies every repository before committing. If every repository is clean or already reverted, the response has the shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "git",
|
||||
"clean": true,
|
||||
"workspace": { "repos": [{ "repo": "api", "classification": "clean" }] }
|
||||
}
|
||||
```
|
||||
|
||||
If one member conflicts, Fusion rolls every touched member back to its pre-call state and commits no member revert. The `granularity` field applies only to the single-repository Git path, not workspace tasks. For the complete task revert contract, see [Reverting Done/Archived tasks](./task-management.md#reverting-donearchived-tasks-git-path--ai-undo-fallback).
|
||||
|
||||
There is an important route/helper distinction when auto-merge is off. `revertWorkspaceTask` refuses the direct integration-branch path, but the task route uses `prepareWorkspaceRevertPrBranches` for a clean classification and opens one PR per repository, returning `mode: "pr"` with the member PR details. Under `auto` mode, a conflicting workspace Git revert can instead create an AI-undo task.
|
||||
|
||||
## Archiving and cleanup
|
||||
|
||||
Archiving a workspace task synchronously removes every recorded member worktree. Fusion holds a per-repository reservation through disposal and branch cleanup. A failed removal is quarantined so a later acquisition can reconcile the orphan; successful siblings are released. `archiveTask(..., { cleanup: false })` intentionally retains worktrees, while self-healing remains a backstop. For the task lifecycle details, see [Workspace worktree cleanup on archive](./task-management.md#workspace-worktree-cleanup-on-archive).
|
||||
|
||||
## Limitations and known sharp edges
|
||||
|
||||
- Landing is non-atomic. A later failure does not undo earlier local integration-ref advances; use task logs, per-repository history, and `landedSha` proof before retrying or manually recovering.
|
||||
- The dashboard task detail does not currently expose a dedicated per-repository land-status view. Use `fn task merge` output, task logs, and run audit for the repository-level state.
|
||||
- Exclusivity is per sub-repository. Two workspace tasks can work in different members concurrently, but cannot acquire or land the same member at the same time.
|
||||
- Detection is intentionally shallow. A Git repository nested below a non-repository direct child is not a workspace member until you restructure or configure a valid direct-child entry.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### A sub-repository was not detected
|
||||
|
||||
`detectWorkspaceRepos` only scans one level. Ensure the repository is a direct child, is not named `node_modules`, `.fusion`, `.git`, or `.pi`, has a `.git` marker, and succeeds as a real Git work tree. Remove or investigate a stray `.git` at the workspace root rather than initializing it: the root should remain non-Git.
|
||||
|
||||
### `fn_acquire_repo_worktree` reports busy
|
||||
|
||||
Another task is temporarily acquiring or landing that same member. Retry the tool shortly, or continue with a different configured member. Do not edit the shared repository checkout while waiting.
|
||||
|
||||
### A task is failed after partial land
|
||||
|
||||
A branch-gone member without landing proof requires manual intervention. Inspect every member's integration history and the task log; determine which work landed, restore or recreate any missing task branch as appropriate, and then retry only when repository state is consistent.
|
||||
|
||||
### Workspace mode appears to re-enable after being toggled off
|
||||
|
||||
Check `.fusion/config.json`: explicit `workspaceMode: false` is the guard that suppresses automatic detection. Also inspect `.fusion/workspace.json`; the setting and member configuration are separate artifacts. Re-register or update the configuration deliberately if the project was previously detected as a workspace.
|
||||
@@ -0,0 +1,74 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const repoRoot = path.resolve(__dirname, "../../../../");
|
||||
|
||||
function readDoc(relativePath: string): string {
|
||||
return readFileSync(path.join(repoRoot, relativePath), "utf8");
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:WorkspaceDocs 2026-08-15-04:31:
|
||||
Workspace mode shipped without an operator guide. This contract preserves the required lifecycle
|
||||
sections, entry-point links, and source names so the guide cannot silently drift or disappear.
|
||||
*/
|
||||
describe("workspace documentation contract", () => {
|
||||
it("includes the canonical guide structure and required cross-references", () => {
|
||||
const workspaceGuide = readDoc("docs/workspaces.md");
|
||||
const docsIndex = readDoc("docs/README.md");
|
||||
const settingsReference = readDoc("docs/settings-reference.md");
|
||||
const gettingStarted = readDoc("docs/getting-started.md");
|
||||
|
||||
expect(workspaceGuide).toContain("# Workspaces (Multi-Repository Projects)");
|
||||
for (const heading of [
|
||||
"## Overview",
|
||||
"## Setup and detection",
|
||||
"## The workspace config file",
|
||||
"## The workspaceMode setting",
|
||||
"## How a workspace task executes",
|
||||
"## Review and verification",
|
||||
"## Merging: the per-repo land loop",
|
||||
"## landedSha idempotency",
|
||||
"## Partial-land recovery and self-healing",
|
||||
"## Reverting a workspace task",
|
||||
"## Archiving and cleanup",
|
||||
"## Limitations and known sharp edges",
|
||||
"## Troubleshooting",
|
||||
]) {
|
||||
expect(workspaceGuide).toContain(heading);
|
||||
}
|
||||
|
||||
expect(docsIndex).toContain("](./workspaces.md)");
|
||||
expect(settingsReference).toContain("](./workspaces.md)");
|
||||
expect(gettingStarted).toContain("](./workspaces.md)");
|
||||
expect(settingsReference).toContain("workspaceMode");
|
||||
});
|
||||
|
||||
it("keeps documented workspace surfaces aligned with source", () => {
|
||||
const workspaceGuide = readDoc("docs/workspaces.md");
|
||||
const repositorySource = readDoc("packages/core/src/git/git-repository.ts");
|
||||
const agentToolsSource = readDoc("packages/engine/src/agent-tools.ts");
|
||||
const mergerSource = readDoc("packages/engine/src/merge/merger-ai.ts");
|
||||
const predicateSource = readDoc("packages/engine/src/merge/workspace-land-predicate.ts");
|
||||
const selfHealingSource = readDoc("packages/engine/src/self-healing.ts");
|
||||
const projectRoutesSource = readDoc("packages/dashboard/src/routes/register-project-routes.ts");
|
||||
const settingsScopeSource = readDoc("packages/core/src/types/settings/settings-scope.ts");
|
||||
|
||||
for (const [surface, source] of [
|
||||
["detectWorkspaceRepos", repositorySource],
|
||||
["workspace.json", repositorySource],
|
||||
["fn_acquire_repo_worktree", agentToolsSource],
|
||||
["landWorkspaceTask", mergerSource],
|
||||
["isRepoLanded", predicateSource],
|
||||
["task:reconcile-workspace-partial-land", selfHealingSource],
|
||||
["/projects/detect-workspace", projectRoutesSource],
|
||||
["workspaceMode", settingsScopeSource],
|
||||
]) {
|
||||
expect(workspaceGuide).toContain(surface);
|
||||
expect(source).toContain(surface);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user