Files
kandev/docs/specs/tasks/system-design/remote-contribution-tasks.md
2026-09-14 09:28:23 +01:00

32 KiB

status, system, requirements, created, updated, owners
status system requirements created updated owners
current tasks
REQ-TASKS-REMOTE-CONTRIBUTION-TASKS-001
REQ-TASKS-REMOTE-CONTRIBUTION-TASKS-002
2026-08-04 2026-09-11
product

Remote Contribution Tasks System Design

Purpose and boundaries

This design record preserves the technical source for the capability mapped to REQ-TASKS-REMOTE-CONTRIBUTION-TASKS-001 while the task system completes its migration.

Requirement mapping

Requirement Design source
REQ-TASKS-REMOTE-CONTRIBUTION-TASKS-001 Migrated legacy design detail below
REQ-TASKS-REMOTE-CONTRIBUTION-TASKS-002 Contribution resume preflight
REQ-TASKS-REMOTE-CONTRIBUTION-TASKS-003 Branch history explanations

Contribution resume preflight

This amendment is implemented in the fix package. The resume preflight decision qualifies the startup preflight policy below for existing contribution sessions. Initial contribution creation retains its current validation and preflight gates.

GitOperator.PushPreflight in internal/agentctl/server/process/git.go keeps a non-mutating probe against the validated contribution remote and exact source ref. Add an optional, preflight-specific reason to its result and the runtime agentctl client response. Preserve Success=false for a rejected push; never encode history rejection as a successful permission check.

Run the probe with porcelain output and a controlled locale. Classify only the exact destination's rejected status with the Git reasons fetch first or non-fast-forward as history_update_required. Ref mismatch, mixed rejection, malformed output, authentication, remote rejection, and unknown results remain blocking. Preserve timeout and context cancellation. A missing remote branch is not history drift and must not be recreated implicitly on resume. Check source-ref existence with a bounded, noninteractive ls-remote against the validated remote during resume. An empty successful result means missing; a failed probe keeps its own failure. This prevents Git's successful dry run for a new branch from being mistaken for a valid existing contribution.

Manager.preflightRemoteContributionPushes consumes the typed reason. Only an authorized resume of an existing contribution may continue after a history-only rejection. Carry explicit resume intent through cold launch and promotion of a workspace-only execution; an ACP token or empty repository key is insufficient evidence of intent. Every repository must qualify independently.

The default repository routing key may be empty. Resolve display identity from the validated binding or task attachment; do not display repository "" or substitute another repository. Unknown identity uses a generic safe label and omits repository-specific actions.

Do not add a second divergence warning. Existing Changes state owns provider history, ancestry classification, and version choices. Startup does not claim that a future push will succeed. Actual pushes keep all existing checks. Blocking startup failures use the task launch failure projection.

Proposed preflight timing amendment

The startup recovery fix package implements criterion 002.6. Use one two-minute context for the complete preflightRemoteContributionPushes loop. An earlier caller deadline or cancellation still ends the operation.

GitPushPreflight currently uses the ordinary client with a 60-second timeout. Give this operation a bounded transport path that honors the complete preflight budget. Reuse the existing transport, authentication, response decoding, and error handling. Do not mutate the shared HTTP client or increase unrelated Git request timeouts. Direct preflight callers without a deadline also receive the two-minute bound.

The normal readiness budget already exceeds this preflight budget. Keep readiness, ACP load, workspace refresh, and Git publication deadlines unchanged. A delayed permission rejection remains a rejection. Expiry never grants admission. Use short test budgets and controlled responses instead of CPU saturation or long sleeps.

Migrated design source

Why

Maintainers often need to help an external contributor finish a pull request or merge request. Today they must manually reconstruct the target repository, contributor fork, head branch, and existing review association before an agent can make a useful change. Kandev should accept the remote change URL at task creation, prepare the contributor's exact branch, and push commits back to that branch without making create_task_kandev materially larger.

The contributor can update or rewrite that branch after the task starts. Kandev keeps the task checkout as the user's working version. The provider remains authoritative for the published change. Kandev must preserve both versions and ask for user intent before one version replaces the other.

What

  • The existing create_task_kandev.repositories[].repository_url value accepts a canonical repository URL, GitHub pull request URL, or GitLab merge request URL. The tool adds no top-level or repository input properties; only the existing argument description changes.
  • Kandev recognizes https://github.com/<owner>/<repo>/pull/<number> and https://<configured-gitlab-host>/<project>/-/merge_requests/<iid>. Query strings, fragments, embedded credentials, malformed paths, and unsupported providers are rejected.
  • The backend resolves the change through the workspace's provider identity. The caller cannot assert the source repository, head branch, head SHA, target branch, or collaboration permission.
  • Only open changes with a live source branch are accepted. Kandev validates branch names as Git refs before invoking Git.
  • The task remains attached to the target repository. A versioned, non-secret contribution binding on that task-repository attachment identifies the existing change and its source repository and branch.
  • The prepared checkout starts at the provider-reported head SHA on the contributor's head branch. origin continues to mean the target repository. A dedicated contribution remote points at the source repository. Normal pushes target the source branch without force.
  • A same-repository change uses the target remote for both read and write. A fork change is accepted only when the provider reports that maintainers may update the source branch.
  • GitHub pull requests and GitLab merge requests are associated with the new task before agent launch, so existing review, CI, and watch surfaces treat the remote change as already existing and do not create a second pull or merge request.
  • Provider title and body remain untrusted remote content. They are not copied into the task title, description, trusted system context, or initial prompt. The agent receives only structured, server-authored contribution identity and branch guidance.
  • Ordinary repository URLs retain their current behavior, including default branch resolution, normal origin pushes, and new-PR creation.
  • Git status keeps two separate divergence concepts: ahead/behind compare the checkout with the target base branch, while remote_ahead/remote_behind compare it with its configured upstream. Push and pull affordances use the upstream-relative values; review scope and base-branch rebase affordances continue to use the base-relative values.
  • For an associated contribution, Kandev compares the local HEAD and upstream snapshot with the provider's current commit history using commit identity and ancestry evidence. It never infers equivalence from commit message, author, timestamp, file statistics, or patch similarity.
  • The Changes panel uses provider evidence only from a pull request whose repository and normalized head branch match the live checkout. Historical pull requests remain available in Review, but they do not affect Changes after the checkout moves to another branch.
  • Associating a pull request with an ordinary task also reconciles the comparison base when the task is attached to the provider-reported head repository but the pull request targets another repository. Kandev stores that repository-qualified target on the exact matching task attachment, materializes a comparison-only remote, and keeps origin and push routing unchanged.
  • A provider sync may update that target branch only for the same associated change. Another pull request, a repository-only match, or a historical sibling branch cannot replace it. Detaching the source association clears only its own target; merge and close retain the base comparison.
  • Provider commit history is optional enrichment for the Changes panel. Kandev shares identical provider reads across Changes consumers and retries one failed read. If the retry fails, the panel keeps the checkout history and does not show a provider-history warning.
  • Provider commit state separates the stable contribution identity from the provider sync version. During a newer read for the same workspace, repository, and pull request, Changes can retain the last successful commit list for display provenance. This rule also applies after a failed read. The current provider head, completeness, loading state, and error remain version-specific. Thus, retained display commits cannot prove alignment, divergence, or permission for a remote mutation. A successful current response replaces the retained display list. A different workspace, repository, or pull request never inherits it.
  • Kandev reconciles provider and checkout commits by SHA. Shared commits keep the normal commit marker. Provider-only commits use the current-PR color and label. Checkout-only commits in a confirmed divergence use a separate local-checkout color and label. The accessible label carries the same meaning as the color.
  • Provider-only commits appear newest first with the rest of the commit history. A complete provider history that contains local HEAD proves a provider-ahead relation even when no upstream is configured. Kandev does not offer Pull until the checkout has a configured upstream.
  • When the provider history no longer contains local HEAD, the Changes panel keeps the task version as the primary version. A yellow warning icon in the Changes toolbar opens the available version actions; the panel body does not repeat the warning. The current provider history remains collapsed behind a PR # version disclosure. The warning title is Task and PR histories differ.
  • A diverged task keeps local edit, commit, amend, reset, and review actions available. Generic Pull is unavailable because it requires a merge strategy. Push becomes an explicit Publish task version... action instead of a disabled control.
  • Publish task version... requires a direct user action and an explicit destructive confirmation. The confirmation identifies the selected repository and the current provider version. Kandev replaces the remote branch only when its head still equals the confirmed provider head.
  • The managed replacement action uses an exact force-with-lease condition. If the provider head moves after confirmation, Kandev changes neither version and asks the user to review the new state.
  • The user can select Restore published PR version... instead. Kandev requires a clean working tree, creates a local recovery branch at the current task HEAD, fetches the confirmed provider head, and resets the task branch to that head. The result shows the recovery branch name.
  • Kandev exposes no replacement action through agent MCP tools or automatic Git operations. Generic contribution force-push requests remain rejected. Direct terminal commands remain outside this UI approval boundary.
  • After equal heads are classified as aligned, a complete provider-ahead history that still contains local HEAD is a safe fast-forward case: the UI may offer Pull, but must not label the existing local commits as unpushed work. A local-ahead history whose tracked upstream equals the provider head may offer a normal Push for exactly remote_ahead commits.

Decisions: ADR-2026-08-04-remote-contribution-bindings, ADR-2026-08-10-remote-contribution-head-drift, ADR-2026-08-12-local-first-contribution-replacement, and ADR-2026-08-13-provider-history-changes-enrichment.

Data model

The target task_repositories.metadata JSON object may contain remote_contribution:

{
  "version": 1,
  "provider": "github",
  "kind": "pull_request",
  "canonical_url": "https://github.com/acme/widget/pull/123",
  "number": 123,
  "state": "open",
  "base_branch": "main",
  "head_branch": "fix/widget",
  "head_sha": "0123456789abcdef",
  "source_repository": {
    "host": "github.com",
    "path": "contributor/widget",
    "provider_id": "optional-provider-repository-id",
    "remote_url": "https://github.com/contributor/widget.git"
  },
  "collaboration_allowed": true
}

provider is github or gitlab; kind is pull_request or merge_request; and number is the GitHub PR number or GitLab project-scoped MR IID. The target repository is the attachment's existing repository_id, not another copy inside the binding. source_repository.remote_url is canonical and credential-free. The binding never stores access tokens, credential-helper state, lease IDs, provider title/body, or other user-authored remote text.

The JSON field is versioned so later providers or collaboration attributes can be added without a database migration. Unknown versions fail closed during materialization and credential authorization.

An ordinary repository task MAY also gain metadata["comparison_target"] after a matching pull request is associated:

{
  "version": 1,
  "provider": "github",
  "kind": "pull_request",
  "number": 1154,
  "head_branch": "fix/cursor-cost",
  "target_branch": "main",
  "head_repository": {
    "host": "github.com",
    "path": "contributor/widget",
    "provider_id": "provider-head-id",
    "remote_url": "https://github.com/contributor/widget.git"
  },
  "target_repository": {
    "host": "github.com",
    "path": "upstream/widget",
    "provider_id": "provider-target-id",
    "remote_url": "https://github.com/upstream/widget.git"
  }
}

The binding is credential-free and server-authored from provider data. It is stored only when the head identity and normalized branch resolve to exactly one task-repository attachment. See ADR-2026-08-19-repository-qualified-comparison-targets.

API surface

The create_task_kandev input schema keeps the same property set. The existing field is documented as:

repository_url: Repository URL, GitHub pull request URL, or GitLab merge request URL.

The normal task response is unchanged. A provider-neutral internal resolver accepts the URL plus the resolved workspace, returns the target repository input and validated contribution binding, and exposes an association operation for the newly created task. Provider-specific API payloads do not cross that internal boundary.

The user-facing WebSocket API adds two session-scoped actions:

  • worktree.replace_contribution accepts session_id, an optional repository scope, and expected_remote_head. It replaces the contribution branch only when the exact lease matches.
  • worktree.use_contribution accepts the same identity fields. It creates a local recovery branch and moves the clean task checkout to the confirmed provider head.

Both actions return the existing Git operation result. worktree.use_contribution also returns the recovery branch name after a successful reset. Neither action appears in the agent MCP catalog.

Permissions

  • Existing MCP authentication, workspace reachability, workflow, profile, and executor checks still apply.
  • Provider reads use the workspace-scoped GitHub or GitLab automation identity. A private contribution is unavailable when that identity cannot read both the target change and source repository.
  • In managed GitHub credential mode, the broker may issue a source-repository scope only when the exact host and owner/repository match a validated remote_contribution binding on the session's linked task repository. The existing target-repository scope remains unchanged.
  • In executor credential mode, Kandev does not mint credentials. Runtime preparation performs a non-mutating push preflight with the executor's effective Git credentials before starting the agent.
  • GitLab uses the configured workspace connection for provider validation and the existing executor credential policy for Git transport. Self-hosted MR URLs must match the configured origin exactly.
  • A user with access to the task can invoke the replacement actions from the task UI. The gateway and handler apply the existing session authorization before they access the execution.
  • Agents, MCP callers, and automatic Git operations cannot invoke the managed replacement actions.

Failure modes

Condition Observable behavior
URL is malformed, credential-bearing, or unsupported Task creation fails before persistence with an argument error.
Provider connection cannot read the change or source repository Task creation fails before persistence with an authorization/not-found error that does not reveal cross-workspace data.
Change is closed, merged, or has no live head Task creation fails before persistence and explains that only open contributions are supported.
Provider returns an invalid head/base ref or inconsistent target identity Task creation fails closed before any Git command.
Fork does not allow maintainer collaboration Task creation fails before persistence with provider-specific remediation guidance.
Task persists but the existing-change association fails Kandev compensates the newly created task and returns failure; it does not launch an agent.
Checkout SHA no longer matches the source branch during preparation Launch fails without checking out or pushing a different revision; retry resolves fresh provider state.
Provider branch advances after launch and still contains local HEAD Kandev identifies a provider-ahead fast-forward, shows the current provider history, and offers Pull instead of Push.
Provider branch is rewritten after launch and no longer contains local HEAD Kandev preserves the task version, shows a history-difference warning, and offers comparison and user-controlled version choices.
Provider head changes after the replacement confirmation opens The exact lease fails. Kandev does not change the remote branch and refreshes the shown provider state.
Provider rejects the leased replacement The task version remains unchanged. Kandev shows the provider or Git error.
The user selects Restore published PR version... with local file changes Kandev does not reset the checkout. It asks the user to commit or discard the file changes first.
The user selects Restore published PR version... and the fetch does not match the confirmed provider head Kandev does not create a recovery branch or reset the checkout. It refreshes the provider state.
Current provider commits cannot be loaded Kandev retries once, keeps the checkout history without a warning, and derives remote actions only from sufficient evidence.
A same-contribution provider refresh is pending or fails after a successful read Changes retains the last confirmed commit provenance for display, while current-evidence action gates remain closed until refresh succeeds.
Effective Git credentials cannot dry-run a push to the source branch The task remains durable, but the session does not start and exposes an actionable credential/collaboration error.
Contribution binding is missing, malformed, or an unknown version Runtime preparation and managed source-scope issuance fail closed.
Agent attempts a normal create-PR action Kandev reuses the existing association and does not open a second remote change.
Associated PR head does not match exactly one task attachment The PR remains linked for Review, but no comparison target changes; Kandev logs the scoped mismatch.
Cross-repository comparison remote collides or cannot fetch Working-tree status remains available, comparison-derived data is explicitly unavailable, and Kandev never falls back to same-named origin.

Persistence guarantees

The contribution binding and GitHub PR or GitLab MR association survive backend restarts. New and reset environments reconstruct the target checkout, contribution remote, upstream branch, and push routing from the binding. Credential leases and preflight results are ephemeral and are recomputed on each launch or resume. A moved or deleted source branch causes a later launch to fail visibly rather than silently falling back to the target repository.

The original contribution head_sha remains creation-time provenance. It is not changed after a provider rewrite. Live provider commits and Git status remain observed state. Drift detection does not reset, rebase, merge, or replace either version without a direct user action.

The frontend provider-history resource can retain the latest successful commit list in memory across same-contribution evidence versions. This display cache is not persisted. It does not retain provider-head authority. The resource discards the cache when it evicts the bounded entry.

A recovery branch created by Restore published PR version... remains in the task repository across backend restarts. Kandev does not persist confirmation dialogs, provider snapshots, or replacement leases.

Repository-qualified comparison targets survive backend and executor restarts. Their deterministic remote/ref is reconstructed without changing origin, the checkout upstream, or push routing.

Scenarios

Create from a same-repository GitHub pull request

GIVEN an open GitHub pull request whose source and target repository are the same WHEN create_task_kandev receives its URL as repository_url THEN Kandev creates a task on the target repository, checks out the exact head branch and SHA, links the existing pull request, and pushes future commits to that head branch

Create from an editable GitHub fork pull request

GIVEN an open fork pull request whose author enabled maintainer edits WHEN a maintainer creates a task from the pull request URL THEN Kandev keeps origin on the target repository, configures the fork as the contribution remote, authorizes only that validated source repository, and pushes normally to the contributor's head branch

Reject a non-editable GitHub fork pull request

GIVEN a fork pull request whose author disabled maintainer edits WHEN a maintainer creates a task from its URL THEN no task is created and the result explains that the contributor must allow maintainer edits

Create from an editable GitLab merge request

GIVEN an open merge request on the workspace's configured GitLab host whose source project permits collaboration WHEN create_task_kandev receives the merge request URL THEN Kandev attaches the target project, checks out the source project branch, links the existing merge request, and routes pushes to that source project

Reject stale provider state

GIVEN a contribution was resolved but its source branch moved before worktree preparation WHEN Kandev prepares the task THEN preparation fails rather than checking out the new head or pushing from the stale SHA

Show a provider fast-forward safely

GIVEN a running contribution task whose provider branch advances without rewriting local HEAD WHEN Kandev loads the current provider commits THEN the Changes panel treats the provider as ahead, does not mark the existing commits as local work to push, and offers Pull rather than Push

Keep a rewritten contribution local-first

GIVEN a running contribution task whose provider branch is force-pushed and no longer contains local HEAD WHEN Kandev loads the current provider commits THEN the Changes panel keeps the task version primary, shows one warning icon in its toolbar, and offers Compare versions, Publish task version..., Restore published PR version..., and Open PR on GitHub from the warning menu AND the warning title is Task and PR histories differ AND comparison is the first action and opens both histories without changing Git state AND desktop action consequences are available through tooltips while phone action consequences appear inline AND the panel body does not repeat the warning

Ignore a historical pull request after the checkout changes branch

GIVEN a task retains a merged pull request for an earlier branch AND a newer pull request exists for the current checked-out branch in the same repository WHEN Kandev computes the Changes relation and provider history THEN it uses only the newer pull request AND the historical pull request remains available in Review without producing a Changes warning

Publish the task version to the PR branch

GIVEN a diverged contribution and a confirmed provider head WHEN the user confirms Publish task version... THEN Kandev replaces the remote branch only when the exact provider-head lease still matches

Reject a stale replacement lease

GIVEN the replacement confirmation names provider head A AND the provider branch advances to head B WHEN the user confirms Publish task version... THEN Kandev leaves head B unchanged and shows the refreshed remote-change state

Restore the published PR version with recovery

GIVEN a diverged contribution with a clean working tree WHEN the user confirms Restore published PR version... THEN Kandev creates a local recovery branch at the prior task HEAD and moves the task branch to the confirmed provider head

Preserve local file changes

GIVEN a diverged contribution with staged, unstaged, or untracked file changes WHEN the user selects Restore published PR version... THEN Kandev keeps the checkout unchanged and asks the user to commit or discard those changes

Provide the same choice on mobile

GIVEN a diverged contribution on a phone viewport WHEN the user opens Changes and its remote-contribution actions THEN the user can replace the PR branch, use the PR version, or inspect the PR version without a desktop workflow through Publish task version..., Restore published PR version..., and Open PR on GitHub, with Compare versions available first

Keep ordinary local-ahead work pushable

GIVEN a contribution checkout whose upstream equals the provider's current head AND the maintainer creates commits on top WHEN Kandev computes Git status THEN Push reports only the commits absent from the upstream and the provider commits are not duplicated

Keep Changes usable when provider history is unavailable

GIVEN Kandev cannot load the current provider commit list WHEN the Changes panel renders the checkout THEN it retries the provider read once and keeps the local checkout history without a warning AND it does not claim the branch was rewritten or compare commits by message or patch similarity

Keep confirmed provenance stable during a provider refresh

GIVEN the Changes panel confirmed that checkout commits belong to an associated pull request AND Kandev starts a newer provider-history read for that same pull request WHEN the new read is pending or fails THEN those commits retain their confirmed published presentation instead of appearing newly unpushed AND Push, Pull, and replacement actions remain gated by the unavailable current evidence AND a successful response replaces the retained presentation with the refreshed commit history

Distinguish commits when the provider is ahead

GIVEN a complete provider history contains local HEAD and one newer provider commit WHEN the Changes panel reconciles the commit lists THEN it shows the provider-only commit first within its repository with the current-PR color and accessible label AND it keeps shared commits neutral and does not mark them as local work to push AND it does not offer Pull when the checkout has no configured upstream

Preserve ordinary repository creation

GIVEN an ordinary GitHub, GitLab, or provider-neutral repository URL WHEN it is passed as repository_url THEN Kandev follows the existing repository task path without a contribution binding or source scope

Reconcile an ordinary fork after PR creation

GIVEN an ordinary task attached to a contributor fork whose main is stale AND the checked-out feature branch opens a PR against upstream/widget:main WHEN Kandev associates the provider PR with the task THEN only the attachment whose repository and checkout branch match the PR head stores the upstream comparison target AND Changes, commits, cumulative diff, Review, and task-card totals use the upstream ref without rewriting origin or the push destination

Keep the MCP catalog compact

GIVEN the external MCP catalog before and after this feature WHEN clients inspect create_task_kandev THEN its input property names and count are unchanged and only the existing repository_url description mentions pull and merge request URLs

Out of scope

  • A new task-creation UI for pasting pull or merge request URLs.
  • Creating tasks from issues, review comments, or arbitrary commit URLs.
  • Azure DevOps or additional source-control providers.
  • Multiple remote contributions in one create call.
  • Automatic force pushes, branch renames, retargeting, merging, or collaboration-setting changes.
  • Automatic reset, rebase, merge, or remote replacement after provider drift.
  • Automatic replay or deduplication of task commits across rewritten provider history.
  • Bulk replacement across multiple repositories from one confirmation.
  • GitLab mobile or Changes-panel drift presentation until that surface supplies complete MR-head evidence. The backend replacement operations remain provider-neutral.
  • Copying remote titles, bodies, comments, or diffs into trusted prompts.
  • Guaranteeing write access after credentials or provider permissions change during a running session.
  • Expanding task Git credentials so an otherwise unreadable private target can be fetched.