* feat(review): persist and publish native code-review findings
Adds the backend core for reviewing a task's working diff before a PR
exists: task-scoped review runs and anchored, advisory findings that the
Changes/Review panel can render as inline comments.
- task_review_runs / task_review_findings schema, models, and repository,
with supersede-on-re-review and a restart sweep that cancels in-flight
runs rather than leaving them looking alive.
- ReviewService as the single write path, publishing bus events that the
WS gateway forwards to the client.
- internal/review: reviewer-identity resolution (agent profile > code-review
utility agent > user default, failing closed on passthrough-only profiles),
change-set collection, prompt batching, response parsing, anchor
extraction, and run orchestration.
- publish_review_findings_kandev MCP tool plus the task.review.* WS actions.
- Go djb2 that matches apps/web/lib/utils/hash.ts, asserted from shared
fixtures on both sides so stale detection agrees across the wire.
Spec: docs/specs/native-code-review/spec.md
Plan: docs/plans/native-code-review/plan.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(review): add run_code_review workflow step and wire the review runner
Makes a review pass triggerable both ways and actually runnable end to end.
- run_code_review on_enter action across models, engine compile, and an
orchestrator callback that logs rather than propagates failures, so a task
never gets stuck in a step because no reviewer was configured.
- The step's optional agent_profile_id is what lets a step review with a
different model than it implemented with; export/import swap it for a
portable agent name/model/mode so it survives a cross-workspace round trip
instead of carrying a dangling id.
- Concrete adapters for reviewer identity, changed-file collection, prompt
rendering from the code-review utility agent's editable prompt, and
inference (session-bound agentctl, falling back to the host instance so a
review works on a task whose agent is not running).
- Boot sweep cancels review runs interrupted by a restart; the E2E reset
clears review state before deleting the tasks that own it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(web): add review findings state, helpers, and live updates
Frontend plumbing for native code review, ahead of the UI that renders it.
- Review types, WS message map, and an API client that preserves the backend's
machine-readable failure code so the surface can show an actionable message.
- A review store slice that merges findings by id (a re-publish refreshes an
issue rather than listing it twice) and keeps a bounded run history.
- Findings helpers: composite file keys matching reviewFileKey, staleness via
the same per-file diff hash that drives review marks, and best-effort
re-anchoring from the stored anchor text so a moved finding follows the code
it was written about instead of pointing at whatever now occupies its line.
- Hooks for backfill, resolve/dismiss/reopen with optimistic rollback, and
sending a finding to the agent as follow-up context without changing its
status — handing a finding to an agent is not the human accepting it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(web): render review findings inline in the Changes diff
Findings now appear where the code is, in the same annotation surface as
anchored review comments, with the run control that produces them.
- ReviewFindingCard: severity chip, category, Markdown body, and the three
advisory actions. A suggestion is labelled "not applied automatically"
because it is display-only in this iteration. Resolved and dismissed
findings collapse to one muted line with an Undo, so a reviewed diff does
not stay cluttered while the human can still change their mind.
- A review-finding annotation type threaded through the diff viewer, anchored
from the same per-file diff hash the backend stamped. A finding whose diff
moved is re-anchored from its stored text when that text survives, and
otherwise reported as unanchored rather than placed against unrelated code.
- Review changes control with idle / running / cancel / failed states, and an
inline Settings link for the one failure the user can actually fix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(web): make the review control reachable from the Review toolbar
Mounts the Review changes control next to the walkthrough action and lets an
inline finding wire its own actions.
The finding card container now reads the active task and session from the store
instead of taking four callbacks threaded through FileDiffViewer and DiffViewer;
an inline finding annotation only exists when the active task has findings, so
the store is always present and every other diff consumer is spared the
plumbing. Staleness likewise hashes the diff the viewer already holds, which is
already normalized, so it matches the hash the backend stamped without passing
it down.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(web): surface stale review findings above their file's diff
A finding whose diff moved and whose anchor text is gone had nowhere to render,
so it was silently dropped. It now appears in a collapsed per-file banner above
the diff: the finding may still be valid, and placing it at its stored line
would point at whatever code now occupies that line instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(review): mark the landed native code-review plan tasks done
Tasks 01-09 (backend persistence through inline diff rendering) are
implemented, tested, and committed. Tasks 10-13 — findings overview and step
editor, mobile parity, E2E, and public docs — remain pending.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(review): cover the on-demand review path end to end
Adds the mock-agent review responder and a Playwright spec, so both the happy
path and the no-reviewer path are verified against a real backend.
The mock agent answers any prompt carrying the review sentinel with fenced JSON,
deriving its anchors from the prompt's own diff rather than hardcoding them, so
findings land on files and lines the fixture really changed. It includes one
deliberately malformed entry to exercise the rejected-entry path.
Writing it caught an off-by-one shared by three diff walkers: splitting on "\n"
leaves an empty element after a trailing newline, which carries no diff marker
and was being counted as a context line, pushing new-side numbering one past the
end of a hunk. Interior blank lines still count, since a whitespace-stripped
blank context line legitimately appears as "".
Also adds a listInferenceAgents helper: default_utility_agent_id needs the
registered agent-type id, and an agent row UUID is rejected as not
inference-capable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(review): close review-run races and stale client findings
Addresses the three blockers and three suggestions from the PR #1957 automated
reviews. Two independent reviewers found each blocker separately.
- Launch claimed the task slot only after inserting the run row, so two
concurrent launches both created a run and the loser returned a pending row
with no goroutine behind it — a review that never finished until the next
restart. The slot is now claimed before any DB write, and freed on every
early return.
- Cancelling only marked the row. The goroutine ran on the shared background
context, finished inference, and its completion overwrote the cancelled
status and published findings the user had declined. Runs now carry a cancel
handle, the WS action routes through the runner so the live context is
cancelled, CompleteRun/FailRun refuse to resurrect a terminal run, and a
cancelled pass neither publishes nor re-reports itself as failed.
- The backend deletes a superseded finding and inserts a replacement with a new
id, but the publish event carried only the new one, so every open browser
showed both at the same anchor until a reload. The event now names the
superseded ids and the slice drops them.
- Run() waited on the shared WaitGroup, blocking on every other task's review;
it now waits on its own pass only.
- createReviewSlice is typed over the `set` it actually uses, so call sites no
longer pass superfluous arguments (CodeQL js/superfluous-trailing-arguments).
- A GetAgentProfile fault no longer collapses into "profile not found", which
would have told the user to configure an agent they already have.
The cancel bug exposed a hole in the original tests: they asserted cancel could
not override a terminal run, but never the reverse. Both directions are covered
now, along with the concurrent-launch race, per-run waiting, and the supersede
path on both sides of the wire. Test fakes now mirror the service's terminal
guard rather than hiding it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: notify user when a kandev update is detected
Adds a background-poller-driven update notification with a saved
preference for delivery channel (desktop / in-app banner / both) and
an enable/disable switch on the Updates settings page.
Backend:
- internal/system/updates: NotifySettings + NotifyStore persist the
enable/channel preference on the existing install-wide settings
table; notifyIfNewUpdate broadcasts a WS event the first time a
newer release is observed, deduped durably via kandev_meta so a
restart or repeat poll doesn't re-announce the same version.
- New GET/PUT /api/v1/system/updates/notification-settings endpoints.
- pkg/websocket: new system.update_available action.
Desktop (Tauri):
- native_notifications.rs: task_id is now optional (Option<String>,
required only for session-scoped events) and the event allowlist
accepts system.update_available: alongside the existing session
events, so the update notification can use the native bridge.
Frontend:
- New UpdateNotificationsCard on the Updates settings page (Switch +
channel Select), wired through the shared settings-save-provider
floating Save/Discard action.
- useUpdateAvailableToast bridges the WS event to an in-app toast
and/or the native/browser desktop notification, per the saved
channel, deduped per version.
* docs: document the update notification channel setting
* fix: hydrate update notification settings and broadcast delivery ack
- Only mark a release notification as delivered when the broadcaster
reports at least one connected client, closing a race where an
update could be silently dropped before the first client connects.
- Hydrate updateNotificationSettings through the settings boot payload
(loadSettingsInitialState -> hydrateState) instead of a component-
level fetch, per the boot-payload -> store -> component data flow.
Widen the hydration types (HydrationState) so callers can hydrate a
partial system slice without fabricating the other system fields.
UpdateNotificationsCard now only reads/writes the subscribed store
state.
- Fix module-scope settings-fetch cache leaking across tests in
use-update-available-toast.test.ts so the fetch-failure test
actually exercises the catch/fallback path.
- Add API-client contract tests for fetchUpdateNotificationSettings
and saveUpdateNotificationSettings (GET cache policy, PUT method
and body).
* test: cover delayed hydration and boot-payload mapping for update notifications
* fix: follow session switch when moving between different-agent steps
Moving a task to a workflow step with a different agent profile makes the
backend complete the current session and promote a new primary session for
the new agent. The chat UI was supposed to follow that swap, but the
follow-focus in handleTaskUpdated was suppressed whenever the user had
pinned the session they were on (clicking into a session pins it). The pin
was on the very session being retired, so the user stayed stranded on the
now-completed session — still showing the old agent and plan mode.
The activeSessionId === previousPrimary guard already scopes the follow to
"user is sitting on the session being replaced", so the pin should only
protect a *different* still-live session (active-session drift), never the
one being swapped away. Extract shouldFollowPrimarySwap encoding that rule.
Because it keys off task.updated's primary_session_id it is independent of
WS event ordering and does not depend on the new session row having loaded.
* fix: address review feedback on session-switch follow-focus
Scope the follow-focus fix to genuine session retirement and fix the real
root-cause race, per Greptile/Codex/CodeRabbit review:
- Revert the task.updated pin bypass. Manual "Set as Primary" swaps the
primary without retiring the old session, so bypassing the pin there would
yank a pinned user off a still-live session. Restore pin preservation and
add a guard so a first-primary assignment (null previous primary) never
steals focus.
- Fix the actual bug in the session state-transition handoff: when a retired
session has no non-terminal successor in the per-task list yet (the switch
promotes the new primary via task.updated before the old session's terminal
state_changed lands), fall back to the task's authoritative primary. This
path only runs when the active session truly reaches a terminal state, so it
never fires for manual set-primary.
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
* feat(web): add app status bar
* feat(web): add app status bar visibility toggle
* feat(web): persist status bar item order
* fix(web): refine app status bar behavior
* fix(web): polish app status bar presentation
* test(web): split plugin slot coverage groups
* fix(web): address status surface review feedback
* fix(web): stabilize status bar overlay tests
* feat(plugins): add capability-gated Host reader for conversation content
Plugins can now read historical user/agent message content over the Host
data API, gated by a new `api_read:messages` capability (ADR 0047). This
unblocks "summarize yesterday"-style plugins that the live `message.added`
bus event alone cannot serve.
- proto: `ListMessages` RPC + `Message`/`MessageFilter` on `service Host`.
- SDK: `Host.Messages() MessageReader`, Go-native `Message`/`MessageFilter`.
- host: `api_read:messages` accessor + denied reader; content sanitized via
`sysprompt.StripSystemContent` (no raw system prompts); RFC3339 `since`/
`until` → `InvalidArgument` on bad input.
- data: new `ListMessagesForPlugin` filtered query (session/task ids, type,
created_at range) through the task service, never the DB file.
- tests: in-process + wire capability gating, content stripping, filter/time
passthrough, pagination, and the SQLite query.
- docs: ADR 0047, spec, plugins-manifest, plugins-authoring.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(plugins): add Host.InvokeUtilityAgent for settings-selectable utility agent
Plugins can now delegate a one-shot LLM step to an operator-configured agent,
gated by a new `agent_invoke` capability (ADR 0048), so a plugin needs no
provider API key of its own.
- setting: new `utility_agent_profile_id` user setting (Settings > System >
Utility Agent), a profile dropdown cloned from the existing utility-agent
path; selectable in the UI.
- capability: boolean `agent_invoke`, enforced like `state`/`secrets`.
- proto/SDK: `InvokeUtilityAgent(prompt) -> text` RPC and
`Host.InvokeUtilityAgent(ctx, prompt)`.
- backend: reuses the sessionless host-utility inference tier (ADR 0002) via a
thin adapter — no agent loop, no task/session/workspace; resolves the
configured profile to (agent type, model). Unconfigured / deleted profile ->
gRPC FailedPrecondition.
- tests: capability gating, completion path (asserts the configured profile is
used and text returned), no-utility-agent and stale-profile error paths,
SDK/wire round-trips, and the settings apply/trim behavior.
- docs: ADR 0048, spec, plugins-manifest, plugins-authoring.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(plugins): resolve utility-agent deps live and bound message filter size
Address CI + review feedback on the host plugin capabilities:
- docs: register the new /settings/system/utility-agent route in
docs/public/coverage.json (fixes the "Validate public docs" CI failure).
- utility agent (ADR 0048): hostUtilityMgr is wired late in boot — after
StartActivePlugins has already spawned boot-active plugins — so snapshotting
the utility deps in hostForPlugin stranded those hosts with nil deps and made
InvokeUtilityAgent return Unimplemented for their lifetime. Read the deps live
from the Service (mutex-guarded) at call time instead; add a regression test
for late wiring reaching an already-spawned host.
- messages (ADR 0047): cap the combined session/task/type filter values at 400
so an oversized filter fails fast with InvalidArgument instead of hitting
SQLite's host-parameter limit at query execution; add a test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: add plugin system with grpc runtime and native ui plugins
- go plugin backends ship as release tarballs (per-platform binaries, checksums) that
kandev installs from url/upload or filesystem sideload+sync, then spawns and
supervises via hashicorp/go-plugin (unix socket / loopback tcp+automtls)
- strict typed grpc contract (kandev.plugin.v1): DeliverEvent/InvokeTool/HandleWebhook
plus a capability-gated host service (state, secrets, emit); author sdk in
pkg/pluginsdk; packaging cli in cmd/plugin-pack
- native frontend bundles: window.registerKandevPlugin + registry/host loader with
dynamic routes, nav items, slots and ws handlers; plugins settings page with
install dialog, sync button and enable/disable/uninstall
- gated behind the plugins feature flag (prod off, dev/e2e on); covered by unit and
integration tests plus playwright e2e against a real packaged fixture plugin
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugins): hydrate base app context on plugin routes
Plugin-owned SPA routes classify as RouteUnknown, so the boot payload shipped
only feature flags — no workspaces, workflows, or kanban context. Native plugin
UI (e.g. host.ui.TaskCreateDialog) then had an empty store. Hydrate the same
base context as the Home route for unknown SPA routes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(plugins): expose TaskCreateDialog on host.ui
Lets a native plugin open kandev's real create-task modal (prefilled via
initialValues) instead of POSTing directly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugins): harden filesystem/URL sinks against CodeQL findings
Add sink-level guards on the plugin trust boundary: securejoin containment on
archive extraction (zip-slip), plugin-id validation before every fs_store path,
http/https scheme validation on install-from-URL, and non-interpolated ws-handler
error logging.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugins): address review findings (races, resource caps, lifecycle)
- serialize per-plugin startup to close a TOCTOU double-spawn race, and let a
plugin restart after exhausting attempts
- bound plugin startup with an explicit 30s timeout
- reject backslash paths and cap decompressed bytes by actual size in pkgtar;
only mark a package Signed when a verifier actually ran and passed
- delete plugin_state on uninstall; validate webhook keys against the manifest
and cap webhook body size; restart an active plugin when a new version installs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugins): sanitize plugin-id path sinks for CodeQL
Pin the plugin id to a single path segment with filepath.Base at recordPath/
configPath and pkgtar.Remove — a barrier the taint analysis recognizes.
Behaviour-identical for valid ids (already validated by safePluginID); clears
the go/path-injection alerts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugins): inline plugin-id validation at path sinks for CodeQL
filepath.Base does not strip '..', so it is not a path-injection barrier. Call
the recognized safePluginID / id-pattern guard inline in Delete/GetConfig/
SetConfig and pkgtar.Remove — the same barrier that already cleared the Get sink.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugins): address code-review findings (backend)
- add capability-gating tests for the Host RPC service (was untested)
- close a stop-during-start orphan race so Disable/Uninstall can't leave a
supervised subprocess behind
- on failed upgrade, remove only the new version dir (not all versions + data);
restart the prior version
- validate manifest version and reject ids ending in .config (record-file alias)
- make Deliverer.Refresh non-blocking and cancel in-flight delivery on worker stop
- clamp out-of-range webhook status, skip-and-warn corrupt records in List,
write records atomically, semver-aware sideload version pick
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugins): address code-review findings (frontend)
- wrap plugin routes + settings routes in a shared PluginErrorBoundary so a
throwing plugin page shows a fallback instead of white-screening the SPA
- fix disable/re-enable in one session: keep the cached registration and
re-run initialize instead of relying on a cached ES-module re-import
- prefix root-relative bundle/style URLs with apiBaseUrl (split-origin/Tauri)
- surface partial-install warnings via toast.warning instead of a bare success
- dedupe/remove injected style links on unload; gate PluginNavItems on the flag
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(plugins): serialize per-plugin lifecycle + guard config writes
- add a per-plugin keyed lock in Service so concurrent Enable/Disable/Install/
Uninstall/UpdateConfig for one id are serialized (no status-machine race)
- take s.mu in SetConfig/GetConfig and write config atomically
- bound each plugin initialize() with a timeout so one hung bundle can't stall
the rest of the boot load
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(plugins): split service_test.go to satisfy file-length lint
Move the install/URL test group into service_install_test.go; no logic change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci: build and ship the e2e fixture plugin package to E2E shards
The plugin E2E specs' global-setup requires apps/backend/.build/kandev-plugin-e2e-1.0.0.tar.gz.
Build it in the E2E build job, upload it as an artifact, and download it in both
the regular and container shard jobs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(scripts): launch dev-isolated backend via __backend subcommand
The kandev binary became a bundle launcher with the restart supervisor
(ADR 0019) and exits without KANDEV_BUNDLE_DIR when invoked bare; the
isolated dev instance must run the backend entrypoint directly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(mcp): session spawning, sibling messaging, and renameable tabs
- spawn_session_kandev spawns an additional agent session on an existing
task (default: the caller's own) via LaunchSession(IntentStart), with
optional agent_profile_id (defaults to the spawner's profile) and a
session name; the initial prompt carries spawner attribution and reply
instructions.
- message_task_kandev accepts an optional session_id to target a specific
session, enabling same-task sibling messaging. Explicit targets are
pinned through dispatch so the idle-path primary-session re-resolution
cannot reroute a sibling message back to its sender (regression-tested).
- task_sessions gains a name column (migration ordered after the
agent_execution_id-drop rebuild), a session.rename WS action, and inline
tab rename UI; custom names take precedence over derived agent/model
labels across the tab, agents dropdown, mobile list, chat header, and
dockview panel titles.
- Inter-session messages stamp sender_session_name into metadata so the
sender badge shows which session sent the message.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(mcp): address review findings on session spawn/rename/messaging
- include name in task-session DTOs (list + full, no omitempty) so
persisted tab names survive list hydration and clears propagate
- sanitize sender task/session ids embedded in <kandev-system> blocks
- reject cross-workspace spawn targets; require sender_task_id
- truncate session names by runes, not bytes
- distinguish not-found from internal errors in message target lookup
- guard TabRenameInput blur-commit after Enter/Escape resolution
- cap optimistic rename length client-side and toast on rename failure
- add tests: spawn launch-failure and cross-workspace paths, renameSession
API client
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(mcp): round-two review fixes for spawn/rename lint and hardening
- extract spawn validation/authorization helpers (cyclop/nestif limits)
- make stripSystemTag replace until stable to block nested tag evasion
- load session before rename so a durable rename can never skip its
broadcast; surface lookup failures to the caller instead
- drop redundant sessionLauncher nil check (guarded registration)
- add maxLength to the rename input matching the server cap
- add tests: nested sanitizer evasion, spawn rename-failure tolerance
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(web): add opt-in notification sound for waiting-for-input
Play a configurable per-device sound when a session starts waiting for
user input. Sounds are synthesized with the Web Audio API (no binary
assets), follow the same suppression rules as desktop notifications,
and work independently of the browser Notification permission.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(web): drop stale sound plays when AudioContext resume is delayed
Scheduling notes on a suspended context queued every pending alert and
played them stacked once the user finally interacted. Schedule only
after resume() fulfills and drop plays older than 2s.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(web): harden notification sound playback edge cases
Catch AudioContext construction failures so a broken audio stack cannot
abort the WS handler before the desktop notification is shown, route
interrupted (iOS) contexts through the resume path, and coalesce plays
queued while the context is not running so only one pending cue fires.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(web): make sound test stubs constructible and refine resume queue
vi.fn with an arrow implementation is not constructible, so the
AudioContext stubs silently misbehaved on CI's Node runtime. Also keep
the newest pending play instead of dropping later requests, recheck the
preference before a resumed waiting-for-input cue fires, and validate
picker values with the existing type guard.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* feat: unarchive tasks with worktree branch recovery
Archived tasks can now be restored. Unarchive works for manual/legacy
archives (empty cascade id, root-only) and cascade archives. Archive
deletes the local branch, so worktree recreate now falls back to
fetching the branch from origin, and unarchive writes the old branch
back as checkout_branch so a fresh session resumes the prior work.
Unrecoverable branches (never pushed) surface a warning. WS archive now
routes through the handoff cascade so it stays stamped and reversible.
UI: unarchive action on /tasks archived rows and the task top bar.
* fix: address PR review feedback on unarchive recovery
Forward PRNumber to the recreate fetch so fork-PR heads (only reachable
via pull/<N>/head) are recoverable, publish task.updated per task in the
cascade unarchive loop so the UI restores without a reload, and
re-publish task.updated after a checkout_branch restore since the
unarchive event goes out with the pre-restore repository rows.
* fix: harden unarchive recovery per review feedback
Propagate branch-probe errors instead of misreporting recoverable work
as gone, and reserve ErrBranchUnrecoverable for a confirmed-missing
remote ref (transient fetch failures keep their own error). Validate
the archive root exists so WS/HTTP archive no longer reports success
for unknown task IDs, CAS-guard manual unarchive on an empty cascade
stamp, detach branch recovery from the request context, and skip
checkout_branch restore for multi-branch repos. UI: in-flight guard on
the list unarchive button, pluralized unrecoverable-branch toast.
* docs: document CAS guard on UnarchiveTask interface contract
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>
* fix(sidebar): keep created subtasks during hydration
* fix(sidebar): preserve placeholder subtasks during hydration
* fix(sidebar): mark websocket placeholder snapshots