Skip to content

Latest commit

 

History

History
730 lines (597 loc) · 44.4 KB

File metadata and controls

730 lines (597 loc) · 44.4 KB

Tracking And Debugging

This guide documents where Nectus Desktop tracks state, which events move that state, and where to look when behavior is wrong. It is the authoritative reference for the SQLite tables, the Tauri command and event catalog, the task/chat fields, reviewer-session-resume, and the debugging flows.

For the connected layer model and the "where does X live?" table, see architecture.md; for the per-file maps, see ../AGENTS.md.

State Sources

SQLite

The desktop app opens a local SQLite database named nectus.sqlite3 inside the Tauri app data directory. The app logs the resolved app data directory during startup. The on-disk connection runs in WAL journal mode with synchronous=NORMAL and a 5s busy_timeout (configure_pragmas in native/src/db/mod.rs) — cheaper commits and reads that don't block the writer, durable across app crashes. All access goes through one connection behind a parking_lot::Mutex; the discipline is to never hold that lock across a subprocess (git/gh) or network call — slow work (e.g. worktree-creation git fetch) is done off the lock, and only the fast SQLite write is locked.

Core tables:

Table Purpose
repos Saved project repositories and each project's default worktree root. collapsed is the sidebar fold state of the project's nested agent list (UI preference).
workspaces Durable, named groups of repos (VSCode-workspace style). collapsed is the sidebar fold state of the workspace's nested agent list (UI preference).
workspace_repos Workspace membership: (workspace_id, repo_id, position). Many-to-many, so a repo can belong to several workspaces; cascade-deletes with either side.
agent_profiles CLI agent configuration, including command, model, args, and env.
app_settings Default agent, worktree pattern, branch prefix, theme, density, and the JIRA board config (selected project + filter flags + jira_filter_statuses + jira_filter_epic; the JQL is built from these). Also the non-secret JIRA REST account email (jira_rest_email); the REST API token itself lives in the macOS Keychain, never here.
tasks Primary work item, status, prompt, optional worktree, legacy session columns (kept for older databases but cleared/ignored by the ACP-only runtime), the persisted attention signal (needs_input/NULL), the archived flag (archived tasks are excluded from default list reads), optional JIRA story link, and optional workspace_id (the workspace a cross-repo task was created in). For a cross-repo task the repo_id/branch_name/worktree_path/pr_url columns describe the primary repo.
task_repos Per-repo working state for a task (Increment B): (task_id, repo_id, branch_name, worktree_path, pr_url, position). The complete repo set; a single-repo task has one row mirroring tasks. Unique on worktree_path and on (repo_id, branch_name). Cascade-deletes with the task or repo.
review_loops Current review configuration and status per task. Includes reviewer_session_id (the active reviewer's session id for resume; reset to NULL when the loop is restarted via start_pair_loop).
review_runs Reviewer prompts, outputs, verdicts, and errors by review attempt.
pr_reviews External pull-request reviews: PR metadata, status, verdict (passed/blockers/inconclusive, set when a review reaches ready), Markdown output, ephemeral worktree path, and the consensus columns mode (single/consensus), max_rounds, rounds_completed, converged. Includes reviewer_session_id (preserved across reruns of the same PR review; cleared only when a new review is created). For consensus, reviewer_profile_id is the synthesizer.
pr_review_reviewers Consensus participants: the reviewer profiles taking part in a consensus PR review, in selection order. Cascade-deletes with the review.
pr_review_runs Consensus per-reviewer, per-round outputs: round, verdict, Markdown output, and error. One row per reviewer per round. Cascade-deletes with the review.
chat_sessions ACP chat sessions by task/profile. Stores Nectus' session row id, the provider's acp_session_id for session/load, the cwd, and runtime_json (latest initialize/session metadata: agent capabilities, commands, modes, config options, title).
chat_messages Settled ACP user/agent turns. parts_json stores normalized ChatPart[]; live streaming turns are cache-only until settled.
chat_permission_policies Saved allow-always / reject-always choices keyed by ACP permission tool title.
chat_checkpoints Git shadow commits captured after settled agent turns for Chat tab restore points.

Schema owner: native/src/db/schema.rs

Row mapping and enum parsing: native/src/db/rows.rs

Persistence APIs:

  • native/src/db/mod.rs: database setup plus project, settings, and task records.
  • native/src/db/workspaces.rs: workspace CRUD with transactional membership (workspace_repos) replacement.
  • native/src/db/agent_profiles.rs: agent profile queries and upserts.
  • native/src/db/review_loops.rs: review-loop and review-run records.
  • native/src/db/pr_reviews.rs: PR-review records and owner/repo → project resolution.

Frontend State

Frontend state lives in three layers, not a single god-hook (the old src/hooks/useApp.ts was deleted). See architecture.md ("State ownership") for the full picture; the short version:

  • Server state — TanStack Query (src/queries/). Every saved project/task/profile/settings/review-loop/PR-review/diff read goes through a Query hook backed by the cache (no useState loading boilerplate). SQLite through the Tauri commands remains the source of truth.
  • UI/runtime state — Zustand store (src/store/appStore.ts). Composed from concern-split slices (navigation, selection, composer, runtime, sessionRuntime, notification). Owns what is not server state: current view / focused workspace, repo/task/agent selection, the New Task composer draft (the composer slice — not a form hook), the push-driven liveLines / taskAttention maps, deletingTaskIds, and toasts/messages.
  • Events — one mount-once bridge (src/hooks/useEventBridge.ts). Mounted in AppLayout, it owns every Rust chat/review/PR subscription and routes each event to the Query cache or the Zustand store. Each channel uses src/hooks/useTauriEvent.ts for the Tauri listen lifecycle, so a failed subscription is surfaced without blocking the other event channels. Because events are centralized, the domain hooks are pure cache consumers callable per component.

Review status is included in task summaries so the board can label cards before a task is opened; review is modeled as a single pass.

ACP Chat Runtime

Task agents run through ACP chat, not an embedded PTY. The backend starts the provider descriptor in native/src/sessions/acp.rs, stores chat sessions and settled messages in SQLite, and streams normalized parts through session_chat. Permission requests are chat parts; answering them calls acp_respond_permission and optional allow/reject-always policies are stored in chat_permission_policies.

At startup the runtime sends ACP v1 initialize with Nectus client info and no filesystem/terminal client capabilities. The returned agentCapabilities are stored in chat_sessions.runtime_json and become authoritative for Chat UI image/resume affordances. Static descriptors from list_acp_providers are only a pre-launch hint. Since filesystem and terminal client APIs are not advertised, unexpected provider requests for those methods fail through the ACP connection's unsupported-method path and show up in backend diagnostics/tracing rather than opening host filesystem or terminal access.

When a provider advertises session/load at runtime, the latest persisted acp_session_id can resume the provider conversation. If the app reloads and the process is gone, the next prompt starts a new ACP child and either calls session/load or creates a fresh ACP session, depending on runtime capability and persisted data. session/new and session/load include cross-repo sibling worktrees as ACP additionalDirectories. Agent profiles can provide optional ACP MCP servers via NECTUS_ACP_MCP_SERVERS_JSON in the profile env; the value is a JSON array matching ACP McpServer[] and is passed to both session setup paths. Each prompt sends the user text, image blocks only when the initialized agent advertised image support, file-resource links for the primary cwd and sibling worktree directories, and an embedded Markdown task-context resource when the agent advertises embeddedContext.

Tauri Commands

Frontend wrapper: src/api.ts

Backend registration: native/src/lib.rs

Current commands:

Command Purpose
add_repo Validate and save a local git project.
list_repos Load saved projects.
set_repo_collapsed Persist the sidebar fold state of a project's nested agent list (repos.collapsed).
rename_repo Rename a project's display name (path and worktree root untouched; duplicate names rejected case-insensitively).
remove_repo Remove a project from Nectus. Refused while any task references the repo; cascades only workspace membership and PR-review history. Never touches the repository on disk.
get_app_settings Load global settings.
update_app_settings Save settings and refresh project worktree roots.
create_task Create a direct-edit task or create a git worktree-backed task (single repo).
create_cross_repo_task Create a task spanning ≥2 repos (Increment B): one worktree per repo as siblings under a shared parent, a single agent rooted in the primary repo's worktree, and a task_repos row per repo. Accepts the same optional jira_issue_key/jira_issue_summary/jira_issue_url link as create_task. Rolls back created worktrees on failure.
list_tasks Load task summaries (each with its taskRepos) and per-repo dirty-state checks. archived: true lists the archive view instead of the live boards.
update_task_metadata Update title, status, or PR URL.
set_task_archived Archive (or restore) a task: hidden from boards/lists, kept on disk until deleted. Stale legacy session markers do not block archive/restore.
delete_task Delete a task and remove its worktree(s) when applicable. Takes a force flag: without it a worktree with uncommitted changes is preserved and an error is returned; with it (after the delete dialog's warning) the worktree is force-removed. The git worktree remove runs off the DB lock (plan under a brief lock → remove worktrees off-lock → delete the row under a brief lock). Each removal also runs git worktree prune (clears stale .git/worktrees/<name> admin entries, incl. when the dir was deleted out-of-band) and deletes the orphaned local task-* branch (git branch -D; the remote branch/PR is never touched).
list_workspaces Load workspaces, each with its ordered member repoIds.
create_workspace Create a named workspace from name + repoIds (membership written transactionally; duplicate ids dropped).
update_workspace Rename a workspace and replace its membership/order.
delete_workspace Delete a workspace (membership cascade-deletes).
set_workspace_collapsed Persist the sidebar fold state of a workspace's nested agent list (workspaces.collapsed; does not bump updated_at).
task_diff_summary List the files a task changed (optional repo_id scopes a cross-repo task to one member repo; default primary): a worktree task's branch vs the locally-resolved base (origin/HEAD merge-base, committed + uncommitted), or a direct-edit task's working tree vs HEAD. Returns the base label plus per-file change kind and +/- counts.
task_diff_file Return the unified patch for one file in a task's diff (lazy-loaded per file; untracked files diff against /dev/null).
github_status Report whether gh is installed, authenticated, and the active account.
github_pull_request_status Fetch live PR state, CI check rollup, and review decision via gh pr view --json. Optional repo_id targets a cross-repo member's branch.
detect_github_pull_request Check whether a worktree task's branch already has a PR (gh pr view) and backfill its URL — the primary repo's onto tasks.pr_url, a non-primary member's onto its task_repos.pr_url (repo_id selects the member).
jira_list_projects List visible JIRA projects for the board's project picker (GET /project/search).
jira_search_board Load board work items; the JQL is built from the structured board config (project + filter flags + epic), so no JQL is typed (POST /search/jql, paginated).
jira_list_epics List a project's epics (issuetype = Epic) for the board's epic-filter picker.
jira_sprint_board Load the sprint view (active/future sprints + backlog, issues carrying their epic) via the Agile REST API. REST-token-gated; errors without a token.
jira_get_work_item Fetch a single work item (e.g. to backfill a story description) (GET /issue/{key}).
jira_create_work_item Create a JIRA work item from project/type/summary (+ optional description, assignee, labels); returns the new item (POST /issue, ADF description, assignee resolved to an account id).
jira_transition_work_item Transition a work item to a target status: the status name is resolved to one of the issue's legal transitions and POSTed; a forbidden move errors and the UI reverts the card.
jira_assign_work_item Assign a work item to a user (PUT /issue/{key}/assignee, resolving @me/email/display name to an account id via /myself / /user/search).
jira_comment_work_item Add a comment to a work item (POST /issue/{key}/comment, plain text wrapped in a minimal ADF doc).
jira_rest_status Report whether the JIRA API token (the connection) is connected (Keychain token present for the configured site + email).
set_jira_api_token Verify a token via GET /myself, then store it in the macOS Keychain and persist the non-secret site/email. Stores nothing on failure.
clear_jira_api_token Disconnect: delete the Keychain token and clear the stored REST email.
jira_list_transitions List an issue's legal transitions (GET /issue/{key}/transitions).
jira_project_statuses Load a project's full workflow status set (GET /project/{key}/statuses), unioned across issue types.
set_task_jira_link Set or clear the local JIRA story link on a task (never writes to JIRA).
list_agent_profiles Load agent profiles.
upsert_agent_profile Create or update an agent profile.
start_pair_loop Configure (persist) the reviewer profile for a task; the inline /review command reads review_loop.reviewer_profile_id from this.
stop_pair_loop Stop reviewer automation for a task.
get_task_review_loop Load a task's current review loop.
list_task_review_runs Load stored reviewer runs for a task.
list_acp_providers Load the static ACP provider descriptor export for Claude Code, OpenCode, Codex, and Antigravity: stable provider id, agent kind, display name, launch argv, and coarse resume/permission/image capability states. These are pre-launch hints; initialized runtime capabilities are authoritative after a session starts.
get_task_chat Load the persisted ACP chat session and settled transcript for a task. Optional agent_profile_id scopes the read to that profile's latest session; omit it to load the latest session across all profiles.
acp_start_chat Start an ACP chat process for a task/profile in the task cwd. Launch uses the ACP provider descriptor (native/src/sessions/acp.rs), the login-shell env, augmented PATH, provider-specific executable env, then profile env as the final override layer. Sends ACP v1 initialize with clientInfo, persists returned runtime metadata, then calls session/load only when the agent advertises loadSession; otherwise it calls session/new. Cross-repo sibling worktrees are sent as additionalDirectories; profile NECTUS_ACP_MCP_SERVERS_JSON is sent as mcpServers.
acp_send_prompt Queue a prompt into a live ACP chat session (optional base64 image attachments). The runtime persists and emits the user turn, streams the agent turn, stores the settled reply, and snapshots a git checkpoint after each settled agent turn.
acp_start_review Run an inline /review: resolve the task's configured reviewer (review_loop.reviewer_profile_id), worktree cwd, and resumable reviewer session, then spawn a headless ACP review that streams a Subagent block into the task chat (optional focus text). Records the run and emits review_loop_updated.
acp_respond_permission Resolve a pending ACP permission request from the Chat tab. Allow/reject-always choices persist in chat_permission_policies.
acp_cancel_prompt Gracefully cancel the active ACP prompt by sending session/cancel. The child process and chat session handle stay alive so the session remains resumable/promptable.
acp_set_session_mode Send ACP session/set_mode for the live chat session. The agent reports the resulting mode through session_chat_runtime.
acp_set_config_option Send ACP session/set_config_option for a select-style live chat config option. The agent reports current config options through session_chat_runtime.
acp_stop_chat Hard stop: abort a live ACP chat process and drop its in-memory session handle. Kept as the escape hatch when graceful cancel is insufficient.
list_chat_permission_policies List persisted allow-always / reject-always tool permission policies.
clear_chat_permission_policies Delete all saved chat permission policies.
list_chat_checkpoints List git shadow checkpoints for a chat session (newest first in the DB; the UI reverses for display).
restore_chat_checkpoint git reset --hard the task worktree to a checkpoint commit.
create_pr_review Resolve a PR URL to a known project (matching each repo's remote off the DB lock, on the blocking pool), queue a review, and start the background reviewer. Takes reviewer_profile_ids + max_rounds: one reviewer runs a single review, two or more run a consensus review.
list_pr_reviews Load all PR reviews, newest first.
get_pr_review Load a single PR review by id.
list_pr_review_runs Load a consensus review's per-reviewer, per-round outputs (empty for single reviews).
post_pr_review_comment Post a finished PR review's stored output back to its pull request as a comment (errors if the review has no output yet).
rerun_pr_review Reset a PR review to queued and re-run it against the latest PR head (same single/consensus mode; clears prior rounds).
delete_pr_review Remove a PR review and any lingering ephemeral worktree.
get_diagnostic_logs Return the buffered backend tracing log lines (oldest first) to backfill the Settings → Diagnostics panel. Reads the dedicated diagnostics ring buffer, never the DB lock, so it stays responsive even while a DB-bound command is stuck.

Events

Backend-to-frontend events:

Event Payload Source
review_loop_updated Review-loop state and optional review run. Emitted after an inline /review run is recorded, so the facts-rail review card + task board refresh. native/src/sessions/mod.rs
session_chat ACP chat update: task id, chat session id, optional agent profile id, normalized ChatMessage, and done flag. Streaming updates are cache-only in the frontend (rAF-batched in useEventBridge); settled user/agent turns are persisted to chat_messages. Also feeds liveLines, chatWorkingTaskIds, and task attention for triage (a pending permission → needs_input; a settled turn → idle/finished, which fires a finish toast/notification unless that task is open). The inline /review Subagent block is also delivered over this event. native/src/sessions/acp_manager.rs, native/src/sessions/review_runtime.rs
session_chat_usage Context-window usage (used / size token counts) for the active chat session. native/src/sessions/acp_manager.rs
session_chat_runtime Latest ACP session metadata: initialized capabilities, agent info/auth methods, slash commands, current mode, config options, title, and updated timestamp. Persisted to chat_sessions.runtime_json and routed into the chat transcript cache. native/src/sessions/acp_manager.rs
chat_session_exited Chat session id, task id, optional agent profile id. Clears ephemeral chat runtime state (liveLines, chatWorkingTaskIds, permission attention) when the ACP connection ends. native/src/sessions/acp_manager.rs
pr_review_output Review id, a chunk of a single PR reviewer's live ACP message, and the chunk's byte offset (a 0 offset starts a new run). Streamed by single PR reviews so the Reviews-view Terminal toggle can watch the reviewer live; consensus reviews keep their round matrix and do not stream. native/src/sessions/pr_review.rs
pr_review_updated Updated external PR review (status, verdict, metadata, Markdown output), plus an optional latest_run carrying the consensus round output that triggered the update. native/src/sessions/pr_review.rs, native/src/sessions/pr_consensus.rs
diagnostic_log One captured backend tracing line (the same text written to the console), streamed live to the Settings → Diagnostics panel. Emitted from the diagnostics ring buffer, which is independent of the DB lock so the stream keeps flowing during a hang. native/src/diagnostics.rs

Frontend event listeners:

  • src/hooks/useEventBridge.ts is the single, mount-once bridge (mounted in AppLayout). It owns chat/review/PR subscriptions (session_chat, session_chat_usage, session_chat_runtime, chat_session_exited, review_loop_updated, pr_review_updated) and routes each event to the Query cache (tasks, chat, review loop/runs, PR reviews) or the Zustand store (liveLines, chatWorkingTaskIds, taskAttention, toasts/notifications). Each bridge channel is subscribed through src/hooks/useTauriEvent.ts, which owns the shared late-unlisten cleanup and subscription-error path.
  • The remaining per-component listener is intentionally not in the bridge: src/hooks/usePrReviews.ts listens for pr_review_output and accumulates the PR reviewer's read-only live output for src/components/ReviewTerminalPane.tsx. Task reviews run inline via /review, surfacing as a Subagent block over session_chat, so they need no separate stream.

Task Tracking Fields

Important tasks columns:

  • status: planned, in_progress, review, or done.
  • prompt: optional task instructions sent to a new ACP chat.
  • agent_profile_id: preferred agent profile for the task.
  • has_worktree: whether the task owns a git worktree.
  • branch_name: set only when has_worktree = 1; blank worktree creation generates a task-... branch name.
  • worktree_path: set only when has_worktree = 1.
  • active_session_id: legacy PTY-session marker. The ACP-only runtime clears stale values on app startup and ignores the column everywhere else.
  • attention: backend-owned attention signal — needs_input when the agent is blocked on the user, else NULL. ACP permission parts set/clear the live prompt/reason detail through the push-driven taskAttention store slice; stale persisted values from the legacy PTY runtime are cleared on app startup.
  • last_session_id / last_session_agent / last_session_cwd / last_session_label: legacy PTY resume metadata. Kept for older databases and serde compatibility, but no current task-agent workflow writes or reads it.
  • jira_issue_key / jira_issue_summary / jira_issue_url: optional local-only link to a JIRA story, captured at attach time; null when the task is unlinked. Set/cleared via set_task_jira_link; never written back to JIRA.

The schema enforces that direct-edit tasks have no branch/worktree path and worktree tasks have both.

Additive columns (such as the jira_* task fields above and the app_settings JIRA board config — jira_board_project, jira_filter_my_issues, jira_filter_unresolved, jira_filter_current_sprint, the REST jira_rest_email, the JSON-encoded jira_filter_statuses status filter, the jira_filter_epic epic filter, plus the legacy jira_board_jql / jira_site_url) are introduced by run_migrations in native/src/db/schema.rs, which ALTER TABLEs any missing column on every open so existing databases upgrade in place. The JIRA REST API token is not a column — it lives in the macOS Keychain (native/src/jira_secret.rs).

run_migrations also runs migrate_legacy_worktree_pattern: a one-time data migration that moves databases still on the legacy worktree default (../{repoName}-worktrees) onto the current ~/.nectus/worktrees/{repoName} default and recomputes every repo's stored default_worktree_root from it (the same refresh_repo_worktree_roots path a Settings change uses). It is self-guarding — once rewritten the pattern no longer matches the legacy value, and a customized pattern is left untouched.

Reviewer Session Resume

Reviews run as a headless ACP session (native/src/sessions/review_runtime.rs), the same mechanism chat uses — not a per-provider CLI spawn, and no provider --json stdout parsing. Resume is ACP-native: the driver sends session/load only when the agent advertises the loadSession capability (no per-AgentKind table); an agent without it starts a fresh session/new each pass. The rule everywhere is "capture once, keep": store the resolved ACP session id from the first successful run and pass it to the reviewer on every subsequent run.

Upgrade note: stored reviewer session ids are now ACP session ids. Ids written before this upgrade are not ACP ids and will not resume, so the first post-upgrade review per task/PR starts fresh (a new session/new); the next run resumes normally.

Only ACP providers (Claude, Codex, OpenCode, Antigravity) can review; a Custom reviewer has no ACP descriptor and the run fails fast with a clear error.

Where ids are persisted:

  • review_loops.reviewer_session_id — the active reviewer's session id for the task loop. Reset to NULL when the loop is restarted via start_pair_loop (a restart is intentionally a fresh context). Reused across all idle rounds while the loop stays running.
  • pr_reviews.reviewer_session_id — the single PR review's reviewer session id. Preserved across reruns (rerun_pr_review re-runs against the latest PR head but continues the same reviewer conversation, so repeat reviews build on earlier findings).
  • Consensus runs keep per-reviewer session ids in memory for the duration of one consensus run. They are not persisted to SQLite (no new column on pr_review_runs or pr_review_reviewers).

Reviewer failure diagnostics: a review fails when the ACP turn errors (the agent process can't launch, the agent reports an error, or the turn never produces a parseable verdict block even after the one-shot self-repair). The driver surfaces the ACP error (Reviewer ACP error: …) or, for a verdict-less turn, the unclear-review error; the captured agent message is still stored so you can read what the reviewer said. A Custom reviewer is rejected up front before any process starts.

Inspect stored ids:

sqlite3 "/path/to/nectus.sqlite3" "select task_id, status, reviewer_session_id from review_loops;"
sqlite3 "/path/to/nectus.sqlite3" "select id, status, verdict, reviewer_session_id from pr_reviews order by id desc limit 10;"

Debug Logging

Rust tracing uses the RUST_LOG environment variable. Default filter:

nectus_desktop_lib=info

Run with more backend detail:

RUST_LOG=nectus_desktop_lib=debug pnpm desktop:dev

In-app: the same lines (under the same filter) are mirrored live into Settings → Diagnostics, so you can read the backend log without a terminal — including while the app is hanging, since the diagnostics buffer is independent of the DB lock. Use its Copy button to attach the log to a bug report. Backed by native/src/diagnostics.rs (the diagnostic_log event + get_diagnostic_logs command).

Useful backend log messages include:

  • Opening the app data directory.
  • Starting ACP chat with task id, chat session id, agent, cwd, and whether session/load was used.
  • Task creation creating the worktree off-lock and then inserting the row under a brief lock, plus each timed create_worktree network step (so a slow/stuck worktree creation is visible in the log without freezing the rest of the app).
  • ACP session/new / session/load failures and chat process exits.
  • Failure to emit chat or review events.
  • Review start, recorded verdict, reviewer output, and review-loop errors.

Database Inspection

The database path is printed at desktop startup. Once you have the path, inspect it with:

sqlite3 "/path/to/nectus.sqlite3" ".tables"
sqlite3 "/path/to/nectus.sqlite3" "select id, title, status, has_worktree, branch_name, attention, archived from tasks order by updated_at desc;"
sqlite3 "/path/to/nectus.sqlite3" "select task_id, status, last_error from review_loops;"
sqlite3 "/path/to/nectus.sqlite3" "select task_id, verdict, error from review_runs order by id desc limit 20;"

Do not edit the database directly unless the user explicitly asks for recovery work.

Common Debugging Flows

Project Cannot Be Added

Check:

  • The path exists and is a directory.
  • git -C <path> rev-parse --show-toplevel succeeds.
  • The desktop app can access the selected folder.

Relevant code:

  • native/src/git_ops/mod.rs (repo/branch validation, worktree lifecycle, is_dirty)
  • native/src/db/mod.rs

Worktree Task Fails To Create

Check:

  • Blank branch names generate a task-... branch; entered branch names must pass git-safe validation.
  • Branch name does not contain whitespace, .., ~, ^, :, ?, *, [, backslash, //, trailing /, or .lock.
  • The worktree path does not already exist.
  • The repo has at least one remote.
  • The default branch resolves: normally from the local refs/remotes/<remote>/HEAD symref (no network); if that symref is unset, from git ls-remote --symref <remote> HEAD.
  • git fetch --no-tags <remote> <default-branch> succeeds (only the default branch is fetched — not every ref — to keep creation fast on large repos).

If the whole app freezes (and only when creating a worktree-backed task — a no-worktree task is fine), the cause is almost always git blocking on authentication for one of those network steps. A Finder/Dock-launched app has no controlling terminal, so without guards git would hang forever waiting on a passphrase/credential prompt — and because worktree creation holds the global DB lock, that hang freezes every other command too. git_command (native/src/git_ops/mod.rs) defends against this by seeding the login-shell env (so SSH_AUTH_SOCK and the user's git/ssh config are present, exactly as in a terminal) and forcing non-interactive auth (GIT_TERMINAL_PROMPT=0, GCM_INTERACTIVE=never, batch-mode GIT_SSH_COMMAND). To see exactly where it gets stuck, open Settings → Diagnostics: create_worktree logs a timed line before each network step (… ls-remote, … fetching, … worktree add), so a "starting" line with no following "done" line pinpoints the stuck command.

If the log instead shows create_task: worktree ready; inserting row (brief lock) and then goes silent, the create finished and the hang was in ACP chat launch — see ACP Chat Does Not Start Or Send.

Relevant code:

  • native/src/git_ops/mod.rs (remote resolution, worktree create/remove/branch lifecycle, the non-interactive git env)
  • native/src/db/mod.rs

Worktrees Or task-* Branches Left Behind After Deleting A Task

Deleting a task removes its worktree(s) and tidies up after them; if something lingers, this is the expected behavior and how it self-heals:

  • Removal runs off the DB lock, in three phases — plan_task_deletion (collect worktrees), TaskDeletionPlan::remove_worktrees (the git work), and delete_task_row. A non-dirty failure mid-removal (e.g. a locked worktree) leaves the task row intact; a retry sees the gone worktrees as clean and short-circuits, completing the delete.
  • Stale admin entries under .git/worktrees/<name> (left when a worktree dir was deleted out-of-band) are cleared by git worktree prune, run after each removal. If git worktree list still shows a ghost, run git worktree prune in the repo manually.
  • task-* branches are always deleted — but only the local branch (git branch -D, best-effort). The remote branch and any open PR are never touched, so pushed work is safe; only commits that were never pushed go with the task. Best-effort: a missing or still-checked-out branch is tolerated and never fails the delete.
  • A non-forced delete of a worktree with uncommitted changes is refused with WORKTREE_HAS_CHANGES (all-or-nothing across a cross-repo task's repos); the delete dialog then offers to force-remove.

Relevant code: native/src/db/tasks.rs (plan_task_deletion, TaskDeletionPlan, delete_task_row), native/src/git_ops/mod.rs (remove_worktree, prune_worktrees, cleanup_task_branch).

Agent Command Fails To Start

Check:

  • The profile command is correct in Settings.
  • The command is executable if it is an explicit path.
  • The GUI environment PATH contains the CLI location, or the CLI is in a known fallback location such as ~/.local/bin, ~/.cargo/bin, /opt/homebrew/bin, or /usr/local/bin.
  • Extra args and environment lines in Settings are valid for that CLI.

Symptom — env: node: No such file or directory with exit status 127: the agent binary was found, but a Finder/Dock-launched app has a minimal PATH so the node-based CLI (e.g. Codex or OpenCode) cannot exec node. The fix is already wired: both ACP launch paths — chat (native/src/sessions/acp_manager.rs) and the headless ACP review driver (native/src/sessions/review_runtime.rs, which launches the agent via the shared launch_argv_for_profile) — set the spawned command's PATH to process_util::augmented_path(). If node still lives somewhere unusual (e.g. nvm), add that dir to process_util::third_party_bin_dirs or set PATH on the agent profile's env. See AGENTS.md → Spawning External CLIs for the full rule.

Relevant code:

  • native/src/sessions/acp.rs (provider descriptors / launch argv)
  • native/src/process_util.rs (augmented_path, third_party_bin_dirs)
  • src/components/SettingsPage.tsx

ACP Chat Does Not Start Or Send

Check:

  • The task has an agent profile id, and list_acp_providers reports a descriptor for that profile's agentKind.
  • acp_start_chat resolved the task cwd (worktree path when present, otherwise repo path) and launched the descriptor command from native/src/sessions/acp.rs.
  • The provider command and any descriptor executable env resolve through the login-shell PATH / augmented PATH rules in process_util.rs.
  • Backend diagnostics show whether the ACP child exited, whether session/new or session/load failed, or whether acp_send_prompt targeted a stale in-memory chat id.
  • Frontend useEventBridge is subscribed to session_chat; settled messages should also be visible through get_task_chat.

Relevant code:

  • native/src/sessions/acp.rs
  • native/src/sessions/acp_manager.rs
  • src/components/chat/ChatPane.tsx
  • src/hooks/useEventBridge.ts

ACP Attention Or Live Line Looks Wrong

Check:

  • session_chat events are arriving for the task id/profile id you expect.
  • Permission parts in the normalized chat message include a pending request; those set needs_input taskAttention until answered or until chat_session_exited clears it.
  • A settled turn with no pending permission sets an idle (finished) taskAttention carrying the closing line — the finished agent state — which the next streaming chunk clears. A missing Finished badge means that settled session_chat (with done: true) never arrived.
  • Text/tool parts are present in the stream; applyChatRuntime mirrors them into liveLines and chatWorkingTaskIds.
  • Stale tasks.attention / active_session_id values from old PTY builds are cleared on app startup by clear_legacy_active_sessions.

Relevant code:

  • src/lib/chat/applyChatRuntime.ts
  • src/sessionAttention.ts
  • src/hooks/useEventBridge.ts
  • native/src/db/tasks.rs

Review Does Not Run

Check:

  • A reviewer is configured for the task (the review_loop row exists). /review fails fast with "Configure a reviewer for this task before running /review" when it does not — pick a reviewer in the Review step first (start_pair_loop).
  • acp_start_review can resolve a review cwd from the task worktree path, a task repo worktree path, or the primary repo path. It does not require a separate task chat session.
  • A completed /review run emits review_loop_updated, refreshing the facts-rail review card and task board.
  • The reviewer profile is an ACP provider (Claude, Codex, OpenCode, Antigravity). A Custom reviewer has no ACP descriptor and the run fails fast with a clear error telling you to choose an ACP provider.
  • The reviewer agent launches and its ACP turn completes. An exit status 127 with env: node: No such file or directory is the minimal-PATH problem — see Agent Command Fails To Start above; the ACP launch sets process_util::augmented_path() to fix it.
  • The reviewer ends its message with a fenced ```json block carrying {"verdict": "clean|blockers|feedback"} (the loop maps these to pass/needs_changes/feedback). No parseable block — even after the driver's one-shot self-repair prompt — → unknown; there is no natural-language fallback.
  • External PR reviews share the same JSON verdict block: a finished one shows Inconclusive when the reviewer omitted a parseable {"verdict": …} block. Inspect PR-review verdicts with select status, verdict from pr_reviews order by id desc limit 10;.
  • Consensus PR reviews never converge while any reviewer stays Inconclusive, so they run to the round cap and the synthesizer decides the verdict. Inspect a run's rounds with select round, reviewer_profile_id, verdict from pr_review_runs where pr_review_id = <id> order by id;.

Relevant code:

  • native/src/sessions/review_runtime.rs (headless ACP review driver)
  • native/src/sessions/review_loop.rs
  • native/src/sessions/verdict.rs (parse_verdict_block)
  • src/hooks/useTaskReviewLoop.ts

macOS Notifications Do Not Appear

Current bundle identifier:

com.hvp17.nectus

Check:

  • native/capabilities/default.json includes notification:default.
  • native/src/lib.rs initializes tauri_plugin_notification.
  • src/api.ts requests permission before sending notifications.
  • System Settings includes the app under Notifications.

Reset notification permission:

tccutil reset UserNotifications com.hvp17.nectus

If macOS does not re-prompt after reset, use System Settings first. For local development only, a temporary bundle identifier change in native/tauri.conf.json can force a fresh prompt. Change it back before shipping.

Auto-Update Does Not Offer An Update

Nectus ships a Tauri 2 auto-updater for the Apple Silicon (aarch64) build. The public repo github.com/hvp17/nectus hosts releases, so the updater reads them directly with no token. Integrity is secured by Tauri minisign signing (independent of Apple); the app is ad-hoc code-signed (bundle.macOS.signingIdentity: "-") but not yet Apple-notarized, so the first download triggers a Gatekeeper "unidentified developer" warning the user clears with right-click → Open. If macOS reports the app as "damaged", the download's quarantine flag is the cause — strip it with xattr -dr com.apple.quarantine "/Applications/Nectus Desktop.app". Notarization is a future add-on, out of scope here.

The update state machine lives in src/hooks/useAppUpdate.ts, with the Tauri-guarded wrapper in src/lib/update.ts (all no-ops outside Tauri). It runs one silent check shortly after launch and again on demand from Settings → About & Updates → "Check for updates" (src/components/settings/UpdateCard.tsx); src/AppRouter.tsx (AppLayout) mounts useAppUpdate plus useAppUpdateToast.ts (the "Update available (vX) → Install" and "Update installed → Relaunch" sonner toasts).

UpdateStatus values:

Status Meaning
idle No check run yet.
checking A check is in flight.
upToDate The endpoint reported no newer version.
available A newer version was found; install not yet started.
downloading Download in progress (progress runs 0..1).
ready Downloaded and installed; relaunch to apply.
error Check or install failed (error holds the message).

The hook also exposes info, currentVersion, progress, error, and lastCheckedAt for the About card. Starting a fresh check clears the previous info, progress, and pending install target before hitting the updater endpoint; if that re-check fails, old toast actions have no stale Update object to install. Overlapping checks are last-request-wins: a slower earlier response is ignored once a newer check has started.

The updater fetches the manifest from:

https://github.com/hvp17/nectus/releases/latest/download/latest.json

Expected latest.json shape (Apple Silicon only):

{
  "version": "X.Y.Z",
  "notes": "release notes",
  "pub_date": "2026-01-01T00:00:00Z",
  "platforms": {
    "darwin-aarch64": { "signature": "<minisign>", "url": "<.app.tar.gz url>" }
  }
}

Common failure symptoms:

  • Signature verification fails on install (error status): a pubkey mismatch — the latest.json signature was produced by a private key that does not match the base64 pubkey in native/tauri.conf.json's plugins.updater block. The two must be from the same minisign keypair (the CI signing secrets TAURI_SIGNING_PRIVATE_KEY / TAURI_SIGNING_PRIVATE_KEY_PASSWORD must match the committed public key).
  • Check silently reports "up to date" (upToDate): the release is missing, draft, or unpublished, so …/releases/latest/download/latest.json 404s and the updater treats it as no update. Confirm the GitHub Release is published and carries latest.json (CI auto-publishes it on a version bump merged to main).
  • No update offered even with a newer build out: the published version is not strictly higher than the installed one. The published version comes from package.json; if it was not bumped, no release was cut.

Rust wiring (native/src/lib.rs run()): registers tauri_plugin_process::init() and tauri_plugin_updater::Builder::new().build(). native/tauri.conf.json sets bundle.createUpdaterArtifacts: true and the plugins.updater block (endpoint

  • base64 pubkey, safe to commit). native/capabilities/default.json grants updater:default and process:allow-restart (the latter powers the relaunch).

Release procedure: see README and AGENTS.md → Product Defaults. In short, package.json's version is the single source of truth (native/tauri.conf.json reads it; native/Cargo.toml is frozen at 0.0.0 and never bumped); .github/workflows/release.yml runs on every push to main, and CI creates the vX.Y.Z tag itself — there is no manual tag step. Installed copies pick up a published release on their next launch check or via the About card.

Relevant code:

  • src/hooks/useAppUpdate.ts, src/hooks/useAppUpdateToast.ts
  • src/lib/update.ts
  • src/components/settings/UpdateCard.tsx, src/components/SettingsPage.tsx
  • native/src/lib.rs, native/tauri.conf.json, native/capabilities/default.json
  • .github/workflows/release.yml

Verification Commands

Full standard gate:

pnpm verify

Frontend tests:

pnpm test

Frontend build:

pnpm build

Rust tests:

cd native
cargo test

Rust formatting and linting:

cd native
cargo fmt --check
cargo clippy --all-targets -- -D warnings

If Rust tests that execute git fail because git cannot be found, rerun with:

cd native
PATH=/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH cargo test

For release-impacting changes:

pnpm desktop:build