Files
kandev/docs/plans/pr-link-fork-launch/plan.md
T
2026-09-23 17:59:26 +01:00

13 KiB

created, status, requirements, system_design, legacy_specs
created status requirements system_design legacy_specs
2026-09-23 done
REQ-WORKSPACES-WORKTREE-BASE-REFRESH-001
REQ-INTEGRATIONS-GITHUB-FORK-REVIEW-START-001
../../specs/workspaces/system-design/worktree-base-refresh.md
../../specs/integrations/system-design/github-fork-review-start.md

Implementation Plan: PR-link fork launch

Overview

Restore user-submitted task startup from a fork PR link. Task 01 repaired attachment-aware base validation. Task 02 proves desktop and phone creation with real Git fixtures and the mock provider. The PR security review also preserves a manual-start boundary for unattended fork review-watch tasks.

Evidence and root cause

Task 979b1b1b-3b52-44c2-8ea5-f317baf39839 failed on 2026-09-23 at 12:39:34 UTC, before agent startup. The backend reported:

pull request identity did not match the task repository binding

The retained attachment had base_branch=main, checkout_branch=feature/archive-force-remove-csd, and only pr_number=3879 in metadata. GitHub returned source nova28/kandev and target kdlbs/kandev. The source branch was feature/archive-force-remove-csd. The target was main.

Commit a2d4ee8c185 (#3878) added the rejecting identity check. resolvePRBaseForLaunch assumes that the attached repository owns the head unless RemoteContribution supplies another source. Browser creation emits ordinary PR-link metadata. Its separate PR association runs asynchronously. The MCP contribution coordinator does not run on this browser creation path.

Evidence came from live task metadata, the GitHub API, source history, and backend bundle 7841647e48331f84267ca55ea09d2bbd. The running build was v0.95.1-20-gf2d52f18263. The archive was size-limited but retained creation and failure. No permanent reproduction test existed when the failure was investigated. Task 01 added the regression and recorded its pre-fix RED result.

Requirement conformance

The workspace system owns preparation identity and materialization. This is an implementation regression against existing criteria, not a new product feature. Reuse REQ-WORKSPACES-WORKTREE-BASE-REFRESH-001, criteria .11 and .15 through .19. The security review identified a separate integration contract: REQ-INTEGRATIONS-GITHUB-FORK-REVIEW-START-001 keeps background review-watch starts disabled for fork or identity-incomplete PRs. See its system design and decision. The design clarification separates target-attached PR checkout from fork-attached checkout.

The user confirmed that creating a task from a fork PR must work. No material intent question remains. This package does not convert ordinary browser tasks into the MCP contribution workflow. That workflow has additional collaboration and push-policy semantics owned by the task system.

Scope

In scope

  • Validated target-attached fork PR launch with existing metadata.
  • Launch before or after asynchronous PR association, and retry before preparation.
  • Exact provider namespace, PR number, head branch, and repository checks.
  • Compatibility with explicit contribution bindings and fork-attached checkouts.
  • Desktop and phone PR-link startup evidence.
  • Preventing unattended GitHub review-watch launches for fork PRs until a user starts the linked task explicitly.

Out of scope

  • New controls, copy, public API fields, database schema, runtime flags, or permission model.
  • Changing source remotes, push routing, or contribution collaboration policy.
  • Resetting an existing checkout, repairing the live task, or automatic retries.
  • Redesigning PR association ordering, GitLab behavior, or comparison reconciliation.
  • Adding a fork trust UI, approval label, or setup-script sandbox.

Technical approach

apps/backend/internal/orchestrator/executor/executor_resume.go owns the attachment-aware decision in resolvePRBaseForLaunch and validPRBaseIdentity. An unbound target-attached checkout can accept a provider-validated fork head only when the target, PR number, and non-empty checkout branch match. An explicit source binding always wins. A mismatch cannot enter this legacy path.

apps/backend/internal/backendapp/orchestrator.go owns namespace lookup and selectLinkedTaskPRForBase. Preserve rejection of ambiguous linked matches. The no-association path queries the attached namespace. It must work during the browser association race. Existing typed errors retain fallback decisions.

GitHub review-watch task creation applies the separate trust contract. Confirmed same-repository PRs retain their configured auto-start token. Fork or missing head identities retain a manual-start marker and no unattended token. Workflow gates and the automatic StartTask boundary enforce it; direct manual start remains available. Ordinary user-submitted PR-link tasks do not carry the marker.

Keep the resolved source identity transient in PRBase. No database migration or new durable binding is required. The target-attached checkout keeps the ordinary target base and target PR-head fetch. Fork-attached checkouts retain qualified upstream base handling. Do not derive permission from PR identity.

Tests

Workspace criterion suffixes refer to AC-WORKSPACES-WORKTREE-BASE-REFRESH-001; the integration row below maps to its separately linked requirement.

Criteria Implemented evidence
.11, .15 executor_pr_base_identity_test.go: TestResolveTaskRepoInfo_TargetAttachedForkPRWithoutContribution recorded RED before the fix and passes after it
.15, .19 Same test file: explicit source mismatch, unrelated target, wrong branch/number, incomplete identity, same-repository compatibility
.15, .17 backendapp/pr_base_resolver_test.go: linked row absent/present, duplicate matches, provider failure, cancellation
.16, .18 apps/backend/internal/orchestrator/executor/executor_pr_base_materialization_test.go: TestTargetAttachedForkPRBasePreparationEndToEnd proves exact head/base and unchanged push routing
.17, .19 executor_pr_base_identity_test.go: TestResolveAllRepoInfoRejectsLaunchWhenOnePRBindingIsInvalid blocks a mixed launch without a fallback checkout
Integration AC 001.1-.4 apps/backend/internal/orchestrator/event_handlers_github_review_test.go: TestBuildReviewTaskRequest_ForkPRRequiresManualStart and TestAutoStart_ForkReviewWaitsForManualStart cover same-repository auto-start, fork or missing-identity suppression, automatic launch rejection, and explicit manual start

E2E tests

Task 02 extends apps/web/e2e/tests/task/create-task-github-url.spec.ts (project chromium) and mobile-create-task-remote-repo.spec.ts (project mobile-chrome). Both paste an upstream PR URL, start the task, observe the agent, reload, and retain the PR association and selected branch. The fixture publishes a fork head into the upstream PR ref namespace. It must not preseed RemoteContribution or ComparisonTarget metadata. These flows cover .11, .15, .16, and .19.

There is no rendered UI change, so no ASCII preview is required. Existing Start Task dialog and mobile creation surface keep their controls, scrolling, and navigation. Phone coverage uses touch submission on the existing surface.

Work orders

Task 02 depends on Task 01. Execution is sequential. No delegation is authorized.

Companion packages

Fork PR base resolution introduced this regression. Its completed work and command results remain historical evidence. This package owns the additional cases and new verification results. Fork PR comparison targets retains comparison ownership and push invariants. Neither package is reopened.

Verification results

Task 01 implementation and focused backend verification passed:

  • The target-attached fork identity regression failed before the executor fix with pull request identity did not match the task repository binding.
  • Focused identity, invalid-identity, explicit-binding, cancellation, multi-repository and real Git preparation tests passed.
  • go test ./internal/orchestrator/executor ./internal/backendapp ./internal/worktree -count=1: passed.
  • python3 scripts/list-docs.py validate, python3 scripts/lint-spec-files.test.py, python3 scripts/lint-spec-files.py --all, and git diff --check: passed.

Task 02 browser implementation and verification passed:

  • (cd apps/web && pnpm e2e:run --project chromium tests/task/create-task-github-url.spec.ts): 10 passed.
  • (cd apps/web && pnpm e2e:run --project mobile-chrome tests/task/mobile-create-task-remote-repo.spec.ts): 7 passed.
  • Both suites ran against freshly built backend and pseudo-locale Vite assets. They created upstream and fork repositories with different main commits, submitted the ordinary PR URL without contribution/comparison bindings, launched the mock agent, and verified the exact fork HEAD and upstream target base commit.
  • After reload, desktop shows the #3879 PR topbar association. Desktop and phone both retain the checkout/base branches and persisted PR summary/association. The phone test submits with touch and opens the created task from the mobile card.
  • Fixture cleanup removes its task, repository, and temporary Git remotes in finally paths.
  • make -C apps/backend build, web pnpm run typecheck, and targeted ESLint on the three changed E2E files: passed.
  • python3 scripts/list-docs.py validate, python3 scripts/lint-spec-files.test.py (36 tests), python3 scripts/lint-spec-files.py --all, and git diff --check: passed.
  • An additional repository-wide pnpm run lint:e2e-sleeps check remains red on unrelated existing violations and unresolved rule references across other files; the changed E2E files pass targeted ESLint.

The fork review-watch security follow-up adds a separate integration contract:

  • go test ./internal/orchestrator -run 'TestBuildReviewTaskRequest_ForkPRRequiresManualStart|TestAutoStart_ForkReviewWaitsForManualStart' -count=1: passed.
  • (cd apps/backend && go test ./internal/orchestrator ./internal/orchestrator/executor ./internal/backendapp ./internal/worktree -count=1): passed.
  • make -C apps/backend build: passed.
  • (cd apps/web && pnpm e2e:run --project chromium tests/task/create-task-github-url.spec.ts): 10 passed.
  • (cd apps/web && pnpm e2e:run --project mobile-chrome tests/task/mobile-create-task-remote-repo.spec.ts): 7 passed.
  • (cd apps/web && pnpm exec eslint e2e/tests/task/create-task-github-url.spec.ts e2e/tests/task/mobile-create-task-remote-repo.spec.ts e2e/tests/task/pr-link-fork-launch-helpers.ts): passed.
  • python3 scripts/list-docs.py validate, python3 scripts/lint-spec-files.test.py, python3 scripts/lint-spec-files.py --all, and git diff --check: passed.
  • Same-repository review watches retain their one-shot auto-start token. Fork or identity-incomplete watches stay available without an unattended launch token; both workflow launch paths and the central automatic StartTask boundary enforce the marker, while explicit manual start succeeds.
  • The contract and its trust boundary are recorded in the integration requirement, system design, and decision linked above. Browser PR-link tasks do not carry the marker.

Historical design-package checks at the 2026-09-23 handoff:

  • python3 scripts/list-docs.py validate: passed (299 decisions, 1124 specifications).
  • python3 scripts/lint-spec-files.test.py: passed (36 tests).
  • python3 scripts/lint-spec-files.py --all: passed.
  • git diff --check: passed.
  • Relative document links and referenced acceptance IDs: validated.
  • At that handoff, the plan and both work orders were pending implementation. Production and test code were unchanged, and no product tests ran during planning. Task 01 and Task 02 were implemented and verified afterward as recorded above.

Risks

  • Accepting any fork head would weaken binding validation. Explicit bindings must never fall through to the ordinary target-attached path.
  • Requiring a linked row would make startup depend on asynchronous timing.
  • A fixture with a same-repository head or preseeded binding would hide this defect.
  • Local Git URL rewrites must remain inside disposable test repositories.

Documentation impact

The implementation restores existing PR-link behavior without new user instructions. The fork review-watch security boundary is recorded in the new integration requirement, system design, and decision linked above. Public docs remain unchanged because no new user-facing controls or instructions were added. The existing repository-qualified comparison ADR remains authoritative for comparison and push invariants.