* 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>
* chore: remove orphan executor tracing file and deprecated ResolveScript wrappers
- Delete internal/agentctl/tracing/executor.go (7 TraceExecutor* funcs)
- Delete internal/orchestrator/executor/script_template.go (deprecated wrapper)
- Delete scriptengine.ResolveScript convenience function
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: remove orphan setter methods across healthpoll, jira, linear, slack, updates
- Delete Poller.SetInterval from integrations/healthpoll (no callers)
- Delete Poller.SetIssueTickHook from jira and linear (no callers)
- Delete Trigger.SetInterval and Trigger.SetMatchHook from slack (no callers)
- Delete Service.SetPollInterval from system/updates (no callers)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: remove orphan wrapper functions and accessor method
- Delete updates.FetchLatestRelease shim (wraps FetchLatestReleaseFrom, no callers)
- Delete usage.NewClaudeUsageClient and NewCodexUsageClient shims (no callers)
- Delete database.Service.Path() accessor (no callers)
- Update ErrGitHubRateLimited godoc to reference FetchLatestReleaseFrom
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: remove unwired handleListExecutors MCP handler
The sibling profile handlers (list/create/update/delete) are registered
via RegisterFunc in handlers.go. handleListExecutors never was — dead flow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: remove stale hook-field comments after setter deletion
SetIssueTickHook (jira, linear) and SetMatchHook (slack) were removed
in the deadcode cleanup. Their struct fields still claimed "tests use
this" — no test or caller can set these fields now.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: remove orphan hook fields and fire methods after setter deletion
issueTickHook (jira/linear) and matchHook (slack) are unexported and
never set after their public setters were removed; fireIssueTickHook /
fireHook were permanent no-ops.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(backend): forward base_branch override for same-repo subtasks via MCP
The create_task_kandev MCP handler dropped base_branch silently unless
the caller also passed a repository identifier. Same-repo subtasks then
inherited the parent's repos verbatim, so "branch off feature/X" calls
landed on the parent's branch instead.
Forward base_branch at the top level of the WS payload and apply it as
an override on the resolved repo list when no per-repo entries were
supplied; explicit per-repo BaseBranch wins as before.
* docs(backend): correct BaseBranch field comment to match actual fallback semantics
The struct doc on `req.BaseBranch` claimed it won over explicit per-repo
entries, but the override only fires when `len(req.Repositories) == 0`.
Reword as a fallback applied to inherited/resolved repos when no
per-repo list is supplied; explicit per-repo BaseBranch stays
authoritative when set. Flagged by CodeRabbit and Greptile.
* refactor(backend): forward top-level base_branch only when no per-repo entries
When the MCP caller supplies a repo identifier, the per-repo base_branch
is already authoritative on the backend side, so emitting a top-level
base_branch in the same payload was dead weight that the backend's
override gate explicitly ignored. Move the top-level forward into the
else of hasRepo so the two paths are mutually exclusive and the wire
contract reads as "top-level base_branch means inherit-with-override".
Flagged by the Claude review bot.
* feat(orchestrator): persist N-entry FIFO message queue
Replaces the single-slot, latest-wins, in-memory queue with a SQLite-backed
FIFO queue (default cap 10 per session, configurable via
KANDEV_QUEUE_MAX_PER_SESSION). Drain stays one-per-turn so each entry gets its
own user message and `on_turn_*` workflow cycle. Inter-task `message_task_kandev`
returns a structured `queue_full` error with sender attribution when the
target session is saturated, so the calling LLM can decide whether to retry.
Surfaces a new WS action `message.queue.remove` for single-entry deletion;
`message.queue.cancel` now means clear-all. Edit/remove are keyed on the
entry's UUID, returning `entry_not_found` when atomic-take won the drain race.
* feat(web): N-entry queue state, API, and optimistic hook
Replaces the single-slot QueueStatus with an ordered `entries[]` per session
plus capacity meta (`count`, `max`). API exposes typed `QueueFullError` and
`QueueEntryNotFoundError`; the hook surfaces optimistic edit/remove with
drain-race rollback (refetch on `entry_not_found`).
* feat(web): inline ghost-message queue UI with sender chips
Replaces the floating QueuedMessageIndicator overlay with a stack of ghost
messages rendered above the chat input. Each ghost shows a sender chip
(user / agent) and per-entry edit/remove. A small "n/max queued · clear all"
pill summarizes capacity. Inter-task entries are visually distinct and edit
is gated to the original sender.
* fix(queue): address review — auth guard, TOCTOU, error UX
- Add `messagequeue.QueuedByUser` / `QueuedByAgent` constants and centralize
the `queue_full` error code so the WS and MCP handlers stay in sync.
- WS `message.queue.{add,append,update}` default empty user_id to QueuedByUser
so the UpdateContent ownership guard always runs against a non-empty owner;
inter-task entries (`queued_by="agent"`) can no longer be overwritten by
user-initiated edits even when `user_id` is omitted from the payload.
- memoryRepository: hold the mutex through the whole append-or-insert path
(prior split released the lock between tail-check and Insert, allowing two
same-sender callers to race past coalescing). TransferSession now shifts
source positions past the destination's max so transferred entries always
drain after pre-existing destination entries, matching SQLite semantics.
- Frontend: surface edit/clear/remove failures via `sonner` toasts (drain race
→ "message already sent"); exit edit mode on failure instead of leaving the
textarea silently stuck. Hide the remove button on read-only (agent) ghosts.
- `useQueue.clearAll` no longer derives `max` from pre-clear entry count;
resets to `metaMax ?? 0` so the next status_changed event lands authoritative
capacity.
- `senderKindOf` drops the unreachable metadata fallback; route on `queued_by`
alone since `dispatchTaskMessage` always sets it.
- Test loggers now write to stderr per CLAUDE.md "stderr for logs" guideline.
* test(web): align queue e2e selectors with new ghost-message UI
The ghost-message rewrite renamed button titles ("Cancel queued message"
→ "Remove queued message", "Edit message" → "Edit queued message") and
introduced a `data-testid="queued-ghost-list"` container. The
`message-queue.spec.ts` Playwright tests still pointed at the old titles.
* fix(queue): address second review pass — auth, drain race, tests
- WS handlers (`add` / `append` / `update`) now reject `user_id="agent"`
outright, closing the gap a hostile WS client could exploit by
impersonating the inter-task sender identity to overwrite agent
entries (greptile P1).
- WS `update` / `remove` now require `session_id` so the post-mutation
`MessageQueueStatusChanged` broadcast can never be silently skipped.
- `sqliteRepository.TakeHead` checks `RowsAffected()` after DELETE;
when two concurrent take callers race past the busy-timeout the
loser sees 0 affected rows and now returns `nil, nil` instead of
the already-drained message (would otherwise dispatch the same
prompt twice).
- `Service.GetStatus` returns `Entries: []` instead of `nil` on the
error path so the wire shape stays consistent (frontend already
guards but strict consumers wouldn't tolerate the type mismatch).
- New tests: `queue-api.test.ts` covers `rethrowQueueError` mappings
(queue_full / entry_not_found / generic / Error pass-through);
`session-slice.upsert.test.ts` covers `setQueueEntries`,
`removeQueueEntry`, and `clearQueueStatus`. Backend gains an
agent-impersonation handler test.
- `rethrowQueueError` now surfaces `WSError.message` for non-queue
errors instead of stringifying the whole object.
- Cleanup: replace unused `a, c := ...` discards in service_test
with `_, _ = ...`.
* fix(queue): scope update/remove by session_id (cross-session deletion guard)
The MCP `queue_full` error payload deliberately discloses sibling-task
entry IDs (so the calling agent's LLM can reason about the wedge state).
Without a session_id filter on `DeleteByID` / `UpdateContent`, a hostile
agent that received that payload could call `wsRemoveEntry` /
`wsUpdateMessage` against another task's entries and delete or rewrite
them — only the per-entry UUID was being checked.
- `Repository.UpdateContent` and `Repository.DeleteByID` now require
`sessionID` and add `AND session_id = ?` to the SQL guard. The memory
repo mirrors the scope (lookup keyed on session before the entry scan).
- `Service.UpdateMessage` / `Service.RemoveEntry` take `sessionID`; the
WS handlers thread `req.SessionID` through (already validated as
non-empty in the previous fixup).
- TypeScript: `session_id` is now required on `updateQueuedMessage` /
`removeQueuedEntry` params so callers fail at compile time, matching
the backend contract.
- Tests cover the cross-session attack: `RemoveEntry`/`UpdateMessage`
with an attacker-supplied session_id but a real entry id from another
session must return `ErrEntryNotFound` and leave the victim entry
untouched. SQLite repo gets parallel coverage.
* fix(queue): propagate transfer error + tighten WSError narrowing
- `Service.TransferSession` now returns an error so callers can fail
closed instead of silently leaving entries orphaned on the previous
session. The two existing call sites pick the right policy:
`switchSessionForStep` (reusing an existing session) propagates the
error so the workflow switch aborts; the new-session path logs and
continues, since the new session is already committed and rolling
back here would leave a half-switched workflow.
- `asWSError` now also narrows on `message` / `details`, so WS payloads
without a `code` field are still mapped to a meaningful `Error.message`
via `rethrowQueueError` (instead of "[object Object]"). Native
`Error` instances explicitly skip the WS-error branch so they pass
through untouched, preserving the existing pass-through guarantee.
- `queue-api.test.ts`: `expect.assertions(N)` on the catch-only tests
so a missing throw fails the test instead of silently passing.
* fix(queue): check TransferSession error in service tests
The TransferSession signature change in 83abe6d7 broke errcheck on two
test sites that hadn't been updated. Wrap them in require.NoError.
* refactor(mcp): reframe cross-task agent message wrapper to authorize action
Receivers were refusing peer-agent messages with "the user can talk to
them directly" and stalling. The old wrapper demoted peer messages
("rather than a direct user instruction") and explicitly licensed
refusal ("you may decline, push back"). New wording states the user is
not reading the thread, frames the peer as coordinating on the user's
behalf, and directs the receiver to act with user-prompt authority.
Reply path through message_task_kandev is unchanged.
* test(mcp): pin authorization wording in wrap_agent_message tests
Three reviewers (CodeRabbit, Greptile, Claude) flagged that the
behavioral intent of the new wrapper lives entirely in the body string
yet has no unit-level guard. Add assertions that pin the reply tool
name, the user-prompt-authority phrase, and the absence of the old
refusal-licensing wording, plus a short comment on the deliberate
privilege-escalation trade-off so future readers understand why peer
agents inherit user-prompt authority.
* feat(backend): multi-question support for ask_user_question_kandev
Reshape the MCP tool from a single prompt+options pair to a questions[]
array (1-4 entries, single-choice each, custom text fallback). The
clarification HTTP handler gates the response until every question has an
answer, fans out one chat message per question (sharing pending_id, with
question_index/question_total in metadata), and returns a JSON map keyed
by question id back to the agent. The orchestrator resume prompt now
formats multi-line Q1/A1, Q2/A2 summaries when the bundle has more than
one question. Mock-agent gets a clarification-multi scenario and
ask_user_question_test.go covers the new schema + map response. No
back-compat shim — local-first tool, single-PR breaking change.
* feat(web): refresh clarification overlay with multi-question + modern UI
Each per-question chat message gets its own card with a "Question N of M"
progress chip, a ✓ Answered badge once committed, and shadcn-aligned
option buttons with kbd hints. A new useClarificationGroup hook coordinates
the per-question commits (decision A) and posts every answer in a single
batch once the user finishes the bundle; X rejects the whole group. The
single-question case keeps the 1-9 / Esc keyboard shortcuts. Playwright
e2e adds 6 multi-question scenarios (rendering, full happy path, partial
state, mixed option/custom, skip-rejects-bundle, sibling-state) on top of
the existing single-question regression suite.
* fix(web): clarification carousel UX with stepper navigation + race fix
Replaces the vertically stacked card layout with a horizontal stepper:
numbered circle dots up top show step state (active/answered/pending),
the single active question card sits in the middle, and Back / Next /
Submit buttons sit at the bottom. Picking an option auto-advances to the
next step (single-question case auto-submits, no explicit Submit button).
The stepper steps are clickable so users can revise prior answers.
Keyboard shortcuts: 1-9 for option pick, ArrowLeft/Right for navigation,
Esc for skip.
Also fixes a race that surfaced as a 400 BadRequest on the
/api/v1/clarification/:id/respond endpoint when the user clicked an
option before all N messages of a multi-question bundle had streamed in:
findPendingClarificationGroup now gates on metadata.question_total and
returns an empty array until the full bundle has arrived.
Playwright suite expanded from 6 to 12 multi-question scenarios:
stepper rendering, auto-advance, Back, stepper jump, Submit gating,
happy path, mixed custom-text, revise-via-jump, skip-from-mid-step,
number-key shortcuts, Esc skip, first-step Back disabled.
* fix(clarification): address CodeRabbit/Greptile/Claude review comments
Backend:
- Single source of truth for question validation: rename/export
NormalizeAndValidateQuestions and call it from both the HTTP and the
WebSocket-side MCP handlers, so duplicate question ids and bad option
counts can't slip through either path.
- httpRespond now validates that every answer references a real question
id from the bundle and that no id is answered twice (regression tests
added).
- questionsFromMessages reorders by metadata.question_index so the
reconstructed bundle preserves the original question order.
- adapters.CreateClarificationRequestMessages rolls back already-created
messages when a later write in the bundle fails.
- extractQuestionAnswers always returns a JSON map (even on rejection,
with an envelope { rejected, reject_reason }) and simplifyAnswer
preserves both selected_option and custom_text when both are present.
- MCP tool description: rewrite the response example to match the actual
output shape; add minItems / maxItems to the questions and options
arrays in the JSON Schema.
- Test helper: drop the local contains() shim, use strings.Contains.
Frontend:
- useMemo([props]) was a no-op (props is always a fresh ref); split into
[message, messages] so it actually memoizes.
- Drop the dead "expired" SubmitState branch — the backend never returns
410 from this endpoint.
- Remove the asymmetric option-list dimming: the agent now receives both
selected_option and custom_text together, so a half-grey state was
misleading.
- New tests: pending-clarification.test.ts covers
findPendingClarificationGroup including the question_total gate, and
use-clarification-group.test.ts covers the hook's record/submit/skip
flows and the override-merge path used by the single-question case.
* fix(web): treat clarification 409 as success so duplicate submit doesn't stick the overlay
* fix(clarification): unblock agent on persistence failure + nil-guard rejected branch
- Add Store.CancelRequest(pendingID) and call it from both the WebSocket
MCP handler and the HTTP create-request handler when message creation
fails. Without it, the agent's WaitForResponse parked for the full
2-hour MCP timeout while no clarification cards ever reached the user.
Both handlers now return an explicit error response on failure.
- applyAnswersToMessages now guards h.repo == nil in the rejected branch
(parity with sibling expectedQuestionIDs).
- extractQuestionAnswers stubs for unanswered questions in a rejected
bundle now carry { answered: false, rejected: true } so agents reading
per-question payloads see the rejection without inspecting the
envelope.
- Hook test: added 409-as-ok regression coverage for both submit and
skip paths so the previous fix can't drift.
- Store test: covers CancelRequest unblocking a parked WaitForResponse.
* fix: drop unused e2e helper + use channel sync in CancelRequest test
* refactor(web): drop dead 'message' prop from clarification overlay
Both call sites pass 'messages' (the bundle group), so the legacy
single-message prop was unused. Tightening the prop type also lets the
useMemo depend on a single value.
* fix(web): ignore modifier-keyed events in clarification carousel shortcuts
Bare 1..9 / ArrowLeft / ArrowRight / Esc still drive the carousel, but
Cmd/Ctrl+1..9 (browser tab switch) and Alt+ArrowLeft (browser back) now
pass through as the user expects.
* feat(slack): add cookie-mode integration that triages messages via utility agent
Adds an install-wide Slack integration that polls the connected user's
own messages for `!kandev <instruction>`, hands the thread context to a
configured utility agent with the Kandev MCP server attached, and
creates the resulting task. Auth uses the user's xoxc- token + d cookie
so it works on locked-down Enterprise Slack tenants where bot installs
aren't possible.
Shared infra:
- ACP host-utility executor accepts McpServers on session/new
- New MCP tool list_repositories_kandev so the triage agent can pick the
right repo
- Template engine exposes Slack-specific variables for prompt injection
- secretadapter + healthpoll reused from jira/linear
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(slack): address PR review feedback round 9
- Drop dead `var _ = time.Second` no-op and its misleading comment in
trigger_test.go.
- `RecordAuthHealth` now invalidates the cached client when the probe
fails, so the poller self-recovers from credential rotation without
requiring a re-save.
- Stable secondary sort key (`workspace_id DESC`) on the legacy →
singleton migration so promoted-row selection is deterministic on
`updated_at` ties.
- Extract `categorySlack` constant in `engine.go` and update the
`VariableInfo.Category` doc-comment to list the full set of values.
- Realign `slack-integration/spec.md` with what shipped: cookie-mode +
`!kandev` triage as v1, with bot install / User OAuth / reaction &
slash & shortcut triggers moved under Future scope.
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>
* feat(orchestrator): carry sender_task metadata through user message paths
Add WithSenderTask builder to UserMessageMeta and a metadata-aware
QueueMessageWithMetadata variant on the message queue. The drain path
in event_handlers_agent merges queued metadata into the recorded user
message so attribution survives the queue → DB transition.
* feat(mcp): attribute message_task_kandev calls to the sending agent
The MCP server inside agentctl now injects sender_task_id and
sender_session_id from its own server context. The backend handler
validates the sender (rejecting missing IDs, self-messages, and unknown
tasks), looks up the sender title, wraps the prompt in a <kandev-system>
attribution block, and propagates structured metadata across all three
delivery paths (queue, created-session, record+prompt). The wrapped
content lets the receiving agent identify the peer both live and on ACP
session resume; the existing sysprompt strip pipeline hides the wrapper
from the UI.
* feat(web): render sender-task badge on cross-task agent messages
User messages whose metadata carries sender_task_id now render a
clickable purple badge above the bubble, linking to the source task.
The badge live-resolves the sender's title from the kanban store and
falls back to a greyed snapshot when the task isn't loaded
(cross-workspace, deleted, etc.). Adds a non-subscribing useTaskById
selector so badges don't fan out WS subscriptions per message.
* test(e2e): cover cross-task agent message attribution
Eight scenarios driven through the mock-agent's MCP script directive:
queue path, prompt path, self-message rejection, unknown-target error,
badge link target, live title re-resolution after rename, kandev-system
wrapper preservation in raw_content, and prompt bodies containing their
own kandev-system tags. Adds an updateTaskTitle helper to ApiClient.
* fix: address CodeRabbit + Greptile P1 findings on PR #819
- requeueMessage now forwards queuedMsg.Metadata via QueueMessageWithMetadata
so sender attribution survives transient delivery failures and retries.
- CREATED-session branch in handleMessageTask records the user message AFTER
StartCreatedSession succeeds, so a launch failure no longer leaves an
orphaned attributed chat row.
- SenderTaskBadge: collapse identical displayTitle/fullTitle locals.
- E2E: extract waitForTargetIdle helper; replace the 1.5s raw sleep in the
collision-check test with the same poll the rest of the suite uses.
* fix: address Claude review suggestions on PR #819
- wrapAgentMessage strips </kandev-system> from the embedded sender title
before formatting. Without this, a title containing the closing tag
would short-circuit the non-greedy strip regex and leak the attribution
tail into the visible UI bubble. Metadata snapshot keeps the original
title so the badge still displays it.
- Add a regression test covering the malicious-title case.
- Document the deliberate cross-workspace messaging policy on the sender
lookup so future readers don't add a workspace-scope check (Q13 of the
feature plan locked this as intentional).
* fix(backend): return mcp message_task once dispatched, not after target turn ends
The MCP message_task_kandev tool was synchronously waiting for the target
session's entire turn to complete before returning, even though its
description advertises a fast dispatch status. For idle target sessions,
the call would hang for the full duration of the agent's response (with
no upper bound — waitForPromptDone has no timeout).
Threads a dispatchOnly flag from the MCP handler down through
PromptTask → executor.Prompt → PromptAgent → SendPrompt. When set,
SendPrompt returns once agentctl.Prompt accepts the prompt instead of
blocking on the agent's complete event. Stale completion signals from
prior dispatch-only prompts are drained at the start of SendPrompt so
they don't poison the next waiting call.
* fix: address PR review feedback
- Replace time.Sleep with channel-based wait for WS connection in new
SendPrompt tests, per CLAUDE.md (channel sync over time.Sleep for
non-subprocess tests).
- Drop redundant Warn that logged the remapped sentinel after the
original error was already logged by the preceding Warn.
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
* feat(orchestrator): reuse existing same-profile session on workflow step switch
When a workflow step transition needs a different agent profile, look up
an existing session on the same task that already uses the target profile
and promote it back to primary instead of creating a fresh one. Reuses
COMPLETED and FAILED sessions (CANCELLED stays excluded — explicit user
stops shouldn't be auto-revived).
Backend (event_handlers_workflow.go):
- findReusableSessionForProfile: most recently updated session matching
the target profile, skipping CANCELLED.
- reuseSessionForStep: promotes the session to primary; if terminal,
revives via reviveReusedSession.
- reviveReusedSession: distinguishes "previously launched" (has
executors_running record -> flip to WAITING_FOR_INPUT, lazy-resume via
ResumeSession with stored token) from "never launched" (no record ->
flip to CREATED so autoStartStepPrompt routes through
StartCreatedSession for a full fresh launch). Clears stale
ErrorMessage/CompletedAt so the UI stops surfacing prior failures.
- createNewSessionForStep + completeAndStopSession: shared helpers.
Backend (task_operations.go):
- StartCreatedSession no longer calls processOnTurnStartViaEngine.
User-initiated messages already fire on_turn_start in
dispatchPromptAsync. Workflow-driven auto-start prompts arrived
because the workflow just transitioned us into this step via
on_turn_complete; firing on_turn_start again cascades the workflow
back out and the redirect-to-active-session logic delivers the prompt
to the wrong session's agent.
Frontend (web):
- task.updated handler follows focus to the new primary when the user
is viewing the task and was sitting on the previous primary.
- pinnedSessionId tracks user-initiated session selection
(setActiveSession sets it; setActiveSessionAuto leaves it alone).
The terminal-handoff branch in maybeAdoptSessionOnTransition skips
replacement when the terminating session matches the pin, so manually
clicking a session no longer yanks focus back to the workflow primary
each time the pinned session terminates.
Tests:
- TestSwitchSessionForStep_ReusesExistingProfileSession (never-launched).
- TestSwitchSessionForStep_ReusesPreviouslyLaunchedSession (launched).
- TestSwitchSessionForStep_ReusesFailedSession (FAILED reuse + cleanup).
- tasks.test.ts: focus-follow behavior across 4 scenarios.
- agent-session.test.ts: pinnedSessionId guard plus migration to
setActiveSessionAuto for WS-driven adoption.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(orchestrator): deferred task move with hand-off prompt + boot/turn-end event split
Adds move_task_kandev MCP tool that lets a workflow agent (e.g. QA at "Review")
move a task to another step with a prompt for the receiving agent. The move is
queued as a PendingMove and applied on the calling agent's turn-end so on_enter
processing doesn't race the still-running turn.
Also splits agent.ready into agent.boot_ready + agent.ready at the lifecycle
layer. Previously both signals shared one event and the orchestrator
disambiguated them via a resumeInProgressSessions flag — which had a race
when the boot signal arrived before persistResumeState wrote state=STARTING,
causing the boot to fire on_turn_complete against the just-revived step and
ping-pong the task forward through the workflow.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix: address PR #743 review feedback (Greptile + CodeRabbit)
Backend:
- isTransientPromptError no longer treats ErrExecutionNotFound as transient,
preventing executeQueuedMessage from infinite-requeueing on permanent
missing-execution errors. autoStartStepPrompt's explicit errors.Is check
before the transient branch keeps the recovery path working (Greptile P2 #1)
- applyPendingMove drains the orphaned hand-off prompt on ApplyTransition
failure so it can't be misdelivered to the source step on a future turn
(Greptile P2 #2)
- handleMoveTask returns errors instead of panicking when messageQueue is
nil; queueMoveTaskPrompt returns an error so the deferred path fails fast
if the queue can't accept the prompt (CodeRabbit CR-2)
- handleMoveTask rejects when no primary session exists and a hand-off prompt
is required, instead of silently dropping it (CodeRabbit CR-3)
- autoStartStepPrompt preserves attachments from queued messages (was: silently
dropped); attachment-only messages are no longer skipped; the fallback path
also forwards attachments (CodeRabbit CR-5)
- applyPendingMove reinserts the pending move on early failure paths (config
missing, task/step load errors) so a future turn-end retries naturally
(CodeRabbit CR-6)
- registerKanbanTools split into 3 helpers (list/profile/task) to satisfy
backend function-size lint limits (CodeRabbit CR-1)
Web:
- maybeAdoptSessionOnTransition's auto-adopt path also checks pinnedSessionId
to handle the reverse-event-ordering race (old COMPLETED arrives before new
STARTING) — pinned sessions are no longer yanked off the user. Regression
test added (CodeRabbit CR-7)
Tests:
- mockAgentManager records (executionID, prompt) pairs; new helper
capturedPromptsForExecution actually filters by exec ID instead of
returning all captured prompts (CodeRabbit CR-4)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(web): respect pinned session in primary-follow-focus path
When the user clicks a session, setActiveSession sets pinnedSessionId. If a
workflow profile switch then updates the task's primarySessionId while the
user happens to still be on the previous primary, the focus-follow block in
tasks.ts was calling setActiveSessionAuto(taskId, newPrimary) and silently
overriding the explicit pin. Add pinnedSessionId !== previousPrimary as a
fifth condition. Regression test in tasks.test.ts covers exactly this
sequence.
Addresses Greptile P1 on PR #743.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix: address CodeRabbit review on PR #743 (3 new comments)
- handleMoveTask idle path now rolls back the queued hand-off prompt when
taskSvc.MoveTask fails, so a "you were moved..." message can't be delivered
on a future turn for a transition that didn't actually happen. Added
TakeQueued to the MessageQueuer interface to support this.
- lookupSession now returns (*models.TaskSession, error) so callers can
distinguish a backend lookup failure from a legitimate "no primary session"
state. handleMoveTask maps the error case to ErrorCodeInternalError instead
of collapsing it into ErrorCodeValidation.
- queueMoveTaskPrompt returns errors instead of log-and-continue (signature
change already shipped); existing tests updated to reflect the new contract.
- Refactored TestPendingMove_ReviewToInProgress_OneTransitionOnly: the
~280-line monolithic test is now split into a pendingMoveScenario builder
+ assertion helpers, each well under the 80-line / 50-statement lint limit.
All 14 review threads resolved.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix: relax move_task_kandev prompt requirement; fix CI lint
CI surfaced two issues after rebasing onto main:
- nestif lint warning on autoStartStepPrompt's queued-message merge block.
Extracted the take-and-merge logic into takeAndMergeHandoffMessage helper,
flattening the nested conditionals.
- E2E test settings/config-management 'agent can move a task to a different
step' failed because my CR-3 fix made the move_task_kandev prompt parameter
required. That broke admin/config-mode moves where there's no agent to
hand off to. Made prompt optional: when supplied AND the source session
is mid-turn, the deferred-move flow runs (queue prompt + record
PendingMove); otherwise just move the task. Refactored handleMoveTask
into deferMoveTaskWithPrompt and applyMoveTaskImmediate to keep each
function under the 80-line lint limit.
Tool descriptions in mcp/server/server.go and mcp/server/config_handlers.go
updated to reflect prompt's now-optional status. moveTaskHandler in
config_handlers.go forwards prompt only when set.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(mcp): treat 'no primary session' as a not-found, not an internal error
GetPrimarySessionByTaskID returns a plain error string ("no primary session
found for task: <id>") when the repo has no rows. My CR-B refactor was
treating ALL lookup errors as ErrorCodeInternalError, which broke the E2E
test 'agent can move a task to a different step' (config-mode move on a
freshly-created task that has no agent yet).
Match the not-found message and return (nil, nil); only real backend
failures bubble up as InternalError. Distinguishing properly with
errors.Is would require an exported sentinel that doesn't exist; matching
the noRowsErr substring is the pragmatic fix.
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>
* feat(mcp): add start_agent and local_path params to create_task
Add control over task creation behavior in MCP create_task tool:
- Add start_agent boolean param (default true) to optionally skip
auto-starting an agent when creating tasks
- Add local_path param for specifying local repository folder path
when creating top-level tasks (worktree flow)
- Add repository_id and base_branch params for explicit repo config
- Improve tool description with clear guidance on when to use
parent_id='self' vs creating top-level tasks
- Update backend handler to respect start_agent flag
- Relax description requirement for subtasks when start_agent=false
This fixes the issue where agents creating tasks from plans would
always auto-start immediately and couldn't specify repositories
for top-level tasks.
* test(mcp): add integration tests for source_task_id inheritance paths
Cover the source-task path in resolveTaskRepositories that had no test
coverage at any layer: valid source_task_id inherits repos and missing
source_task_id silently falls through rather than failing.
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
---------
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: João Salavisa <undefined@users.noreply.github.com>
* feat: add MCP mode pipeline for config vs task tool registration
Thread McpMode field through the entire instance creation chain:
CreateInstanceRequest → InstanceOverrides → InstanceConfig → MCP Server.
This allows config-mode MCP sessions to register a different set of
tools than the default task-mode sessions.
* feat: add config-mode MCP tools and agentctl handlers
Register config-mode tools (workflow steps, agents, MCP config, tasks)
when MCP server mode is "config". Add 16 new WS action constants and
agentctl-side handlers that forward tool calls to the backend via the
existing BackendClient channel.
* feat: add backend MCP handlers for config-mode operations
Add backend WebSocket handlers for workflow step CRUD, agent CRUD,
MCP config get/update, and task move/delete/archive/state operations.
Wire config dependencies via SetConfigDeps pattern to avoid breaking
the existing NewHandlers constructor.
* feat: add config-mode system prompt and unit tests
Add ConfigContext system prompt listing all config-mode MCP tools.
Add unit tests for mode-based tool registration, agentctl config
handler forwarding, backend handler validation, and system prompt
content.
* test: add exhaustive E2E tests for config-mode MCP handlers
38 end-to-end tests covering the full handler → service → DB pipeline
for all 16 config-mode operations: workflow step CRUD, agent CRUD,
agent profiles, MCP config, and task management (move, delete, archive,
update state). Includes handler registration verification and two
full pipeline integration tests.
* fix: remove E2E test that depends on agent CLIs installed in CI
TestE2E_Agent_CreateMultipleAndList tried to create claude-code and
codex agents, which aren't available in CI. Agents are unique by type
ID so creating duplicates also fails. Removed the test since
CreateAndList already validates the create+list flow.
* test: remove Go E2E tests for config-mode handlers
Replacing with Playwright E2E tests.
* test: add Playwright E2E tests for config-mode features
13 tests covering workflow steps, task operations, agent/profile
settings pages, MCP config, and workflow settings round-trips.
* feat: add dynamic MCP mode reconfiguration for agentctl instances
Add SetMode() to the MCP server that clears and re-registers tools for
the new mode. Expose it via PUT /api/v1/mcp/mode on per-instance servers,
a SetMcpMode client method, and a lifecycle Manager.SetMcpMode wrapper.
This enables switching from task to config MCP tools on workspaces that
were prepared before plan_mode was set on the session.
* feat: wire McpMode through executor launch and resume pipelines
Add McpMode to LaunchOptions so callers can set it at launch time.
StartTask and StartTaskWithSession now pass McpMode="config" when
plan mode is active. startAgentOnExistingWorkspace reconfigures the
MCP server via SetMcpMode before starting the agent subprocess.
The AgentManagerClient interface gains SetMcpMode; the lifecycle
adapter, executor, and all mock implementations are updated.
* fix: pass plan_mode from workflow step when auto-starting moved tasks
handleTaskMovedNoSession was hardcoding planMode=false when calling
StartTask. Now it loads the target step and checks for enable_plan_mode
in on_enter actions, passing the correct value so config-mode MCP tools
are registered for the agent.
* test: fix config-mode E2E tests and add SetMcpMode mock stubs
- Add SetMcpMode stubs to all AgentManager mock implementations
- Fix strict mode violations with { exact: true } on tool name assertions
- Expand collapsed tool call groups before asserting individual names
- Remove temporary diagnostic code
* fix: address CodeRabbit review comments
- Add servers parameter to update_mcp_config tool schema
- Add mutex protection to SetMode for thread safety
- Create session before building prompt so config context has session ID
- Fix inconsistent ask_user_question tool name reference (use _kandev suffix)
* refactor: use canonical ws.ActionMCP constants and handle marshal errors
- Import ActionMCP* constants from pkg/websocket instead of duplicating
them locally in config_handlers.go and handlers.go
- Handle json.MarshalIndent error in forwardToBackend instead of ignoring it
* fix: extract McpModeConfig constant and fix Prettier formatting
- Add McpModeConfig constant to avoid repeated "config" string (goconst)
- Fix Prettier formatting in config-management E2E spec
* fix: validate task state before update and strengthen sysprompt tests
- Add TaskState validation in handleUpdateTaskState to return proper
validation error instead of internal error for invalid states
- Strengthen format-contract assertions (exact %s counts, no %! markers)
- Extract repeated test data into constants per project guidelines
* refactor: decouple config MCP mode from plan_mode, use session metadata
Replace plan_mode-based config MCP activation with config_mode in
session metadata. Add isConfigModeSession() helper used across
executor_execute, executor_interaction, and executor_resume.
Propagate task.Metadata to session.Metadata in PrepareSession so
config_mode flows from task creation through to the agent launch.
* refactor: remove task tools from config MCP mode and update sysprompt
Remove task management tools (list_tasks, move_task, delete_task,
archive_task) from ModeConfig registration — they remain available in
ModeTask. Update ConfigContext system prompt to remove task tool docs
and add example requests section. Update MCP server tests for new tool
count (18→14) and remove deleted task handler tests.
* feat: add config-chat endpoint and workspace config agent profile
Add default_config_agent_profile_id column to workspaces with SQLite
migration. Add POST /workspaces/:id/config-chat endpoint that creates
an ephemeral task with config_mode metadata and starts a session with
config MCP tools. Extract resolveConfigChatDefaults helper to stay
within lint complexity limits.
* feat: add config chat UI with workspace settings integration
Add config chat modal component and hook for starting config sessions.
Add configChat state slice with open/close actions. Add workspace API
client for config-chat endpoint. Add configuration agent profile
selector to workspace settings form. Add config chat button to
settings layout sidebar.
* test: rewrite config-mode E2E tests to use metadata and session_id
Rewrite config-management E2E tests to use createTaskWithAgent with
config_mode metadata instead of a custom fetch helper. Fix navigation
to use task.session_id instead of task.id. Add metadata parameter
support to ApiClient.createTaskWithAgent.
* fix: use floating FAB for config chat and pass default agent profile
Move config chat button from header to a floating action button in the
bottom-right corner with a sparkles icon. Pass the workspace's default
agent profile ID when opening the config chat to avoid "no agent
profile configured" error. Add default_config_agent_profile_id to
workspace slice type.
* feat: async agent launch for config-chat and extract helpers
The config-chat endpoint now launches the agent asynchronously when a
prompt is provided, matching the start_agent flow used by task creation.
Extract deleteTaskOnError and launchConfigChatAgent helpers. Remove
LaunchWorkspace from prepare step to avoid blocking.
* feat: first-interaction agent profile selection for config chat
Open the config chat modal immediately on FAB click. If no default
config agent profile is set, the empty state now shows a profile
selector before starting. The selected profile is saved as the
workspace default_config_agent_profile_id for future sessions.
* test: fix config E2E tests — remove duplicate method, use real tools
Remove duplicate startConfigChat method in ApiClient that overrode the
correct 3-param signature. Replace invalid create_agent test (requires
registered agent type) with update_agent test. Remove fragile tool calls
group UI assertion from multi-tool test.
* fix: resolve 'no active session' error in config chat
Add openConfigChatModal action that only sets isOpen and workspaceId
without touching sessionId. Previously, open() called openConfigChat
with empty sessionId which was truthy, causing QuickChatContent to
render with an invalid session.
* feat: add minimal toolbar mode for config chat input
Thread minimalToolbar prop through ChatInputArea → ChatInputContainer →
ChatInputBody → ChatInputToolbar → QuickChatContent. When enabled,
renders only the submit/cancel button, hiding plan toggle, MCP
indicator, context popover, model selector, and other controls.
* feat: replace config chat dialog with floating popover panel
Replace Dialog with Popover from @kandev/ui so the config chat floats
up from the FAB button. Reduce FAB size to h-12/w-12 with larger h-7/w-7
icon. Rename tooltip to "Configuration Chat". Remove standalone
ConfigChatFAB component from settings layout.
* feat: filter config chat tasks from quick chat listing
Add exclude_config query parameter to ListTasksByWorkspace that filters
out tasks with metadata.config_mode=true. Apply it in the quick chat
sessions API call to prevent config chat tasks from appearing as tabs.
* refactor: move config agent profile setting to agents page
Remove the Configuration Agent Profile selector from workspace settings
and add a dedicated ConfigChatAgentSection to the agents settings page
with auto-save on change.
* feat: redesign config chat empty state with inline text input
Replace clickable template buttons with subtle suggestion hints that
fill the textarea on click and disappear once the user starts typing.
Add a persistent Textarea + send button at the bottom so users can
type directly without clicking templates. This prevents the "no active
session" error since QuickChatContent only renders after startSession
succeeds.
* fix: config chat UX polish — non-clickable suggestions, profile persistence
- Make suggestion prompts purely visual (non-interactive text hints)
- Update workspace store after saving config agent profile so the
profile selector doesn't reappear on subsequent opens
- Use useAppStoreApi for store access in callbacks
* fix: add inline error display and prevent accidental popover dismiss
- Show API errors inline below the input instead of via toast (which
can be hidden behind the popover)
- Prevent popover from closing on outside click via onInteractOutside,
so the user doesn't lose their typed input or active session
- Surface error state from useConfigChat hook to the empty state UI
* fix: use config-specific placeholder in config chat input
Add placeholderOverride prop through ChatInputArea and QuickChatContent
so the config chat shows "Ask anything about your configuration..."
instead of the default "Continue working on the task..." placeholder.
* feat: add default_config_agent_profile_id to workspace DTO and API
Wire the field through WorkspaceDTO, FromWorkspace(), HTTP/WS request
structs, service layer, and public API types so the config chat default
profile persists across page reloads and SSR hydration.
* feat: add config chat store actions and prompt delivery via WS
Add configChat slice actions (open, close, rename, tab management),
pendingPrompt state in useConfigChat hook, and initialPrompt support
in QuickChatContent so prompts are delivered via WS after subscription
is established rather than via HTTP (avoiding race condition).
* fix: resolve dual store issue and message fetch race condition
Replace StateProvider with StateHydrator in settings layout to prevent
a shadow Zustand store that intercepted WS events. Fix message fetch
race condition by merging WS-delivered messages with fetch results
instead of replacing them.
* feat: make config chat global with command panel integration
Add ConfigChatProvider at root layout so the popover is available on all
pages. FAB button only visible on settings pages; hidden anchor on other
pages for popover positioning. Add "Configuration Chat" command to the
command panel (Cmd+K). Add close button and header bar to the popover.
* test: add E2E tests for config chat popover and command panel
Add tests for opening config chat via FAB on settings page (with full
agent response flow) and opening via command panel on kanban page.
Fix api-client endpoint and add workspace update fields for E2E setup.
* fix: compress props destructuring to satisfy max-lines-per-function lint
Rebase merged both sides' additions into ChatInputEditorArea and
ChatInputToolbar, pushing them over the 100-line function limit.
Use object parameter with compact destructuring to reduce line count.
* fix: register config task tools and add exhaustive E2E coverage
Config-mode MCP task tools (list_tasks, move_task, delete_task,
archive_task) were defined but never registered — registerConfigTaskTools()
was never called in registerTools(). Fixed the registration and expanded
E2E tests from 5 to 15, covering all testable config-mode MCP tools
(workflow, agent, MCP config, and task management) with API-side
verification of side effects.
* feat: replace create/delete agent with create/delete agent profile in config mode
Agent creation/deletion via MCP isn't practical since agents map to
registered types. Replace with create_agent_profile and
delete_agent_profile tools that manage the per-agent configuration
presets users actually need to work with. Wires full chain: agentctl
MCP tools → WS actions → backend handlers → controller layer.
* chore: fix Prettier formatting
* fix: compress props destructuring to satisfy max-lines-per-function lint
* test: update MCP tests for agent profile tools and correct config tool count
* fix: address CodeRabbit review — normalize MCP mode, set default, consolidate tests
* fix: address CodeRabbit review — error leaks, search filter bug, stale prompt, sysprompt tools
* feat: expose all workflow step fields in config-mode MCP tools
* feat: add workflow CRUD to config-mode MCP tools
* fix: split workflow CRUD E2E test into separate tests
* fix: prevent cascade deletion of config session in delete workflow E2E test
The config-chat endpoint picks the newest workflow (ORDER BY created_at
DESC) for the ephemeral task. When the test created a deletable workflow
right before starting the config session, the session's own task landed
under that workflow. Deleting the workflow then cascaded and wiped the
running session's task, causing the MCP tool call to hang.
Add a "Config Anchor" bumper workflow so the config session uses it
instead of the deletable one.
* feat: add executor management to config-mode MCP tools
Add 8 new MCP tools for executor and executor profile CRUD in config
mode: list/create/update/delete for both executors and profiles.
Wired through the full stack (WS actions, backend handlers, agentctl
tools, sysprompt) with unit tests and E2E coverage.
* fix: auto-focus config chat input and rename popover title
* fix: remove executor CRUD MCP tools, keep executor profile management
Executors are pre-defined system entities (local, worktree, docker,
sprites). Only executor profiles should be managed via config chat.
Removes create_executor, update_executor, delete_executor MCP tools
while keeping list_executors and all executor profile tools.
* fix: add realtime UI sync for workflow steps and agent profiles via MCP
Workflow step and agent profile changes made through the config chat
MCP tools were not broadcasting events to the frontend. Executor
profile events worked because they went through the task service which
has event bus integration.
- Add WorkflowStep and AgentProfile event types to the event bus
- Add WS notification action constants for workflow step events
- Publish events from MCP handlers after step/profile CRUD operations
- Register gateway subscriptions for the new event types
- Add frontend WS handlers for workflow.step.created/updated/deleted
* fix: reuse existing config chat session when opened from command panel
The command panel always called openConfigChatModal() which reset the
session state, causing the profile picker to show again. Now it checks
for an existing active session first, matching the FAB behavior.
* fix: suppress FAB tooltip while config chat popover is open
When closing the popover via the X button, the mouse ends up over the
FAB area and the tooltip re-appears. Force the tooltip closed while
the popover is open so it cannot re-trigger on close.
* fix: thread default_config_agent_profile_id through all workspace data paths
The field was defined on the workspace type but never populated in:
- SSR mapper (page.tsx) — missing from initial hydration
- WS workspace handlers — wiped on workspace.created/updated events
- Backend publishWorkspaceEvent — not included in event data
This caused the config chat to always show the profile picker when
opened from the command panel, since the saved default was lost.
* fix: address PR review comments — workspace sync, type fixes, missing tool doc
- Sync configChat.workspaceId when promoting next tab in closeConfigChatSession
- Add update_task_state_kandev to ConfigContext system prompt
- Fix workflow step WS event types (workflow.step.* instead of step.*)
- Fix const reassignment in E2E tests (const → let)
* refactor: clean up dead code and register missing MCP tools
- Register update_task_state, delete_workflow_step, and
reorder_workflow_steps as MCP tools in agentctl config mode
- Remove dead handleCreateAgent/handleDeleteAgent WS handlers
and their ActionMCP constants (only profiles are managed)
- Rename openConfigChatModal to startNewConfigChat across frontend
- Update sysprompt, tests, and tool counts
* fix: remove extra next.config.ts machine
* fix: address CI failures — prettier formatting and funlen split
* fix: address coderabbit review comments across config-mode stack
- Include prompt field in workflow step broadcast payloads
- Align sysprompt task-state values with actual backend enum
- Return error instead of continuing when MCP mode switch fails
- Validate mode values in agentctl set-mcp-mode endpoint
- Use errors.Is() for sentinel error comparison
- Fix nullish coalescing to allow clearing config agent profile
- Expand StepPayload type with missing config-step fields
- Derive config agent Select value from store instead of local state
* fix: reuse persisted config chat session and reduce toolbar complexity
- Look up configChat.sessions for workspace before creating new session
- Extract MinimalToolbar and move defaults to module-level object to
reduce ChatInputToolbar cyclomatic complexity from 16 to 7
* fix: E2E executor test strict mode violation and invalid cleanup
- Use exact match for list_executors text to avoid matching 2 elements
- Remove cleanup that tried to delete pre-existing system executor
- Reuse persisted config chat session for workspace in global commands
- Reduce ChatInputToolbar cyclomatic complexity with module-level defaults