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.
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 andowner/repo→ project resolution.
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 (nouseStateloading 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 (thecomposerslice — not a form hook), the push-drivenliveLines/taskAttentionmaps,deletingTaskIds, and toasts/messages. - Events — one mount-once bridge (
src/hooks/useEventBridge.ts). Mounted inAppLayout, it owns every Rust chat/review/PR subscription and routes each event to the Query cache or the Zustand store. Each channel usessrc/hooks/useTauriEvent.tsfor the Taurilistenlifecycle, 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.
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.
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. |
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.tsis the single, mount-once bridge (mounted inAppLayout). 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 throughsrc/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.tslistens forpr_review_outputand accumulates the PR reviewer's read-only live output forsrc/components/ReviewTerminalPane.tsx. Task reviews run inline via/review, surfacing as aSubagentblock oversession_chat, so they need no separate stream.
Important tasks columns:
status:planned,in_progress,review, ordone.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 whenhas_worktree = 1; blank worktree creation generates atask-...branch name.worktree_path: set only whenhas_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_inputwhen the agent is blocked on the user, elseNULL. ACP permission parts set/clear the live prompt/reason detail through the push-driventaskAttentionstore 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 viaset_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.
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 toNULLwhen the loop is restarted viastart_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_reviewre-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_runsorpr_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;"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:devIn-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/loadwas used. - Task creation creating the worktree off-lock and then inserting the row under a
brief lock, plus each timed
create_worktreenetwork step (so a slow/stuck worktree creation is visible in the log without freezing the rest of the app). - ACP
session/new/session/loadfailures and chat process exits. - Failure to emit chat or review events.
- Review start, recorded verdict, reviewer output, and review-loop errors.
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.
Check:
- The path exists and is a directory.
git -C <path> rev-parse --show-toplevelsucceeds.- 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
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>/HEADsymref (no network); if that symref is unset, fromgit 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
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(thegitwork), anddelete_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 bygit worktree prune, run after each removal. Ifgit worktree liststill shows a ghost, rungit worktree prunein 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).
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
Check:
- The task has an agent profile id, and
list_acp_providersreports a descriptor for that profile'sagentKind. acp_start_chatresolved the task cwd (worktree path when present, otherwise repo path) and launched the descriptor command fromnative/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/neworsession/loadfailed, or whetheracp_send_prompttargeted a stale in-memory chat id. - Frontend
useEventBridgeis subscribed tosession_chat; settled messages should also be visible throughget_task_chat.
Relevant code:
native/src/sessions/acp.rsnative/src/sessions/acp_manager.rssrc/components/chat/ChatPane.tsxsrc/hooks/useEventBridge.ts
Check:
session_chatevents are arriving for the task id/profile id you expect.- Permission parts in the normalized chat message include a pending request; those
set
needs_inputtaskAttentionuntil answered or untilchat_session_exitedclears it. - A settled turn with no pending permission sets an
idle(finished)taskAttentioncarrying the closing line — thefinishedagent state — which the next streaming chunk clears. A missing Finished badge means that settledsession_chat(withdone: true) never arrived. - Text/tool parts are present in the stream;
applyChatRuntimemirrors them intoliveLinesandchatWorkingTaskIds. - Stale
tasks.attention/active_session_idvalues from old PTY builds are cleared on app startup byclear_legacy_active_sessions.
Relevant code:
src/lib/chat/applyChatRuntime.tssrc/sessionAttention.tssrc/hooks/useEventBridge.tsnative/src/db/tasks.rs
Check:
- A reviewer is configured for the task (the
review_looprow exists)./reviewfails 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_reviewcan 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
/reviewrun emitsreview_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 directoryis the minimal-PATH problem — see Agent Command Fails To Start above; the ACP launch setsprocess_util::augmented_path()to fix it. - The reviewer ends its message with a fenced
```jsonblock carrying{"verdict": "clean|blockers|feedback"}(the loop maps these topass/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 withselect 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.rsnative/src/sessions/verdict.rs(parse_verdict_block)src/hooks/useTaskReviewLoop.ts
Current bundle identifier:
com.hvp17.nectus
Check:
native/capabilities/default.jsonincludesnotification:default.native/src/lib.rsinitializestauri_plugin_notification.src/api.tsrequests permission before sending notifications.- System Settings includes the app under Notifications.
Reset notification permission:
tccutil reset UserNotifications com.hvp17.nectusIf 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.
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 (
errorstatus): a pubkey mismatch — thelatest.jsonsignaturewas produced by a private key that does not match the base64pubkeyinnative/tauri.conf.json'splugins.updaterblock. The two must be from the same minisign keypair (the CI signing secretsTAURI_SIGNING_PRIVATE_KEY/TAURI_SIGNING_PRIVATE_KEY_PASSWORDmust match the committed public key). - Check silently reports "up to date" (
upToDate): the release is missing, draft, or unpublished, so…/releases/latest/download/latest.json404s and the updater treats it as no update. Confirm the GitHub Release is published and carrieslatest.json(CI auto-publishes it on a version bump merged tomain). - No update offered even with a newer build out: the published
versionis not strictly higher than the installed one. The publishedversioncomes frompackage.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.jsongrantsupdater:defaultandprocess: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.tssrc/lib/update.tssrc/components/settings/UpdateCard.tsx,src/components/SettingsPage.tsxnative/src/lib.rs,native/tauri.conf.json,native/capabilities/default.json.github/workflows/release.yml
Full standard gate:
pnpm verifyFrontend tests:
pnpm testFrontend build:
pnpm buildRust tests:
cd native
cargo testRust formatting and linting:
cd native
cargo fmt --check
cargo clippy --all-targets -- -D warningsIf 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 testFor release-impacting changes:
pnpm desktop:build