* feat(workflow): explicit step-completion signal for auto-advance
Adds `step_complete_kandev` MCP tool + `WorkflowStep.auto_advance_requires_signal`
column so workflow steps can gate their `on_turn_complete` transitions on an
agent-emitted completion signal instead of inferring "done" from any turn-end.
See docs/decisions/0015-explicit-completion-signal-for-auto-advance.md for the
motivation, alternatives considered, and trade-offs.
* fix(workflow): address PR review feedback for explicit completion signal
- Dedup `LoadPendingStepSignal` into `internal/task/models` so the MCP
handler (write site) and the orchestrator (read site) share one decoder.
- Gate the out-of-band subscriber on `WorkflowStep.AutoAdvanceRequiresSignal`
so `step_complete_kandev` is not an unintended manual-advance path for
signalless steps.
- Fail closed on step-load errors in `processOnTurnCompleteViaEngine`: a
loader hiccup must block the transition rather than silently bypass the
gate.
- Reject `step_complete_kandev` for terminal sessions
(Completed / Failed / Cancelled) so the agent gets a clear error instead
of `accepted: true` followed by a no-op.
- Surface bus-publish failures back to the agent instead of swallowing
them — the idempotency guard makes the retry safe and prevents the
session from getting stuck.
- New `TestOnStepCompletionSignaled` covers the subscriber paths
(RUNNING / WAITING+match / stale step / not signal-gated).
- ADR doc fixes: payload field list now matches the wire shape, writer
ownership is documented, the unsubstantiated 250ms-debounce claim is
removed, and the late-signal re-close race is recorded as a deferred
open question.
- Drop the new toggle's HelpTip in favour of an inline description — the
shadcn tooltip portal was intercepting clicks on the agent-profile
dropdown options in the workflow settings E2E.
* fix(workflow): round-2 review fixes for explicit completion signal
- Split `handleStepComplete` into `resolveStepCompleteTarget` +
`publishStepCompletionEvent` helpers so the entrypoint clears funlen
(88 → under 80 lines) and the two halves are independently testable.
- Classify session/task load errors instead of returning `NotFound` for
every failure: use the exported `taskrepo.ErrTaskNotFound` sentinel for
task lookups, substring-match for the session repo (no sentinel today),
and surface anything else as `InternalError` with the raw error logged
but not leaked to the agent — so a transient DB hiccup triggers a retry
rather than session abandonment.
- Pin `step_complete_kandev` explicitly in the mode-specific tool-count
tests: `Contains` in `ModeTask`, `NotContains` in `ModeConfig` and
`ModeOffice`, so accidental dereg or cross-mode registration is caught
by the diff instead of by a still-equal count.
* fix(workflow): round-3 review fixes + defer pipeline-editor toggle
- Defer the "Wait for agent completion signal" sub-toggle to a follow-up
PR. Adding it to the step config card shifts the layout enough that the
agent profile select's Radix popper opens into a sibling tooltip's hover
path, breaking workflow-agent-profile.spec.ts on shard 7. The backend
column ships in this PR — workflows can opt in via the YAML import or
the MCP `update_workflow_step_kandev` tool — and the UI affordance gets
its own iteration where the test interference can be investigated
without blocking the rest of the change.
- Re-publish the bus event on the `already_signaled` short-circuit when
the session is WAITING_FOR_INPUT. Without this, an agent's retry after
the first call's publish failed would short-circuit on the persisted
bag, never fire the subscriber, and leave the session stuck.
- handleStepComplete docstring: drop the non-existent feature-flag claim
and the "audit entry" wording — gating is purely
`WorkflowStep.AutoAdvanceRequiresSignal` and the bag is cleared on the
next turn start with no separate audit trail.
- `step_complete_kandev` tool description: same fix — clarify that the
call succeeds for non-gated steps but the signal is discarded on the
next turn start (no audit history to query later).
* test(workflow): cover handleStepComplete + reword defer-comment
- Add `TestHandleStepComplete_*` (6 subtests, all passing) covering the
uncovered handler paths flagged by Claude/cubic on round 3:
missing-fields validation, session-does-not-belong-to-task ownership
guard, terminal-session rejection (Completed/Failed/Cancelled),
first-call happy path (bag persisted + bus payload shape pinned), dedup
while RUNNING (no re-publish), and dedup while WAITING_FOR_INPUT (the
retry-after-publish-failure path — must re-publish so the subscriber
fires).
- Reword the deferred-toggle comment to drop the YAML-export claim:
the workflow export model doesn't yet serialize
`auto_advance_requires_signal`, so MCP `update_workflow_step_kandev`
is the only configuration path that round-trips today. Note the
follow-up needs to extend the export schema alongside the UI toggle.
* test(workflow): pin signaled_at + drop redundant GetTaskSession on bag-clear
- Assert `signaled_at` is present and parses as RFC3339Nano in
`TestHandleStepComplete_FirstCallAccepted`. The field is part of the
handler's documented response contract; pinning it catches accidental
drop/rename regressions.
- Add `clearPendingStepSignalByID(ctx, sessionID)` as a thin DB-only
variant of `clearPendingStepSignal` and use it from
`executeStepTransition`, where the session struct isn't read after the
clear. Avoids an extra `GetTaskSession` round-trip per transition.
`clearPendingStepSignal(session)` still exists for the
in-memory-mutation case (subscriber's stale-step path).
* fix(workflow): subscriber stale-clear guard + ADR/comment drift fixes
- onStepCompletionSignaled's stale-step path now reads the bag and only
clears it when its current StepID matches the (stale) event's stepID.
Previously the unconditional clear could destroy a valid signal that
the new step's agent had just written when sessions are reused across
steps without auto_start_agent. (Greptile P1.)
- Drop the "feature flag" phrase from WorkflowStep.AutoAdvanceRequiresSignal
docstring — leftover from an earlier draft. The gating is purely the
boolean field; the global flag was removed two rounds ago.
- Merge the two adjacent `if triggerOnEnter` blocks in
executeStepTransition so the full triggerOnEnter=true path reads top-
to-bottom in one place. (CodeRabbit + cubic.)
- ADR 0015 Mechanism section: re-label the SessionStepHistory.Metadata
audit annotation, the `step_complete_signaled` system message, and the
expvar telemetry counters as deferred. They're planned shapes, not
current behaviour — calling them "Audit" / "Chat visibility" /
"Telemetry" in present tense was misleading for future readers.
* fix(workflow): drop subscriber complexity + reload session on stale-clear
- Extract `parseStepCompletionEvent` and `stepIsSignalGated` helpers from
`onStepCompletionSignaled`. Cyclomatic complexity goes 17 → under 15
(cyclop on new code, CI lint regression after the round-6
stale-clear guard).
- The stale-step branch now re-loads the session from the DB before
deciding whether to clear the bag. The previous round-6 fix checked
the local `session.Metadata` snapshot, which can be older than a
concurrent write from the new step's agent — that check could see a
stale signal that had since been overwritten with a valid one and
erase the new bag entry. The narrower load-on-clear is cheap and
closes the cubic P1 hole. (The broader race in
`processOnTurnCompleteViaEngine` reading session.Metadata is the
monotonic-marker open question in ADR 0015; out of scope here.)
* test(workflow): pin stale-event guard's preserve-current-bag side + dedup StringFromAny
- Add `TestOnStepCompletionSignaled` subtest "stale event → valid bag
for CURRENT step is preserved" — fires a stale step-A event while
the bag holds a valid step-B signal, asserts the bag still contains
StepID=step_b after the subscriber runs. Pins the negative side of
the round-6 `existing.StepID == stepID` guard, so a regression that
drops or inverts the guard (and would silently leave signal-gated
steps stuck) fails the test instead of slipping through.
- Promote `pendingSignalString` to exported `models.StringFromAny`
and reuse it from the orchestrator subscriber's
`parseStepCompletionEvent`. Eliminates the byte-for-byte duplicate
helper across the two packages; the bag-shape decoder and the bus-
event decoder now share one source of truth for "missing string".
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
* 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
* chore: remove dead non-ACP provider client packages
Delete five Go packages under apps/backend/pkg/ that were unreachable
from any binary: amp, codex, claudecode, copilot, opencode. These were
the client libraries for legacy non-ACP agent implementations removed in
a5b54b53 — that commit deleted the callers but missed the client libraries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: drop orphaned copilot-sdk dep and stale architecture doc
go mod tidy removes github.com/github/copilot-sdk/go v0.1.18 and its
transitive dep github.com/google/jsonschema-go v0.4.2; both were only
imported by the deleted pkg/copilot/client.go.
Delete docs/agents/claude_code.md: it describes the pre-ACP streamjson
transport and pkg/claudecode/ package. Every source file it references
has been removed (pkg/claudecode/ in this PR; streamjson/ and
agents/claude_code.go in a5b54b53).
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>