* fix(worktree): promote FallbackBaseBranch when BaseBranch is empty
CreateRequest.Validate previously hard-rejected an empty BaseBranch even
when the caller supplied a FallbackBaseBranch. The fallback is plumbed
specifically so a missing primary branch can degrade into the repo's
default — this lets that path actually fire.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(task): probe provider default branch in add_branch resolve path
Adds an opt-in ProviderDefaultBranchProber seam (impl wired in cmd/kandev)
and threads a ResolveProviderDefaults flag through TaskRepositoryInput so
the add_branch_to_task GitHub-URL path can resolve a real default_branch
before writing the Repository row. Also backfills the existing repo's
default_branch when the probe succeeds and the workspace already had a row
with an empty value. The create_task path keeps its zero-default contract
(executor still backfills after clone).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(task): surface add_branch_to_task failures instead of silent orphans
AddBranchToTask used to return a success envelope even when the
post-insert worktree materialize step failed on a live task. The
task_repositories row stayed in place with no on-disk worktree, and
provider-URL flows also left an orphan repositories row behind.
Three coordinated changes:
- Gate empty base_branch after resolution with a typed error so no row
is written when nothing can be materialized.
- Track whether the repository row was created by this call and roll it
back on any subsequent failure.
- materializeBranch now returns its error; on a live worktree-executor
task, AddBranchToTask rolls back the task_repositories row and surfaces
the failure. Pre-launch tasks keep the legacy best-effort path so the
next session launch still materializes via the multi-repo prepare flow.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(mcp): classify add_branch base_branch resolution failure as validation
AddBranchToTask now returns a "cannot resolve base_branch" error when the
provider repo's default branch could not be probed. Map it to
ErrorCodeValidation so MCP agents see a user-fixable input issue rather
than an opaque internal error.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(task): defer task.updated until after worktree materializes
Publishing task.updated before materializeBranch ran meant a failed
materialize on a live task would push the new task_repository row to WS
clients and then silently roll it back, leaving stale UI state until the
next poll. Move the event after materialize success so the bus only sees
durable rows. Test now also asserts no task.updated fires on the rollback
path. Renames stubProber.lastProvid to lastProviderArg for clarity.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(task): emit repository.deleted on add_branch orphan rollback
Cleanup callback previously called repoEntities.DeleteRepository directly,
which soft-deletes the row but bypasses Service.DeleteRepository's event
publish. The create path emitted repository.created, so skipping the
delete event left WS subscribers and frontend caches holding a phantom
row after a resolve-then-fail sequence. Route through the service method
so the events stay symmetric. Test now asserts the symmetry.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(task): skip provider probe when workspace already has the repo
resolveRepoInput's add_branch probe fired on every add_branch_to_task
call with a github_url, even when the workspace already had the
provider repo registered with a non-empty default_branch. The existing
row's value would win seconds later via the repo.DefaultBranch fallback,
so the remote git ls-remote round-trip was pure waste — and unnecessarily
hit network on every call. Peek at the workspace registry first and
short-circuit when a usable default_branch is already on file.
Also adds a QA-tier test file covering probe-error fallthrough, bad URL
rejection, registered-repo probe skipping, empty-default backfill, and
the repository_id fast path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(task): detach rollback paths from caller ctx, log probe-lookup errors
Two adjacent robustness fixes flagged on PR #1264:
1. resolveRepoInput silently discarded the GetRepositoryByProviderInfo
error before the provider probe. A transient DB failure was
indistinguishable from a clean "not found" lookup, so the code would
pay for a network probe instead of failing fast. Surface the error in
logs and skip the probe — FindOrCreateRepository below already hits
the same DB and will return the real cause.
2. The repository-create cleanup callback in resolveBranchAddRepoRef and
the task_repositories rollback in AddBranchToTask both captured the
caller ctx for their DELETE queries. Rollback often runs precisely
because that ctx was cancelled (timeout, client disconnect), and
reusing it would kill the cleanup and orphan the row we just created.
Use context.WithoutCancel so the rollback survives ctx death.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(task): close orphan-row gaps from caller cancel and concurrent create
Two findings from Claude's review on PR #1264:
1. (blocker) materializeBranch called taskAlreadyLaunched(ctx, ...) after
MaterializeBranch returned, using the same caller context that often
failed the materialize in the first place. A cancelled ctx made the
post-failure DB lookup return (nil, err), taskAlreadyLaunched returned
false, and the live-task rollback path silently demoted itself to the
pre-launch best-effort branch — leaving the orphan task_repositories
row the PR exists to prevent. Snapshot the launch state BEFORE
MaterializeBranch (launch state can't change during the synchronous
call) so the rollback decision is anchored to a live-ctx answer.
2. (suggestion) resolveBranchAddRepoRef inferred "this call created the
repo" by comparing the resolved ID against a workspace ListRepositories
snapshot taken before FindOrCreateRepository. A concurrent add_branch
for the same GitHub URL could create the row between snapshot and
lookup; the second request would treat it as "not in my snapshot, must
be mine" and register cleanup against the other request's data. On any
later failure, cleanup would DELETE a repository this call didn't
create and emit a spurious repository.deleted event.
Thread an explicit `created bool` out of CreateRepository through
FindOrCreateRepository → resolveRepoInput → ResolveRepositoryRef so
the cleanup callback fires only when this call physically inserted
the row. The flag is set at the CreateRepository / FindOrCreateRepository
sites themselves and cannot be spoofed by concurrent activity.
resolveRepoInput's branches were extracted into resolveRepoInputGitHub /
resolveRepoInputLocal helpers to keep the parent function inside the
cyclomatic-complexity and cognitive-complexity budgets after the signature
change.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: retrigger cubic CI
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>