Delete unreachable/unused backend symbols confirmed by staticcheck U1000
and golang.org/x/tools/deadcode, plus deprecated back-compat aliases kept
alive only by their own tests.
Removed (unused):
- parseGitHubRepoURL, syncTaskStateForBulkWorkflowMove (task/service)
- resolvePrimaryRepoInfo (orchestrator/executor)
- resolveWorkflowAgentProfile, workflowDefaultAgentProfile wrappers (mcp/handlers)
- cleanupExitedContainer (runtime/lifecycle) + orphaned fmt import
- VscodeManager.setEnv (agentctl/server/process)
- repoRootDir const (cmd/preview)
Removed (deprecated shims):
- Canceller.CancelSessionAndNotify alias -> callers use DetachSessionAndNotify
- ScriptPlaceholder type alias (live scriptPlaceholders var kept)
- Placeholder{QueueRun,ClearDecisions,QueueRunForEachParticipant}Callback
no-op structs and their self-referential test
No behavior change; build, vet, and affected-package tests pass.
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(executor): re-clone provider repos when local path is missing or not a git repo
Previously the re-clone guard only fired when local_path was empty, so a
set-but-invalid path (e.g. deleted checkout, or relocated by the provider
clone-path/identity change) was fed straight to the worktree preparer,
which hard-failed with "repository is not a git repository" instead of
recovering.
Two-part fix:
- resolveTaskRepoInfo now also re-clones when local_path exists but isn't
a valid git checkout (new isLocalGitRepo helper), self-healing on next
launch.
- validateWorktreeRequest now fails the "Validate repository" step with an
honest error naming the offending path when it truly isn't a git repo,
instead of passing and cascading into a misleading "sync interrupted"
failure downstream.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(executor): gate stale-path reclone to provider repos and cover resume + multi-repo
Restrict the stale-local-path re-clone to genuinely provider-backed repos
(SourceType != "local") so a local checkout that's temporarily missing is
never silently overwritten. Centralize the re-clone guard into a shared
ensureRepoLocalPath helper and call it from the single-repo resume path
(applyResumeRepoConfig) so resume self-heals like a fresh launch. Apply the
same non-git-path validation used by the single-repo prepare path to the
multi-repo prepareOneRepo path so a stale secondary repo fails fast with a
clear message instead of surfacing worktree.Manager's ErrRepoNotGit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(plugins): add capability-gated Host reader for conversation content
Plugins can now read historical user/agent message content over the Host
data API, gated by a new `api_read:messages` capability (ADR 0047). This
unblocks "summarize yesterday"-style plugins that the live `message.added`
bus event alone cannot serve.
- proto: `ListMessages` RPC + `Message`/`MessageFilter` on `service Host`.
- SDK: `Host.Messages() MessageReader`, Go-native `Message`/`MessageFilter`.
- host: `api_read:messages` accessor + denied reader; content sanitized via
`sysprompt.StripSystemContent` (no raw system prompts); RFC3339 `since`/
`until` → `InvalidArgument` on bad input.
- data: new `ListMessagesForPlugin` filtered query (session/task ids, type,
created_at range) through the task service, never the DB file.
- tests: in-process + wire capability gating, content stripping, filter/time
passthrough, pagination, and the SQLite query.
- docs: ADR 0047, spec, plugins-manifest, plugins-authoring.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(plugins): add Host.InvokeUtilityAgent for settings-selectable utility agent
Plugins can now delegate a one-shot LLM step to an operator-configured agent,
gated by a new `agent_invoke` capability (ADR 0048), so a plugin needs no
provider API key of its own.
- setting: new `utility_agent_profile_id` user setting (Settings > System >
Utility Agent), a profile dropdown cloned from the existing utility-agent
path; selectable in the UI.
- capability: boolean `agent_invoke`, enforced like `state`/`secrets`.
- proto/SDK: `InvokeUtilityAgent(prompt) -> text` RPC and
`Host.InvokeUtilityAgent(ctx, prompt)`.
- backend: reuses the sessionless host-utility inference tier (ADR 0002) via a
thin adapter — no agent loop, no task/session/workspace; resolves the
configured profile to (agent type, model). Unconfigured / deleted profile ->
gRPC FailedPrecondition.
- tests: capability gating, completion path (asserts the configured profile is
used and text returned), no-utility-agent and stale-profile error paths,
SDK/wire round-trips, and the settings apply/trim behavior.
- docs: ADR 0048, spec, plugins-manifest, plugins-authoring.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(plugins): resolve utility-agent deps live and bound message filter size
Address CI + review feedback on the host plugin capabilities:
- docs: register the new /settings/system/utility-agent route in
docs/public/coverage.json (fixes the "Validate public docs" CI failure).
- utility agent (ADR 0048): hostUtilityMgr is wired late in boot — after
StartActivePlugins has already spawned boot-active plugins — so snapshotting
the utility deps in hostForPlugin stranded those hosts with nil deps and made
InvokeUtilityAgent return Unimplemented for their lifetime. Read the deps live
from the Service (mutex-guarded) at call time instead; add a regression test
for late wiring reaching an already-spawned host.
- messages (ADR 0047): cap the combined session/task/type filter values at 400
so an oversized filter fails fast with InvalidArgument instead of hitting
SQLite's host-parameter limit at query execution; add a test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(tasks): search tasks by PR number in command panel
Typing a PR number (e.g. 1243 or #1243) in the Cmd+K panel now surfaces
the task that PR is associated with, alongside the existing
title/description/repo search.
The github store gains a workspace-scoped ListTaskIDsByPRNumber lookup
over github_task_prs; the task service resolves it through an injected
PRTaskResolver interface (github service wired in cmd/kandev) and merges
PR matches into search results, deduped and respecting the
archived/ephemeral/config filters. Keeping the lookup at the service
layer avoids coupling the task repository to the github schema, so
existing search queries and tests are untouched.
* fix(tasks): scope PR-number search to page 1 and unfiltered queries
Address review feedback on PR-number search:
- Only augment results on the first page of an unscoped search. Running
on every page re-prepended the PR match and truncated a real result
off each subsequent page; running under a workflow/repository filter
leaked tasks that don't satisfy the filter.
- Add idx_github_task_prs_pr_number so the lookup is index-backed
instead of a full scan (pr_number is the 3rd column of the existing
UNIQUE constraint, unusable as a leading key).
- Log instead of silently dropping a task when GetTask fails in the
PR-match loop.
* test(github): dedup PR-lookup test setup into shared newTestStore
Add workspace_id to newTestStore's tasks table and drop the near-identical
newTestStoreWithWorkspaces helper, per review feedback.
* perf(tasks): batch-fetch PR-matched tasks instead of N+1 GetTask
Add a repository GetTasksByIDs and use it in augmentWithPRMatches so PR-number
search loads all matched tasks in one query instead of one GetTask per id,
addressing the N+1 noted in review. Fetch+filter is extracted into a
fetchPRMatchedTasks helper that preserves resolver order and dedup.
* feat(tasks): multi-branch tasks — N (repo, branch) pairs per task
Extends multi-repo into multi-branch so a single task can hold multiple
worktrees on the same repo (one per branch). Sibling worktrees layout at
<task>/<repo>/ + <task>/<repo>-<slug>/, dedicated PRWatch per branch,
live agentctl tracker rescan + frontend store updates so the UI splits
PRs / files / commits per branch without a session restart. MCP
add_branch_to_task_kandev tool is shielded to the worktree executor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(web): unify multi-branch repo label as <repo> · <branch> across panels
PR Changes and Commits panels rendered repo labels differently for multi-branch
tasks — Commits showed "kandev · feature/x" while PR Changes leaked the raw
"kandev-feature-x" agentctl tracker tag. Reformat tracker-tagged repo names via
formatRepoLabel using the longest known-repo prefix so kandev-cli-* doesn't
collapse to kandev · cli-*.
* fix(tasks): address multi-branch PR review — data races, slug collisions, MCP error codes
Backend
- agentctl process.Manager: add repoTrackersMu RWMutex guarding repoTrackers
+ workspaceTracker. Reads snapshot under RLock; RescanRepositories builds
trackers first, takes Lock for swap/append. Validates newWorkDir via
os.Stat before publishing cfg.WorkDir.
- worktree.Manager: reject invalid explicit BranchSlug up-front; cache key
uses sanitized wt.BranchSlug so read/write paths are symmetric.
- worktree.store: narrow isDuplicateColumnError to "duplicate column name".
- task service AddBranchToTask: slug-collision guard rejects feat/a vs feat-a.
- orchestrator resolvePushRepo: two-pass exact-name match before subdir
prefix fallback so "backend" doesn't swallow pushes for "backend-admin".
- executor buildRepoSpecs: duplicate repos always get a non-empty BranchSlug
(falls back to branch-N when both branch fields sanitize to empty).
- MCP handler: classifyAddBranchError maps service errors to NOT_FOUND /
CONFLICT / VALIDATION_ERROR; add ErrorCodeConflict constant.
- MCP server: extract registerAddBranchToTaskTool, register in task mode
only (external mode contract: config tools + create_task_kandev).
- workspace_rescan handler: reject malformed JSON with 400 instead of
silently rescanning the current root.
- branch_materializer: split MaterializeBranch into prepare + finalize
helpers to satisfy the 80-line cap.
Frontend
- existingPrUrl returns undefined when a task has >1 PR so per-repo View PR
buttons take over (no silent linking to a sibling).
- formatRepoLabel checks exact known-repo match before prefix splitting so
"kandev-cli" isn't mislabeled as "kandev · cli".
- Repo chip key now includes base_branch so siblings differing only on
base render as distinct chips.
Docs
- multi-branch spec uniqueness wording aligned with the canonical 4-column
key (task_id, repository_id, base_branch, checkout_branch).
- worktree.CreateRequest.BranchSlug doc describes the sibling layout
(<repo>-<slug>) instead of the obsolete nested form.
* fix(tasks): scope slug-collision check to repo only + use errors.Is for ErrTaskNotFound
- service_branches: drop base_branch from the slug-collision check. Worktree
path is (repo, slug); two rows on the same repo cut from different bases
still collide on disk.
- service_branches: prepareBranchAdd wraps taskrepo.ErrTaskNotFound via
fmt.Errorf with %w so classifiers don't have to substring-match the
formatted task UUID.
- mcp handlers: classifyAddBranchError uses errors.Is(err, ErrTaskNotFound).
* fix(agentctl): sanitize rescan work_dir before os.Stat to clear CodeQL path-injection
CodeQL flagged the rescan endpoint as a path-injection sink (the
HTTP-supplied work_dir flowed straight into os.Stat and downstream
scanRepositorySubdirs). Add validateRescanPath which:
- requires the input to be a clean absolute path with no ".." segments
- rejects anything that isn't equal to or an ancestor of the current
cfg.WorkDir — the only legitimate use case is the branch materializer
promoting <task>/<repo>/ to <task>/
Defense in depth: agentctl binds to localhost, but any colocated process
could otherwise direct it at /etc or the user's home by POSTing
/api/v1/workspace/rescan.
* fix(agentctl): tighten validateRescanPath, close TOCTOU on existingTrackers, add tests
- Drop the strings.Contains(clean, "..") guard. filepath.Clean already
resolves traversal; the substring check rejects legitimate names like
/home/u/..hidden without adding defense (the ancestor check via
filepath.Rel is the real boundary).
- Drop the rel == "." branch in the ancestor check — early return for
clean == currentClean already covers identity.
- Read existingTrackers inside the write-lock section (not under RLock
then re-Lock) so two concurrent rescans can't both see 0 trackers and
double-bootstrap the multi-repo set.
- Add table-driven TestValidateRescanPath covering rejection paths
(relative, descendant, sibling, traversal-cleaned, empty input),
legitimate ancestor promotion, exact match, no-anchor first-launch,
and a /home/u/..hidden dotted-segment case.
* fix(agentctl): break taint flow into os.Stat — derive rescan path from cfg, not HTTP body
CodeQL's go/path-injection trace still flagged manager_rescan.go after the
prior sanitizer because the user-supplied newWorkDir flowed (after
validation) into os.Stat and then into cfg.WorkDir → tracker paths →
workspace_monitor os.Stat sinks.
Replace validateRescanPath with resolveRescanPath: it returns a path
DERIVED from currentWorkDir (no-op = currentClean, promotion =
filepath.Dir(currentClean)) and uses newPath only for comparison, never
as a source for the returned string. os.Stat now receives a path whose
provenance is trusted manager config.
Functionally the legitimate caller (branch materializer promoting
<task>/<repo>/ to <task>/) is unchanged.
* test(agentctl): make TestResolveRescanPath windows-portable
The Linux Backend Tests job passed but the Backend (windows) job rejected
every "absolute" case because filepath.IsAbs("/task/repo") is false on
Windows (no volume letter).
Build inputs via filepath.Join against absRoot() (filepath.Abs of "/" /
"\\") so the same table covers POSIX (`/...`) and Windows (`C:\...`)
without per-OS branches. Keep the traversal-after-Clean case POSIX-only
since filepath.Clean is the shared primitive and that input form needs a
volume to be considered absolute on Windows anyway.
* fix(agentctl): serialize RescanRepositories + re-check trackers under write lock
Two concurrent RescanRepositories calls could both observe an empty
tracker set (between the snapshot read and the bootstrap branch) and both
enter transitionToMultiRepoMode, leaking a duplicate bare-root tracker
and duplicate per-repo trackers. Same race in appendNewRepoTrackers would
double-append the same new child across both callers.
Fix:
- Add rescanMu sync.Mutex on Manager. Acquired for the full body of
RescanRepositories so the snapshot → branch → mutate sequence is
serialized. repoTrackersMu still guards individual field reads from
unrelated goroutines (subscribe/unsubscribe/poll-mode).
- appendNewRepoTrackers re-checks membership inside the write-lock
before appending and drops already-present trackers (Stop + detach
outside the lock). Defense in depth.
- Test: add `dotted segment is legal as promotion` case so the
..hidden-segment promotion path stays exercised.
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>