orca/docs/source-control-push-failure...

20 KiB

Source Control Push Failure AI Recovery

Problem

Push, force-push, publish, and sync push-stage failures flow through the shared remote operation formatter in src/renderer/src/lib/source-control-remote-error.ts:94 and the editor-store toast paths in src/renderer/src/store/slices/editor.ts:3716 and src/renderer/src/store/slices/editor.ts:3821. A local pre-push hook or lint hook is not an auth, protected-branch, or transport problem, so generic remote guidance sends the user in the wrong direction.

PR #7787 proves the behavior is useful, but the prototype needs tightening before it is maintainable:

  • src/renderer/src/components/right-sidebar/SourceControl.tsx:1938 and src/renderer/src/components/right-sidebar/SourceControl.tsx:5799 repeat push-failure predicates and feed live branch/file state into a prompt that says "at failure time".
  • src/renderer/src/components/right-sidebar/use-source-control-recovery-ai.ts:63 imports push detection again instead of consuming one derived recovery model.
  • src/renderer/src/components/right-sidebar/source-control-ai-commit-failure-launch.ts:24 and src/renderer/src/components/right-sidebar/source-control-ai-push-failure-launch.ts:24 are copied launch flows with action-specific copy.
  • src/shared/source-control-commit-failure-agent-command.ts:6 and src/shared/source-control-push-failure-agent-command.ts:6 are identical command-template builders.
  • src/renderer/src/components/right-sidebar/source-control-fix-split-button.tsx:21 already contains a reusable split button, while SourceControl.tsx:6415 carries a second inline version.
  • src/renderer/src/lib/source-control-remote-error.ts:148 currently treats any isSync error as push-like for hook detection. syncBranch calls the formatter for fetch, upstream-status, pull, and push failures, so the renderer cannot prove a sync error came from the push stage without an explicit marker.
  • src/renderer/src/components/right-sidebar/SourceControl.push-failure-recovery.test.ts:2 imports prompt helpers through the large React module instead of shared prompt modules.

Goal

Add first-class recovery for pre-push hook failures without growing a parallel push-only subsystem:

  1. Toasts for push, force-push, publish, and sync push-stage failures say "blocked" for detected pre-push or lint-hook output.
  2. Source Control shows a concise Push blocked panel with Details and AI Fix only for a failure snapshot that is both push-like and hook-like.
  3. AI Fix launches the configured fixPushFailure Source Control action with a safe, provider-neutral prompt.
  4. Commit-failure recovery and push-failure recovery share launch, command-template, split-button, and dense error-panel primitives.

Non-goals

  • Do not fix server-side pre-receive hooks, hosted CI failures, or provider-side protected-branch failures from this entry point.
  • Do not bypass hooks, add --no-verify, push from the launched agent, create a PR, or assume GitHub-specific terminology.
  • Do not add app-wide persistence for this transient failure panel.
  • Do not redesign the Source Control panel or the Source Control AI settings model.
  • Do not fold unrelated launch surfaces such as checks recovery into this change unless a tiny shared helper is already needed for commit/push recovery.

Design

  1. Keep push-hook detection and prompt building in shared code.

    • src/shared/source-control-push-failure.ts owns normalization, bounded scanning, summary text, Details eligibility, prompt truncation, prompt rules, and changed-file list bounding.
    • Detection must stay conservative: explicit pre-push/prepush, hook declined to push, hook-runner output in push context, or lint output in push context. Auth, non-fast-forward, protected branch, pre-receive, submodule, and generic transport failures stay on the existing remote-error path.
    • Keep the output scan bounded at 64 KiB and the prompt failure-output section bounded. Also cap prompt file lines to a small constant with an omitted-count line; do not rely on the git-status cap, which is far too high for prompt context.
  2. Make remote-error formatting sync-stage aware.

    • Extend RemoteOperationErrorOptions with an explicit push-stage flag for sync, for example isSyncPushStage.
    • Gate push-hook classification on isPush, isForcePush, publish, or isSyncPushStage; do not use bare isSync as evidence of a push failure.
    • In syncBranch, pass the push-stage flag only inside the two inner pushRuntimeGit catch blocks. The outer fetch, upstream-status, and pull catches still pass isSync for Sync-shaped generic copy but must not render blocked hook copy.
    • Carry the same push-stage fact back to the renderer, either by tagging the thrown Error with a narrow exported marker or by wrapping it in a typed error that preserves the original message/cause. SourceControl must set syncPushStage only from that marker, not from kind === 'sync' plus hook-like stderr.
    • Keep submodule push messages before hook detection so submodule guidance remains more specific than AI recovery.
  3. Replace one-off command builders with one shared recovery command builder.

    • Add a shared builder that accepts { actionId, promptOverride, commandInputTemplate, basePrompt } and renders the existing Source Control AI command template for launch actions.
    • Keep compatibility exports for commit and push helpers if nearby tests or call sites still use those names, but make them wrappers over the generic builder.
    • Move prompt-builder tests to import from shared modules, not SourceControl.tsx.
  4. Capture and derive push recovery state once.

    • Extend SourceControlActionError with the raw error, a push-stage marker for sync failures, the branch name at failure time, a bounded status-entry snapshot at failure time, and a per-worktree sequence token.
    • runRemoteAction clears the worktree's previous error and increments its sequence before starting. A catch may write an error only if its sequence still owns that worktree, so a slow failure cannot overwrite a newer retry or success.
    • Add a focused renderer helper that accepts the captured SourceControlActionError and the current branch name, then returns either null or a model with raw/sanitized detail text, summary, details flag, kind label, and AI prompt.
    • The helper returns null unless the operation is push, force_push, publish, or sync with the sync push-stage marker and isPushHookFailure(rawError) passes.
    • If the current branch is known and differs from the captured branch, hide or clear the model instead of launching an agent with wrong branch context.
    • Pass that single model to useSourceControlAi and CommitArea; neither prop construction nor CommitArea should repeat the push-hook predicate.
  5. Use one generic recovery launcher for commit and push failures.

    • Replace the separate commit and push launch files with source-control-ai-recovery-launch.ts, or keep the old files as one-line wrappers if import stability is cheaper.
    • The generic launcher owns connection resolution, SSH/local agent discovery, saved-agent validation, agent-args validation, agent selection, terminal launch, focus, and success/failure toasts.
    • Resolve connection without collapsing local and unresolved states: read const worktreeConnectionId = getConnectionId(worktreeId), use it when it is a string or null, and only fall back to sourceRepoConnectionId when the worktree lookup returns undefined. If the fallback is also undefined, show the workspace-connection error instead of launching locally. null means proven local; a string means SSH/remote.
    • Validate saved CLI args before agent detection or terminal creation. On Windows, keep using PowerShell-safe planning through planAgentCliArgsSuffix(..., 'powershell').
    • Action-specific data is limited to action id, base prompt, empty-prompt copy, unavailable-agent copy, and success copy.
  6. Keep the recovery hook thin.

    • use-source-control-recovery-ai.ts builds the commit prompt, accepts the already-derived push recovery prompt/model, keeps independent loading flags, and calls the generic launcher.
    • It should not import push-hook detection, resolve agents, or duplicate launch plumbing.
  7. Extract and reuse the recovery UI.

    • Reuse or replace the existing src/renderer/src/components/right-sidebar/source-control-fix-split-button.tsx rather than adding another split-button component.
    • Move the dense recovery notice and Details dialog into focused renderer modules named for source-control recovery. Avoid helpers, utils, and new max-lines suppressions.
    • Reuse the same notice/dialog component for commit and push recovery with action-specific labels, summary, details, prompt, saved recipe, and launch callback.
    • Keep normal remote errors filtered out when a push recovery model is rendered, so the user does not see both Push blocked and generic remote error copy for the same failure.
  8. Retain fixPushFailure in the existing Source Control AI action model.

    • The prototype already adds fixPushFailure to src/shared/source-control-ai-actions.ts; keep it as a launch action like fixCommitFailure.
    • Verify default settings, normalizers, global settings rows, repository override rows, labels, descriptions, and variable chips all include Push failure fixes.
    • Do not create a push-specific settings store or migration. Existing normalizeSourceControlAiSettings hydrates missing action defaults.

Data Flow

  • User clicks Push, Force Push, Publish Branch, or Sync.
  • runRemoteAction clears that worktree's previous remote error, records a new sequence token, and starts the editor-store remote action.
  • Git push fails and throws.
  • pushBranch or sync's inner push-stage catch calls resolveRemoteOperationErrorMessage with push-like options. Sync fetch, pull, and upstream-status failures do not pass the push-stage flag.
  • Sync's inner push-stage catch marks the rethrown error as push-stage so runRemoteAction can preserve syncPushStage in the captured snapshot.
  • The formatter classifies hook output before auth/transport fallbacks only for push-like options and shows blocked toast copy.
  • SourceControl catches the same error, and if the sequence still owns the worktree, stores { kind, rawError, message, syncPushStage, branchName, entriesSnapshot }.
  • The push recovery helper derives one pushRecovery model from that snapshot.
  • CommitArea renders normal remote errors or the Push blocked recovery notice.
  • AI Fix builds the fixPushFailure command input and launches the selected agent in the owning local or SSH runtime.

Edge Cases

  • Auth, missing repo, protected-branch, pre-receive, non-fast-forward, and transport failures must not show the Push blocked panel.
  • Submodule push errors keep their existing specialized messages before push-hook detection.
  • Sync failures from fetch, upstream-status, or pull stages must not show Push blocked, even if stderr contains words like "lint" or "hook".
  • Create PR intent remote failures use the same runRemoteAction plumbing; if CommitArea is not visible, blocked toast copy is still required but no hidden panel work is needed.
  • A newer remote operation for the same worktree must clear stale recovery state immediately and prevent an older in-flight failure from writing after it.
  • Switching worktrees or branches must not show another worktree or branch's push failure.
  • External edits or another Orca window may make the failure stale. Do not add cross-window persistence for this feature; rely on local retry/remount/branch mismatch clearing and list this as residual risk.
  • Source Control AI hidden for a repo still shows the blocked notice and Details; it hides AI Fix.
  • SSH worktrees must detect and launch agents on the owning connection, not a local fallback.
  • Windows launch planning must continue to use PowerShell-safe agent args.
  • Very large hook output and very large changed-file sets must be bounded before prompt generation.
  • ANSI/control output must not leak into summaries or Details comparison logic. Details may show sanitized text while preserving enough line breaks for debugging.
  • Empty custom command templates must stay empty so the launcher rejects them with a clear settings error.
  • Prompt text must treat file paths, branch names, and hook output as data, not instructions.

Test Plan

  • Shared unit tests:
    • src/shared/source-control-push-failure.test.ts covers detection positives, auth/protected/pre-receive/non-fast-forward negatives, ANSI/control stripping, bounded scanning, details comparison, prompt output truncation, prompt file-list capping, and provider-neutral prompt rules.
    • Shared command-template tests cover the generic recovery builder plus commit/push wrappers, including empty templates and prompt overrides.
    • src/shared/source-control-ai-actions.test.ts covers fixPushFailure label, default template, variables, normalization, and action-list inclusion.
  • Remote formatter and store tests:
    • src/renderer/src/lib/source-control-remote-error.test.ts covers push, force-push, publish, and sync push-stage blocked copy plus sync non-push stage negatives, auth/non-fast-forward/protected/pre-receive/submodule regressions.
    • src/renderer/src/store/slices/editor.test.ts covers the pushBranch toast path and both sync push-stage and sync non-push-stage toasts.
  • Renderer model and UI tests:
    • A focused push recovery derivation test covers operation kind, sync push-stage marker, branch mismatch, stale sequence ownership, snapshot file entries, summary, details flag, prompt contents, and ordinary remote errors.
    • CommitArea tests cover the Push blocked notice, Details dialog, AI Fix visible/hidden, ordinary remote errors, no duplicate remote error when a push model exists, and unchanged commit-failure recovery rendering.
    • Settings tests cover global and repository Source Control AI action rows showing Push failure fixes.
  • Launcher tests:
    • Generic recovery launcher tests cover commit and push action ids, invalid CLI args before detection, empty template rejection, unavailable saved agent, local versus SSH agent detection, successful launch/focus, and success copy.
  • Checks:
    • Run targeted vitest files above, pnpm typecheck, pnpm lint, and pnpm check:max-lines-ratchet.

UI Quality Bar

The Source Control recovery UI must match docs/STYLEGUIDE.md: monochrome, quiet, dense, token-based, and consistent with adjacent commit-failure recovery. Use existing shadcn Button, DropdownMenu, and Dialog primitives, lucide icons, card/border/destructive/muted tokens, and the existing action recipe row patterns. The Push blocked notice should keep the summary readable at narrow right-sidebar widths, avoid nested cards, avoid new color values, show stable split-button geometry while launching, and keep Details as progressive disclosure for long hook output. Dialogs and dropdowns must remain usable in light/dark mode and under SSH latency.

Review Screenshots

  1. Source Control after a simulated pre-push hook failure: Push blocked notice with summary, AI Fix, and Details.
  2. Push failure Details dialog showing hook output and Fix with AI.
  3. AI Fix customize-launch dialog for fixPushFailure.
  4. Ordinary push auth/protected/remote failure state: regular remote error, no Push blocked notice.
  5. Sync fetch/pull-stage failure state: Sync-shaped remote error, no Push blocked notice.
  6. Source Control AI hidden for the repo: Push blocked notice and Details, no AI Fix.
  7. Source Control AI settings showing the Push failure fixes launch recipe.
  8. Adjacent commit-failure recovery notice still rendering correctly after the shared UI extraction.

Rollout

  1. Tighten shared push-failure classifier/prompt bounds and add the generic recovery command builder.
  2. Make remote formatter/store sync-stage aware and update toast tests.
  3. Add the SourceControlActionError snapshot/sequence data and one push recovery derivation helper.
  4. Replace duplicated commit/push launchers with the generic recovery launcher and thin hook wiring.
  5. Extract/reuse source-control recovery UI components, including the existing split-button module.
  6. Verify fixPushFailure settings/default/normalizer coverage and i18n labels.
  7. Update targeted tests and run typecheck, lint, and max-lines ratchet.
  8. Validate rendered Electron states with screenshots before opening the PR.

Lightweight Eng Review

  • Scope: Reduced from PR #7787's push-specific parallel path to a shared recovery layer. Keep the feature limited to local pre-push/lint-hook detection, blocked copy, an inline recovery notice, Details, and AI launch. No persistence or provider-specific recovery.
  • Architecture/data flow: Shared code owns classification, prompt text, and command-template rendering; the editor store owns stage-aware toast formatting; SourceControl owns transient worktree-scoped failure snapshots and sequence invalidation; the generic launcher owns local/SSH agent discovery and terminal launch.
  • Failure modes covered:
    • Sync stage ambiguity: only the inner push stage may classify as hook-blocked.
    • Auth/protected/pre-receive/non-fast-forward/submodule/transport errors stay out of push recovery.
    • Branch or worktree switches suppress stale recovery models.
    • Per-worktree sequence tokens prevent older in-flight failures from overwriting newer retries or successes.
    • Empty custom templates, invalid CLI args, unavailable saved agents, missing worktree context, SSH host detection, and Windows shell planning fail before terminal launch.
    • Large or hostile hook output and file lists are bounded and treated as data in prompts.
  • Test coverage required:
    • Shared classifier/prompt/command/action-model tests in src/shared.
    • Remote formatter and editor-store tests for push, force-push, publish, sync push-stage, and sync non-push-stage paths.
    • Renderer derivation and CommitArea tests for Push blocked, Details, AI Fix, hidden AI actions, stale branch/sequence, ordinary remote errors, and existing commit failure recovery.
    • Generic launcher tests for local/SSH, invalid args, saved-agent unavailable, empty template, and success launch.
    • Settings tests for global and repo Push failure fixes rows.
    • Electron validation for all required screenshots; no separate E2E is required if renderer tests cover the model and Electron validates the real UI.
  • Performance/blast radius: No polling, watchers, migrations, or startup work. Runtime cost is one bounded classifier pass over at most 64 KiB, bounded prompt output, and a bounded file snapshot only when a remote action fails. Largest blast radius is the shared recovery UI and launcher used by existing commit failures, so commit recovery needs tests and a screenshot.
  • UI quality bar: Match docs/STYLEGUIDE.md and adjacent Source Control density: token-based error state, stable split-button geometry, progressive Details disclosure, no nested cards, concise copy, light/dark compatibility, and visible disabled/loading state under SSH latency.
  • Required review screenshots:
    1. Push blocked notice after pre-push hook failure.
    2. Push failure Details dialog with Fix with AI.
    3. fixPushFailure customize-launch dialog.
    4. Ordinary push auth/protected/remote failure with no Push blocked panel.
    5. Sync non-push-stage failure with no Push blocked panel.
    6. Source Control AI hidden: Push blocked without AI Fix.
    7. Source Control AI settings showing Push failure fixes.
    8. Existing commit-failure recovery notice after shared UI extraction.
  • Residual risks: Classifier false positives remain possible for unusual local hook-runner output that mentions push context; keep tests biased toward preserving auth, protected-branch, and non-fast-forward behavior. Failure panels are renderer-local, so another Orca window or an external terminal can fix the problem while the old panel remains visible until local retry, remount, or branch/worktree change. Electron validation may need a mocked or temporary repo scenario to trigger a real pre-push hook without mutating user data.