mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-15 03:51:22 +02:00
## Thinking Path > - Paperclip manages AI agents and their work. > - Isolated workspaces must resolve a starting Git ref before an agent starts. > - A task can explicitly select a branch that the repository does not have. > - The current recovery notice calls this a missing secret and offers no branch repair. > - This pull request explains the branch failure and adds a task-scoped repair action. > - Operators can correct the branch and request a guarded retry from the task thread. ## Linked Issues or Issue Description Related: Refs #11892 added unresolved-ref recovery. Refs #15719 added bounded failure diagnostics. This change uses that recovery path and adds its operator repair UI. **What happened?** A task that selects `main` in a repository with only `master` stops before the adapter starts. The task shows a missing-secret message and a generic retry. Repeating the run cannot fix the stored branch setting. **Expected behavior** Explain that the starting ref could not be resolved. Offer a verified repository default, or let the operator enter a branch. Save only the task's starting branch and retry its recorded owner through the existing admission gates. **Steps to reproduce** 1. Create an isolated-workspace task in a repository with `master` and no `main`. 2. Set the task's base ref to `main` and assign an agent. 3. Wait for the configuration failure. 4. Use the inline branch repair and confirm the agent completes in a worktree based on `master`. ## What Changed - Replace the missing-secret diagnosis for unresolved workspace refs. Keep uncertain fetch/access failures described as uncertain. - Read the remote's advertised default branch with the existing Git credentials and a bounded timeout. Do not guess or silently change the branch. - Add an atomic branch repair to the existing recovery resolution endpoint. Check company access, board runtime permission, incident identity, current owner, task/project state, dependencies, interactions, approvals, budget, and active execution before requesting a retry. - Preserve the effective inherited workspace setup when saving the task branch override, and attach one repair notice per failed run even when the incident is reused. - Bind the shared repair card to the exact recovery notice and failed run in both task-thread renderers. Validate branch syntax and prevent duplicate submissions. - Add Storybook examples, regression tests, and a documented local acceptance fixture with a real Git repository and process adapter. ## Verification - Red/green regressions cover the repair endpoint and preservation of inherited agent setup. The regression failed before each fix and passed afterward. - Focused integration suite: 162 tests passed; task-thread/card suite: 230 passed. After review refinements, all 13 repair-route tests, 10 card tests, 77 recovery-action tests, and the repeated-failure heartbeat regression passed locally. - Real browser acceptance passed for suggested-branch and custom-entry repairs. Invalid syntax left the task blocked. Both valid repairs ran the authenticated worker in a real isolated worktree and reached Done; reload preserved the result. - Repeated failures reused the recovery incident and retained an actionable notice on the latest run. The inherited-setup journey verified the provisioned file, agent branch template, worktree parent directory, and actual Git commit before completing. [Before/after screenshots](https://github.com/paperclipai/paperclip/pull/15726#issuecomment-6088734666). - Full local typecheck and build passed; server typecheck passed again after review fixes. Token gates passed. - The monolithic local Vitest run was stopped after resource-contention timeouts and stale assertions. Targeted regressions were rerun; the full CI matrix passed on final revision `26c543ff490bec29ddc77a49ee532c7f84fb96dc`. One signoff E2E worker-cancellation failure passed on a rerun without code changes. The two opt-in Storybook workflow jobs were skipped; the Storybook journey was checked manually. - Repeat the live journey with `tests/e2e/workspace-base-ref/README.md`. Storybook examples use simulated callbacks and are not the live acceptance proof. ## Risks - Default-branch discovery can add up to 10 seconds to an unresolved-ref failure. Lookup failure leaves manual entry available. - A valid branch name can still be absent or inaccessible. The retried workspace preparation remains authoritative and can block again with new evidence. - Retry remains subject to normal admission gates. The repair changes only the task override; it does not alter project or agent defaults. - No database migration or new dependency. ## Model Used OpenAI GPT-6 through Codex, with tool execution, repository analysis, and browser testing. The exact serving variant and context-window size are not exposed in this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
123 lines
7.9 KiB
TypeScript
123 lines
7.9 KiB
TypeScript
import { useId, useRef, useState } from "react";
|
||
import { isValidExistingBranchName } from "@paperclipai/shared";
|
||
import { Check, ChevronDown, GitBranch, Loader2, TriangleAlert } from "lucide-react";
|
||
import { Button } from "./ui/button";
|
||
import { Input } from "./ui/input";
|
||
import { Label } from "./ui/label";
|
||
import { SystemNoticeMetadataSections } from "./SystemNotice";
|
||
import { cn } from "../lib/utils";
|
||
|
||
export interface WorkspaceBaseRefRecoveryNoticeProps {
|
||
requestedRef: string;
|
||
repository: string;
|
||
/** Only supply a default confirmed by the repository, never a guessed main/master. */
|
||
defaultBranch?: string | null;
|
||
agentName: string;
|
||
/** Verified provenance, when available. */
|
||
configuredBy?: string | null;
|
||
taskOverride?: boolean;
|
||
failureKind?: "missing_branch" | "unresolved_ref";
|
||
unavailableReason?: string | null;
|
||
/** Resolve only after both the task setting and retry have server receipts. */
|
||
onRepair: (branch: string) => Promise<void>;
|
||
}
|
||
|
||
/** Inline task repair, also used by the interactive Storybook preview. */
|
||
export function WorkspaceBaseRefRecoveryNotice({
|
||
requestedRef, repository, defaultBranch, agentName, configuredBy,
|
||
taskOverride = true, failureKind = "unresolved_ref", unavailableReason, onRepair,
|
||
}: WorkspaceBaseRefRecoveryNoticeProps) {
|
||
const id = useId();
|
||
const [editing, setEditing] = useState(!defaultBranch);
|
||
const [branch, setBranch] = useState(defaultBranch ?? "");
|
||
const [details, setDetails] = useState(false);
|
||
const [pending, setPending] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [repairedBranch, setRepairedBranch] = useState<string | null>(null);
|
||
const inFlight = useRef(false);
|
||
const changed = branch.trim().length > 0 && branch.trim() !== requestedRef;
|
||
|
||
async function repair(nextBranch: string) {
|
||
const value = nextBranch.trim();
|
||
if (!value || value === requestedRef || unavailableReason || inFlight.current || repairedBranch) return;
|
||
if (!isValidExistingBranchName(value)) {
|
||
setError("Enter a valid branch name, such as master or release/next.");
|
||
return;
|
||
}
|
||
inFlight.current = true;
|
||
setPending(true);
|
||
setError(null);
|
||
try {
|
||
await onRepair(value);
|
||
setRepairedBranch(value);
|
||
} catch (cause) {
|
||
setError(cause instanceof Error ? cause.message : "Refresh the task to check its current state, then try again.");
|
||
} finally {
|
||
inFlight.current = false;
|
||
setPending(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<section aria-labelledby={`${id}-title`} className="flex min-w-0 gap-3 rounded-lg border border-border bg-muted/20 p-4" data-testid="workspace-base-ref-recovery">
|
||
{repairedBranch
|
||
? <Check aria-hidden className="mt-0.5 size-4 shrink-0 text-(--status-task-icon-done)" />
|
||
: <TriangleAlert aria-hidden className="mt-0.5 size-4 shrink-0 text-(--status-task-icon-blocked)" />}
|
||
<div className="flex min-w-0 flex-1 flex-col gap-3">
|
||
<div className="flex flex-col gap-1" role="status" aria-live="polite">
|
||
<h2 id={`${id}-title`} className="break-words text-sm font-semibold">
|
||
{repairedBranch ? "Branch updated · retry requested" : failureKind === "missing_branch" ? <>Branch <code className="font-mono">{requestedRef}</code> was not found</> : <>Couldn’t use branch <code className="font-mono">{requestedRef}</code></>}
|
||
</h2>
|
||
<p className="text-sm leading-relaxed text-muted-foreground">
|
||
{repairedBranch
|
||
? <>This task now starts from <code className="font-mono text-foreground">{repairedBranch}</code>. A retry has been requested for {agentName}.</>
|
||
: <><span className="break-all text-foreground">{repository}</span>{failureKind === "missing_branch" ? <> has no branch named <code className="font-mono text-foreground">{requestedRef}</code>.</> : " could not resolve the starting branch. Check the branch name and repository access."} {agentName} hasn’t started.</>}
|
||
</p>
|
||
</div>
|
||
{!repairedBranch && (
|
||
<>
|
||
{defaultBranch && !editing && <p className="flex flex-wrap items-center gap-1.5 text-sm"><GitBranch aria-hidden className="size-4 text-muted-foreground" /><code className="font-mono">{defaultBranch}</code><span className="text-muted-foreground">is the repository’s default branch.</span></p>}
|
||
<p className="text-xs text-muted-foreground">Changes only this task’s starting branch.</p>
|
||
{unavailableReason && <p id={`${id}-unavailable`} className="text-sm text-muted-foreground">{unavailableReason}</p>}
|
||
{error && <p role="alert" id={`${id}-error`} className="text-sm text-destructive">Couldn’t confirm the repair. {error}</p>}
|
||
{editing ? (
|
||
<form className="flex flex-col gap-3" onSubmit={event => { event.preventDefault(); void repair(branch); }}>
|
||
<div className="flex flex-col gap-1.5">
|
||
<Label htmlFor={`${id}-branch`}>Starting branch</Label>
|
||
<Input id={`${id}-branch`} autoFocus value={branch} disabled={pending || Boolean(unavailableReason)} onChange={event => setBranch(event.target.value)} placeholder="Branch name" className="font-mono" aria-describedby={error ? `${id}-error` : undefined} autoComplete="off" spellCheck={false} />
|
||
</div>
|
||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||
{defaultBranch ? <Button type="button" variant="ghost" size="sm" disabled={pending} onClick={() => { setEditing(false); setError(null); }}>Cancel</Button> : <span />}
|
||
<Button type="submit" size="sm" disabled={pending || !changed || Boolean(unavailableReason)} aria-describedby={unavailableReason ? `${id}-unavailable` : undefined}>
|
||
{pending && <Loader2 aria-hidden className="size-3.5 animate-spin motion-reduce:animate-none" />}{pending ? "Saving & requesting retry…" : "Save branch & retry"}
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
) : (
|
||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||
<Button type="button" variant="ghost" size="sm" disabled={pending || Boolean(unavailableReason)} onClick={() => setEditing(true)}>Choose another branch</Button>
|
||
<Button type="button" size="sm" disabled={pending || Boolean(unavailableReason) || defaultBranch === requestedRef} aria-describedby={unavailableReason ? `${id}-unavailable` : undefined} onClick={() => void repair(defaultBranch!)}>
|
||
{pending && <Loader2 aria-hidden className="size-3.5 animate-spin motion-reduce:animate-none" />}{pending ? "Saving & requesting retry…" : `Use ${defaultBranch} & retry`}
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
<div>
|
||
<Button type="button" variant="ghost" size="xs" className="text-muted-foreground" aria-expanded={details} aria-controls={`${id}-details`} onClick={() => setDetails(value => !value)}>
|
||
{details ? "Hide details" : "Why did this happen?"}<ChevronDown aria-hidden className={cn("size-3", details && "rotate-180")} />
|
||
</Button>
|
||
{details && <div id={`${id}-details`} className="mt-2 rounded-md border border-border bg-background">
|
||
<SystemNoticeMetadataSections tone="neutral" sections={[{ rows: [
|
||
{ kind: "text", label: "Setting", value: taskOverride ? "Task-specific starting branch" : "Workspace starting branch" },
|
||
{ kind: "code", label: "Requested branch", value: requestedRef },
|
||
...(configuredBy ? [{ kind: "text" as const, label: "Set by", value: configuredBy }] : []),
|
||
{ kind: "text", label: "Why it stopped", value: taskOverride ? "An explicit task branch overrides the repository default. Paperclip does not silently switch branches." : "The configured starting branch must resolve before the agent can start. This repair overrides it for this task." },
|
||
] }]} />
|
||
</div>}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|