* feat(backend): full GitLab parity — store, service, poller, WS, presets
Add the watcher/poll/write-action surface on top of the existing GitLab
client + service skeleton:
* 5 new SQLite tables (mr_watches, review_watches, review_mr_tasks,
issue_watches, issue_watch_tasks, action_presets) + CRUD
* Client + Service: MergeMR, GetProjectMergeMethods, GetProtectedBranch,
ListUserProjects, SearchProjects, SetMRLabels, SetMRAssignees
* Service: review/issue/MR watch CRUD + Check/Trigger, GetStats,
CleanupAllReview/IssueTasks (cleanup-policy aware), TriggerMRSync,
action-presets CRUD with defaults
* Background poller (MR / review / issue loops) wired from main.go
* ~35 GitLab WS action constants + handlers.go registrations
* Mock controller for E2E seeding (MRs, issues, pipelines, discussions,
approvals, branches)
* eventBus + taskDeleter + taskSessionChecker plumbed through Service
* Event types: GitLabMRFeedback / NewReviewMR / NewIssue / TaskMRUpdated
* feat(web): GitLab frontend parity — types, API, store slice, domain hooks
Extend the frontend GitLab integration on top of the new backend surface:
* HTTP API client expansion (gitlab-api.ts): full CRUD for review/issue/MR
watches, MR write actions (merge/approve/labels/assignees), MR
files/commits/feedback, action presets, projects autocomplete, stats
* Store slice expansion: review/issue/MR watches lists, action presets
by workspace, stats + status with loading flags. Properties prefixed
with gitlab to avoid collision with GitHub slice
* Domain hooks: use-gitlab-status, use-gitlab-stats,
use-gitlab-review-watches, use-gitlab-issue-watches,
use-gitlab-action-presets — mirror github hook shapes
* Type model: ReviewWatch, IssueWatch, MRWatch, ActionPresets, Stats,
ProjectMergeMethods, MRApproval, Pipeline, MRFeedback, etc.
* Backend controller HTTP routes for watches/presets/write-actions/
projects/stats so the frontend HTTP API client has endpoints to hit
All passes: backend (build + vet + lint + tests), web (typecheck + lint +
2400 tests).
* fix(gitlab): wire orchestrator event handlers, fix label-filter drop, add tests
Address Claude review blockers on the parity PR:
1. Wire orchestrator subscriptions for GitLab review/issue watch events.
Add event_handlers_gitlab.go with handleGitLabNewReviewMR /
handleGitLabNewIssue, dedup reservation handshake, and task-creator
interfaces (mirrors event_handlers_github.go). main.go now calls
orchestratorSvc.SetGitLabService so the dedup APIs are usable
2. Fix silent label-filter drop: when a custom_query is set on an issue
watch, labels were being appended to the unused filter arg. Fold
labels into customQuery (or default filter) so they actually reach
GitLab; extracted appendLabelsToQuery helper with explicit handling
of pre-existing labels= clauses
3. Switch event payloads to pointer types (mirrors GitHub) so
handlers can type-assert pointer events
4. Backend tests: store_watches_test.go (CRUD + reserve/assign for all
4 new tables + presets), service_watches_test.go (label-filter
helper, cleanup policy, project normalisation), action_presets_test.go
5. Frontend slice tests: review/issue watch CRUD round-trips, action
presets + stats reducer coverage
All green: backend (build+vet+lint+tests), web (typecheck+lint+2403 tests).
* fix(gitlab): address remaining blockers — cleanup wiring, safe defaults, nil guards
Address the second round of Claude review blockers:
1. main.go: wire SetTaskDeleter + SetTaskSessionChecker on services.GitLab
(mirrors GitHub). Without these, manual cleanup sweeps would always
error with "task deleter not configured"
2. service_cleanup.go: default cleanup policy on transient DB error is now
CleanupPolicyNever (preserve tasks) instead of CleanupPolicyAuto
(silently delete). Genuine "watch was deleted" path also falls under
the safe-side default; user can manually delete the orphan tasks
3. service_watches.go: add nil guards on requireStore() so the 6 MR-watch
list/get/delete methods return an errStoreUnavailable error instead
of panicking when the SQLite store failed to construct at boot
4. use-gitlab-status.ts + use-gitlab-stats.ts: add per-mount attemptedRef
so an unreachable GitLab doesn't trigger an infinite re-fetch loop —
the previous useEffect re-ran every render because the failure path
left status null, satisfying the !status guard
* fix(gitlab): propagate session-check errors, clamp poll interval on update
Address remaining Claude blockers:
* service_cleanup.go: HasUserAuthoredMessage transient errors are now
preserved (return false → skip delete) instead of silently ignored;
the alternative was occasionally deleting tasks a user had touched
* service_watches.go: extract clampPollInterval helper applying the same
bounds (0 → default, <60 → 60) as the create path; both
applyReviewWatchPatch and applyIssueWatchPatch use it so user-supplied
zero or tiny values via UpdateXxxWatchRequest no longer hammer GitLab
* fix(gitlab): clear dupl lint, fix preset retry loop + per-workspace watch cache
* service_watches.go: applyReviewWatchPatch / applyIssueWatchPatch get
nolint:dupl markers — they share shape but per-domain field validation
lives in the create paths, so deduplicating via generics would obscure
the contract. Restores backend lint to 0 issues
* use-gitlab-action-presets.ts: per-workspace attemptedRef set so a
failing preset fetch doesn't retry on every render
* use-gitlab-review-watches.ts + use-gitlab-issue-watches.ts: track
lastFetchedRef per consumer so a workspace switch triggers a refetch.
The slice-level loaded flag is shared across instances and can't
double as a per-workspace cache key
* fix(gitlab): add nil-store guards across review/issue watch + cleanup paths
Round-3 Claude finding: ReviewWatch / IssueWatch / preset / reservation
methods still called s.requireStore().X() directly. If NewStore fails at
boot (table migration error), the service struct's store is nil and every
list/get/update/create/reserve method panics on first request.
Apply the same store-nil → errStoreUnavailable pattern from f95a929 to
every remaining method:
* CreateReviewWatch / CreateIssueWatch
* GetReviewWatch / ListReviewWatches / ListAllReviewWatches /
UpdateReviewWatch / TriggerReviewWatchAll
* GetIssueWatch / ListIssueWatches / ListAllIssueWatches /
UpdateIssueWatch / TriggerIssueWatchAll
* ReserveReviewMRTask / AssignReviewMRTaskID / ReleaseReviewMRTask
* ReserveIssueWatchTask / AssignIssueWatchTaskID / ReleaseIssueWatchTask
* lookupReviewPolicy / lookupIssuePolicy (return CleanupPolicyNever)
* fix(gitlab): nil-store guards on CheckXxxWatch + DeleteXxxWatch + cleanup paths
Round-4 Claude finding: 4 remaining panic sites the previous nil-guard
pass missed — these run from the background poller (every 5 min) and the
manual delete/cleanup flows, so a boot-time NewStore failure would
crash the orchestrator process on first tick.
* service_watches.go: add store-nil guards to DeleteReviewWatch /
DeleteIssueWatch / CheckReviewWatch / CheckIssueWatch
* service_cleanup.go: guard CleanupAllReviewTasks / CleanupAllIssueTasks
entry points; wrap the trailing DeleteReviewMRTask / DeleteIssueWatchTask
calls in `if store := s.requireStore(); store != nil` blocks so the
cleanup succeeds even when the dedup-row delete is unreachable
* refactor(gitlab): split service_watches.go to stay under 800-line revive limit
CI's --new-from-rev lint caught service_watches.go at 808 lines (limit
800). Split into three focused files:
* service_events.go (94 LOC) — publish helpers for MR feedback /
new review MR / new issue / watch lifecycle events
* service_issue_watches.go (302 LOC) — Issue watch CRUD + Check +
Trigger + fetch + helpers
* service_reservations.go (59 LOC) — Reserve/Assign/Release dedup
handles used by the orchestrator event handlers
service_watches.go is now 505 lines and contains MR watch + Review
watch only. No behavioral change.
* fix(gitlab): address inline code-review feedback
Address actionable inline comments from Claude/CodeRabbit/cubic/Greptile:
* appendLabelsToQuery: use url.ParseQuery for exact key match instead
of strings.Contains; previously false-matched keys like mylabels=
and silently dropped the watch's labels (test added)
* fetchReviewMRs: drop dead `filter = watch.CustomQuery` assignment —
SearchMRs's buildMRSearchQuery returns customQuery verbatim and
ignores filter when customQuery is non-empty
* store.go: add workspace_id indexes on gitlab_review_watches and
gitlab_issue_watches (the 5-min poller and HTTP list endpoints did
full table scans)
* mock_client.SearchProjects: switch from == to case-insensitive
Contains so partial-query autocomplete returns the seeded project,
matching the doc comment
* Poller: add sync.Mutex to guard `started` field; Start/Stop are
now safe to call concurrently (go test -race would have flagged
the previous read/write race)
* Add ErrWatchNotFound sentinel; controller_watches.go maps it to
HTTP 404 via httpRespondError helper. Update/Trigger handlers
previously returned 500 for missing watches, hiding the distinction
from real server faults
* service_events.go: log Publish errors on watch lifecycle events
instead of swallowing them silently
* handlers.go: reject malformed JSON payloads in list-style WS
handlers rather than silently falling through to broader
list/search behavior
* event_handlers_gitlab.go: remove dead gitlabRepoSlug helper +
blank-identifier suppression
* ci: re-trigger to clear flaky E2E shard 3
* fix(gitlab): address final review round — nil guards, store merge, logs, body validation
- CheckMRWatch, UpdateReviewWatch, UpdateIssueWatch: nil guards for store/req
- handlers: wsNewDiscussionNote rejects empty body
- action_presets: Update reads raw stored row (no default freezing);
Reset validates workspace_id
- service_search.GetStats: log Warn per sub-call failure before zero
- event_handlers_gitlab: log Release* dedup-row errors with context
- web/store.ts: post-slice merge restores every GitLab sub-state field;
extract buildStateOverrides helper
- web/default-state.ts: add GitLab fields to defaultState +
mergeInitialState via mergeGitLabFields helper
- web/gitlab-api.ts: UpdateXxxWatchRequest now Omit workspace_id
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix(agentctl): reap disconnected ACP sessions + MCP child trees (#1247)
Two independent leaks let opencode acp sessions accumulate indefinitely:
1. instance.Manager never reaped disconnected sessions. StopInstance
only ran via the kandev DELETE route, so any session whose backend
went away (UI closed mid-run, WS dropped, opencode failed to emit
complete) was orphaned forever.
2. Even when DELETE fired, the ACP adapter unconditionally returned
RequiresProcessKill=false. opencode acp keeps an HTTP server + MCP
child tree alive after stdin EOF, so closing stdin was a no-op and
the children re-parented to init.
Fix both:
- Plumb RuntimeConfig.RequiresProcessKill from agents.OpenCodeACP
through the ExecutorCreateRequest -> CreateInstanceRequest ->
InstanceConfig -> adapter.Config -> shared.Config chain. acp.Adapter
now reads cfg.RequiresProcessKill instead of hardcoding false.
- Belt-and-braces: process.Manager.waitForProcessExit timeout fallback
now killProcessGroup first (only falls back to single-process kill
if that errors), so any future ACP CLI that ignores stdin EOF still
gets reaped.
- Add an idle-instance reaper to instance.Manager. Per-instance
activity middleware tracks inflight HTTP requests + lastActivity;
a background goroutine sweeps every minute and StopInstance's any
instance idle past KANDEV_ACP_IDLE_TIMEOUT (default 1h, set to 0
to disable). Long-lived /agent/stream WS connections keep the
inflight counter above zero so active sessions are never reaped.
- Validate stdio MCP commands at CreateInstance time via
exec.LookPath; missing entries are dropped with a warn instead of
spawning a permanently broken child every session (fixes the
stale /snap/bin/brave MCP repro in the issue).
Regression tests: process-group kill (parent+child fixture), idle
reaper end-to-end, MCP validation, opencode-acp opt-in vs other
ACP agents' default.
* fix: address PR review feedback on #1247 fix
- idle_reaper: extract reapSingleIdleInstance helper with defer cancel(),
add reaperStop polling between sweep iterations so Shutdown drains in
one StopInstance window instead of N * timeout (cubic, claude, greptile).
- instance.Manager: use sync.Once instead of m.mu to gate reaper-stop
channel close (claude).
- mcpStdioValidationError: tolerate compound Command strings by
resolving only the first whitespace-delimited token; reject entries
with neither URL nor Command outright; document agentctl-vs-agent
PATH skew in Docker/SSH executor modes (claude, greptile, cubic).
- manager_killgroup_test: treat zombie state as dead via /proc/<pid>/stat
on Linux + bump Eventually window to 15s — CI containers' PID 1 can
delay harvesting orphans long enough to flake signal(0) probes.
- instance package: add goleak TestMain for the new reaper goroutine
(coderabbit).
- AGENTS.md: correct SIGKILL vs SIGTERM wording; clarify the timeout
fallback only fires on ctx.Done (coderabbit, cubic).
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
* fix(acp): handle claude-acp async_launched subagents end-to-end
Claude Agent SDK's Task tool with run_in_background=true returns
session/prompt with stop_reason=end_turn while subagents stay alive
in the background, reporting `_meta.claudeCode.toolResponse.status =
"async_launched"` instead of a terminal top-level status. kandev
previously treated stop_reason as authoritative, cancelled the
subagent card on prompt return, and fired an "agent finished without
producing any output" notice — even though the dispatch did produce
work and the subagent's child events would arrive seconds later.
Backend:
- `SubagentTaskPayload` gains `IsAsync` / `OutputFile` /
`CanReadOutputFile` so the async-launched envelope is preserved.
- `convertToolCallResultUpdate` recognizes async_launched as terminal
for the Task tool itself (dispatch is the completion).
- `cancelActiveToolCalls` preserves subagent_task tool calls through
the prompt-return sweep so a late terminal tool_call_update from
the SDK still lands correctly (covers the in-turn-complete case).
Frontend:
- `subagentMetaChips` emits a "background" chip when `is_async` or
`status == "async_launched"` so the card is visually distinct.
- `computeEmptyTurnNotice` suppresses the notice when any subagent
task message exists for the turn (the dispatch IS output).
Reproduced via `acpdbg prompt` against `claude-acp` 0.31.1; captured
frames in acp-debug/claude-acp-prompt-20260602-110119.jsonl drive the
backend test fixtures verbatim.
* fix(acp): harden async_launched override against future SDK frames
Address PR review feedback (greptile #1244, claude review):
- Move the async_launched override inside the mutex lock so it can
inspect the stored payload kind before flipping the status.
- Drop the `status == ""` precondition. The earlier in_progress
synthesis (Title/RawInput/Content/Locations) was implicitly trumping
the override; a future SDK frame that combines async_launched with
any of those fields would have left the card stuck in_progress
forever. The override is now unconditional for async_launched
subagent_task frames.
- Gate the override on `payload.Kind() == ToolKindSubagentTask`. A
hypothetical future claude-acp tool that reuses the `async_launched`
literal in its own `_meta.claudeCode.toolResponse.status` would have
been incorrectly marked complete by the prior unscoped check.
Pinned by two new tests:
- TestConvertToolCallResultUpdate_AsyncLaunched_OverridesInProgress
- TestConvertToolCallResultUpdate_AsyncLaunched_NonSubagentNotTerminated
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Three areas were broken in the MCP add_branch_to_task flow:
1. Predictable branch names. autoBranchName counted up from existing
branch-N rows. Concurrent add_branch calls raced onto the same name
and the worktree directory leaked ordering. Now uses a random
branch-<3 random chars> suffix via worktree.SmallSuffix.
2. Sibling materialize did not flow to the UI. The materializer
published AgentctlReady with the NEW sibling worktree's path, which
the frontend handler wrote onto the session row — clobbering the
primary's worktree_id/branch and pointing the file browser at the
sibling instead of the task root. Added TaskWorkspacePath to the
payload, detected the sibling case on the frontend (existing
worktree_id != payload's), kept the primary's id/branch, repointed
worktree_path to the task root, bumped commits refetch, and dropped
the pre-multi-repo legacy git-status entry so the Changes panel
stops surfacing the frozen single-repo snapshot. File browser's
resetKey now includes the worktree count so the tree reloads when
the sibling lands.
3. workspace_path promotion + agentctl rescan refused to act on
historical envs. A few envs had workspace_path pointing at the
source repo's local_path (orchestrator computeWorkspacePath fallback
to req.RepositoryPath when resp.WorktreePath is empty) instead of
the primary worktree. The materializer's promotion gate compared
against env.WorkspacePath's parent, so those envs were never
promoted, and agentctl's resolveRescanPath only accepted
parent-of-current so the rescan to the real task root was rejected.
Trackers stayed on the wrong directory and the sibling's commits
never appeared in the Commits panel even after a full reload.
Anchored promotion on env.TaskDirName (basename cross-check) and
added a recovery branch to resolveRescanPath that accepts any
absolute directory which actually contains git-repo subdirs — the
bearer-token middleware already gates the endpoint and
scanRepositorySubdirs validates the children, so the relaxed check
stays safe.
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* 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>
* feat: add service-managed self-update
* chore: add temporary self-update test scripts
* chore: simplify temporary script markdown fences
* chore: run frontend in temporary self-update scripts
* fix: show self-update job progress after apply
* chore: add real npm self-update test script
* fix: isolate npm self-update prefix
* fix: detect npm service installs from bin shim
* chore: reset service before real npm update test
* chore: expand real npm status diagnostics
* fix: preserve service home during self-update
* fix(cli): tee self-update helper output to a log file
* fix(cli): avoid launchd bootstrap race when reloading the macOS service
* fix(backend): run macOS self-update helper once via transient launchd plist
* fix(web): show self-update progress and lock the apply button until the update lands
* fix(cli): reinstall via the kandev wrapper on Homebrew self-update
* chore: remove temp self-update test scripts
* fix(web): drop unused apply path from useUpdates hook
* fix(backend): snapshot install state once in apply preflight
* docs: align self-update docs with transient LaunchAgent flow
* chore(release): run cli bundle from apps workspace root
* fix(backend): guard against concurrent self-update applies
* fix(backend): check scheme and helper paths in self-update apply
* fix(backend): drop duplicate binary name from npx manual command
* fix(cli): allow --help for service self-update without --intent
* fix(cli): harden self-update dir perms and darwin uid resolution
* fix(web): set required POST fields last in applyUpdate
* docs: use JSON body notation for the apply endpoint
* fix(backend): expire self-update guard so a stuck helper cannot wedge apply
* refactor(cli): collapse redundant writeSync branch in self-update log
* feat: add voice mode for chat input
Adds a voice input button to the chat composer with three transcription engines:
in-browser Web Speech API, in-browser Whisper via @huggingface/transformers, and
a server-side OpenAI Whisper fallback gated on KANDEV_VOICE_OPENAI_API_KEY.
User-configurable from a new /settings/voice-mode page:
- Master on/off toggle (defaults to on; opt-out, not opt-in)
- Engine preference (auto / Web Speech / Whisper Web / Whisper Server)
- Language hint (auto-detect or BCP-47 pinned)
- Activation mode (click-to-toggle or hold-to-talk)
- Auto-send after transcription
- Whisper Web model size (tiny / base / small)
- Editable Cmd+Shift+M keyboard shortcut
Voice settings live under user_settings.voice_mode as a nested JSON object —
no schema migration. Setting changes broadcast via the existing
user.settings.updated WS event so any open chat tab updates live.
The mic button is rendered on the right of the composer alongside the send
button (round primary fill; destructive fill with pulse ring while recording).
On browsers without a usable engine (e.g. iOS Chrome over HTTP) the button
stays visible as a muted greyed icon — tapping it surfaces a toast with the
reason (typically a missing secure context).
Whisper Web runs in a Web Worker with fp16 decoder weights (avoids the
MatMulNBits scale-missing crash that q4/q8 hit on WASM). Models are pulled
from the onnx-community/* mirror and cached by transformers.js in the Cache
Storage on first use.
* fix: address voice mode PR review feedback and E2E selector clash
Bot-review fixes from #1159:
- Map http.MaxBytesError from c.FormFile to 413 instead of bare 400.
- Strip BCP-47 region suffix before passing language hint to Whisper Web.
- Recover from worker crash: terminate + null worker + reset ready flag so
next init/transcribe creates a fresh worker instead of hanging.
- Read live store state via storeApi.getState() in voice-mode settings
savers to avoid stale-closure clobbering concurrent settings updates.
- Detach Web Speech callbacks before abort() so trailing onend/onerror
events don't mutate hook state after cancel().
- Spread caller init before method/body in voice-api fetch so a caller-
supplied init can't override the multipart POST.
- Replace t.Fatalf with t.Errorf+return inside test HTTP handler goroutine
(FailNow from non-test goroutine is unsafe).
- Add Enabled/AutoSend zeroing assertions in partial-update test so the
bool-overwrite-on-PATCH behavior is locked in explicitly.
E2E fix:
- toolbar-overflow spec used button.rounded-full which now matches both
the send and voice buttons (strict-mode violation). Switched to the
submit-message-button testid for an unambiguous locator.
* fix: scope voice settings rollback + whisper worker error to changed keys
Round 2 of PR review feedback on #1159:
- voice-mode settings rollback now restores only the keys patched by the
failing request (not the whole snapshot), preserving concurrent edits
to unrelated voiceMode fields.
- Shortcut rollback diffs previous vs next and reverts only the changed
keys (restore prior value or delete if absent), preserving other
concurrent shortcut edits.
- Whisper worker error handler captures the worker reference at attach
time and only clears refs when this.worker still matches it — a stale
error from a previously replaced worker no longer nukes the active one.
- Update stale whisper-web-models JSDoc that still pointed at Xenova
mirrors.
* fix: voice mode progress scale, hold-mode race, race-aware rollback
Round 3 of PR review feedback on #1159:
- Normalise Whisper progress to 0–1 in ensureWhisperClient (transformers.js
emits 0–100, the button's display + the `ready: 1` convention expect
0–1 — fixes the 5000% mid-download display).
- Claim driverRef synchronously at the top of finishCapture so concurrent
pointerup + pointerleave invocations early-return instead of racing and
clobbering a freshly-started recording's ref.
- Race-aware rollback: voice-mode and shortcut catch branches now skip
reverting a key when latest[key] !== next[key]. A failed earlier request
no longer overwrites a later successful save to the same key.
- Update the Worker wire-protocol comment to show onnx-community/whisper-base
instead of the stale Xenova/* example.
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
* fix(orchestrator): auto-resume dormant sessions after workflow queue
When a workflow auto-start prompt is queued after a transient ACP stream
disconnect, no live agent exists to drain the queue via agent.ready.
The prompt sits orphaned until the user manually resumes the session.
Add scheduleAutoResumeForWorkflowQueue (called from queueAutoStartPrompt)
which fires tryEnsureExecution in the background when GetExecutionBySession
shows no live agent. This drives ResumeSession → agent.boot_ready →
handleAgentBootReady → drainQueuedMessageAfterTransition — the same path
a manual resume would trigger.
Guard with s.executor == nil so existing tests that don't wire the executor
are unaffected. Add isAgentRunningFn override to mockAgentManager for
mid-sequence state transitions, and add a regression test that exercises
the full chain end-to-end.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(orchestrator): clarify scheduleAutoResumeForWorkflowQueue boundary condition
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(backend): make repository setup script failures non-fatal
A failing repository setup script aborted environment preparation and
stopped the agent ("Environment setup failed"), and Resume / Start-fresh
couldn't recover because each relaunch re-ran the same failing script.
runWorktreeSetupScript (run inside worktree creation) treated a non-zero
exit as fatal: it deleted the worktree and returned an error, which
cascaded to EnvPrepareResult{Success: false} and aborted the launch. This
contradicted the already-non-fatal task/profile-level setup script path.
The repo-level script is now non-fatal too: on failure it logs a warning,
records SetupScriptWarning on the worktree, keeps the worktree, and lets
the launch proceed. The env preparer surfaces it on the "Create worktree"
step so the panel shows completed_with_warnings instead of failed. The
full script output still streams to the script_execution chat message.
Adds backend regression tests and an e2e test (verified red before / green
after) asserting the agent still launches when the repo setup script fails.
* fix(backend): preserve base-branch warning when setup script also fails
Addresses review feedback: the post-Create block surfacing SetupScriptWarning
on the "Create worktree" step unconditionally overwrote BaseBranchFallbackWarning
when both occurred, silently dropping the "wrong branch" notice. Append instead
of replace so both survive.
Also updates TestWorktreePreparer_SetupScriptFailure_KeepsCreateWorktreeStepCompleted
to the non-fatal contract: a failed setup script now yields a successful prepare
with a warning on the step, not a failed prepare.