feat(ui): show worktree run-engine boot truth

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-25 19:30:45 +00:00
1 parent 4d677ff672
commit 26a70232d5
14 files changed
+621 -120

No files matched your search

+4
View File
@@ -822,6 +822,9 @@ export type {
ManagedSettingMetadata,
IssueGraphLivenessAutoRecoveryPreview,
IssueGraphLivenessAutoRecoveryPreviewItem,
WorktreeRunExecutionSuppressedReason,
WorktreeRunExecutionActivationState,
WorktreeRunEngineStatus,
BackupRetentionPolicy,
Agent,
AgentAccessState,
@@ -1611,6 +1614,7 @@ export {
MIN_ISSUE_GRAPH_LIVENESS_AUTO_RECOVERY_LOOKBACK_HOURS,
MAX_ISSUE_GRAPH_LIVENESS_AUTO_RECOVERY_LOOKBACK_HOURS,
PAPERCLIP_CLOUD_MANAGED_BY,
WORKTREE_SEED_QUARANTINE_ERROR_CODE,
} from "./types/instance.js";
export type {
+4
View File
@@ -100,7 +100,11 @@ export type {
BackupRetentionPolicy,
IssueGraphLivenessAutoRecoveryPreview,
IssueGraphLivenessAutoRecoveryPreviewItem,
WorktreeRunExecutionSuppressedReason,
WorktreeRunExecutionActivationState,
WorktreeRunEngineStatus,
} from "./instance.js";
export { WORKTREE_SEED_QUARANTINE_ERROR_CODE } from "./instance.js";
export type {
SmokeLabServiceStatus,
SmokeRun,
+56
View File
@@ -151,6 +151,62 @@ export interface InstanceExperimentalSettingsWithManaged extends InstanceExperim
managedKeys?: Partial<Record<ManagedExperimentalFeatureKey, ManagedSettingMetadata>>;
}
/**
* Error code stamped on heartbeat runs that were copied into a worktree seed and
* quarantined (cancelled, inert) so they never execute in the preview instance.
* Owned by the CLI worktree seed flow (cli/src/commands/worktree.ts) and surfaced
* in the UI as "inherited — inactive" runs.
*/
export const WORKTREE_SEED_QUARANTINE_ERROR_CODE = "worktree_seed_quarantine";
/**
* Why the worktree run engine is suppressed. Mirrors the server's fail-closed
* ladder in `resolveWorktreeRunExecutionActivation`.
*/
export type WorktreeRunExecutionSuppressedReason =
| "not_worktree_runtime"
| "flag_disabled"
| "missing_cutoff"
| "missing_instance_id"
| "missing_seed_epoch"
| "instance_id_mismatch"
| "settings_read_error";
/**
* Resolved worktree run-engine activation. `armed` means the scheduler executes
* runs created after `cutoff`; otherwise `reason` explains the suppression.
*/
export type WorktreeRunExecutionActivationState =
| {
armed: true;
cutoff: string;
activationInstanceId: string;
instanceNonce: string;
seedEpoch: string;
reason: null;
}
| {
armed: false;
cutoff: null;
activationInstanceId: string | null;
reason: WorktreeRunExecutionSuppressedReason;
};
/**
* Authoritative boot-truth for the worktree run engine, served to the UI so a
* preview user can tell "inherited, inert" state from live execution at a glance.
*/
export interface WorktreeRunEngineStatus {
/** True when the server runtime is a worktree preview (`PAPERCLIP_IN_WORKTREE`). */
inWorktree: boolean;
/** Activation resolved server-side (mirrors the scheduler gate). */
activation: WorktreeRunExecutionActivationState;
/** Current boot's server-managed instance identity (nonce), if stamped. */
instanceNonce: string | null;
/** Count of inherited heartbeat runs quarantined at seed time (durable evidence). */
quarantinedRunCount: number;
}
export interface InstanceSettings {
id: string;
defaultEnvironmentId: string | null;
+8
View File
@@ -242,6 +242,14 @@ export function instanceSettingsRoutes(db: Db) {
},
);
router.get("/instance/settings/experimental/worktree-run-engine", async (req, res) => {
// Boot-truth for the worktree run engine (PAP-14312). Readable by any
// authenticated org member or instance admin so the persistent banner and
// inherited-run surfaces render for everyone viewing a preview instance.
assertBoardOrgAccess(req);
res.json(await svc.getWorktreeRunEngineStatus());
});
router.post(
"/instance/settings/experimental/issue-graph-liveness-auto-recovery/preview",
validate(issueGraphLivenessAutoRecoveryRequestSchema),
+34 -25
View File
@@ -1,6 +1,6 @@
import { randomUUID } from "node:crypto";
import type { Db } from "@paperclipai/db";
import { companies, instanceSettings } from "@paperclipai/db";
import { companies, heartbeatRuns, instanceSettings } from "@paperclipai/db";
/**
* A `Db` or an open transaction handle — the subset of query builders the
@@ -17,6 +17,7 @@ import {
DEFAULT_BACKUP_RETENTION,
DEFAULT_ISSUE_GRAPH_LIVENESS_AUTO_RECOVERY_LOOKBACK_HOURS,
PAPERCLIP_CLOUD_MANAGED_BY,
WORKTREE_SEED_QUARANTINE_ERROR_CODE,
instanceGeneralSettingsSchema,
type InstanceGeneralSettings,
instanceExperimentalSettingsSchema,
@@ -28,6 +29,9 @@ import {
type InstanceSettings,
type PatchInstanceSettings,
type PatchInstanceExperimentalSettings,
type WorktreeRunExecutionSuppressedReason,
type WorktreeRunExecutionActivationState,
type WorktreeRunEngineStatus,
} from "@paperclipai/shared";
import { and, eq, sql } from "drizzle-orm";
import { getManagedInstanceConfig, type ManagedInstanceConfig } from "./managed-config.js";
@@ -44,30 +48,10 @@ interface InstanceSettingsServiceOptions {
generateSeedEpoch?: () => string;
}
type WorktreeRunExecutionSuppressedReason =
| "not_worktree_runtime"
| "flag_disabled"
| "missing_cutoff"
| "missing_instance_id"
| "missing_seed_epoch"
| "instance_id_mismatch"
| "settings_read_error";
export type WorktreeRunExecutionActivationState =
| {
armed: true;
cutoff: string;
activationInstanceId: string;
instanceNonce: string;
seedEpoch: string;
reason: null;
}
| {
armed: false;
cutoff: null;
activationInstanceId: string | null;
reason: WorktreeRunExecutionSuppressedReason;
};
export type {
WorktreeRunExecutionSuppressedReason,
WorktreeRunExecutionActivationState,
} from "@paperclipai/shared";
export function isTruthyRuntimeEnvValue(value: string | undefined) {
return typeof value === "string" && TRUTHY_RUNTIME_ENV_VALUES.has(value.trim().toLowerCase());
@@ -509,6 +493,31 @@ export function instanceSettingsService(db: Db, options: InstanceSettingsService
return toInstanceSettings(updated ?? current);
},
getWorktreeRunEngineStatus: async (): Promise<WorktreeRunEngineStatus> => {
const experimental = normalizeExperimentalSettings((await getOrCreateRow()).experimental);
const inWorktree = isTruthyRuntimeEnvValue(runtimeEnv.PAPERCLIP_IN_WORKTREE);
const activation = await resolveWorktreeRunExecutionActivationState({
getExperimental: async () => experimental,
runtimeEnv,
});
// Only quarantined-run rows survive a seed (P1 deletes the wakeups/monitors
// it clears), so this count is the durable evidence of what was neutralized.
let quarantinedRunCount = 0;
if (inWorktree) {
const [row] = await db
.select({ count: sql<number>`count(*)::int` })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.errorCode, WORKTREE_SEED_QUARANTINE_ERROR_CODE));
quarantinedRunCount = row?.count ?? 0;
}
return {
inWorktree,
activation,
instanceNonce: experimental.worktreeRunExecutionInstanceNonce,
quarantinedRunCount,
};
},
listCompanyIds: async (): Promise<string[]> =>
db
.select({ id: companies.id })
+3
View File
@@ -6,6 +6,7 @@ import type {
PatchInstanceSettings,
PatchInstanceGeneralSettings,
PatchInstanceExperimentalSettings,
WorktreeRunEngineStatus,
} from "@paperclipai/shared";
import { api } from "./client";
@@ -22,6 +23,8 @@ export const instanceSettingsApi = {
api.get<InstanceExperimentalSettingsWithManaged>("/instance/settings/experimental"),
updateExperimental: (patch: PatchInstanceExperimentalSettings) =>
api.patch<InstanceExperimentalSettingsWithManaged>("/instance/settings/experimental", patch),
getWorktreeRunEngine: () =>
api.get<WorktreeRunEngineStatus>("/instance/settings/experimental/worktree-run-engine"),
previewIssueGraphLivenessAutoRecovery: (input: { lookbackHours?: number }) =>
api.post<IssueGraphLivenessAutoRecoveryPreview>(
"/instance/settings/experimental/issue-graph-liveness-auto-recovery/preview",
+17
View File
@@ -608,4 +608,21 @@ describe("IssueRunLedger", () => {
expect(container.querySelector('[data-testid="responsible-user-denial-notice"]')).toBeNull();
});
it("marks worktree-seed quarantined runs as inherited and inactive", () => {
renderLedger({
runs: [
createRun({
runId: "run-inherited-1",
status: "cancelled",
livenessState: null,
errorCode: "worktree_seed_quarantine",
}),
],
});
expect(container.textContent).toContain("Inherited · inactive");
// The inherited badge replaces the noisy liveness classification.
expect(container.textContent).not.toContain("No liveness data");
});
});
+34 -10
View File
@@ -1,6 +1,10 @@
import { useMemo, useState, type ReactNode } from "react";
import type { ActivityEvent, Issue, Agent } from "@paperclipai/shared";
import { isResponsibleUserDenialCode, responsibleUserLabel } from "@paperclipai/shared";
import {
isResponsibleUserDenialCode,
responsibleUserLabel,
WORKTREE_SEED_QUARANTINE_ERROR_CODE,
} from "@paperclipai/shared";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Link } from "@/lib/router";
import { accessApi, type CurrentBoardAccess } from "../api/access";
@@ -292,6 +296,15 @@ function isActiveRun(run: Pick<LedgerRun, "status" | "isLive">) {
return run.isLive || ACTIVE_RUN_STATUSES.has(run.status);
}
/**
* PAP-14312: runs copied into a worktree seed are quarantined (cancelled +
* `worktree_seed_quarantine`) so they never execute in the preview. Render them
* as "inherited — inactive" instead of a scary failed/cancelled run.
*/
function isInheritedInactiveRun(run: Pick<RunForIssue, "errorCode">) {
return run.errorCode === WORKTREE_SEED_QUARANTINE_ERROR_CODE;
}
function runSummary(run: LedgerRun, agentMap: ReadonlyMap<string, Pick<Agent, "name">>) {
const agentName = compactAgentName(run, agentMap);
if (run.status === "running") return `Running now by ${agentName}`;
@@ -696,6 +709,7 @@ export function IssueRunLedgerContent({
return <div key={`activity:${item.id}`}>{renderActivityEvent?.(item.event)}</div>;
}
const run = item.run;
const inheritedInactive = isInheritedInactiveRun(run);
const liveness = livenessCopyForRun(run);
const stopReason = stopReasonLabel(run);
const duration = formatDuration(run.startedAt, run.finishedAt);
@@ -734,21 +748,31 @@ export function IssueRunLedgerContent({
<span className="rounded-md border border-border px-1.5 py-0.5 text-(length:--text-micro) capitalize text-muted-foreground">
{statusLabel(run.status)}
</span>
{inheritedInactive ? (
<span
className="rounded-md border border-slate-400/40 bg-slate-400/10 px-1.5 py-0.5 text-(length:--text-micro) font-medium text-slate-600 dark:text-slate-300"
title="Copied from another instance when this worktree was seeded. It never executed here."
>
Inherited · inactive
</span>
) : null}
{run.isLive ? (
<span className="inline-flex items-center gap-1 rounded-md border border-blue-500/30 bg-blue-500/10 px-1.5 py-0.5 text-(length:--text-micro) text-blue-700 dark:text-blue-300">
<span className="h-1.5 w-1.5 rounded-full bg-blue-500" />
live
</span>
) : null}
<span
className={cn(
"rounded-md border px-1.5 py-0.5 text-(length:--text-micro) font-medium",
liveness.tone,
)}
title={liveness.description}
>
{liveness.label}
</span>
{inheritedInactive ? null : (
<span
className={cn(
"rounded-md border px-1.5 py-0.5 text-(length:--text-micro) font-medium",
liveness.tone,
)}
title={liveness.description}
>
{liveness.label}
</span>
)}
{exhausted ? (
<span className="rounded-md border border-red-500/30 bg-red-500/10 px-1.5 py-0.5 text-(length:--text-micro) font-medium text-red-700 dark:text-red-300">
Exhausted
+2
View File
@@ -17,6 +17,7 @@ import { KeyboardShortcutsCheatsheet } from "./KeyboardShortcutsCheatsheet";
import { ToastViewport } from "./ToastViewport";
import { MobileBottomNav } from "./MobileBottomNav";
import { WorktreeBanner } from "./WorktreeBanner";
import { WorktreeRunEngineBanner } from "./WorktreeRunEngineBanner";
import { DevRestartBanner } from "./DevRestartBanner";
import { StandaloneBrowserControls } from "./StandaloneBrowserControls";
import { RouteErrorBoundary } from "./RouteErrorBoundary";
@@ -592,6 +593,7 @@ export function Layout() {
Skip to Main Content
</a>
<WorktreeBanner />
<WorktreeRunEngineBanner variant="strip" />
<DevRestartBanner devServer={health?.devServer} />
<div className={cn("min-h-0 flex-1", isMobile ? "w-full" : "flex overflow-clip")}>
{isMobile && sidebarOpen && (
@@ -0,0 +1,154 @@
// @vitest-environment jsdom
import { flushSync } from "react-dom";
import { createRoot, type Root } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { WorktreeRunEngineStatus } from "@paperclipai/shared";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { WorktreeRunEngineBanner } from "./WorktreeRunEngineBanner";
function render(status: WorktreeRunEngineStatus | undefined, variant: "strip" | "detail" = "detail") {
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
flushSync(() => {
root.render(
<QueryClientProvider client={queryClient}>
<WorktreeRunEngineBanner variant={variant} status={status} />
</QueryClientProvider>,
);
});
return { container, root };
}
function armed(overrides: Partial<WorktreeRunEngineStatus> = {}): WorktreeRunEngineStatus {
return {
inWorktree: true,
activation: {
armed: true,
cutoff: "2026-07-10T18:34:00.000Z",
activationInstanceId: "nonce-aaaaaaaa1111",
reason: null,
},
instanceNonce: "nonce-aaaaaaaa1111",
quarantinedRunCount: 0,
...overrides,
};
}
function suppressed(
reason: Extract<WorktreeRunEngineStatus["activation"], { armed: false }>["reason"],
overrides: Partial<WorktreeRunEngineStatus> = {},
): WorktreeRunEngineStatus {
return {
inWorktree: true,
activation: {
armed: false,
cutoff: null,
activationInstanceId: reason === "instance_id_mismatch" ? "nonce-oldoldold9999" : null,
reason,
},
instanceNonce: "nonce-current0000",
quarantinedRunCount: 0,
...overrides,
};
}
describe("WorktreeRunEngineBanner", () => {
let roots: Root[] = [];
beforeEach(() => {
roots = [];
});
afterEach(() => {
flushSync(() => {
roots.forEach((root) => root.unmount());
});
document.body.innerHTML = "";
});
function mount(status: WorktreeRunEngineStatus | undefined, variant: "strip" | "detail" = "detail") {
const { container, root } = render(status, variant);
roots.push(root);
return container;
}
it("renders nothing outside a worktree runtime", () => {
const container = mount({
inWorktree: false,
activation: { armed: false, cutoff: null, activationInstanceId: null, reason: "not_worktree_runtime" },
instanceNonce: null,
quarantinedRunCount: 0,
});
expect(container.textContent).toBe("");
});
it("renders nothing while the query has no data", () => {
const container = mount(undefined);
expect(container.textContent).toBe("");
});
it("shows the armed cutoff and a matching identity when armed", () => {
const container = mount(armed({ quarantinedRunCount: 3 }));
expect(container.textContent).toContain("Run engine armed since");
expect(container.textContent).toContain("matches");
expect(container.textContent).not.toContain("mismatch");
expect(container.textContent).toContain("3 inherited runs were quarantined");
});
it("reads an identity mismatch as bound to another instance", () => {
const container = mount(suppressed("instance_id_mismatch"));
expect(container.textContent).toContain("bound to another instance");
expect(container.textContent).toContain("mismatch");
// Both identities are surfaced so the mismatch is legible.
expect(container.textContent).toContain("nonce-ol");
expect(container.textContent).toContain("nonce-cu");
});
it("explains a missing activation cutoff", () => {
const container = mount(suppressed("missing_cutoff"));
expect(container.textContent).toContain("missing activation cutoff");
expect(container.textContent).toContain("Toggle it off and back on");
});
it("explains a flag-disabled run engine", () => {
const container = mount(suppressed("flag_disabled"));
expect(container.textContent).toContain("Run engine off");
});
it("renders a dense single-line strip variant", () => {
const container = mount(armed(), "strip");
const strip = container.querySelector('[data-testid="worktree-run-engine-strip"]');
expect(strip).not.toBeNull();
expect(strip?.textContent).toContain("Run engine");
expect(strip?.textContent).toContain("armed since");
});
it("surfaces quarantined counts in the strip when suppressed", () => {
const container = mount(suppressed("flag_disabled", { quarantinedRunCount: 5 }), "strip");
const strip = container.querySelector('[data-testid="worktree-run-engine-strip"]');
expect(strip?.textContent).toContain("5 inherited runs inactive");
});
// PAP-14415: the strip supplies its own bold "Run engine" subject, so the
// suppressed predicate must not restate it (no "Run engine run engine off").
it("does not stutter the run-engine subject in the suppressed strip", () => {
const container = mount(suppressed("flag_disabled", { quarantinedRunCount: 4 }), "strip");
const text = container
.querySelector('[data-testid="worktree-run-engine-strip"]')
?.textContent?.replace(/\s+/g, " ");
expect(text).toContain("Run engine off");
expect(text).not.toMatch(/run engine run engine/i);
});
it("uses a bare predicate for a mismatched-identity strip", () => {
const container = mount(suppressed("instance_id_mismatch"), "strip");
const text = container
.querySelector('[data-testid="worktree-run-engine-strip"]')
?.textContent?.replace(/\s+/g, " ");
expect(text).toContain("Run engine inactive — bound to another instance");
expect(text).not.toMatch(/run engine toggle inactive/i);
});
});
@@ -0,0 +1,237 @@
import { useQuery } from "@tanstack/react-query";
import { AlertTriangle, Fingerprint, Pause, Play } from "lucide-react";
import type {
WorktreeRunEngineStatus,
WorktreeRunExecutionActivationState,
} from "@paperclipai/shared";
import { instanceSettingsApi } from "@/api/instanceSettings";
import { queryKeys } from "../lib/queryKeys";
import { cn } from "../lib/utils";
function formatActivationTimestamp(iso: string): string {
const parsed = new Date(iso);
if (Number.isNaN(parsed.getTime())) return iso;
return parsed.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" });
}
function shortIdentity(value: string | null | undefined): string {
if (!value) return "—";
return value.length > 8 ? value.slice(0, 8) : value;
}
/**
* `headline`/`detail` drive the fuller detail banner. `stripLabel` is the bare
* predicate the dense strip appends after its own bold "Run engine" subject —
* the detail headlines each restate that subject, so reusing them there stutters
* ("Run engine run engine off"). Keeping a dedicated field stops the two copies
* from drifting.
*/
type SuppressionCopy = { headline: string; detail: string; stripLabel: string };
/**
* Human-readable copy for each fail-closed reason. `instance_id_mismatch` is
* handled separately because it interpolates the two identities.
*/
function suppressionCopy(
activation: Extract<WorktreeRunExecutionActivationState, { armed: false }>,
instanceNonce: string | null,
): SuppressionCopy {
switch (activation.reason) {
case "flag_disabled":
return {
headline: "Run engine off",
detail:
"The scheduler will not execute tasks in this worktree. Inherited tasks stay parked. Turn on “Run tasks in this worktree” to arm execution.",
stripLabel: "off",
};
case "missing_cutoff":
return {
headline: "Execution suppressed — missing activation cutoff",
detail:
"This setting has no activation cutoff, so no tasks run automatically. Toggle it off and back on to arm execution for tasks created here.",
stripLabel: "suppressed — missing cutoff",
};
case "missing_instance_id":
return {
headline: "Execution suppressed — no instance identity",
detail:
"This worktree has not stamped an instance identity yet, so execution fails closed. Toggle off and back on to arm execution.",
stripLabel: "suppressed — no identity",
};
case "instance_id_mismatch":
return {
headline: "Toggle inactive — bound to another instance",
detail: `This setting was armed against instance ${shortIdentity(
activation.activationInstanceId,
)} and copied here; this boot is ${shortIdentity(
instanceNonce,
)}. No tasks run automatically. Toggle off and back on to re-arm execution for this instance.`,
stripLabel: "inactive — bound to another instance",
};
case "settings_read_error":
return {
headline: "Execution suppressed — settings unavailable",
detail:
"The run engine could not read its settings, so it fails closed and no tasks run automatically.",
stripLabel: "suppressed — settings unavailable",
};
case "not_worktree_runtime":
default:
return {
headline: "Execution suppressed",
detail: "No tasks run automatically in this instance.",
stripLabel: "suppressed",
};
}
}
function QuarantineLine({ count }: { count: number }) {
if (count <= 0) return null;
return (
<p className="text-xs text-muted-foreground">
{count} inherited {count === 1 ? "run was" : "runs were"} quarantined at seed time and remain
inactive. Cleared wakeups and monitors are not retained.
</p>
);
}
function IdentityLine({
activation,
instanceNonce,
}: {
activation: WorktreeRunExecutionActivationState;
instanceNonce: string | null;
}) {
const matches =
activation.armed || activation.activationInstanceId === instanceNonce;
return (
<p className="flex flex-wrap items-center gap-1.5 text-xs text-muted-foreground">
<Fingerprint className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
<span>
Bound to <span className="font-mono text-foreground">{shortIdentity(activation.activationInstanceId)}</span>
</span>
<span aria-hidden="true">·</span>
<span>
this boot <span className="font-mono text-foreground">{shortIdentity(instanceNonce)}</span>
</span>
<span
className={cn(
"rounded-md border px-1.5 py-0.5 text-(length:--text-micro) font-medium",
matches
? "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300"
: "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300",
)}
>
{matches ? "matches" : "mismatch"}
</span>
</p>
);
}
/**
* PAP-14312: persistent boot-truth for the worktree run engine. Renders nothing
* outside a worktree runtime. Otherwise it reports whether the scheduler is armed
* or suppressed (and why), which instance identity the toggle is bound to, and how
* many inherited runs were quarantined at seed time.
*
* `variant="strip"` is a single dense line for the app shell; `variant="detail"`
* is the fuller block used on the Experimental Settings page.
*/
export function WorktreeRunEngineBanner({
variant = "detail",
status: statusOverride,
}: {
variant?: "strip" | "detail";
/** Injected in tests/stories to bypass the network query. */
status?: WorktreeRunEngineStatus;
}) {
const query = useQuery({
queryKey: queryKeys.instance.worktreeRunEngine,
queryFn: () => instanceSettingsApi.getWorktreeRunEngine(),
enabled: statusOverride === undefined,
});
const status = statusOverride ?? query.data;
if (!status || !status.inWorktree) return null;
const { activation, instanceNonce, quarantinedRunCount } = status;
const armed = activation.armed;
if (variant === "strip") {
return (
<div
role="status"
data-testid="worktree-run-engine-strip"
className={cn(
"flex items-center gap-2 border-b px-3 py-1.5 text-xs",
armed
? "border-emerald-500/30 bg-emerald-500/5 text-foreground"
: "border-amber-500/30 bg-amber-500/5 text-foreground",
)}
>
{armed ? (
<Play className="h-3.5 w-3.5 shrink-0 text-emerald-600" aria-hidden="true" />
) : (
<Pause className="h-3.5 w-3.5 shrink-0 text-amber-700" aria-hidden="true" />
)}
<span className="min-w-0 truncate">
<span className="font-medium">Run engine</span>{" "}
{armed ? (
<>armed since {formatActivationTimestamp(activation.cutoff)}</>
) : (
<>{suppressionCopy(activation, instanceNonce).stripLabel}</>
)}
{quarantinedRunCount > 0 ? (
<span className="text-muted-foreground">
{" · "}
{quarantinedRunCount} inherited {quarantinedRunCount === 1 ? "run" : "runs"} inactive
</span>
) : null}
</span>
</div>
);
}
if (armed) {
return (
<div
role="status"
data-testid="worktree-run-engine-banner"
className="space-y-2 rounded-md border border-emerald-500/30 bg-emerald-500/5 px-3 py-2.5 text-sm text-foreground"
>
<div className="flex items-center gap-2">
<Play className="h-4 w-4 shrink-0 text-emerald-600" aria-hidden="true" />
<span>
Run engine armed since{" "}
<span className="font-medium">{formatActivationTimestamp(activation.cutoff)}</span>. Tasks
created after this point run automatically.
</span>
</div>
<IdentityLine activation={activation} instanceNonce={instanceNonce} />
<QuarantineLine count={quarantinedRunCount} />
</div>
);
}
const copy = suppressionCopy(activation, instanceNonce);
// The identity binding is only meaningful when it's the reason execution is
// suppressed; showing a "mismatch" badge on a plainly-off engine reads as noise.
const showIdentity = activation.reason === "instance_id_mismatch";
return (
<div
role="status"
data-testid="worktree-run-engine-banner"
className="space-y-2 rounded-md border border-amber-500/30 bg-amber-500/5 px-3 py-2.5 text-sm"
>
<div className="flex items-start gap-2">
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-700" aria-hidden="true" />
<div className="space-y-0.5">
<p className="font-medium text-foreground">{copy.headline}</p>
<p className="text-muted-foreground">{copy.detail}</p>
</div>
</div>
{showIdentity ? <IdentityLine activation={activation} instanceNonce={instanceNonce} /> : null}
<QuarantineLine count={quarantinedRunCount} />
</div>
);
}
+1
View File
@@ -388,6 +388,7 @@ export const queryKeys = {
generalSettings: ["instance", "general-settings"] as const,
schedulerHeartbeats: ["instance", "scheduler-heartbeats"] as const,
experimentalSettings: ["instance", "experimental-settings"] as const,
worktreeRunEngine: ["instance", "worktree-run-engine"] as const,
},
health: ["health"] as const,
cloud: {
@@ -15,6 +15,7 @@ import { queryKeys } from "../lib/queryKeys";
const mockInstanceSettingsApi = vi.hoisted(() => ({
getExperimental: vi.fn(),
updateExperimental: vi.fn(),
getWorktreeRunEngine: vi.fn(),
previewIssueGraphLivenessAutoRecovery: vi.fn(),
runIssueGraphLivenessAutoRecovery: vi.fn(),
}));
@@ -183,6 +184,20 @@ describe("InstanceExperimentalSettings — Conference Room Chat card (PAP-11233)
currentExperimentalSettings = { ...currentExperimentalSettings, ...patch };
return { ...currentExperimentalSettings };
});
// Boot-truth banner (PAP-14312) has its own coverage in
// WorktreeRunEngineBanner.test.tsx; here it defaults to not-in-worktree so
// the page tests focus on the toggle and delegation.
mockInstanceSettingsApi.getWorktreeRunEngine.mockResolvedValue({
inWorktree: false,
activation: {
armed: false,
cutoff: null,
activationInstanceId: null,
reason: "not_worktree_runtime",
},
instanceNonce: null,
quarantinedRunCount: 0,
});
});
afterEach(() => {
@@ -429,59 +444,30 @@ describe("InstanceExperimentalSettings — Conference Room Chat card (PAP-11233)
"Only tasks created after enabling will run automatically",
);
expect(container.textContent).toContain("Toggling off and on resets the cutoff.");
// Off => no armed banner and no fail-closed hint.
expect(container.textContent).not.toContain("Running tasks created after");
expect(container.textContent).not.toContain("Execution is suppressed");
});
it("shows the armed timestamp when the flag matches the current instance", async () => {
// The armed/suppressed/identity boot-truth states now live in the
// WorktreeRunEngineBanner (PAP-14312); the settings card only delegates to it.
// Full state coverage lives in WorktreeRunEngineBanner.test.tsx.
it("renders the run-engine boot-truth banner inside the worktree card", async () => {
setWorktreeRuntimeMeta(true);
setWorktreeInstanceIdMeta("inst-current");
currentExperimentalSettings = {
...currentExperimentalSettings,
enableWorktreeRunExecution: true,
worktreeRunExecutionActivatedAt: "2026-07-10T18:34:00.000Z",
worktreeRunExecutionActivationInstanceId: "inst-current",
};
mockInstanceSettingsApi.getWorktreeRunEngine.mockResolvedValue({
inWorktree: true,
activation: {
armed: true,
cutoff: "2026-07-10T18:34:00.000Z",
activationInstanceId: "nonce-current",
reason: null,
},
instanceNonce: "nonce-current",
quarantinedRunCount: 2,
});
await renderPage();
expect(container.textContent).toContain("Running tasks created after");
expect(container.textContent).not.toContain("Execution is suppressed");
const toggle = container.querySelector<HTMLButtonElement>(WORKTREE_RUN_EXECUTION_TOGGLE_SELECTOR);
expect(toggle?.getAttribute("aria-checked")).toBe("true");
});
it("fails closed with a re-enable hint when the flag was armed in another instance", async () => {
setWorktreeRuntimeMeta(true);
setWorktreeInstanceIdMeta("inst-current");
currentExperimentalSettings = {
...currentExperimentalSettings,
enableWorktreeRunExecution: true,
worktreeRunExecutionActivatedAt: "2026-07-10T18:34:00.000Z",
worktreeRunExecutionActivationInstanceId: "inst-other",
};
await renderPage();
expect(container.textContent).toContain("Execution is suppressed");
expect(container.textContent).toContain("armed in a different instance");
expect(container.textContent).toContain("Toggle it off and back on");
expect(container.textContent).not.toContain("Running tasks created after");
});
it("fails closed with a re-enable hint when the activation cutoff is missing", async () => {
setWorktreeRuntimeMeta(true);
setWorktreeInstanceIdMeta("inst-current");
currentExperimentalSettings = {
...currentExperimentalSettings,
enableWorktreeRunExecution: true,
worktreeRunExecutionActivatedAt: null,
worktreeRunExecutionActivationInstanceId: null,
};
await renderPage();
expect(container.textContent).toContain("Execution is suppressed");
expect(container.textContent).toContain("missing its activation cutoff");
expect(container.textContent).not.toContain("Running tasks created after");
const banner = container.querySelector('[data-testid="worktree-run-engine-banner"]');
expect(banner).not.toBeNull();
expect(banner?.textContent).toContain("Run engine armed since");
expect(banner?.textContent).toContain("2 inherited runs were quarantined");
});
it("renders and patches the Built-in Agents experimental toggle", async () => {
+33 -37
View File
@@ -13,6 +13,7 @@ import { experimentalSettingKey } from "@paperclipai/shared";
import { instanceSettingsApi } from "@/api/instanceSettings";
import { useHiddenSettings } from "@/hooks/useHiddenSettings";
import { getWorktreeInstanceId, isWorktreeRuntime } from "../lib/worktree-branding";
import { WorktreeRunEngineBanner } from "@/components/WorktreeRunEngineBanner";
import { useBreadcrumbs } from "../context/BreadcrumbContext";
import { queryKeys } from "../lib/queryKeys";
import { ToggleSwitch } from "@/components/ui/toggle-switch";
@@ -39,43 +40,6 @@ function formatRecoveryState(state: string) {
return state.replace(/_/g, " ");
}
type WorktreeRunExecutionDisplayState =
| { kind: "off" }
| { kind: "armed"; activatedAt: string }
| { kind: "fail_closed"; reason: "missing_cutoff" | "missing_instance_id" | "instance_mismatch" };
/**
* Mirror of the server's `resolveWorktreeRunExecutionActivation` fail-closed
* ladder (server/src/services/instance-settings.ts) so the card never claims a
* copied/legacy row is arming execution. The derived fields are display-only —
* the PATCH the toggle sends still writes just the boolean.
*/
function resolveWorktreeRunExecutionDisplayState(
settings:
| Pick<
InstanceExperimentalSettings,
| "enableWorktreeRunExecution"
| "worktreeRunExecutionActivatedAt"
| "worktreeRunExecutionActivationInstanceId"
>
| undefined,
currentInstanceId: string | null,
): WorktreeRunExecutionDisplayState {
if (settings?.enableWorktreeRunExecution !== true) return { kind: "off" };
if (!settings.worktreeRunExecutionActivatedAt) return { kind: "fail_closed", reason: "missing_cutoff" };
if (!currentInstanceId) return { kind: "fail_closed", reason: "missing_instance_id" };
if (settings.worktreeRunExecutionActivationInstanceId !== currentInstanceId) {
return { kind: "fail_closed", reason: "instance_mismatch" };
}
return { kind: "armed", activatedAt: settings.worktreeRunExecutionActivatedAt };
}
function formatActivationTimestamp(iso: string): string {
const parsed = new Date(iso);
if (Number.isNaN(parsed.getTime())) return iso;
return parsed.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" });
}
// PAP-11233: keep Conference Room code intact, but hide the user-facing opt-in for now.
const SHOW_CONFERENCE_ROOM_EXPERIMENTAL_SETTING = false;
@@ -277,6 +241,7 @@ export function InstanceExperimentalSettings() {
queryClient.setQueryData(queryKeys.instance.experimentalSettings, updatedSettings);
await Promise.all([
queryClient.invalidateQueries({ queryKey: queryKeys.instance.experimentalSettings }),
queryClient.invalidateQueries({ queryKey: queryKeys.instance.worktreeRunEngine }),
queryClient.invalidateQueries({ queryKey: ["built-in-agents"] }),
queryClient.invalidateQueries({ queryKey: queryKeys.health }),
]);
@@ -467,6 +432,37 @@ export function InstanceExperimentalSettings() {
</div>
)}
{inWorktree ? (
<Card className="block p-5">
<div className="flex flex-col gap-4">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1.5">
<div className="flex flex-wrap items-center gap-2">
<h2 className="text-sm font-semibold">Run tasks in this worktree</h2>
{worktreeRunExecutionManaged ? <ManagedByCloudBadge /> : null}
</div>
<p className="max-w-2xl text-sm text-muted-foreground">
This is an isolated git-worktree preview instance. Turn this on to let the scheduler execute runs
here. Only tasks created after enabling will run automatically — copied/pre-existing tasks stay
parked. Toggling off and on resets the cutoff.
</p>
</div>
<ToggleSwitch
checked={enableWorktreeRunExecution}
onCheckedChange={(checked) => {
if (worktreeRunExecutionManaged) return;
toggleMutation.mutate({ enableWorktreeRunExecution: checked });
}}
disabled={toggleMutation.isPending || worktreeRunExecutionManaged}
aria-label="Toggle worktree run execution setting"
/>
</div>
<WorktreeRunEngineBanner variant="detail" />
</div>
</Card>
) : null}
<ExperimentalToggleCard
title="Apps"
description="Show the Apps navigation and allow access to app connections, gateways, and advanced app tooling."