FN-9062: surface workspace repository land status

Expose per-repository workspace landing progress and failures in Task Detail.

- Persist display-only repository land failure breadcrumbs during merge and recovery
- Render landed, pending, and failed repository statuses with partial-land detail
- Document the workspace status behavior and add coverage for UI and recovery paths

Files changed:
 .changeset/fn-9062-workspace-land-status.md        |   7 ++
 docs/dashboard-guide.md                            |   4 +
 packages/core/src/types/task/task-core.ts          |  11 ++-
 .../dashboard/app/components/TaskDetailModal.css   |  33 +++++++
 .../app/components/WorkspaceWorktreesSummary.tsx   | 106 ++++++++-------------
 .../__tests__/WorkspaceWorktreesSummary.test.tsx   |  46 ++++++++-
 .../src/__tests__/self-healing-workspace.test.ts   |  19 ++++
 .../engine/src/__tests__/workspace-merger.test.ts  |   6 ++
 packages/engine/src/merge/merger-ai.ts             |   7 +-
 .../engine/src/merge/workspace-land-failure.ts     |  21 ++++
 packages/engine/src/self-healing.ts                |  16 ++++
 11 files changed, 208 insertions(+), 68 deletions(-)

Fusion-Task-Id: FN-9062

Fusion-Task-Lineage: 8c12b9f9-f6ed-4766-a6f1-49cdf9ea8926

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-08-15 00:48:06 -07:00
parent 43889bc684
commit 7bb7d450e6
11 changed files with 208 additions and 68 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": minor
---
summary: Show per-repository landing status and failure detail in workspace task details.
category: feature
dev: Adds `landFailure`, `workspace-land-failure.ts`, and `deriveWorkspaceRepoStatus`.

View File

@@ -2452,3 +2452,7 @@ The shared Task Detail Definition view shows the persisted spec alignment, lates
Saving or removing API keys, completing OAuth login/manual-code flows, logging out, and removing credential instances invalidate that registry's generation and clear both windows. If a credential change happens while an uncancellable refresh is already running, the model list temporarily serves its retained rows rather than overlapping the refresh. Once that old refresh settles, the first following request starts a current-credential refresh with no additional cache-window wait.
The Memory view also includes a fourth **Knowledge Graph** tab. It provides capped search, node detail, edge and neighbor drill-down, bounded shortest-path navigation, and explicit artifact rebuilding for the deterministic project knowledge graph.
### Workspace per-repository land status
Task Detail shows each acquired workspace repository as **landed**, **pending**, or **failed**. Landed repositories include a short commit SHA; a partial land shows the aggregate landed count and retained task failure detail. The compact TaskCard chip deliberately remains count-only. The engine records a durable per-repository failure for both `landWorkspaceTask` failed-result branches and self-healing's unrecoverable partial-land park; busy leases, aborts, persist-after-advance recovery, and empty merges intentionally do not record one. Older partial-land rows and empty repositories remain pending, with any available failure detail shown only in the aggregate block.

View File

@@ -722,8 +722,17 @@ export interface Task {
* `revertBoundarySha` is the integration-branch commit after a completed git-mode revert
* (the revert commit, or pre-revert HEAD when already reverted). A proven landing at or behind
* it is stale and must re-land; `landedSha` remains for attribution and diff consumers.
*
* FNXC:Workspace 2026-08-15-07:05:
* `landFailure` is a display-only durable breadcrumb for dashboard per-repo status. It never
* participates in landed predicates, land control flow, retries, or park decisions, and a
* later `landedSha` supersedes it. Exactly the two `landWorkspaceTask` failed-result seams and
* self-healing's unrecoverable FORK-A park write it; busy, abort, persist-after-advance, and
* empty-merge paths deliberately do not. Legacy rows without it render pending because task
* error prose is never parsed for attribution. FN-9047/FN-9048 stale-state clearing must drop
* it alongside `landedSha`.
*/
workspaceWorktrees?: Record<string, { worktreePath: string; branch: string; baseCommitSha?: string; landedSha?: string; revertBoundarySha?: string }>;
workspaceWorktrees?: Record<string, { worktreePath: string; branch: string; baseCommitSha?: string; landedSha?: string; revertBoundarySha?: string; landFailure?: { message: string; at: string; branch?: string } }>;
steps: TaskStep[];
currentStep: number;
/**

View File

@@ -3873,6 +3873,39 @@ Read-only list/placeholder only — not the deferred rich per-repo-status compon
.workspace-worktrees-branch {
color: var(--text-muted, inherit);
}
.workspace-worktrees-status {
border-radius: var(--radius-sm);
padding: 0 var(--space-xs);
font-family: var(--font-mono, monospace);
font-weight: 600;
}
.workspace-worktrees-status--landed {
color: var(--color-success);
background: color-mix(in srgb, var(--color-success) 15%, transparent);
}
.workspace-worktrees-status--pending {
color: var(--color-warning);
background: color-mix(in srgb, var(--color-warning) 15%, transparent);
}
.workspace-worktrees-status--failed,
.workspace-worktrees-failure,
.workspace-worktrees-failure-message {
color: var(--color-error);
background: var(--status-error-bg);
}
.workspace-worktrees-failure,
.workspace-worktrees-failure-message {
padding: var(--space-xs);
border-radius: var(--radius-sm);
}
.workspace-worktrees-sha {
color: var(--color-success);
}
@media (max-width: 768px) {
.workspace-worktrees-item--wrapping {
gap: var(--space-xs);
}
}
/*
FNXC:TaskDetailTerminal 2026-07-10-00:00:

View File

@@ -2,91 +2,67 @@ import { useTranslation } from "react-i18next";
import type { Task } from "@fusion/core";
/*
FNXC:Workspace 2026-06-21-00:00:
Dashboard "doesn't look broken" floor (Phase A U3 / master U10, KTD5).
A workspace-mode task has NO singular `task.worktree`/`task.branch`; instead it carries
`task.workspaceWorktrees` — one acquired git worktree per sub-repo, keyed by repo path
relative to the workspace root. Existing display surfaces (TaskCard branch row, TaskDetail
metadata) key off the singular `task.branch`, so a workspace task would render an EMPTY
branch area — looking broken. This guard renders a static placeholder ("N repos acquired")
plus a flat read-only per-repo path/branch list so the task is observable, never crashing
and never blank.
Scope ceiling: flat read-only list / placeholder ONLY. A rich per-repo-status component
(live diff/lease/merge state per repo) is the deferred registration UI — out of scope here.
Single-repo rendering is untouched: callers only mount this when `isWorkspaceTask(task)`.
FNXC:Workspace 2026-08-15-07:05:
Task Detail lifts the former flat-list ceiling with durable landed/pending/failed repository
status. TaskCard remains count-only because its dense layout cannot safely grow a per-repo list;
legacy and empty rows stay pending because task error prose cannot attribute a repository failure.
*/
/**
* True when the task is a workspace-mode task: no singular `worktree` recorded
* and at least one acquired per-sub-repo worktree in `workspaceWorktrees`.
* Single-repo tasks (populated `worktree`, no `workspaceWorktrees`) return false,
* keeping their existing rendering byte-for-byte unchanged.
*/
export function isWorkspaceTask(task: Pick<Task, "worktree" | "workspaceWorktrees">): boolean {
if (task.worktree) return false;
const entries = task.workspaceWorktrees;
return Boolean(entries && Object.keys(entries).length > 0);
}
type WorkspaceEntry = NonNullable<Task["workspaceWorktrees"]>[string];
type WorkspaceStatus = "landed" | "pending" | "failed";
export function deriveWorkspaceRepoStatus(
entry: WorkspaceEntry,
repoRelPath: string,
mergeDetails?: Task["mergeDetails"],
): { status: WorkspaceStatus; landedSha?: string; failureMessage?: string } {
const landedSha = entry.landedSha ?? mergeDetails?.workspaceLandedShas?.[repoRelPath];
if (landedSha) return { status: "landed", landedSha };
if (entry.landFailure) return { status: "failed", failureMessage: entry.landFailure.message };
return { status: "pending" };
}
interface WorkspaceWorktreesSummaryProps {
task: Pick<Task, "worktree" | "workspaceWorktrees">;
/** Compact variant for the dense TaskCard surface (placeholder only). */
task: Pick<Task, "worktree" | "workspaceWorktrees" | "mergeDetails" | "error">;
compact?: boolean;
}
/**
* Read-only summary of a workspace task's acquired sub-repo worktrees.
*
* - `compact` (TaskCard): renders just the "N repos acquired" placeholder chip.
* - default (TaskDetail): renders the placeholder plus a flat per-repo list of
* `repo → worktreePath (branch)`.
*
* Renders nothing for non-workspace tasks; mount only behind `isWorkspaceTask`.
*/
export function WorkspaceWorktreesSummary({ task, compact = false }: WorkspaceWorktreesSummaryProps) {
const { t } = useTranslation("app");
const entries = task.workspaceWorktrees;
if (!isWorkspaceTask(task) || !entries) return null;
const repos = Object.entries(entries);
const placeholder = t("tasks.workspaceReposAcquired", "{{count}} repos acquired", { count: repos.length });
const statuses = repos.map(([repoRelPath, entry]) => ({ repoRelPath, entry, ...deriveWorkspaceRepoStatus(entry, repoRelPath, task.mergeDetails) }));
const landedCount = statuses.filter(({ status }) => status === "landed").length;
const hasStatusEvidence = statuses.some(({ status }) => status !== "pending");
const fullyLanded = landedCount === repos.length;
const placeholder = hasStatusEvidence
? t("tasks.workspaceReposLanded", "{{landed}} of {{count}} repos landed", { landed: landedCount, count: repos.length })
: t("tasks.workspaceReposAcquired", "{{count}} repos acquired", { count: repos.length });
if (compact) {
return (
<div className="card-branch-row" aria-label={t("tasks.workspaceWorktrees", "Workspace repos")}>
<span className="card-branch-chip" data-testid="workspace-worktrees-placeholder" title={placeholder}>
<span className="card-branch-label">{t("tasks.workspace", "Workspace")}</span>
<span className="card-branch-value">{placeholder}</span>
</span>
</div>
);
return <div className="card-branch-row" aria-label={t("tasks.workspaceWorktrees", "Workspace repos")}><span className="card-branch-chip" data-testid="workspace-worktrees-placeholder" title={t("tasks.workspaceReposAcquired", "{{count}} repos acquired", { count: repos.length })}><span className="card-branch-label">{t("tasks.workspace", "Workspace")}</span><span className="card-branch-value">{t("tasks.workspaceReposAcquired", "{{count}} repos acquired", { count: repos.length })}</span></span></div>;
}
return (
<div
className="workspace-worktrees-summary"
data-testid="workspace-worktrees-summary"
aria-label={t("tasks.workspaceWorktrees", "Workspace repos")}
>
<div className="workspace-worktrees-placeholder" data-testid="workspace-worktrees-placeholder">
{placeholder}
</div>
<ul className="workspace-worktrees-list">
{repos.map(([repoRelPath, info]) => (
<li key={repoRelPath} className="workspace-worktrees-item">
<span className="workspace-worktrees-repo" title={repoRelPath}>
{repoRelPath}
</span>
<span className="workspace-worktrees-path" title={info.worktreePath}>
{info.worktreePath}
</span>
<span className="workspace-worktrees-branch" title={info.branch}>
{info.branch}
</span>
</li>
))}
</ul>
</div>
);
return <div className="workspace-worktrees-summary" data-testid="workspace-worktrees-summary" aria-label={t("tasks.workspaceWorktrees", "Workspace repos")}>
<div className="workspace-worktrees-placeholder" data-testid="workspace-worktrees-placeholder">{placeholder}</div>
{!fullyLanded && task.error && <div className="workspace-worktrees-failure" data-testid="workspace-partial-land-detail">{task.error}</div>}
<ul className="workspace-worktrees-list">
{statuses.map(({ repoRelPath, entry, status, landedSha, failureMessage }) => <li key={repoRelPath} className="workspace-worktrees-item workspace-worktrees-item--wrapping">
<span className="workspace-worktrees-repo" title={repoRelPath}>{repoRelPath}</span>
<span className={`workspace-worktrees-status workspace-worktrees-status--${status}`} data-testid={`workspace-repo-status-${status}`} aria-label={`${repoRelPath}: ${status}`}>{status}</span>
{landedSha && <span className="workspace-worktrees-sha">{landedSha.slice(0, 8)}</span>}
<span className="workspace-worktrees-path" title={entry.worktreePath}>{entry.worktreePath}</span>
<span className="workspace-worktrees-branch" title={entry.branch}>{entry.branch}</span>
{failureMessage && <span className="workspace-worktrees-failure-message">{failureMessage}</span>}
</li>)}
</ul>
</div>;
}

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { WorkspaceWorktreesSummary, isWorkspaceTask } from "../WorkspaceWorktreesSummary";
import { WorkspaceWorktreesSummary, deriveWorkspaceRepoStatus, isWorkspaceTask } from "../WorkspaceWorktreesSummary";
/*
FNXC:Workspace 2026-06-21-00:00:
@@ -73,6 +73,50 @@ describe("WorkspaceWorktreesSummary", () => {
expect(screen.queryByText("/wt/repo-a")).toBeNull();
});
it("renders landed, failed, and pending statuses with the partial-land detail", () => {
render(<WorkspaceWorktreesSummary task={{ worktree: undefined, error: "Workspace partial-land failed", workspaceWorktrees: {
"repo-a": { worktreePath: "/wt/repo-a", branch: "fusion/fn-1-a", landedSha: "abcdef1234567890" },
"repo-b": { worktreePath: "/wt/repo-b", branch: "fusion/fn-1-b", landFailure: { message: "squash failed: conflict", at: "2026-08-15T12:00:00.000Z" } },
"repo-c": { worktreePath: "/wt/repo-c", branch: "fusion/fn-1-c" },
} }} />);
expect(screen.getByText("abcdef12")).toBeTruthy();
expect(screen.getAllByTestId("workspace-repo-status-landed")).toHaveLength(1);
expect(screen.getAllByTestId("workspace-repo-status-failed")).toHaveLength(1);
expect(screen.getAllByTestId("workspace-repo-status-pending")).toHaveLength(1);
expect(screen.getByText("squash failed: conflict")).toBeTruthy();
expect(screen.getByText(/1 of 3 repos landed/i)).toBeTruthy();
expect(screen.getByTestId("workspace-partial-land-detail")).toHaveTextContent("Workspace partial-land failed");
expect(screen.getByRole("list").firstElementChild).toHaveClass("workspace-worktrees-item--wrapping");
});
it("renders legacy partial land failures as pending without parsing task.error", () => {
render(<WorkspaceWorktreesSummary task={{ worktree: undefined, error: "repo-b conflict", workspaceWorktrees: {
"repo-a": { worktreePath: "/wt/repo-a", branch: "fusion/fn-1-a", landedSha: "abcdef1234567890" },
"repo-b": { worktreePath: "/wt/repo-b", branch: "fusion/fn-1-b" },
} }} />);
expect(screen.getByText("abcdef12")).toBeTruthy();
expect(screen.getAllByTestId("workspace-repo-status-pending")).toHaveLength(1);
expect(screen.queryByTestId("workspace-repo-status-failed")).toBeNull();
expect(screen.getByTestId("workspace-partial-land-detail")).toHaveTextContent("repo-b conflict");
});
it("keeps compact mode free of per-repository status details", () => {
render(<WorkspaceWorktreesSummary compact task={{ worktree: undefined, workspaceWorktrees: {
"repo-a": { worktreePath: "/wt/repo-a", branch: "fusion/fn-1-a", landedSha: "abcdef1234567890", landFailure: { message: "stale", at: "now" } },
} }} />);
expect(screen.queryByTestId(/workspace-repo-status/)).toBeNull();
expect(screen.queryByText("abcdef12")).toBeNull();
expect(screen.queryByText("stale")).toBeNull();
});
it("derives landed proof before failure, including finalize recovery proof", () => {
const failure = { message: "failed", at: "now" };
expect(deriveWorkspaceRepoStatus({ worktreePath: "/wt", branch: "x", landedSha: "landed", landFailure: failure }, "repo-a")).toMatchObject({ status: "landed", landedSha: "landed" });
expect(deriveWorkspaceRepoStatus({ worktreePath: "/wt", branch: "x", landFailure: failure }, "repo-a", { workspaceLandedShas: { "repo-a": "recovered" } })).toMatchObject({ status: "landed", landedSha: "recovered" });
expect(deriveWorkspaceRepoStatus({ worktreePath: "/wt", branch: "x", landFailure: failure }, "repo-a")).toMatchObject({ status: "failed" });
expect(deriveWorkspaceRepoStatus({ worktreePath: "/wt", branch: "x" }, "repo-a")).toMatchObject({ status: "pending" });
});
it("renders nothing for a single-repo task, leaving existing rendering unchanged", () => {
const { container } = render(<WorkspaceWorktreesSummary task={singleRepoTask} />);
expect(container.firstChild).toBeNull();

View File

@@ -440,6 +440,25 @@ describeIfGit("workspace-aware self-healing (Phase D U1)", () => {
expect(store.enqueued).not.toContain(TASK_ID);
});
it("FORK-A: persists failure breadcrumbs for every concurrently unrecoverable repo", async () => {
fx = await createWorkspaceFixture(["repo-a", "repo-b"]);
// Both branches are absent and neither repository has a landing proof.
const task = workspaceTask({
"repo-a": { worktreePath: fx.repoPath("repo-a"), branch: BRANCH, landedSha: "landed-a" },
"repo-b": { worktreePath: fx.repoPath("repo-b"), branch: BRANCH },
});
const store = createStore([task]);
const manager = makeManager(store, fx.rootDir);
const n = await manager.reconcileWorkspacePartialLands();
const entries = store.tasks.get(TASK_ID)?.workspaceWorktrees;
expect(n).toBe(1);
expect(store.tasks.get(TASK_ID)?.status).toBe("failed");
expect(entries?.["repo-a"]?.landFailure).toMatchObject({ branch: BRANCH });
expect(entries?.["repo-b"]?.landFailure).toMatchObject({ branch: BRANCH });
});
it("skips a restored fully-disposed workspace task after restore clears its map", async () => {
/*
FNXC:WorkspaceArchiveRestore 2026-08-15-05:39:

View File

@@ -275,6 +275,7 @@ describeIfGit("landWorkspaceTask — per-repo merge loop (Phase C U1)", () => {
"repo-b": { worktreePath: fx.repoPath("repo-b"), branch: BRANCH },
});
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(task);
const result = await landWorkspaceTask(store, task, fx.rootDir, {}, {
mergeAgent: squashMergeAgent(BRANCH),
reviewAgent: approveReviewAgent,
@@ -285,6 +286,11 @@ describeIfGit("landWorkspaceTask — per-repo merge loop (Phase C U1)", () => {
expect(byRepo["repo-a"].status).toBe("landed");
expect(byRepo["repo-b"].status).toBe("failed");
expect(byRepo["repo-b"].error).toMatch(/conflict/i);
expect(store.updateTask).toHaveBeenCalledWith(TASK_ID, expect.objectContaining({
workspaceWorktrees: expect.objectContaining({
"repo-b": expect.objectContaining({ landFailure: expect.objectContaining({ message: expect.stringMatching(/conflict/i), branch: BRANCH }) }),
}),
}));
// Repo A landed locally (its ref advanced).
expect(fx.git("repo-a", "git rev-parse refs/heads/main")).not.toBe(tipABefore);

View File

@@ -106,6 +106,7 @@ module so self-healing can import the predicate without re-entering the self-hea
import cycle (merger-ai-worktree imports `MIN_TEMP_WORKTREE_REAP_AGE_MS` from self-healing).
*/
import { isRepoLanded, findProvenLandedCommit, FUSION_TASK_ID_TRAILER_KEY } from "./workspace-land-predicate.js";
import { persistWorkspaceRepoLandFailure } from "./workspace-land-failure.js";
import { finalizeProvenAutoMergeTask } from "./auto-merge-finalization.js";
import { getCommitTaskOwnership, detectAlreadyLandedOnMain } from "./already-merged-detector.js";
import { resolveLegacyAiMergeRootPath } from "../worktree/worktree-paths.js";
@@ -2011,6 +2012,9 @@ export async function landWorkspaceTask(
} catch (err: unknown) {
const message = getErrorMessage(err);
await log(`AI merge (workspace): failed to resolve integration branch for sub-repo ${repoRel}: ${message}`);
// FNXC:Workspace 2026-08-15-07:05: failure breadcrumbs are UI-only best effort; unlike
// landedSha, losing one cannot cause a double squash, so it must not affect this result.
await persistWorkspaceRepoLandFailure(store, taskId, repoRel, { message, at: new Date().toISOString(), branch: entry.branch }).catch(() => undefined);
repos.push({ repo: repoRel, repoRootDir, integrationBranch: "", branch: entry.branch, status: "failed", error: message });
allLanded = false;
break;
@@ -2142,6 +2146,7 @@ export async function landWorkspaceTask(
const message = getErrorMessage(err);
await log(`AI merge (workspace): sub-repo ${repoRel} land failed: ${message}`);
await audit.git({ type: "merge:ai-no-branch", target: entry.branch, metadata: { taskId, kind: "workspace-repo-land-failed", repo: repoRel, error: message } }).catch(() => undefined);
await persistWorkspaceRepoLandFailure(store, taskId, repoRel, { message, at: new Date().toISOString(), branch: entry.branch }).catch(() => undefined);
repos.push({ repo: repoRel, repoRootDir, integrationBranch, branch: entry.branch, status: "failed", error: message });
allLanded = false;
// Stop on first failure and return a partial result. The already-landed repos'
@@ -2259,7 +2264,7 @@ async function persistRepoLandedSha(
if (!entry) return; // entry vanished — nothing to merge into
// FNXC:Workspace 2026-08-15-06:45: a new landing is strictly after its revert boundary,
// so clear that invalidation marker while retaining the fresh landedSha as normal proof.
const next = { ...current, [repoRel]: { ...entry, landedSha, revertBoundarySha: undefined } };
const next = { ...current, [repoRel]: { ...entry, landedSha, landFailure: undefined, revertBoundarySha: undefined } };
await store.updateTask(taskId, { workspaceWorktrees: next });
}

View File

@@ -0,0 +1,21 @@
import type { TaskStore } from "@fusion/core";
/**
* FNXC:Workspace 2026-08-15-07:05:
* Persist a display-only per-repository landing failure without altering merge control flow.
* A fresh read preserves concurrent updates to sibling workspace entries and callers swallow errors.
*/
export async function persistWorkspaceRepoLandFailure(
store: TaskStore,
taskId: string,
repoRel: string,
failure: { message: string; at: string; branch?: string },
): Promise<void> {
const latest = await store.getTask(taskId);
const current = latest?.workspaceWorktrees ?? {};
const entry = current[repoRel];
if (!entry) return;
await store.updateTask(taskId, {
workspaceWorktrees: { ...current, [repoRel]: { ...entry, landFailure: failure } },
});
}

View File

@@ -94,6 +94,7 @@ imported it from merger-ai while merger-ai imports `MIN_TEMP_WORKTREE_REAP_AGE_M
self-healing — a real import cycle. Importing from the predicate module breaks the cycle.
*/
import { isRepoLanded } from "./merge/workspace-land-predicate.js";
import { persistWorkspaceRepoLandFailure } from "./merge/workspace-land-failure.js";
import { getCommitTaskOwnership } from "./merge/already-merged-detector.js";
import { getTaskCompletionBlockerForStore } from "./execution/task-completion.js";
import { shouldReclaimWedgedMerge } from "./merge/merge-reclaim-policy.js";
@@ -10172,6 +10173,21 @@ const movedTask = await this.store.moveTask(task.id, completeLane);
if (unrecoverableRepos.length > 0) {
// FORK-A: at least one repo is proven branch-gone and not landed → park failed.
const error = `Workspace partial-land unrecoverable: sub-repo(s) ${unrecoverableRepos.join(", ")} have no fusion/${task.id.toLowerCase()} branch and no landedSha — manual intervention required.`;
/*
FNXC:Workspace 2026-08-15-07:17:
This is the third and final writer of the display-only failure breadcrumb. Persist each
repo sequentially because the helper fresh-reads then replaces the JSON map; concurrent
writes could otherwise lose sibling breadcrumbs. Best-effort persistence must never
influence FORK-A's pre-existing park classification or decision.
*/
for (const repoRel of unrecoverableRepos) {
const entry = workspaceWorktrees[repoRel];
await persistWorkspaceRepoLandFailure(this.store, task.id, repoRel, {
message: "Workspace branch is gone and the repository is provably not landed; manual intervention required.",
at: new Date().toISOString(),
branch: entry?.branch,
}).catch(() => undefined);
}
await this.store.updateTask(task.id, { status: "failed", error });
await this.store.logEntry(task.id, error);
await auditor.database({