Commit Graph

1214 Commits

Author SHA1 Message Date
Carlos Florêncio fc4bf8a40c feat: add checkout service make targets (#1311) 2026-06-08 19:30:21 +01:00
Nicolas Lenepveu aac2afbf93 fix(agent-settings): stop reseeding agent profiles the user deleted (#1305) 2026-06-08 17:49:34 +01:00
Carlos Florêncio 637123f84f feat: allow remote external mcp access (#1307) 2026-06-08 17:41:29 +01:00
Carlos Florêncio c68323c9e0 fix: preserve settings sidebar on refresh (#1303) 2026-06-08 17:40:54 +01:00
Carlos Florêncio 351db61e5b fix: avoid stale sibling PR sync (#1302) 2026-06-08 17:40:16 +01:00
Carlos Florêncio c4133a9e6e fix(web): focus sidebar-created tasks (#1301) 2026-06-08 17:39:37 +01:00
José Almeida 46f20e0dfe fix(web): keep dockview group alive on task switch to stop layout collapse (#1308)
* fix(web): keep dockview group alive on task switch to stop layout collapse

On a fast-path env switch, the incoming session chat was added only after
removeEphemeralPanels closed the outgoing chat. When that chat was the last
surviving panel in its group (e.g. the default layout's chat column), removing
it first emptied — and so destroyed — the group. The captured group id then no
longer existed, and post-#1165 the old `referenceGroup: "sidebar"` fallback is
dead, so addPanel ran with an undefined position: dockview dropped the incoming
chat into the active (terminal) group and collapsed the grid root to a vertical
stack. The corrupted layout was then persisted, so it survived reloads.

Add the incoming session panel before removing the outgoing one, anchored at the
outgoing panel's current index, so the group stays alive throughout the swap.
Removing the panels ahead of the insert point shifts the new panel into the same
final slot, so computeSurvivingIndex is no longer needed.

* docs(web): note the raw-index equivalence assumption in env switch
2026-06-08 17:33:54 +01:00
João Salavisa 2b74f4e189 fix: gate step_complete_kandev tool on per-step signal flag (#1300)
* fix: gate step_complete_kandev tool on per-step signal flag

Two ADR-0015 follow-up bugs:

- "Wait for agent completion signal" checkbox snapped back because
  updateWorkflowStepAction dropped auto_advance_requires_signal from the
  PUT body. Add it to the Pick<>, body builder, BackendWorkflowStep, and
  transformWorkflowStep.
- step_complete_kandev MCP tool was injected into every first-turn
  prompt, polluting context and tempting agents on non-signal-gated
  steps to call it. Replace the static description in
  kandev-context.md with a {step_complete_section} placeholder filled
  only when the current step has auto_advance_requires_signal=true.
  Add orchestrator.Service.StepRequiresCompletionSignal helper and
  thread it through all four InjectKandevContext call sites.

* fix: address PR 1300 review feedback

- Hoist ProcessOnTurnStart above the first-turn MCP wrap so an
  on_turn_start step transition is reflected in the
  auto_advance_requires_signal flag baked into the stored/forwarded
  prompt (coderabbit, cubic).
- Add Service.WorkflowStepRequiresCompletionSignal step-ID variant and
  use it from task_operations.go / autoStartStepPrompt to skip the
  redundant GetTask round-trip at hot first-turn launch sites (claude).
- Add table-driven tests covering every silent-false branch and the
  positive path of both helpers (greptile, claude, cubic).

* docs(sysprompt): document stepCompleteSection trailing newline contract

Address Claude review suggestion on PR 1300: make explicit that the
constant must end with "\n" because the kandev-context.md template inlines
{step_complete_section} immediately before the next bullet, so the
trailing newline is what separates the two list items.

* fix: restore task-id lookup at startTask wrap site

Address PR 1300 review regression: the previous swap to
WorkflowStepRequiresCompletionSignal(ctx, workflowStepID) silently hid
step_complete_kandev on manual starts where the workflowStepID parameter
is empty but the task is already bound to a signal-gated step in the DB.

The `task` variable in scope at this call site is `*v1.Task` (from the
scheduler) and does not carry WorkflowStepID, so the cheaper step-ID
variant is not applicable here — go back through StepRequiresCompletionSignal
so the repo lookup pulls the canonical step.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-06-08 08:14:52 +01:00
Carlos Florêncio f81b5133bf fix(agentctl): preserve codex reasoning model ids (#1296) 2026-06-07 23:06:35 +01:00
Carlos Florêncio 3f922d3911 fix(web): sort kanban cards by newest created (#1298) 2026-06-07 22:55:42 +01:00
Clément Fiere 0b40d09b10 fix(utility): list opencode models when ACP probe is empty (#1278) 2026-06-07 22:52:12 +01:00
Carlos Florêncio eb74a3d329 fix: alert when git executable is missing (#1297) 2026-06-07 22:44:57 +01:00
Carlos Florêncio 210737cb24 fix(web): use recent task after removal (#1295) 2026-06-07 22:30:52 +01:00
Carlos Florêncio 372c07b55a fix(web): gate changes push button on ahead count (#1294) 2026-06-07 22:14:11 +01:00
Carlos Florêncio 6661255024 fix(web): restore dockview sidebar layout (#1293) 2026-06-07 21:41:40 +01:00
Carlos Florêncio 1484ae7d28 fix: expose user agent bins to system service (#1292) 2026-06-07 21:13:28 +01:00
João Salavisa f6f6fc3877 fix(agentctl): read models from configOptions fallback (#1291)
* fix(agentctl): read models/modes from configOptions fallback

claude-agent-acp v0.42 dropped the unstable `models` field from
session/new and publishes models only via `configOptions[]` with
category=model. The probe still relied on the legacy field, so
the inference-agents endpoint reported zero models and the UI
showed "Claude advertised no models."

Read the legacy fields first, then fall back to configOptions
when they are absent. Same fallback applied to modes for parity
with future agents.

* fix(agentctl): gate configOptions fallback on nil legacy field

Switch the fallback guard in `applySessionProbeFields` from
`len(out.Models) == 0` to `sessionResp.Models == nil` (same for
modes). A non-nil `Models` struct with an empty `AvailableModels`
slice is schema-valid: the legacy branch already stamps
`CurrentModelID`, and the previous guard would then fire the
configOptions fallback and overwrite it with a different source.

Adds `TestApplySessionProbeFields_LegacyEmptyModelsBlocksFallback`
covering the edge case.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-06-07 14:36:51 +01:00
João Salavisa 31d4ba67fa perf(stats): split stats endpoint per-section and rewrite GetGlobalStats (#1289)
* perf(stats): split stats endpoint per-section and rewrite GetGlobalStats

Stats page used to wait on a single endpoint that ran 7 SQL queries in parallel
and rendered nothing until the slowest one returned. Cold first-paint was the
bottleneck users actually hit.

- Backend: split /api/v1/workspaces/:id/stats into 7 per-section endpoints
  (global, tasks, daily-activity, completed-activity, agent-usage,
  repositories, git). Each handler runs one repo call and returns its DTO.
- Backend: rewrote GetGlobalStats from 9 independent correlated subqueries
  into 4 CTEs (task / session / turn / message). Each underlying table is now
  scanned at most once per request instead of 1-3 times.
- Frontend: replaced the monolithic fetchStats + single PanelState with
  useStatsSections hook (7 parallel fetches, per-section SectionStatus).
  Each panel renders skeleton -> data -> error independently; light queries
  (overview, agent usage, git) paint immediately, heavy ones stream in.
- Tests: added TestGetGlobalStats_AggregatesAcrossTables to lock the
  CTE-based query against regressions for turns, user/tool messages, and
  total duration.

* fix(stats): address PR review feedback

- Restore cache: "no-store" on section fetches; Gin emits no Cache-Control
  headers so the browser could otherwise serve stale stats on revisit
  (Greptile P1, Cubic).
- Stop blanking the Overview panel when /git fails: render OverviewCards as
  soon as `global` is ready and treat `git` as optional, falling back to the
  Averages card. Updates GitOrAveragesCard / OverviewCards types (Claude).
- Fix TaskStatsHasMore off-by-one at exactly taskStatsLimit rows: probe
  taskStatsLimit+1, slice back to the page size, only flag hasMore on a
  confirmed extra row (Claude, Cubic).
- Rename anyError → firstError with a comment documenting the deterministic
  insertion-order iteration; clearer signal that callers should use it as
  "any section failed?" and surface per-section errors in their own panels
  (Claude).

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-06-07 13:24:32 +01:00
José Almeida 607201be79 fix(web): tighten dockview topbar button sizing (#1290)
* fix(web): tighten dockview topbar button sizing

* fix(web): keep editor button focus ring inside clipped container
2026-06-07 13:00:19 +01:00
João Salavisa 48a621a566 fix(utility): surface inference-agent probe status + add refresh endpoint (#1287)
* fix(utility): surface probe status in inference-agents endpoint

GET /api/v1/utility/inference-agents previously filtered out agents whose
host-utility probe didn't reach StatusOK, so a Claude entry with auth or
install issues vanished from the response. The frontend then defaulted
agent_id to "claude-acp", failed to match anything in the list, and
silently rendered an empty Model picker with no explanation.

Include every registered ACP-capable agent in the response with a new
`status` field (and sanitized `status_message`) so the UI can render an
inline note + Refresh button. Add a companion
POST /api/v1/utility/inference-agents/:id/refresh endpoint backed by the
existing hostutility.Manager.Refresh so users can re-probe after fixing
auth or installing the CLI without restarting kandev.

Credential-looking substrings ("api_key=...", "token=...") in probe
errors are stripped before reaching the wire.

Refs #1269, #1263.

* fix(web): show probe status note + Refresh on utility-agent model picker

Previously the Model picker rendered empty and disabled whenever the
selected inference agent was missing from /inference-agents (probe
hadn't reached StatusOK) or advertised no models, with no way for the
user to understand or recover.

Wire the new `status` field through the API client and render an inline
note ("Sign in to Claude to load models.", "Claude CLI is not installed
on this machine.", "Setting up Claude…") with a Refresh button that
calls the new POST /inference-agents/:id/refresh endpoint and merges the
updated agent back into local state — no full refetch, so a slow probe
on a different agent doesn't block the UI.

Both call sites — the create/edit dialog and the Default Model row in
the settings page — render the note. Component-level unit tests cover
the status-to-copy mapping, fallback ("no longer available") copy, and
the in-flight Refresh button disabled state. The e2e test asserts the
auth_required → click Refresh → status:ok state machine end to end.

Refs #1269.

* test(utility): add HTTP integration tests for inference-agents endpoints

End-to-end coverage of the new /api/v1/utility/inference-agents and
/api/v1/utility/inference-agents/:id/refresh routes against a real gin
router with a stateful in-memory host stub.

- ProbeStateMachine: GET (auth_required) → POST refresh → GET (ok+models)
- RefreshUnknownAgent: 404 without invoking the host probe
- RefreshPathFuzz: traversal, null byte, 512-char id — no 5xx, no probe
- ConcurrentRefresh: 50 parallel POSTs under -race, no panic
- StatusMessageSanitizationOnTheWire: 110KB payload + unicode probe
  errors — no secret material reaches the wire, unicode prose preserved

Caught the gap unit tests had: the multi-request GET→POST→GET sequence
on a shared cache, and the wire format (Content-Type, JSON shape)
through the real router.

* fix(utility): tighten credential regex + drop Go 1.22 loop-var shadow

Address two inline review comments from claude bot on PR #1287:

- Drop `ia := ia` shadow in httpRefreshInferenceAgent — pre-Go 1.22
  pointer-capture workaround, dead weight under Go 1.24+ (per
  apps/backend/AGENTS.md).
- Split the credential sanitizer into two patterns so prose without a
  real `=`/`:` separator is no longer mangled. The single regex
  previously turned "access token was revoked" into
  "access token=<redacted> revoked" by consuming the next word as the
  value. New shape:
    - credentialKVPattern — requires `[:=]` (api_key, token, secret,
      password)
    - credentialBearerPattern — allows bare whitespace, anchored to
      "bearer" only
  Existing redaction cases still pass; two new prose cases guard the
  regression.

* fix(utility): address cubic + greptile review findings on PR #1287

Round-2 review feedback:

- P0 (cubic): postEmpty called `t.Fatalf` from `go func()` in
  TestIntegration_ConcurrentRefresh. Go panics on t.Fatalf from non-test
  goroutines. Split into `postEmptyE` (error-returning, goroutine-safe)
  and `postEmpty` (main-goroutine wrapper). Concurrent test now
  forwards the error through the existing `errs` channel.
- P1 (cubic + greptile): tightened the credential sanitizer regex.
    * `api[ _-]?key` now also matches a literal space ("invalid api key=…")
    * separators are restricted to `[^\S\n]` so multi-line stderr like
      "invalid token\ncaused by network" no longer eats words across lines
  Test coverage added for both regressions (and for tab as a valid
  horizontal-whitespace separator).
- P2 (cubic): refresh-failure log now feeds `err.Error()` through
  sanitizeStatusMessage before zapping it — same wire-side guard, but
  applied to server logs too. Closes the "credentials in journalctl"
  hole.
- P2 (cubic): InferenceAgent.status is now optional in the TS type and
  the status note treats an undefined status with `models.length > 0`
  as healthy, so older API responses don't flash a spurious warning.

Deferred (acknowledged in PR replies, not changed here):

- cubic P2 useInferenceAgents drift — the hook is currently called in
  both the dialog and the parent section. Drift is bounded by the
  dialog's own `onSuccess` refetch and only visible inside an open
  dialog. Lifting state to the parent and prop-drilling is the right
  fix but touches multiple files; out of scope for this review round.

* docs(utility): fix stale comments flagged by claude review on PR #1287

Two doc-only items, no code change:

- handlers_integration_test.go: TestIntegration_ConcurrentRefresh
  comment said "we don't assert on call count" but the test DOES
  assert `host.refreshCalls != N` two lines later. Updated to match
  reality (singleflight lives in the real Manager, the stub has none
  → every call must reach the stub).
- utility-agents.spec.ts: the ACP-probe E2E said "backend filters out
  non-StatusOK agents", which is the exact behavior this PR removes.
  Updated to explain the real reason only Mock appears (KANDEV_MOCK_AGENT=only
  registers only `mock-agent`).

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-06-07 12:38:28 +01:00
José Almeida 2ed65cfde3 fix(web): guard dockview measure against mid-transition sidebar width (#1288)
* fix(web): guard dockview measure against mid-transition sidebar width

The unified AppSidebar (#1165) animates its width over 300ms; the dockview
content container (a flex sibling) can be measured mid-transition at a tiny
positive width in onReady, so the default layout builds too narrow and dockview
collapses the horizontal columns into a vertical stack. measureDockviewContainer
trusted any positive parent clientWidth. Reject an implausibly-small live width
(< 50% of viewport) as a not-yet-laid-out transient and fall back to the
viewport; the resize observer then snaps to the exact size.

* fix(web): preserve live height when width guard fires; assert it
2026-06-07 12:35:29 +01:00
João Salavisa 91d5861519 fix(web): preserve manual task selection during archive/delete (#1286)
* fix(web): preserve manual task selection during archive/delete

When the user archived (or deleted) a task and then immediately picked
another task, the in-flight archive flow would auto-switch to a third
task, clobbering the manual selection.

The shared `removeTaskFromBoard` was honoring the pre-API captured
`wasActiveTaskId` unconditionally. That capture exists to survive the
WS `task.deleted` / `task.updated(archived_at)` handler clearing
`activeTaskId` to null. But it also overrode the legitimate case where
the current store value reflected a user-initiated navigation, so the
equality check still passed and `switchToNextTask` ran.

New guard: switch only when (a) the store still points at the removed
task, or (b) the store has been WS-cleared to null AND the captured
pre-API id matches the removed task. Any other state means the user
moved on — leave them where they chose to go.

* chore(web): tighten null check + no-opts test in use-task-removal

Addresses non-blocking suggestions from claude bot review on #1286.

- Use strict `=== null` in `shouldSwitchAfterRemoval` for consistency
  with the surrounding strict-equality comparison.
- Add unit test documenting that a no-opts caller skips the auto-switch
  when the WS handler has already cleared `activeTaskId` to null.

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>
2026-06-07 11:57:54 +01:00
João Salavisa 99d8d8f173 fix(github): handle UNIQUE collision in UpdatePRWatchBranchIfSearching (#1285)
* fix(github): handle UNIQUE collision in UpdatePRWatchBranchIfSearching

When a sibling PRWatch already owns the destination
(session_id, repository_id, branch) triple, the raw UPDATE tripped
the UNIQUE constraint and the change was logged as an error
("failed to update PR watch branch"). Wrap the update in a tx that
first checks for a sibling; if one exists, delete the source watch
(pr_number=0, no PR state to preserve) and cede branch ownership.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(github): close TOCTOU and cover sibling-has-PR collision case

Address PR review feedback on #1285:

- Catch SQLite "UNIQUE constraint failed" on the no-sibling UPDATE path
  and treat it as the same collision case (Greptile P2 inline). A
  concurrent CreatePRWatch that commits between our existence probe
  and the UPDATE would otherwise re-trigger the bug this PR fixes.
- Switch the existence probe from `SELECT id` + `siblingID` dummy to
  `SELECT 1` scanned into a typed `probe int` with an inline comment
  (Claude inline readability nit).
- Extract `dropSourceAndCommit` helper so the probe-hit branch and the
  TOCTOU fallback share one code path.
- Add `TestUpdatePRWatchBranchIfSearching_CollidesWithSiblingHasPR_DropsSource`
  exercising the realistic race: sibling has already found its PR
  (pr_number != 0) when source tries to migrate (Claude / Greptile P2 /
  cubic P3 all flagged the gap). Asserts sibling row + PR number
  survive untouched after source is dropped.

* test(github): cover missing-row no-op and reframe TOCTOU comment

Address Claude bot's follow-up suggestions on #1285:

- store.go: rewrite the UNIQUE-catch comment. The original wording
  implied an in-process race, but SQLite's writer pool is
  SetMaxOpenConns(1) so a concurrent in-process CreatePRWatch cannot
  commit between probe and UPDATE. Reframe as defensive belt-and-
  suspenders for an external SQLite writer / future pool changes.
- service_pr_watch_multi_branch_test.go: add
  TestUpdatePRWatchBranchIfSearching_MissingRow_NoOp covering the
  ErrNoRows -> tx.Commit() idempotency contract that the new
  transaction code introduces. Locks the prior UPDATE-by-id semantics
  of "0 rows affected, no error" for a nonexistent id.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 11:50:13 +01:00
João Salavisa f86ab2312f feat(workflow): ship explicit completion signal toggle + YAML round-trip (#1284)
* feat(workflow): ship explicit completion signal toggle + YAML round-trip

ADR 0015's "Wait for agent completion signal" UI shipped as a deferral
comment in PR #1276 instead of a working toggle, and the
auto_advance_requires_signal flag never reached the workflow export
format. Add the pipeline-editor sub-toggle and propagate the field
through StepPortable so the setting survives workflow export/import.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(workflow): guard completion-signal toggle + cover import in tests

Address PR review:
- hide ExplicitCompletionToggle when on_turn_complete has no transition
  (flag has no effect there; visible checkbox was misleading)
- add service-level import test for auto_advance_requires_signal so the
  field has explicit ImportWorkflows coverage, mirroring the existing
  show_in_command_panel pattern

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(workflow): drop omitempty from AutoAdvanceRequiresSignal tags

Match the sibling bool fields (IsStartStep, ShowInCommandPanel,
AllowManualMove) so a `false` value still appears in exported YAML/JSON.

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>
2026-06-07 11:43:28 +01:00
Edan Binshtok 8b6a62cab2 fix(backend): defer move_task when session is running (#1277)
defer RUNNING/STARTING moves instead of immediate MoveTask; add normalizeTaskState aliases; regression test for promptless Work to Done.

Co-authored-by: cybertronic <cybertronic@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-07 10:48:04 +01:00
José Almeida e626d33a7c fix(web): make toggle-sidebar shortcut work on every route (#1283)
* fix(web): make toggle-sidebar shortcut work on every route

TOGGLE_SIDEBAR (Cmd/Ctrl+B) was handled only inside useEditorKeybinds,
mounted just by the dockview desktop layout, so it was dead on Kanban,
the task list, Office, Settings, etc. Lift it to a global app-root
listener (useAppShortcuts, mounted in GlobalCommands) that honors user
overrides via getShortcut and keeps the isEditableTarget guard, and drop
the now-duplicate branch from useEditorKeybinds. Adds a hook unit test
and a Playwright e2e covering Kanban, Settings, and the dockview session.

* fix(web): hydrate userSettings into the root store on settings routes

Each StateProvider creates its own store; the settings/general page mounts
a nested one that the root-mounted GlobalCommands/AppSidebar can't see. The
settings layout hydrated the root store but omitted userSettings, so
override-driven global shortcuts (TOGGLE_SIDEBAR, Quick Chat) were dead on
every /settings route. Hydrate userSettings there too, matching the
kanban/tasks/office/github routes.

* test(web): cover the macOS Cmd path; note the settings-layout catch trade-off

Address review feedback: add a metaKey (Cmd) variant to the useAppShortcuts
test so both ctrlOrCmd branches are exercised, and document that the settings
layout's outer catch intentionally drops userSettings overrides in the
already-degraded error path.
2026-06-06 23:09:12 +01:00
José Almeida a8d84474c4 fix(web): point sidebar Home to office dashboard in office mode (#1282)
* fix(web): point sidebar Home to office dashboard in office mode

The AppSidebar Home item always navigated to the Kanban view (/), even on
office routes. Make its destination office-aware so Home targets the office
dashboard (/office) while in office mode and the board (/) otherwise.

* test(web): start Kanban Home e2e from a non-home route

Per PR review: the Kanban-mode Home test started at / and clicked Home (also
/), so no navigation occurred. Start from /settings/system/status so the click
is a real navigation back to the board.
2026-06-06 23:00:47 +01:00
José Almeida dd4cb2c3ee fix(web): align office topbar border with sidebar header (h-10) (#1281)
* fix(web): align office topbar border with sidebar header (h-10)

The OfficeTopbar was h-12 (48px) while the AppSidebar header is h-10 (40px),
so the office top bar's bottom border sat ~8px below the sidebar's
under-workspace-picker border, breaking the continuous seam where the sidebar
meets the page content. Drop the topbar to h-10 to match the sidebar header and
the PageTopbar/dockview top bars used elsewhere.

Add data-testid hooks to both bordered sections and a Playwright regression
asserting they resolve to the same height and bottom y-position.

* test(web): force expanded sidebar in topbar alignment check

Addresses review feedback (Greptile/Claude): the alignment assertion only
holds for the expanded sidebar header (its border sits under the workspace
picker, and that layout carries data-testid="app-sidebar-header"). Set the
appSidebar.collapsed flag off via addInitScript before load so leftover
collapsed state can't turn the check into a confusing timeout instead of a
real assertion.
2026-06-06 22:38:31 +01:00
José Almeida b001aa1eb8 feat(web): unified app sidebar (#1165)
* feat(web): introduce unified AppSidebar and retire legacy nav surfaces

Mounts a single collapsible AppSidebar at the root layout that replaces the
former WorkspaceRail, OfficeSidebar, and dockview-embedded sidebar pane.
Wires per-section expand + collapse state through a new UI slice with
localStorage persistence, ports projects/agents/settings/tasks sections, and
trims integrations / quick-chat / settings buttons + the home breadcrumb
from the dockview top bar (debug-only MoreToolsMenu kept). Strips the
sidebar column from every preset and the panel registry; persisted
sidebar panels are dropped by the existing sanitize layer at restore time.

* test(web): point workflow-filter e2e at AppSidebar's Home link

The dockview top bar no longer renders the IconHome breadcrumb after the
unified sidebar landed — the home navigation now lives in AppSidebar's
primary nav. Update the workflow-filter spec to click that link instead.

* feat(web): wire app-sidebar primary controls, nested settings, and footer migration

Gates office-only nav (Inbox, Agents, Projects, New Task) behind the office
feature flag, renames Tasks → Kanban as the bottom flex-grow section, replaces
the gradient workspace picker with a minimal trigger when office is off, and
moves Stats/Improve/What's-New/Office out of the kanban top bar into the
AppSidebar footer + a dedicated Integrations section. Expands Settings into a
collapsible deep tree mirroring the old SettingsAppSidebar so /settings pages
can drop their nested SidebarProvider.

* feat(web): make app-sidebar resizable and inline task top bar actions

Adds a persistent drag-resize handle on the AppSidebar's right edge that
clamps between APP_SIDEBAR_EXPANDED_WIDTH and 30vw; width is stored on the
appSidebar slice and persisted to localStorage. Drops the dockview top bar's
overflow popover (TopbarActionOverflow) in favor of always-inline clusters,
and replaces the debug "More tools" dropdown with a single debug toggle button.

* feat(web): polish app-sidebar headers and consolidate task views

Section headers now lead with a clean uppercase label, drop the mono
clash, lift contrast from /60 to /70, and end with the collapse chevron
at the right so the section body aligns flush under the label.

The Tasks section's filter bar (view chips + gear in a card-tinted
strip) is replaced by a compact view dropdown that lives in the section
header. Drag-to-reorder lives in the existing filter popover, which is
now reachable from the same gear button.

Settings moves below Tasks so the kanban list keeps the flex-grow seat
in the sidebar.

* feat(web): move settings group chevrons to the right

* fix(web): drop chat from home topbar; calm tasks group headers

The kanban top bar's Quick Chat button is removed now that Quick Chat
lives in the AppSidebar; workspaceId stops being threaded into the
tablet and desktop header variants.

Group headers inside the Tasks section (kanban workflow steps) lose
the opaque background strip and the section-label uppercase styling
so they read as inline subgroup rows, not duplicate section dividers.
The tasks-section wrapper drops its negative margin so the group rows
sit flush under the TASKS label instead of bleeding out past it.

* fix(web): restore agent logos on settings tree profile leaves

SettingsLeaf gains a leadingIcon slot for pre-rendered visuals; agents
group passes <AgentLogo agentName={agent.name}> so each profile row
shows its agent's logo like it did before the old SettingsAppSidebar
was retired.

* feat(web): brand the sidebar, move New Task off the kanban top bar

The Add task button (CreateTaskTopbarButton) leaves both the desktop
and tablet kanban headers; New Task is now an unconditional primary
nav item on the AppSidebar, falling back to a disabled affordance when
no workspace is active.

AppSidebar header gains a Kandev wordmark that links to /, with a
single-letter K mark in collapsed-rail mode. PageTopbar's root variant
no longer renders the parent brand label when backLabel is empty, so
the home view's top-left "Kandev" goes away in favor of the sidebar.

* feat(web): unbind sidebar toggle by default; drop Home from breadcrumbs

TOGGLE_SIDEBAR's configurable default is now UNBOUND_SHORTCUT (the new
sentinel for "no binding"); matchesShortcut already returns false on
an empty key, so the keybind hook needs no change. Keyboard shortcuts
settings show "Unbound", expose a Clear (X) button when a custom
binding is set, and the reset icon falls back to unbound for entries
whose default is unbound.

PageTopbar's breadcrumb variant no longer prepends the Home back-link
when backHref is "/" (the AppSidebar provides Home navigation).

* feat(web): move New Task into the sidebar, retire the workspace selector

- Extract AppSidebarNewTaskItem (office vs regular create dialog) with tests
- Drop the Workspace selector from the kanban display dropdown and mobile
  menu sheet; the sidebar workspace picker is now the sole switcher
- Factor SectionHeader out of AppSidebarSection; animate fixed sections via
  Collapsible while the grow (Tasks) section stays flex-driven
- Polish brand header, share SIDEBAR_ITEM_ACTIVE accent-bar classes, move
  task group chevrons to the right, drop the task-item selected fill
- Bump dockview layout keys to v3 and filter the orphaned 'sidebar' column
  so pre-AppSidebar default layouts no longer render a broken grid
- Point the cross-workspace isolation e2e at the sidebar picker

* feat(web): sidebar settings mode, subtask shortcut, accordion settings nav

- Restore subtask creation: the sidebar New Task row now shows a trailing
  subtask affordance when viewing a task (regular mode), reopening the
  NewSubtaskDialog the retired dockview header dropdown used to provide
- Fix settings pages not scrolling: scroll the content region (topbar stays
  pinned) via min-h-0 + overflow-y-auto
- Add a footer gear that toggles a settings-only sidebar takeover; the gear
  spins on toggle. Mode is transient (never persisted) and force-expands a
  collapsed rail on entry
- Make the settings nav a single-open accordion: opening one top-level group
  closes the others and reveals its subsections; route changes re-sync the
  open group. Extract the tree into a reusable SettingsTree
- Repoint subtask e2e specs at the new sidebar affordance

* test(web): restore create-task e2e flow after New Task moved to sidebar

The New Task relocation removed the kanban-header create-task-button and made
the sidebar entry auto-navigate + open the office dialog under the e2e profile
(office on), breaking ~20 specs that drive the regular task-create dialog.

- Give the sidebar New Task the create-task-button testid so KanbanPage
  resolves it again
- Drop the post-create auto-navigation so creating a task stays on the board
  (matches the retired kanban button; specs find/click the new card)
- Ungate the subtask affordance from office mode (subtasks use the compact
  NewSubtaskDialog in both modes, as the old dropdown did)
- Add a useRegularMode() helper that runs a spec file with KANDEV_FEATURES_OFFICE
  disabled (backend.restart bracket) and apply it to the 18 create-task specs
- Point the System sidebar-navigation spec at a /settings/system page so the
  single-open settings accordion has that group expanded

Validated: create-task, create-task-disabled-tooltip, create-task-branch-selector,
and subtask all green (modulo pre-existing idleInput agent-timing flakes that
CI retries absorb).

* fix(web): sidebar task navigation, settings-mode close + transitions

- Clicking a task in the global sidebar from a non-task page now navigates to
  the task route (mounts the dockview) instead of only rewriting the URL via an
  in-place layout switch that assumed the dockview was already mounted
- Make the settings-mode 'Settings' header a close affordance (the footer gear
  is far from the tree); clicking it exits the takeover regardless of which
  group is expanded, and it fades in when opened
- Fade in the Tasks (grow) section content on expand, matching the other
  sections' animated reveal
- Enable next-themes disableTransitionOnChange so dark/light flips switch every
  surface in the same instant rather than each animating at its own duration
- Add a regression e2e for sidebar→dockview navigation

* fix(web): clear sidebar task highlight when off the task route

The AppSidebar is global, so tasks.activeTaskId lingered after navigating Home,
leaving the last-opened task visually selected in the sidebar. Gate the
selected/active highlight by route — only highlight while on /t/ or
/office/tasks/. Expose data-active on the sidebar task item for testability.

Adds an e2e: entering a task highlights it; returning Home clears it.

* fix(web): never build the dockview default layout at zero width

On a fresh client-side navigation into a task the dockview can mount before the
grid has painted, so measureDockviewContainer returned api.width/height = 0.
Building the default layout at width 0 makes dockview collapse the horizontal
columns into a vertical stack (chat / files+changes / terminal) instead of the
intended side-by-side layout. Fall back to the viewport when neither the live
container nor the api report a usable size; the resize observer then snaps the
grid to the exact container size.

TDD: dockview-measure.test.ts covers the zero-size fallback.

* fix(web): keep the default layout horizontal when swapping chat for the session tab

Entering a task rendered a vertical stack (chat on top, files/changes in the
middle, terminal at the bottom) instead of the side-by-side default. The
chat->session-tab swap removed the 'chat' placeholder BEFORE adding the session
panel; when chat was the only panel in the center group, removing it destroyed
that group, the stored centerGroupId went stale, resolveInitialPosition
returned undefined, and the session panel was appended as a new row — flipping
the grid root to VERTICAL with three stacked groups.

Fix: add the session panel into chat's live group first (resolveInitialPosition
now prefers the chat panel's group), THEN remove the chat placeholder, so the
center group never empties and the horizontal split is preserved.

Also fixes the pre-existing pane-resize reload/clamp e2e failures, which were
the same collapse. TDD: task-default-layout.spec.ts asserts the right column
sits beside chat.

* fix(web): remove the sidebar Settings section; settings is gear-only

Settings was still a collapsible nav section in the sidebar AND auto-expanded on
/settings routes. That also caused the reported bug: toggling the footer gear
off while on a settings page revealed the normal sidebar with the Settings
section already expanded, so the settings tree appeared to 'not close'.

- Drop the Settings nav section (and its /settings route auto-expand) — the
  settings tree is reached only via the footer gear takeover
- Delete the now-unused sidebar settings-section component
- e2e: gear opens the tree and closes it even after navigating into a section

* fix(web): show office sidebar sections only while inside Office

Inbox, Projects, and Agents were gated on the office feature flag, so they were
always visible when the feature was enabled. Gate them on actually being in the
Office surface (an /office route, reached via the footer Office button) via a
new useInOffice() hook — they stay hidden in the regular workspace and appear
only after entering Office.

Unit test for the hook; e2e (office fixture) asserts the sections appear on
/office and are absent on the regular home.

* fix(web): drop the colorful workspace selector, restore the minimal one

The workspace picker swapped to a gradient/initials trigger (and gradient
swatches in the dropdown) whenever office was enabled. Always use the plain
name + chevron trigger and drop the gradient swatches, matching the previous
design. Removes the now-unused workspace-gradient helper.

* fix(web): align sidebar header, scope office New Task, exit settings on nav

- Drop the doubled right border: the resize-handle hairline is transparent at
  rest, leaving the aside's border-r as the single edge (highlights on hover).
- Collapse the expanded sidebar header into one h-10 row (brand · workspace
  picker · collapse) so it lines up with the page/dockview top bar.
- Gate the sidebar New Task dialog on useInOffice() (actual /office route)
  instead of the bare office feature flag, so Kanban mode keeps the classic
  create dialog while office is enabled.
- Close the settings takeover when navigating off the /settings surface
  (Home, Kandev brand, Office, etc.), tracked on a real pathname change so the
  gear still opens it in place and in-tree navigation keeps it open.
- Lock in the already-working collapsed-gear-expands behaviour with an e2e.

Tests: new-task-item unit (route gating), settings-gear e2e (Home closes,
collapsed gear expands), office gating e2e (Kanban vs Office dialog).

* fix(web): realign task top bar to h-10, refine sidebar workspace picker

- Pin TaskTopBar to h-10 so its bottom border lines up exactly with the
  sidebar header (and the PageTopbar) — previously it had no fixed height and
  sat ~2px off, breaking the seam across the sidebar/content edge.
- Redesign the workspace picker as a compact, muted secondary control inlined
  after the Kandev brand: brand and workspace share text-sm so they sit on a
  common baseline, with a faint '/' separator; the workspace reads as muted and
  the brand stays primary.
- Drop the workspace switcher (and its irrelevant folder glyph) from the
  collapsed rail — collapsed now shows just brand + expand.

Adds an e2e that measures the task top bar bottom edge against the sidebar
header (within 1px).

* fix(web): replace the /github and /gitlab presets rail with a scope bar

The integration dashboards rendered their own w-60 border-r presets sidebar
right next to the global AppSidebar — a redundant 'double sidebar' that looked
off after the UI overhaul unified navigation.

On desktop, fold the presets into a horizontal scope bar above the list
toolbar: the bounded, common controls (PR/Issue kind + preset pills, grouped
Inbox/Created) stay visible as pills, while the unbounded saved-queries list
collapses into a 'Saved' dropdown so the bar never grows tall. The dashboard is
now single-column like Kanban/Tasks. Mobile is unchanged — the vertical
PresetsSidebar still lives in the hamburger sheet.

Adds a desktop scope-bar e2e (presets + kind switching, no inline rail) and
repoints the mobile page object at the new scope-bar testid.

* fix(web): address CodeRabbit/cubic review — a11y, dedup, dup nav, lazy-load

- Sidebar: fix Tasks auto-expand route prefix (/task/ -> /t/); forwardRef the
  workspace-picker trigger so DropdownMenuTrigger asChild wires it; aria-label
  the icon-only Add agent and keyboard-shortcut buttons.
- Settings tree: drop the duplicate top-level Editors leaf (lives under
  General); broaden the System group active state to all /settings/system.
- CSS: honour prefers-reduced-motion for .sidebar-fade-in.
- Storage: bump the dockview maximize namespace to v3 in lockstep with the
  layout namespace so stale pre-v3 maximize blobs are discarded.
- Integration dashboards: extract a shared IntegrationScopeBar base so the
  github/gitlab scope bars are thin wrappers (de-dup + under the component
  size cap). Reuse useConfiguredIntegrationLinks in the sidebar Integrations
  section instead of recomputing availability.
- Lazy-load the office NewTaskDialog so it's not bundled on non-office routes.
- tasks-view-picker: use onSelect for keyboard-activatable menu items.
- Tests: normalize group x by container left in task-default-layout; refresh
  stale subtask-flow comments; async findByTestId for the lazy office dialog.

* fix(web): gate the scope-bar delete button pointer-events while hidden

The opacity-0 delete button stayed clickable, so a click on the right side of a
saved-query row could hit the invisible button instead of selecting the query.
Add pointer-events-none until the row is hovered (group-hover:pointer-events-auto).

* fix(web): use onSelect for scope-bar saved-query menu items (keyboard a11y)

Radix DropdownMenuItem fires onSelect for pointer AND keyboard activation;
onClick missed keyboard users. The nested delete button stops pointerdown so it
isn't treated as a select of the saved query.

* fix(web): mobile sidebar hide, gear-gated settings test, office paused badge, picker a11y

- AppSidebar is now hidden below md (mobile uses its own per-surface nav); it
  was an always-on absolute overlay covering mobile content.
- Restore the sidebar-agent-paused-badge testid on the office agent row.
- Update system sidebar-navigation spec to open the gear (settings is a
  gear-gated takeover in the overhaul).
- Workspace picker menu items use onSelect for keyboard a11y.

* test(web): reconcile main's E2E suite with the unified-sidebar overhaul

Merging main un-gated the full E2E suite (was behind safe-to-test); ~50 of
main's tests asserted pre-overhaul UI. Updated them to the overhaul:

- Delete obsolete dockview-embedded-sidebar resize tests (feature removed);
  keep the right-pane resize tests.
- Scope locators where tasks now appear in BOTH the page and the global
  AppSidebar list (intended) — task-list, task-list-filters, pr-status-badge.
- Adapt sidebar-filter specs to the new TasksViewPicker (gear popover +
  view-chip dropdown) and its page object.
- Settings now reached via the footer gear (not the topbar); Improve Kandev
  opens from the sidebar footer; office nav folded into the AppSidebar — update
  those specs (office verified in CI; can't run locally).
- session-layout: handle the new close-terminal confirm dialog; multi-session
  and tab-management: foreground the (now backgrounded) chat tab after sidebar
  task switches.
- Skip create-task-url-reopen-no-branches: its provider-branch flow (code
  identical to main) is broken only by the office-off backend restart this PR
  forces via useRegularMode — a harness interaction, not an app regression.

* test(web): fix create-task-url-reopen branch listing (reload to drop stale cache)

Un-skipped and fixed properly. Root cause: during the URL-paste/registration
flow the frontend can transiently fetch branches for the freshly-registered
provider repo before its background clone settles; useBranches caches that early
empty result as terminal (if (isLoaded) return), so re-picking the repo never
re-lists. Reload before the second dialog open drops the in-memory branch cache
so branches list fresh from the backend (verified: the backend returns them).
The provider-API path and product behaviour are unchanged from main.

* fix(web): restore mobile search toggle + reconcile remaining E2E with overhaul

- Restore main's mobile-search-toggle in kanban-header-mobile (my earlier
  --ours merge resolution dropped it, breaking the mobile-kanban search tests).
- Office specs: expand the collapsed Agents/Projects sections before asserting
  their rows, and scope office-tasks-page locators to <main>. [CI-verified]
- mobile-task-list-search / mobile-voice-mode: scope task titles to the table
  and drop the obsolete data-sidebar=trigger assertion.
- app-sidebar.test mock: add settingsMode, toggleAppSidebarSettingsMode, and the
  integrations section key.
- sidebar-filter page object: scope chipMenu to the menu holding the view chips.

* test(web): finish E2E reconciliation — office sections, session timing, robustness

- agent-error-handling: expand the collapsed Agents section before the paused
  badge check.
- office execution-indicator/task-sorting: wait for a non-empty .font-mono
  identifier (assigned async by the backend).
- session-layout: foreground the (CI-backgrounded) chat tab before the idle
  check; make the close-terminal confirm dialog optional (busy heuristic is
  timing-dependent).
- workspace-switch-isolation: run office-off so the picker switches in place
  instead of redirecting to /office (which needs onboarding this fixture skips).
- app-sidebar-page: condense JSDoc to one line per project convention.

* test(web): match main's office identifier assertion (revert over-scoping)

The round-2 office agent scoped the .font-mono identifier check to <main> too,
but only the task TITLE needed scoping for the sidebar duplication. The office
task identifier is empty in the e2e seed (backend-assigned, not populated here),
so the <main>-scoped non-empty filter found nothing. Revert the identifier
assertion to main's exact form (first .font-mono on the page); keep the title
scoped to <main>.

* test(web): restore non-empty .font-mono filter for office identifier checks

Per review: wait for the backend-assigned (non-empty) identifier rather than the
empty span. Unscoped so it finds the identifier wherever it renders, 15s window.

* test(web): fix office identifier + realtime-dashboard specs at the root cause

Ran office specs locally (they DO run here) and root-caused all three:

- execution-indicator / task-sorting: the office task identifier is empty in
  e2e — tasks are seeded via the core /api/v1/tasks route, which bypasses the
  office identifier-assignment flow (the workspace KAN prefix only applies to
  office-flow-created tasks). Main's .font-mono.first() was a false-positive
  matching an element the overhaul removed. Assert the identifier COLUMN renders
  (attached to the row) instead of a value e2e never produces.
- realtime-dashboard: the Recent Tasks card is fed by dashboard.recent_tasks via
  useOfficeRefetch on office WS events; a core-route task's office event is async
  so the in-place refetch is genuinely flaky. Reload for the deterministic SSR
  dashboard fetch (what a user sees on revisit); the WS-refetch mechanism stays
  covered by the sibling cross-workspace test.
2026-06-06 21:58:39 +01:00
github-actions[bot] 2557b69c0e release: 0.56.0 2026-06-06 20:43:33 +00:00
João Salavisa 97c1f689e4 feat(workflow): explicit step-completion signal for auto-advance (#1276)
* 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>
2026-06-05 18:32:27 +01:00
José Almeida 4601dbc2d2 feat(tasks): search tasks by PR number in command panel (#1268)
* feat(tasks): search tasks by PR number in command panel

Typing a PR number (e.g. 1243 or #1243) in the Cmd+K panel now surfaces
the task that PR is associated with, alongside the existing
title/description/repo search.

The github store gains a workspace-scoped ListTaskIDsByPRNumber lookup
over github_task_prs; the task service resolves it through an injected
PRTaskResolver interface (github service wired in cmd/kandev) and merges
PR matches into search results, deduped and respecting the
archived/ephemeral/config filters. Keeping the lookup at the service
layer avoids coupling the task repository to the github schema, so
existing search queries and tests are untouched.

* fix(tasks): scope PR-number search to page 1 and unfiltered queries

Address review feedback on PR-number search:

- Only augment results on the first page of an unscoped search. Running
  on every page re-prepended the PR match and truncated a real result
  off each subsequent page; running under a workflow/repository filter
  leaked tasks that don't satisfy the filter.
- Add idx_github_task_prs_pr_number so the lookup is index-backed
  instead of a full scan (pr_number is the 3rd column of the existing
  UNIQUE constraint, unusable as a leading key).
- Log instead of silently dropping a task when GetTask fails in the
  PR-match loop.

* test(github): dedup PR-lookup test setup into shared newTestStore

Add workspace_id to newTestStore's tasks table and drop the near-identical
newTestStoreWithWorkspaces helper, per review feedback.

* perf(tasks): batch-fetch PR-matched tasks instead of N+1 GetTask

Add a repository GetTasksByIDs and use it in augmentWithPRMatches so PR-number
search loads all matched tasks in one query instead of one GetTask per id,
addressing the N+1 noted in review. Fetch+filter is extracted into a
fetchPRMatchedTasks helper that preserves resolver order and dedup.
2026-06-05 16:20:30 +01:00
Nicolas Lenepveu 0eac403499 fix(repo): exclude archived tasks from repository delete guard (#1274) 2026-06-05 15:59:21 +01:00
João Salavisa d83c1dd982 feat(diff): use stored base_branch for diff stats + per-task compare picker (#1273)
* feat(diff): use stored base_branch for diff stats + per-task compare picker

Task-card additions/deletions and the Changes panel were computed against a
hardcoded origin/main → master priority list inside agentctl. For tasks
branched off a release/staging branch this inflated the counts by every
commit since the actual fork point, and the Changes panel grouped unrelated
files alongside the task's real work.

This wires the per-repo task_repositories.base_branch end-to-end into the
agentctl WorkspaceTracker so each repo's diff stats resolve against the
recorded base instead of the integration-branch fallback. A picker on the
Changes panel branch hover card lets the user change the base mid-task; the
new value is persisted via PATCH /tasks/:id/repositories/:repo_id, pushed
live to the running agentctl instance, and the session's cached
base_commit_sha is cleared so the commits panel + cumulative diff refetch
against the new base without a session restart.

Falls back to the existing origin/main → master list for legacy tasks
(empty base_branch) and to the branch tip when no merge-base exists with
HEAD (unrelated histories — local rebase backups, etc.) so the stats and
commits panels stay anchored on the same SHA in every failure mode.

* fix(agentctl): sanitize user-controlled base_branch refs before git exec

CodeQL flagged the picker's user-controlled base_branch payload reaching
exec.Command("git", ..., baseBranch) inside workspace_git_cmd.go as a
command-built-from-user-controlled-source vulnerability. A malicious caller
could smuggle a leading-"-" argument like "-upload-pack=..." that git
itself reinterprets as a flag, even though exec.Command keeps args
separate from the shell.

Adds an IsSafeGitRef sanitizer at every boundary the value crosses on its
way to git: the service write path, the agentctl HTTP handlers
(/workspace/base-branches, /git/log, /git/cumulative-diff), and
WorkspaceTracker.SetBaseBranch itself. Unsafe values are rejected at the
service tier and silently downgraded to the no-override fallback inside
agentctl so a misbehaving caller can't half-break the tracker.

* fix(diff): address PR review blockers and CI lint

- Split UpdateRepositoryBaseBranch into validate/load/apply helpers to drop
  cyclomatic complexity below the cyclop limit that gated Run Backend Tests.
- Replace direct m.cfg.BaseBranches reads/writes with getBaseBranches /
  setBaseBranches under a dedicated RWMutex. UpdateBaseBranches used to
  write under repoTrackersMu while rescan + lazy-subpath callers read under
  workspaceTrackersMu, leaving an unsynchronized data race on the map.
- Drop the false 'PR target is auto-wired' claim from the MCP tool
  description — GitOperator.CreatePR takes baseBranch as an explicit arg
  and never reads the tracker's value.
- Picker no-op guard now compares against currentBase instead of storedBase
  so selecting the displayed fallback (legacy task before any picker
  change) is treated as no-op instead of triggering a spurious save.
- HTTP handler now maps non-validation service errors to 500 instead of
  400, classifying via the existing isValidationError helper plus the new
  unsafe-git-ref message.

* fix(diff): regex-based ref sanitiser at every git subprocess sink

CodeQL kept flagging workspace_git_cmd.go:43 even after SetBaseBranch
rejected unsafe values — the taint tracker doesn't follow sanitised stores
through struct fields, so it still saw user input flowing into
exec.Command("git", ..., baseBranch).

Switches IsSafeGitRef to a single regex check (a pattern CodeQL recognises
as a sanitiser barrier) and adds SanitizeGitRef calls inline at every
agentctl call site that splices a user-controlled ref into a git arg:

  - workspace_git_status.resolveStoredRef (before the rev-parse probes)
  - workspace_git_status.computeBaseCommit (before merge-base/rev-parse)
  - workspace_git_status.getAheadBehindCounts (before rev-list)
  - agentctl/server/api/git.go runGitLogForRepo + handleGitCumulativeDiff
    (before computeMergeBase/GetRevParse on the HTTP TargetBranch)

Behaviour is unchanged for valid refs — only narrows the allowlist (now
[A-Za-z0-9_./-]) so the sanitiser barrier is recognisable to static
analysis. SetBaseBranch's field-store sanitiser stays as defence in depth.

* fix(diff): transform sanitiser at every map read for CodeQL taint flow

CodeQL still flagged workspace_git_cmd.go:43 because:
- handleSetBaseBranches used an IsSafeGitRef bool guard before a verbatim
  map[k] = v copy. CodeQL recognises regex-transforming sanitisers but not
  bool-guarded passthroughs.
- lookupBaseBranch returned m.cfg.BaseBranches[k] verbatim. Even though
  the map was sanitised on write, taint flow across map stores + field
  stores loses the sanitised state in the analyser.

Switches both sites to the transforming SanitizeGitRef helper so every
read produces a freshly-sanitised value before it flows to a downstream
git argument. Behaviour is unchanged for valid refs.

* fix(diff): regex transformer sanitiser + guard live push from empty map

CodeQL still flagged the previous SanitizeGitRef because it returned the
input string unchanged on success — an identity passthrough that the taint
tracker doesn't recognise as a sanitiser barrier.

Switches to safeGitRefPattern.FindString so the output comes from the
regex match (not the parameter). IsSafeGitRef now wraps SanitizeGitRef so
the predicate and the transformer share one allowlist.

Also addresses two Claude review blockers:

- collectTaskBaseBranches returns a nil/empty map when the task has no
  recorded base_branches; pushing that to agentctl wipes every tracker's
  override. Now skipped — the DB row is the source of truth for the next
  session launch.

- applyBaseBranchSideEffects now runs on context.WithoutCancel so an HTTP
  / WS request cancel after the task_repositories UPDATE commits can't
  leave session.base_commit_sha stale + the live agentctl tracker map
  un-pushed, drifting from the persisted row.

* fix(diff): share securityutil.IsValidBranchName + harden error classification

CodeQL still flagged the picker's regex sanitiser as taint passthrough.
Refactors SanitizeGitRef / isSafeBaseBranchRef to delegate to the existing
securityutil.IsValidBranchName helper that agentctl's Rebase / Merge /
RenameBranch already use without being flagged. The shared allowlist also
fixes a Claude review blocker: 'feature/@2024' used to pass the service
validator but get dropped by the agentctl-side check at push time,
silently wiping every tracker's override.

Plus the two error-classification blockers Claude raised on the previous
HEAD:

- WS handler (task_ws_handlers.go wsUpdateTaskRepository) now distinguishes
  validation errors (surfaced verbatim) from internal faults (opaque
  500-style message). HTTP path already did this via the existing
  isValidationError helper.
- MCP handler (handlers.handleUpdateRepositoryBaseBranch) gets the same
  treatment via a new isValidationError local to the mcp/handlers
  package so raw service error strings stop leaking across the MCP
  boundary.

* fix(diff): inline securityutil.IsValidBranchName at every git subprocess sink

CodeQL's interprocedural taint tracking wasn't recognising SanitizeGitRef
as a sanitiser even after it delegated to securityutil.IsValidBranchName
— the wrapper helper + map/field hops between source and sink kept the
flow tainted in the analyser.

Inlines the IsValidBranchName allowlist check (already used inline by
Rebase/Merge in git.go without being flagged) at every call site that
splices a user-controlled ref into a git subprocess argument:

- workspace_git_status.resolveStoredRef (before rev-parse --verify)
- workspace_git_status.computeBaseCommit (before merge-base / rev-parse)
- workspace_git_status.getAheadBehindCounts (before rev-list)
- agentctl/server/api/git.go runGitLogForRepo + handleGitCumulativeDiff
- agentctl/server/api/workspace_base_branches.go handleSetBaseBranches

Behaviour unchanged: same allowlist as Rebase / Merge / RenameBranch
already use. SanitizeGitRef / IsSafeGitRef survive as defence-in-depth
helpers for callers that want a one-line check; they delegate to the same
securityutil helper so the rules stay aligned.

* fix(diff): inline regexp.MatchString at every git subprocess sink

CodeQL still flagged workspace_git_cmd.go:43 even with the inline
securityutil.IsValidBranchName helper — the taint tracker apparently does
not follow into helper functions for the go/command-injection rule on
this path.

Hoists the regex to package-scope vars (safeBranchRefPattern in both
process and api packages) and calls regexp.MatchString directly at each
sink site. Behaviour is identical to securityutil.IsValidBranchName; only
the call form changes.

* fix(diff): SHA-shape allowlist on BaseCommit before downstream git diff

CodeQL still flagged workspace_git_cmd.go:43 because update.BaseCommit
is the OUTPUT of an earlier merge-base / rev-parse call whose argument
list contained a user-controlled branch ref — the analyser treats that
command's output as still tainted even when the input ref was sanitised.

Adds an inline 40-hex SHA-shape allowlist check in enrichWithBranchDiff
right before `git diff --numstat <update.BaseCommit>` so the sanitiser
barrier sits in the same function as the next subprocess call. Behaviour
is unchanged: merge-base / rev-parse already produce SHA-shaped output.

* fix(diff): async tracker refresh + tighten init spread order

Two CodeRabbit/Claude review fixes:

- UpdateBaseBranches stamps each tracker's new baseBranch synchronously
  then runs RefreshGitStatus on a background goroutine instead of inside
  the picker-save HTTP handler. Per-tracker git subprocesses can each
  take ~3-5 calls; serialising them on the request goroutine made the
  PATCH response wait for every refresh to finish. The picker UI re-fetches
  via the existing WS stream once Refresh emits, so the handler doesn't
  need to block.

- updateTaskRepositoryBaseBranch now spreads options.init BEFORE the
  required method/body fields so an upstream caller's init overrides
  can't replace the PATCH method or the base_branch payload. Matches
  the convention deleteTask / moveTask already follow.

* fix(diff): narrow mcp isValidationError + opaque 500 + post-sanitize log count

Three final Claude review threads on the latest commit:

- mcp/handlers isValidationError drops the broad 'invalid' substring so
  DB / IO errors whose messages happen to contain 'invalid' don't
  misclassify as ErrorCodeValidation.

- task_http_handlers httpUpdateTaskRepository now logs the raw service
  error server-side and returns an opaque 'failed to update task
  repository' on the 500 path. Stops potential DB / IO detail leakage
  through the HTTP boundary.

- workspace_base_branches debug log reports accepted + rejected counts
  (post-sanitisation) instead of the pre-filter total so the metric
  actually reflects what hit UpdateBaseBranches.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-06-05 15:53:39 +01:00
João Salavisa 3652f0fd79 fix(agentctl): order workspace stream wg.Add before publish to fix Close race (#1272)
* fix(agentctl): order workspace stream wg.Add before publish to fix Close race

Client.StreamWorkspace published the new WorkspaceStream pointer to
c.workspaceStream under the lock, then released the lock and called
stream.wg.Add(2) afterwards. A concurrent Client.Close captured
c.workspaceStream under the same lock and then called ws.Wait() outside
of it, racing the subsequent Add(2) on a WaitGroup whose counter was
still 0. The race detector flagged this as Add-after-Wait on the same
counter address — reproduced in CI in
TestInitializeAndPrompt_StreamBeforeInitialize and
TestManager_RestartAgentProcess_StopErrorIsNonFatal, both of which use a
deferred client.Close after the StreamManager spawns the workspace
stream goroutine.

Move stream.wg.Add(2) under the same lock that publishes the pointer.
The unlock/lock pair now carries the Add into Close's view, so any
caller that observes c.workspaceStream is guaranteed to see the counter
already bumped before invoking Wait.

Adds TestClientClose_StreamWorkspaceAddWaitRace as a deterministic
regression: it waits for the server to observe the WS upgrade, then
fires Close in parallel with StreamWorkspace — pre-fix this triggers
the race within a handful of iterations under -race.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(agentctl): defer ctx cancel in StreamWorkspaceAddWaitRace iter

Wrap the per-iteration body in a runIter closure so `defer cancel()`
covers every exit path (timeout fatals as well as the normal flow),
replacing the explicit cancel() calls. Pure test hygiene — addresses
Claude review comment on PR #1272.

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>
2026-06-04 12:19:02 +01:00
Nicolas Lenepveu 08189ec539 feat(integrations): add Sentry integration with issue watcher (#1133) 2026-06-04 10:18:17 +01:00
Carlos Florêncio 5e5e115105 fix(web): prevent cross-task session leak in dockview layout switch (#1265) 2026-06-04 09:31:50 +01:00
João Salavisa 01e4917df1 fix(utility): use claude-acp as default inference agent id (#1271)
* fix(utility): use claude-acp as default inference agent id

The utility-agent SQLite schema, frontend dialog defaults, and edit
fallback all pointed at the legacy "claude-code" identifier, but the
only registered Claude inference agent is "claude-acp" (since the ACP
migration in #566). Opening Settings > Utility Agents and picking
Claude resolved to an unknown agent id, so the Model dropdown rendered
"no model found" and the agent could not be configured.

Switch the defaults to "claude-acp" and migrate existing rows so
previously-seeded built-ins (which were saved with agent_id="" and
later coerced to "claude-code" by the dialog fallback) now populate
their model list correctly.

Closes #1269

* fix(utility): cover empty agent_id seeds and add migration test

Address PR review feedback on #1271:

- Extend the agent_id backfill migration to also rewrite rows seeded
  with an empty string. seedBuiltinAgents() inserts built-in rows with
  ON CONFLICT(id) DO NOTHING, so older deployments that landed
  agent_id="" never got repaired by re-seeding.
- Set AgentID = "claude-acp" explicitly in getBuiltinAgents() so future
  first-run seeds match the schema default without depending on the
  dialog fallback or the backfill migration.
- Add sqlite_migration_test.go covering both the legacy "claude-code"
  and empty-string backfill paths, plus a seed-contract assertion that
  every built-in row references "claude-acp" on first run.

* fix(utility): clarify migration comment and check rows.Err in test

Address PR review feedback (round 2) on #1271:

- Reword the agent_id backfill comment as a legacy-data backfill note
  and point at builtinSeedAgentID as the current seed path, so a future
  reader doesn't chase the old "seeds insert agent_id=''" behavior that
  this PR already fixed.
- Add rowsxDB.Err() check after the seed iteration loop, before the
  count guard, so a mid-scan error fails the test instead of silently
  shrinking the row count.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-06-04 03:16:39 +01:00
João Salavisa de334186bc fix(worktree): rmdir empty task parent after worktree removal (#1267)
* fix(worktree): rmdir empty task parent after worktree removal (#1266)

removeWorktreeDir tore down only the inner {repoName}/ subdirectory under
{tasksBase}/{taskDirName}/, leaving the now-empty parent task directory
behind on every archive, delete, and rollback path. Husks accumulated
under ~/.kandev/tasks/ over time.

Now also attempts rmdir on the parent when it is a direct child of
TasksBasePath. os.Remove no-ops if the parent still has siblings or
workspace-scoped content, so multi-branch siblings and the tasksBase
itself are preserved.

* fix(worktree): normalize tasksBase path + cover multi-branch sibling case

Address PR #1267 review feedback:

- Apply filepath.Clean to ExpandedTasksBasePath() before comparing against
  filepath.Dir(parent). A configured TasksBasePath with a trailing slash
  or doubled separators previously fell through the equality guard,
  silently re-introducing the issue-1266 husk for those users.
- Use cfg.ExpandedTasksBasePath() in the regression test for consistency
  with production code.
- Add two test cases:
  - multi-branch sibling worktrees share a parent; removing one preserves
    the parent, removing both clears it.
  - trailing-slash TasksBasePath still triggers cleanup.

* test(worktree): check ExpandedTasksBasePath error in tasksbase survival assert

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-06-04 01:19:57 +01:00
Carlos Florêncio df9a68a103 fix(agentctl): unblock next prompt when agent does not acknowledge cancel (#1259) 2026-06-03 22:36:24 +01:00
João Salavisa be9821fc55 fix(task): surface add_branch_to_task failures instead of silent orphans (#1264)
* fix(worktree): promote FallbackBaseBranch when BaseBranch is empty

CreateRequest.Validate previously hard-rejected an empty BaseBranch even
when the caller supplied a FallbackBaseBranch. The fallback is plumbed
specifically so a missing primary branch can degrade into the repo's
default — this lets that path actually fire.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(task): probe provider default branch in add_branch resolve path

Adds an opt-in ProviderDefaultBranchProber seam (impl wired in cmd/kandev)
and threads a ResolveProviderDefaults flag through TaskRepositoryInput so
the add_branch_to_task GitHub-URL path can resolve a real default_branch
before writing the Repository row. Also backfills the existing repo's
default_branch when the probe succeeds and the workspace already had a row
with an empty value. The create_task path keeps its zero-default contract
(executor still backfills after clone).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(task): surface add_branch_to_task failures instead of silent orphans

AddBranchToTask used to return a success envelope even when the
post-insert worktree materialize step failed on a live task. The
task_repositories row stayed in place with no on-disk worktree, and
provider-URL flows also left an orphan repositories row behind.

Three coordinated changes:
- Gate empty base_branch after resolution with a typed error so no row
  is written when nothing can be materialized.
- Track whether the repository row was created by this call and roll it
  back on any subsequent failure.
- materializeBranch now returns its error; on a live worktree-executor
  task, AddBranchToTask rolls back the task_repositories row and surfaces
  the failure. Pre-launch tasks keep the legacy best-effort path so the
  next session launch still materializes via the multi-repo prepare flow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(mcp): classify add_branch base_branch resolution failure as validation

AddBranchToTask now returns a "cannot resolve base_branch" error when the
provider repo's default branch could not be probed. Map it to
ErrorCodeValidation so MCP agents see a user-fixable input issue rather
than an opaque internal error.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(task): defer task.updated until after worktree materializes

Publishing task.updated before materializeBranch ran meant a failed
materialize on a live task would push the new task_repository row to WS
clients and then silently roll it back, leaving stale UI state until the
next poll. Move the event after materialize success so the bus only sees
durable rows. Test now also asserts no task.updated fires on the rollback
path. Renames stubProber.lastProvid to lastProviderArg for clarity.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(task): emit repository.deleted on add_branch orphan rollback

Cleanup callback previously called repoEntities.DeleteRepository directly,
which soft-deletes the row but bypasses Service.DeleteRepository's event
publish. The create path emitted repository.created, so skipping the
delete event left WS subscribers and frontend caches holding a phantom
row after a resolve-then-fail sequence. Route through the service method
so the events stay symmetric. Test now asserts the symmetry.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(task): skip provider probe when workspace already has the repo

resolveRepoInput's add_branch probe fired on every add_branch_to_task
call with a github_url, even when the workspace already had the
provider repo registered with a non-empty default_branch. The existing
row's value would win seconds later via the repo.DefaultBranch fallback,
so the remote git ls-remote round-trip was pure waste — and unnecessarily
hit network on every call. Peek at the workspace registry first and
short-circuit when a usable default_branch is already on file.

Also adds a QA-tier test file covering probe-error fallthrough, bad URL
rejection, registered-repo probe skipping, empty-default backfill, and
the repository_id fast path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(task): detach rollback paths from caller ctx, log probe-lookup errors

Two adjacent robustness fixes flagged on PR #1264:

1. resolveRepoInput silently discarded the GetRepositoryByProviderInfo
   error before the provider probe. A transient DB failure was
   indistinguishable from a clean "not found" lookup, so the code would
   pay for a network probe instead of failing fast. Surface the error in
   logs and skip the probe — FindOrCreateRepository below already hits
   the same DB and will return the real cause.

2. The repository-create cleanup callback in resolveBranchAddRepoRef and
   the task_repositories rollback in AddBranchToTask both captured the
   caller ctx for their DELETE queries. Rollback often runs precisely
   because that ctx was cancelled (timeout, client disconnect), and
   reusing it would kill the cleanup and orphan the row we just created.
   Use context.WithoutCancel so the rollback survives ctx death.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(task): close orphan-row gaps from caller cancel and concurrent create

Two findings from Claude's review on PR #1264:

1. (blocker) materializeBranch called taskAlreadyLaunched(ctx, ...) after
   MaterializeBranch returned, using the same caller context that often
   failed the materialize in the first place. A cancelled ctx made the
   post-failure DB lookup return (nil, err), taskAlreadyLaunched returned
   false, and the live-task rollback path silently demoted itself to the
   pre-launch best-effort branch — leaving the orphan task_repositories
   row the PR exists to prevent. Snapshot the launch state BEFORE
   MaterializeBranch (launch state can't change during the synchronous
   call) so the rollback decision is anchored to a live-ctx answer.

2. (suggestion) resolveBranchAddRepoRef inferred "this call created the
   repo" by comparing the resolved ID against a workspace ListRepositories
   snapshot taken before FindOrCreateRepository. A concurrent add_branch
   for the same GitHub URL could create the row between snapshot and
   lookup; the second request would treat it as "not in my snapshot, must
   be mine" and register cleanup against the other request's data. On any
   later failure, cleanup would DELETE a repository this call didn't
   create and emit a spurious repository.deleted event.

   Thread an explicit `created bool` out of CreateRepository through
   FindOrCreateRepository → resolveRepoInput → ResolveRepositoryRef so
   the cleanup callback fires only when this call physically inserted
   the row. The flag is set at the CreateRepository / FindOrCreateRepository
   sites themselves and cannot be spoofed by concurrent activity.

resolveRepoInput's branches were extracted into resolveRepoInputGitHub /
resolveRepoInputLocal helpers to keep the parent function inside the
cyclomatic-complexity and cognitive-complexity budgets after the signature
change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: retrigger cubic CI

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 15:43:41 +01:00
João Salavisa 9aaf3170e0 fix(lifecycle): drain StreamManager goroutines in tests (#1227)
* fix(lifecycle): drain StreamManager goroutines in tests

Goroutines spawned by `agentctl.Client.StreamUpdates` /
`StreamWorkspace` and by `lifecycle.StreamManager.connectWorkspaceStream`
could outlive the tests that created them on slow CI runners, causing
intermittent `goleak.VerifyTestMain` failures in
`internal/agent/runtime/lifecycle` even though every individual test
passed. The leak required the test to race: `client.Close()` could
return while a workspace dial was still in flight, leaving the
just-spawned WS read/write loops with nobody to drain them, and
`StreamManager.Wait()` only fired the drain when the external `stopCh`
had been closed first.

Make the close paths absolute drain barriers:

- `Client.Close()` now tracks every stream goroutine it spawns
  (`streamWG` + a per-stream `WorkspaceStream` reference) and blocks
  until all of them have exited. A `closed` flag flipped under the
  client mutex makes subsequent `StreamUpdates` / `StreamWorkspace`
  calls reject the dial, so a Close that races a dial in flight cannot
  leave behind a stranded WS connection. `manager_interaction.go`
  switches the restart path to per-stream Close helpers so it can keep
  reusing the client after teardown.
- `StreamManager.Wait()` closes a new internal `waitCh` that the retry
  backoff (`sleepOrStop`) and the connected `<-ws.Done() / <-stop>`
  select also observe. This makes `Wait` an absolute drain barrier
  regardless of whether the caller ever closes the external `stopCh`.
  `streamContext` carries both stops through `stopChannelContext` so
  in-flight WebSocket dials cancel on either signal.

Local stress (`make test-lifecycle-goleak LIFECYCLE_GOLEAK_COUNT=20`)
passes 20/20 with `-race` after the fix. The new
`TestClientClose_DrainsStreamGoroutines` covers the drain barrier and
the post-Close rejection path. Added a `test-lifecycle-goleak`
Makefile target as the repro hook for the CI-only flake.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(lifecycle): track mergeStops goroutine and guard workspace stream races

Address PR review feedback:
- stopChannelContext now registers the mergeStops goroutine on an optional
  *sync.WaitGroup so sm.wg.Wait stays a true drain barrier even for the
  connectUpdatesStream path (outer goroutine returns immediately after dial).
- StreamWorkspace re-checks workspaceStreamConn after dial to seal the
  concurrent-callers race that could orphan a conn.
- readWorkspaceStream's defer now guards the workspaceStreamConn reset by
  identity so an old read goroutine cannot zero out a newer conn pointer.
- Clarify comments on streamWG, Client.Close, and the mock.connected wait
  in the close-barrier test.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(lifecycle): drop agent-stream drain barrier to unblock workflow cascade

The previous Close drain barrier for the agent (updates) stream blocked
the on_turn_complete cascade flow. completeAndStopSession calls Close
synchronously on the current session's client, and Close was waiting on
streamWG (which tracked the read goroutine). The wait stalled the
cascade long enough that Step2's auto-started session never got its
profile name into the tab — surfacing as a flaky "Profile B tab not
visible" failure in workflow-agent-switch E2E shard 7.

Drain barrier is still in place for the workspace stream (where the
original goleak flake lived). Agent stream is closed but not awaited:
the cascade legitimately stops + restarts the agent stream on the same
client, and the workspace stream's existing barrier is sufficient for
the leak coverage.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-06-03 11:06:17 +01:00
João Salavisa 5143a76e35 fix(agents): emit --dangerously-skip-permissions for Claude CLI passthrough (#1262)
* fix(agents): emit --dangerously-skip-permissions for Claude CLI passthrough (#1261)

Claude ACP declared empty PermSettings in passthrough mode, so the launch path's
PermissionValues map (which carries the profile's DangerouslySkipPermissions
toggle) had no matching CLI-flag mapping to emit. Bypass / auto-approve was
silently ignored. Wire the permission setting to --dangerously-skip-permissions
and skip the key in seedCLIFlags so the toggle does not double-surface in the
curated cli_flags list.

* test(agents): use PermissionKeyDangerouslySkipPermissions constant in claude_acp_test

* test(agents): use constant in claude_acp_test failure message too

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-06-03 10:14:03 +01:00
João Salavisa 0b7bbd93d5 feat(mcp): accept repository_url and local_path on add_branch_to_task_kandev (#1256)
* feat(mcp): accept repository_url and local_path on add_branch_to_task_kandev

Mirror create_task_kandev's repo-locator surface so agents can attach a
branch without first looking up the repository UUID. The MCP tool now
takes repository_id OR repository_url OR local_path; the service
resolves alternatives via the same workspace-scoped find-or-create
path used by create_task.

* fix(mcp): run executor guard before repo resolution on add_branch_to_task

ResolveRepositoryRef may FindOrCreateRepository / CreateRepository
inside the github_url / local_path paths. Running it before
requireWorktreeExecutorForBranchAdd let a rejected add_branch call
on a docker/sprites/SSH task leak an orphan Repository row into
the workspace.

Move the executor check ahead of resolution and add a regression
test that asserts ListRepositories count is unchanged after a
rejected call with a brand-new github_url.

* test(mcp): assert task_id default in add_branch local_path test

* fix(mcp): reject multiple repo locators on add_branch_to_task

resolveRepoInput applies a silent precedence (repository_id >
github_url > local_path), so an agent that accidentally supplies
two of the three locators got a behaviour change with no signal.
Reject at the WS handler with a clear error so the mistake is
surfaced.

Also refresh the AddBranchToTask docstring to document the new
LocalPath / GitHubURL inputs and to correct the workspace-membership
check ownership (now enforced by ResolveRepositoryRef → resolveRepoInput
for the alternative paths).

* fix(mcp): classify resolve_repository errors as validation, not internal

Failures from ResolveRepositoryRef are user-fixable input mistakes
(malformed URL, unknown local_path, cross-workspace repository_id).
Without a matching case the wrapper "resolve repository: ..." message
fell through to ErrorCodeInternalError, so an agent that supplied
e.g. a bitbucket URL saw a server-fault code instead of a validation
error it could correct.

* fix(mcp): narrow add_branch error classification + use wire field name

Two follow-ups to the round-3/4 review feedback:

- Mutual-exclusion error mentioned the MCP tool alias `repository_url`
  but the WS handler sees the wire field `github_url`. A direct WS
  caller would see an error naming a field it never passed.
- The broad `"resolve repository: "` prefix classified ALL wrapped
  errors as Validation, including DB / system failures from
  CreateRepository / ListRepositories inside ResolveRepositoryRef.
  Narrow to specific user-fixable patterns (GitHub URL parse failures,
  cross-workspace repository_id) so genuine internal errors no longer
  masquerade as validation rejections.

* fix(mcp): mutual-exclusion check at MCP tier names agent-facing alias

WS-tier rejection works for direct WS callers but its error mentions
github_url, the wire field — confusing for MCP agents that supplied
repository_url. Move the check up to addBranchToTaskHandler so the
error reads "pass at most one of repository_id, repository_url,
local_path" using the names the agent actually typed. WS handler
still validates as defense-in-depth.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-06-03 03:04:04 +01:00
github-actions[bot] d2ab3cd509 release: 0.55.0 2026-06-02 23:11:24 +00:00
Carlos Florêncio 7917c95496 feat(backend): inject MCP servers in CLI passthrough mode per agent (#1158) 2026-06-03 00:09:05 +01:00
Carlos Florêncio 79633ea376 fix(web): preserve repo metadata in kanban.update and check legacy repositoryId (#1258) 2026-06-03 00:02:44 +01:00
Carlos Florêncio dfc132c6f1 fix(orchestrator): block workflow advance during pending clarifications (#1251) 2026-06-02 23:38:41 +01:00
João Salavisa d44abe327b fix(web): stop refetching GitLab status on every tab refocus (#1257)
useGitLabAvailable registered a window "focus" listener that re-fired
GET /api/v1/gitlab/status on every browser tab switch. IntegrationsMenu
mounts globally, so the hammer was site-wide.

Route the hook through the store-cached useGitLabStatus so consumers
share a single fetch and the status no longer re-probes on focus.
Matches the useGitHubStatus pattern. Jira/Linear use the 90s poll in
useIntegrationAuthed and were unaffected.

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-06-02 23:32:38 +01:00
Nicolas Lenepveu 72e3a49718 fix(watcher): self-heal orphaned watchers when agent profile is soft-deleted (#1094) 2026-06-02 23:17:56 +01:00