In the task detail the Move dropdown opens but clicking an option does not change the status, while the keyboard shortcuts do. The task records the suspected cause (a hidden status select sent along with the clicked button), the expected behaviour and the verification checks. |
||
|---|---|---|
| .agents/skills/integrate-design | ||
| cmd/nexus | ||
| design | ||
| docs | ||
| internal | ||
| nx | ||
| web-tests | ||
| .gitignore | ||
| .nx-config.yaml | ||
| AGENTS.md | ||
| go.mod | ||
| go.sum | ||
| mise.toml | ||
| package-lock.json | ||
| package.json | ||
| prompt.md | ||
| README.md | ||
| SPEC.md | ||
Nexus
Nexus is a local task manager for software projects. Tasks are Markdown files with YAML metadata, so they remain readable and portable; Nexus provides a CLI, a terminal Kanban, and a local web board. Task data can be private to your local Git repository or tracked alongside the project.
Install
The default installation method is go install (Go 1.27.3 or newer):
go install github.com/mgdelacroix/nexus/cmd/nexus@latest
Ensure $(go env GOPATH)/bin (or $(go env GOBIN), if set) is on your PATH. Check the installed command with:
nexus --help
To build from a checkout instead, run mise run build; the executable is written to dist/nexus.
Get started
Run these commands from inside the Git repository you want Nexus to manage:
nexus init --mode private
nexus create --title "Document the release process" --priority normal --tag docs
nexus list
nexus ready
nexus serve
init configures Nexus for the repository. Choose private to keep task files in the shared Git metadata directory and out of version control, or tracked to store task files under the worktree and commit them with the project. For tracked mode, select a repository-relative directory with --path:
nexus init --mode tracked --path docs/tasks
The first example uses private mode; do not run both initialization examples in the same repository unless you intend to switch/configure a different repository. Outside a Git repository, see Standalone repositories. In interactive mode Nexus can prompt for missing choices; use the documented flags for noninteractive use.
Documentation
Start at docs/README.md: getting started, an implemented-versus-planned capability inventory, a reference of special Markdown blocks and file conventions, workflow recipes (solo with an agent, tracked team repository, human checkpoints) and notes on keeping the docs accurate and on trust for agent-loaded files. Coding agents get the same guidance from nexus skill show (topics include recipes). The CLI help remains the authoritative reference.
CLI commands
Run nexus --help or nexus <command> --help for the authoritative syntax and flags. Commands that support --json provide machine-readable output.
| Command | Purpose |
|---|---|
nexus init |
Initialize storage and configuration for the current Git repository, or for the current directory when it is not inside Git (tracked mode only). --mode is private or tracked; --path selects the tracked task directory, --name sets an optional display name (stored as the name configuration key) that the web interface shows as the source name instead of the folder name, and --register opts the repository into global views. |
nexus create |
Create a task with a title and optional Markdown body (--content or --content-file), ID, priority, dependencies (--deps), and repeatable tags (--tag). --bulk FILE (- for stdin) creates one task per line instead: a line without leading whitespace is a title (the ID is its slug), and indented lines are the Markdown body of the previous task (common indentation removed, blank lines kept). --bulk cannot be combined with --title, --id, --content or --content-file; --priority, --deps and --tag apply to every task, and nothing is created if any title is empty, repeated or already exists. --repo, --worktree, and --shared control repository-level creation scope. |
nexus list [query] |
List tasks in the current scope, optionally filtered with the web search expression. --repo reads repository scope, --global reads registered repositories (read-only), --wide includes titles, and --json emits JSON. |
nexus tag list |
List labels configured in the current repository/worktree. Shows configured color and description when present; --json returns { "labels": [...], "default_color": ... }. |
nexus registry list|add|remove |
Manage the global repository registry used by list --global and the web interface. list prints each registered path with active or stale (the path no longer exists); --json returns { "repositories": [{ "path": ..., "stale": ... }] }. add [path] (default: current directory) registers the root of the initialized Nexus repository containing path and fails, without writing, when there is none; adding a registered root is a no-op. remove <path> removes the entry for that absolute (or cwd-relative) path without checking the filesystem, so stale entries can be removed, and fails when the path is not registered. Stale entries are never removed automatically. add and remove also accept --json. |
nexus ready [query] |
List pending tasks whose dependencies are complete, optionally filtered with the shared task query expression. Results are always ordered most urgent first: priority (urgent, high, normal, low), then creation time, then task ID; --limit, --simple, --json, and --wait keep that order. Supports --repo, --json, and --wait; --limit N caps results (zero means unlimited) and --simple prints only task IDs, one per line (--simple and --json are incompatible). |
nexus show <id> |
Show a task; --history includes its event history. Supports --repo and --json. |
nexus edit <id> |
Replace or append to a task's Markdown body with --content or --content-file; --append appends. Supports explicit repository/worktree targeting and JSON output. |
nexus rename <id> <title> |
Change a task's title and generated ID, updating dependent task references. Supports repository/worktree targeting and JSON output. |
nexus dependants <id> |
List tasks depending on the given task. Supports --repo and --json. |
nexus deps <id> |
Replace dependencies with --deps <id[,id...]>, or remove all with --clear. Validates dependency existence, ambiguity, self-reference, and cycles. Supports repository/worktree targeting and JSON output. |
nexus tags <id> |
Add and remove tags on an existing task with --add <tag[,tag...]> and --remove <tag[,tag...]> (at least one required). Tags must be non-empty with no whitespace or commas, and cannot be in both lists. Adding a tag the task has, or removing one it lacks, is a no-op; a call that changes nothing writes nothing and records no event. Otherwise one tags-changed history event is recorded. Supports repository/worktree targeting and JSON output. |
nexus status <id> <status> |
Change task status (todo, in-progress, done, or canceled). Supports repository/worktree targeting and JSON output. |
nexus priority <id> <priority> |
Change priority (urgent, high, normal, or low). Supports repository/worktree targeting and JSON output. |
nexus note <id> |
Add an attributed note to task history using --author and --content or --content-file. Supports repository/worktree targeting and JSON output. |
nexus history <id> |
Inspect task history; supports filters such as --author, --type, --since, --until, --limit, and --reverse, as well as --repo and --json. |
nexus attachment add <id> <source> |
Copy a file or directory into the task's attachments (see Attachments). --as sets the destination path, --include-hidden also copies dot-files, --force replaces existing files, --author (agent by default, or user) is recorded in the history event. Supports --repo/--worktree and --json. |
nexus attachment list <id> |
List the task's attachment files with their sizes. Supports --repo and --json. |
nexus attachment remove <id> <path> |
Delete an attachment file, or a directory with --recursive, even if Markdown links to it. Supports --author, --repo/--worktree and --json. |
nexus attachment prune <id> |
Delete the task's attachment files that nothing references. --dry-run only reports; a real prune asks for confirmation on a terminal, and --yes skips it (required with --json or without a terminal). Supports --author, --repo/--worktree and --json. |
nexus link <id> --pr <owner>/<repo>#<number> |
Link an existing task to a GitHub pull request and run its first sync, writing the <id>.pr.md copy. Fails without writing if the task is already linked or gh cannot read the PR. Supports --dry-run, --repo --worktree <branch> and --json. See GitHub PR sync. |
nexus link <id> --jira <KEY> |
Link an existing task to a Jira ticket and run its first sync, writing the read-only <id>.jira.md copy through acli. Fails without writing if the task is already linked, the key is invalid or acli cannot read the ticket. Supports --repo --worktree <branch> and --json. See Jira tickets. |
nexus unlink <id> --jira |
Remove the task's <id>.jira.md copy and nothing else (no event, no status change); not_linked if there is none. Supports --repo --worktree <branch> and --json. |
nexus sync [<id>] |
Refresh the <id>.pr.md and <id>.jira.md copies of linked tasks (one task with <id>, otherwise all in scope). Pull requests are limited to the GitHub repository that gh repo view resolves for the current directory (it fails, suggesting --global, when there is none); --global syncs pull requests of every repository. --discover also creates tasks for open PRs of that repository where review is requested from you or your teams (tickets are never discovered). A done task whose linked PR had no review request at the previous sync and has one now (the author asked you for a re-review) goes back to todo with an agent note; a done task that was still requested before is left alone. Supports --dry-run (pull requests only), --repo --worktree <branch> and --json. |
nexus review local <id> [--base <ref>] [--agent <name>] [--with-ticket] |
Start a detached local review of the task's worktree; includes committed, staged, unstaged and untracked changes without changing the checkout. The default base is origin/HEAD, then main, then master. --with-ticket includes the linked Jira copy as untrusted context (a review started from the web tab includes it whenever the task has a non-empty Jira copy, with no switch). Follow-up messages continue the same session and add report revisions; starting a fresh local review after an accepted report is not yet supported. Supports --repo --worktree <branch> and --json. |
nexus review run <id> |
Start the configured coding agent on the task's linked PR as a detached process and return at once. --agent <name> overrides agent.default; --force reviews again even if the stored review already covers the head commit; --second-opinion [--see-first] starts an independent review in a new lineage (see below). Fails if the task is not linked, no agent is configured, a review is already running, or the stored review already covers the PR's head commit. Supports --repo --worktree <branch> and --json. See PR reviews with a coding agent. |
nexus review status <id> [--local] |
Show PR review state, or local review run state with --local. Read-only. Supports --repo --worktree <branch> and --json. |
nexus review stop <id> [--run <run-id>] [--local] |
Stop a review run that is in progress: the run becomes failed with the error stopped and its detached process is asked to end the agent. --run is needed when several runs are in progress (for example a second opinion next to the primary review). --local stops a run of the local review (a review or verification turn); the notes the run was delivering go back to the queue. Supports --repo --worktree <branch> and --json. |
nexus review submit <id> --run <run-id> --file <path|-> [--local] |
Validate a structured review report and store it as the next revision. Use --local for a local review run; the rendered report is also attached under reviews/ as agent. Prints every error and stores nothing when invalid. Supports --repo --worktree <branch> and --json. |
nexus review make-task <id> <finding> --local |
Create a dependent task from a finding in the latest accepted local report (f-<n> or <lineage>/f-<n>). Supports --repo --worktree <branch> and --json. |
nexus review finding <id> <finding> --status open|reviewed|fixed|wontfix [--reason <text>] [--local] |
Store your own status of a finding (f-<n> or <lineage>/f-<n>) in state.md; wontfix needs --reason. The finding must exist in the latest accepted report of the review (PR, or local with --local). The agent never overwrites it, and a later revision does not change it. Supports --repo --worktree <branch> and --json. |
nexus review ask <id> <question> |
Queue a free-form PR-review message. If a turn is running it is delivered when that turn ends; if idle it immediately resumes the session. Answers are stored in the next report revision. Supports --repo --worktree <branch> and --json. |
nexus review message <id> <text> [--finding <id>|--check <c-n>] [--local] |
Queue a message for a PR or local-review session, optionally targeting a finding or (for local reviews) a verification check. Messages are delivered immediately when idle or after the running turn; --local selects local review. Supports --repo --worktree <branch> and --json. |
nexus verify list|add|edit|remove <id> ... |
Manage task-local checks in <id>.reviews/local/checks.md; removing a check preserves evidence. List reads repository scope without requiring a worktree; writes require an explicit target. Supports JSON output. |
nexus verify link|unlink <id> <check> <attachment-path>... |
Link existing task attachments (any safe path from nexus attachment list) to a check as evidence, or remove links. Nothing is copied or deleted: unlinking keeps the file, and links survive verify edit and verify mark. The whole batch is rejected for unsafe paths, attachments that do not exist, duplicate links or more than 20 links per check. Supports JSON output. |
nexus verify mark <id> <check> --status passed|failed|skipped |
Record a result, note, and optional repeatable --evidence <path> file copies under verify/<check-id>/. --author agent requires the current live verification --run and an included check. Existing evidence names are never overwritten or auto-suffixed: filename collisions reject the entire evidence batch. Removing a check never deletes evidence or reuses its ID. |
nexus verify run <id> [<checks>...] |
Start a detached verification turn in the task's existing local-review session, using the configured agent and shared local queue. Selects pending checks by default and resumes the agent/session from the completed local review. --agent, if supplied, must match that session owner; Nexus cannot switch agents while reusing a session. Supports repository/worktree targeting and JSON output. |
nexus review cancel <id> <note> [--local] |
Remove a queued message before delivery. Delivered or answered messages cannot be canceled. --local selects local review. Supports --repo --worktree <branch> and --json. |
nexus review diff <id> --from <N> --to <M> [--lineage <name>] [--local] |
Compare two accepted revisions of a review lineage (default primary; --local selects the local review): findings that are new, resolved, reopened (listed again without a resolved reason) or changed with the names of the fields that differ and the previous title, plus the notes the later revisions answered. A revision is each valid submit, including follow-ups. --json prints a list of {id, change, fields, …} (change is new, resolved, reopened, changed or answered). Read-only. Supports --repo --worktree <branch>. |
nexus review comment list <id> |
List the suggested comments of the review's latest revisions with their reference (primary/f-1), state (draft, edited, posted, dismissed), location and effective text. Read-only. Supports --repo and --json. |
nexus review comment edit <id> <comment> --body <text> (or --body-file <path|->) |
Replace the text of a draft or edited comment; it becomes edited and the agent never overwrites it. <comment> is f-<n>, or <lineage>/f-<n> for a lineage other than primary. Supports --repo --worktree <branch> and --json. |
nexus review comment add <id> <comment> --body <text> (or --body-file <path|->) |
Write your own comment for a finding that has no suggested comment. The finding must be in the latest revision of its lineage, unresolved and have a location (the posted comment uses its file and line); empty text is refused. The comment is edited and shows as user_authored in --json; it can be edited, dismissed, restored and posted like a suggestion, and it is kept in later revisions. Running it on a finding that already has a comment edits that comment instead of creating another. Supports --repo --worktree <branch> and --json. |
nexus review comment dismiss|restore|mark-posted|unmark-posted <id> <comment> |
dismiss leaves a draft or edited comment out of posting; restore brings a dismissed one back (as edited if it had your text, else as a draft); mark-posted records by hand that a draft or edited comment was posted on GitHub; unmark-posted undoes that for a comment that was only marked by hand (back to edited if it has your text, else a draft), and refuses one that review post created on GitHub. Supports --repo --worktree <branch> and --json. |
nexus review post <id> [--comment <comment>...] [--event comment|approve|request-changes | --pending] [--body <text> | --body-file <path|->] [--dry-run] |
Create one review on the PR with the selected comments (default: every draft or edited comment) through gh api, anchored to the reviewed commit, and record each confirmed comment's GitHub ID and URL as posted. Without --event (or with --pending) the review stays pending and you submit it on GitHub; with --event Nexus submits it with that verdict. Approving your own PR fails before anything is sent. Comments written against an older commit are moved to the reviewed commit when GitHub's comparison allows it (and reported), otherwise they are not sent. Comments whose line is not in the PR's diff (file not changed, line outside every hunk of the stated side, a range that crosses hunks) are not sent either, because GitHub would reject the whole review; they are listed as skipped with the reason and stay unposted. Fails if you already have a pending review on the PR. --dry-run prints the request and sends nothing. Supports --repo --worktree <branch> and --json. See PR reviews with a coding agent. |
nexus serve |
Start the local web interface. Defaults to 127.0.0.1:8080; --host and --port configure the listener. --terminal (Unix only, requires a loopback --host) adds an interactive Terminal tab to task details; see Web terminal. --log-level debug|info|warn|error (default info) and --log-file PATH configure the server log. |
When run without a command in an interactive terminal, Nexus opens its terminal Kanban. nexus serve starts the web board; open the printed/local address in a browser. An open board updates itself within about a second when tasks change from the CLI, an agent, a git checkout or another tab; unsaved input is never replaced, and an update waits until you finish. Global listing is read-only. For repository-wide writes, follow command help and provide an explicit worktree target where required.
Query and output examples
nexus create --title "Fix task search" --priority high --tag bug --tag cli --content "Investigate the failing query."
nexus create --title "Document task search" --content-file search-notes.md
nexus create --title "Verify task search" --deps fix-task-search
nexus create --bulk tasks.txt --tag backlog # one task per unindented line; indented lines are its body
nexus deps verify-task-search --deps fix-task-search,document-task-search
nexus deps verify-task-search --clear
nexus ready --wait --json
nexus ready --simple --limit 1
nexus ready 'id=task-a,task-b priority=urgent' --wait --json
nexus ready 'id=task-a' --json
nexus tags fix-task-search --add kind/epic --remove draft
nexus status fix-task-search in-progress
nexus note --author agent fix-task-search --content "Investigating the query behavior."
nexus show fix-task-search --history --json
nexus history fix-task-search --type note --reverse
nexus list 'tags=bug status=in-progress'
Search terms separated by spaces are combined with AND. The query language supports partial text, field comparisons, alternatives, negation, and readiness/blocking conditions; tag comparisons match tag membership. The control-center keys source (name of the registered source), link (jira, pr, none), review (none, running, done, outdated, failed; the latest PR review run, or outdated when the PR has new commits since it, or the latest local review) and ci (passing, failing, running; checks of an open linked PR) work in nexus list and in the web filter, for example nexus list 'link=pr ci=failing' or nexus list '!link=none review=outdated'; an invalid value such as link=github is an error. Use the same expression with nexus list; omit it to list all tasks. See the CLI help and query contract in SPEC.md for details.
Task model and storage
A task has a stable ID, title, status, priority, dependencies, optional tags, a free-form Markdown body, and an append-only history of Nexus events. A task is blocked when one or more dependencies are not done; blocked is calculated, not a stored status. ready returns unblocked todo tasks.
- Private mode: task files and configuration are stored under Git's shared metadata directory and are not committed with project files. Worktrees share this task store.
- Tracked mode: configuration and task files live in the worktree (by default, configure the task directory with
init --path) and can be committed. Tracked task data is separate between worktrees.
Nexus discovers the repository and worktree from the current directory. Read/write scopes are command-specific; consult --help before using --repo or targeting another worktree. Register a repository when initializing it with nexus init --mode private --register (or choose its intended mode) before expecting it in list --global.
Attachments
nexus attachment add|list|remove manage files stored with a task. Each task instance owns a folder next to its Markdown file, <id>.attachments/: for example tasks/<id>.attachments/ in tracked mode, and <git-common-dir>/nexus/tasks/<id>.attachments/ in private mode. Tracked instances with the same ID in different worktrees have separate folders.
Verification checks live in <id>.reviews/local/checks.md. nexus verify mark --evidence copies evidence into the task attachment store under verify/<check-id>/; the local review tab of the web task detail offers the same check management and result controls. nexus verify link (and the Link evidence control in each check's evidence list on the web) references attachments already in the task instead of copying them; linked evidence is listed with nexus verify list and opens through the normal attachment viewer. A nexus-check fenced block (text, source, optional finding) at the top level of a task description is shown in the web task detail as a read-only verification card; it is presentation only, so status, evidence and editing stay with nexus verify. A block that does not parse (unknown or duplicate fields, missing text/source, text over 500 characters, no closing fence) is shown as an invalid card or ordinary code and never hides the content after it; blocks inside quotes or lists, in other fences, in history notes and in ticket or PR text stay ordinary code. Check removal and re-marking do not delete prior evidence or rendered review reports. Deletion is explicit: use nexus attachment remove or nexus attachment prune; prune follows the attachment-reference rules and may remove evidence not linked from task Markdown.
-
addcopies; it never links back to the source. A directory is copied recursively under its own name (or--as). Hidden entries (names starting with.) are skipped and reported unless--include-hiddenis given. Symlinks and special files in the source are rejected, and nothing is copied when any check fails. -
An existing destination is an error; only
--forcereplaces it. Names that differ from an existing one only in case or Unicode normalization are always rejected. -
Paths are relative to the folder and must not be empty or absolute, contain
..,.or empty segments,\,?or control characters. -
Defaults: 50 MiB per file, 200 MiB per
add, 2000 files, 16 directory levels, 255 bytes per name and 1024 per path. The first four can be overridden in the repository configuration:attachments: max_file_bytes: 52428800 max_total_bytes: 209715200 max_files: 2000 max_depth: 16 -
Every operation appends one history event (
attachment-added,attachment-replacedwhen--forceoverwrote at least one file, orattachment-removed) listing the affected paths, with the size and SHA-256 of each file for additions, and updates the task'supdatedtime. Files and event are written together: if the task cannot be saved, the files are restored. -
removedeletes the file even when the body or a note links to it, without warning, and leaves those links as written. It refuses a directory without--recursive, and afterwards removes empty parent directories (and the folder itself when it is empty). -
pruneworks on one task instance. It keeps every file linked as[text](attachment:path)or(or as a reference definition) in the task body or in a note, and the resources an HTML attachment linked that way references directly:srcofscript,img,audio,video,source,track,iframeandembed,hrefoflinkanda, eachsrcsetcandidate,posterandobjectdata. Only one level is followed: resources loaded by CSS (url(...)), by JavaScript, or by a second HTML file are not protected and may be deleted. External URLs, absolute paths and references that leave the attachments folder protect nothing and are reported as warnings. -
The files to delete are always found by walking the task's own attachments folder; a reference can only protect a file, never select one, so a link can never make
prunetouch a file outside the folder or in another task. Symlinks and special files are never followed or deleted. Empty subfolders (and the folder itself, if empty) are removed afterwards, and a singleattachment-prunedevent lists the deleted files.--dry-runchanges nothing: no file, folder,updatedtime or event. -
nexus servealso starts a second listener on127.0.0.1with a random port that serves attachment files from an origin of their own, separate from the Nexus pages and API. The Nexus server mints a short-lived token per viewed attachment (POST /tasks/<id>/attachments/open, answered with the address on the isolated origin;POST /attachments/revokeinvalidates it). A token reads only the files under the directory of the attachment it was minted for, in that one task instance. The isolated origin sets no cookies, has no Nexus routes, checks theHostheader, sends a closed Content-Security-Policy (no network access, forms, frames or plugins),nosniff,no-referrerandno-store, never follows symlinks, and serves unknown types as downloads. Because it listens on loopback only, attachments cannot be opened from another machine even with a non-local--host. -
In the web interface,
[text](attachment:path)andwork in a task body and in notes (notes are now rendered as Markdown too). Each reference gets a viewer below its paragraph, chosen by file type: an image, a video or audio player, an HTML prototype in an<iframe sandbox="allow-scripts">markedSANDBOXED, or a PDF in the embedded pdf.js viewer (pages, text layer, page navigation and zoom;n/p,+/-/0keys), also sandboxed and with a Download button; any other format shows a download and an "open raw" link. PDF JavaScript, forms, links, annotations and embedded files are never run or drawn; an encrypted, corrupt or empty PDF shows a message instead. pdf.js runs its worker on the viewer page's thread (the browser does not allow module workers in an opaque-origin sandbox), and JPEG 2000/JBIG2 images and CJK text without embedded fonts are not supported. Images, video and HTML can be expanded into a modal view (Esccloses it and returns focus and reading position); HTML can be reloaded. Repeated references each show a viewer. Prototypes load when scrolled into view, and when a page has more than three they start on a click. A missing, invalid or unreachable attachment shows an error message in place of the viewer, which is also what a link left behind byremoveorpruneshows. Viewers need the isolated origin, so they are not available in the global view. The jsdom tests cover this behavior; they do not prove the sandbox, the CSP or real loading, which need a real browser. -
The Attachments panel in the task details lists each file with its type, size, whether the body or a note links to it (by note number) and an
agentmark for files an agent added. It uploads files or a folder (with an optional destination folder and "replace existing"), removes a file, and prunes: the preview lists what would be deleted and nothing changes until you confirm the list. These actions use the same code as the CLI, are recorded asuserevents, and are disabled in the global view.
Implemented: these four commands, the web viewers and panel, and the storage and event contracts of SPEC.md §13.7. Sandbox isolation, CSP enforcement and resource loading, including the PDF viewer, are checked in Chromium by mise run test:browser:attachments.
Standalone repositories outside Git
A directory that is not inside a Git repository can hold tasks too. Only tracked mode is supported there (private mode needs Git's metadata directory), and the configuration is the same .nx-config.yaml file, placed at the directory root:
mkdir ~/reviews && cd ~/reviews
nexus init --mode tracked --path tasks --register
nexus create --title "Review PR 123"
nexus list
- Run from any subdirectory: Nexus first asks Git; if the directory is not in a Git repository, it uses the nearest parent directory containing
.nx-config.yaml. Inside a Git repository, Git discovery always wins, so a standalone repository must live outside Git. initoutside Git uses the current directory as the root unless a parent already holds.nx-config.yaml(then that parent is used andinitis idempotent). A failedinitnever overwrites an existing config.- There are no worktrees: task files carry no worktree, and
--repowrites (which need--worktree) fail with a clear error. --registeradds the absolute directory to the same global registry file (seenexus registry);list --globalthen includes it. JSON task objects from standalone repositories carry"kind": "standalone"(omitted for Git-backed repositories), and theirworktreeis empty.- A standalone directory nested inside a Git repository is not supported.
Configuration files may declare an ordered labels catalog with name, optional hexadecimal color, and optional description fields, plus default_color (true, false, or a #RGB/#RRGGBB fallback). Descriptions should state when to use and when not to use a tag; existing entries without descriptions remain valid. nexus tag list shows the configured catalog in the current repository/worktree. The storage layer validates and deterministically aggregates these definitions; omitted default_color means unstyled. The web board and task details render configured colors while leaving undeclared tags visible. Colors are restricted to the validated hexadecimal configuration syntax.
Coding-agent configuration
Status:
nexus review runandnexus review localuse this configuration (see PR reviews with a coding agent); no other command requires it.
The coding agents Nexus can run live in a global user file, $XDG_CONFIG_HOME/nexus/config.yaml (~/.config/nexus/config.yaml when XDG_CONFIG_HOME is unset), next to the repository registry. A repository config (.nx-config.yaml or the private config) can define its own agent section, which replaces the global one entirely; sections are not merged. A malformed agent section only produces an error when an agent is needed; task commands keep working.
agent:
default: claude
# Optional local-review skill override; unset uses the embedded topic.
local_review_skill: .agent/local-review.md
agents:
claude:
start: [claude, -p, --session-id, "{session_id}", --permission-mode, dontAsk,
--allowedTools, "Bash(gh *) Bash(nexus review submit *) Write Read Grep Glob"]
resume: [claude, -p, --resume, "{session_id}", --permission-mode, dontAsk,
--allowedTools, "Bash(gh *) Bash(nexus review submit *) Write Read Grep Glob"]
prompt: stdin
timeout: 30m
pi:
start: [pi, -p, --session-id, "{session_id}", "{prompt}"]
resume: [pi, -p, --session-id, "{session_id}", "{prompt}"]
prompt: arg
With --permission-mode dontAsk the agent can only run what --allowedTools lists, and a compound command (cd … &&, a pipe, a heredoc) is allowed only when every part of it is. That is why the examples list Write (the agent writes the report file with it, not with a shell heredoc) and Read Grep Glob (it reads the code with them, not with grep or sed), and why the review prompt tells the agent to run nexus review submit as one plain command. Bash(gh *) also lets the agent run gh api write requests, which the prompt forbids but the permissions do not stop; narrow it (for example Bash(gh pr view *), Bash(gh pr diff *)) if you want the permissions to enforce that.
The flags in these examples were checked against claude --help and pi --help for the versions installed when this was written; re-check them for yours.
startandresumeare argument lists. Nexus runs them directly, never through a shell, so quoting and metacharacters in values have no special meaning.- Placeholders are replaced inside each argument:
{session_id},{prompt},{run_id},{task_id}. Replaced values are inserted literally and never expanded again. Other braces are literal text; an unknown{name}is an error. prompt: stdinsends the prompt on standard input ({prompt}must not appear).prompt: argrequires exactly one{prompt}instartand inresume.resumemust contain{session_id}. Ifstartcontains{session_id}, Nexus generates the ID and passes it in. Otherwise setsession_id_fromto the JSON field of the agent's output that holds it.timeoutis a Go duration such as30m(default30m).defaultis required when several agents are defined and optional with a single one.- The
local-reviewskill is embedded in Nexus and can be inspected withnexus skill show local-review.agent.local_review_skillselects a replacement Markdown file for local reviews; relative paths resolve from the task repository root. The file must be readable, non-empty UTF-8 and at most 64 KiB; a configured file that is missing or invalid is an error, never a fallback to the embedded skill. The repositoryagentsection replaces the global section, including this key. Run local reviews withnexus review local; the agent submits reports withnexus review submit --local. - Errors name the offending field, for example
agent.agents.claude.resume: must contain {session_id}.
GitHub PR sync
nexus link and nexus sync copy pull request data into a <id>.pr.md file next to the task, using the installed gh CLI (it must be on PATH and logged in with gh auth login). Syncs are pull-only: Nexus runs only gh auth status, gh repo view, gh search prs and gh pr view for them (posting a review, with nexus review post, is the one command that writes), passes arguments as a list (never through a shell), and never reads or stores a GitHub token. Errors from gh are redacted before they are shown.
nexus link my-task --pr acme/widgets#7 # link and sync once
nexus sync # refresh the linked tasks of the current GitHub repository
nexus sync my-task # refresh one task (any repository)
nexus sync --discover # also create tasks for PRs of this repository awaiting your review
nexus sync --global # every GitHub repository, not only the current one
nexus sync --discover --dry-run --json # report what would change, write nothing
- The
.pr.mdcopy is overwritten in full on every sync. Its front matter holdsurl,owner,repo,number,title,author,state(open,merged,closed),draft,base,head_sha,review_requested,resolved,synced_at,checks,reviewsandcomments; the body is the PR description. The task file itself is not changed, and a sync adds no history events. checks,reviewsandcommentscome from the samegh pr viewcall (statusCheckRollup,reviews,reviewRequests,comments) and are left out when empty.checkslists each CI check withname,state(passed,failed,running,queued) and theurlof its log; a neutral or skipped check counts as passed and anything that is not a known success counts as failed.ghdoes not say which tests failed, so none are stored.reviewshas one entry per reviewer withuser,state(approved,changes_requested,commented,pending), thecommitit was made on andstale: truewhen that is not the PR's current head; an approval or change request stands until the reviewer submits another one (a later plain comment does not replace it) and dismissed reviews are ignored. Reviewers whose review is requested again, and requested teams (team:<slug>), appear asrequested.commentskeeps the latest 10 comments of the conversation (oldest first) with their text cut at 1000 characters, andchecksat most 100 entries. All of it is written by other people and by CI: treat it as untrusted, and escape it wherever it is shown. The web interface shows it in the GitHub review tab.review_requestedis true when the PR appears ingh search prs --review-requested=@me --state=open, which includes requests made through a team.resolvedis true when the PR is merged or closed or no longer requests your review; the task status is never changed.- Discovered tasks get the ID
pr-<owner>-<repo>-<number>, the PR title, and the tagsreview/github-prandrepo/<owner>-<repo>. Running discovery again creates no duplicates; it only refreshes the copy. - One failing PR does not stop the others: each failure is reported (in the
resultsarray with--json) and the command exits non-zero. Ifghis missing, logged out, or the review-request search fails, the command stops before writing anything. - Without
--global,syncandsync --discoveronly refresh and discover pull requests of the GitHub repositorygh repo viewreports for the current directory (so it followsgh repo set-defaultand prefersupstreamon a fork); linked tasks whose PR is in another repository are skipped. Ifghcannot resolve a repository (no GitHub remote, or a standalone folder outside Git), the command fails and suggests--global.sync <id>ignores the filter, and the web sync prs button still refreshes every linked PR of the source. Jira tickets are not affected. - With
--repo,--worktree <branch>is required and only that worktree's tasks are synced or created.
Verified against real GitHub in a standalone repository (nexus init --mode tracked --path tasks, then nexus sync --discover twice): the first run created one task per pending review and the second created none, only refreshing the copies. Agent runs, follow-ups and posting are verified with fake gh and fake agents in the Go and jsdom tests; posting to a real PR has not been tried.
This section covers sync and linking only. Agent reviews are described in PR reviews with a coding agent; checks and posting review comments are described in SPEC.md §14; Jira links are below.
Jira tickets
nexus link <id> --jira <KEY> and nexus sync keep a read-only copy of a Jira ticket in <id>.jira.md next to the task, using the installed acli (on PATH and logged in with acli jira auth login). Nexus only runs acli jira auth status, acli jira workitem view and, when the ticket has more comments than the first answer carried, acli jira workitem comment list --key <KEY> --json --paginate; arguments are passed as a list, never through a shell, and nothing is ever written to Jira. Nexus never reads, stores or logs credentials: from auth status it keeps only the Site: host, and the stderr of acli is never stored or printed, only classified into one of the error codes below.
nexus link my-task --jira MM-1234 # link and sync once
nexus sync # refreshes PR and Jira copies in scope
nexus sync my-task # one task
nexus unlink my-task --jira # delete the copy, nothing else changes
- Sidecar. Front matter
key,url(https://<site>/browse/<KEY>),status,type,priority,assignee,reporter,synced_at; the body is the summary as a heading, a generated## Descriptionand## Comments. The Atlassian document format of the description and the comments is converted to Markdown (paragraphs, headings, bold, italic, inline and block code, links, bullet, numbered and task lists, quotes, simple tables, mentions); anything else becomes its plain text plus[unsupported: <type>]. Headings inside the ticket are moved down so they never replace the generated sections. Ticket text is untrusted content. - Refreshing rewrites the whole file atomically and adds no event to the task history; a failed refresh keeps the previous copy. Repeating
syncgives the same content apart fromsynced_at. - Comments. When the paginated listing is needed its output carries no dates, so those comments show
unknownand the result has the warningcomments_partial_dates; if that call fails the comments received are kept and the warning iscomments_truncated. The paginated output is plain text; Nexus reads it as blocks separated by a blank line, usingAuthor: textwhen a block has that form. This parsing is verified only against a fakeacli, not a real one. - Errors (
--jsonprints{"error":{"code","message"}}on stderr):invalid_jira_key(must match^[A-Z][A-Z0-9_]+-[0-9]+$, checked beforeacliruns),acli_not_found,acli_not_authenticated,jira_not_found_or_forbidden(acligives the same answer for both),acli_malformed_output,acli_failed,already_linked,not_linked. Insync --jsonthe tickets are in ajiraarray, each withid,jira(key,url,status,synced_at) andwarnings, orid,keyanderror; one failing ticket does not stop the others and the exit code is non-zero.synconly needsghwhen there are pull requests to refresh or--discover. - Web (
nexus serve). A task linked to a ticket has ajira <KEY>tab (/tasks/<id>#tab=jira) with the read-only copy: a bannerKEY · Type · read-only copy, the sidecar name and sync time, chips for key, status, type and priority, the summary, the Description and Comments (rendered by the escaping Markdown subset, so ticket text can never inject markup, and never treated as agent instructions) and a side column with Details (assignee, reporter), Sync and Use in task. Sync has Refresh (likenexus sync <id>: rewrites the copy, adds no history event, keeps the previous copy on failure and shows the fixed error text of the codes above), Open in JIRA ↗ (keyoon the tab, never while typing) and Unlink… (asks for confirmation; likenexus unlink, it deletes only the copy). Use in task has Add acceptance criteria to the task: it looks in the description, without regard to case, for a heading or bold line namedAcceptance Criteria,Acceptance,ACorDefinition of Done, takes the text up to the next heading of the same or a higher level and appends## Acceptance criteria (from KEY)plus that text to the task body as abody-editedevent byuser; it copies nothing and says so when there is no match, and a second copy asks for confirmation (earlier sections are never updated). The ticket is included as review context because the sidecar exists (review local --with-ticket); the web has no separate switch. The Links section of the Details tab shows the ticket (and PR) summary with a link to its tab and, when the task has no ticket, a Link jira form that runs the same first sync asnexus link --jiraand opens the new tab. The write endpoints arePOST /tasks/<id>/jira/{link,refresh,unlink,criteria}behind the same-origin check of every web mutation; the tab is read-only in the all-sources view. Copying acceptance criteria has no CLI command yet. Not done yet: theLinkedcounts opening a list.
PR reviews with a coding agent
nexus review local reviews a task's current worktree, while nexus review run, status, submit, ask, message, cancel, diff, comment and post run or manage a coding agent on a PR linked with nexus link and keep the result next to the task. review message --local and review cancel --local manage follow-ups to local-review sessions; their web tab is described in Local review tab. They need an agent configured as described in Coding-agent configuration. In nexus serve, a task linked to a PR has a GitHub review tab. On the board of another registered source (/?scope=global&source=<key>) the tab has the same controls and acts on that source's repository: the agent configuration and the working directory come from the source, and the detached review process starts in its checkout. The all-sources view (scope=global without a source) shows the stored report read-only. It shows the PR header (state, author, draft and resolved marks, last sync), the run cards (status running, done, failed or interrupted, with run, re-run, second opinion and stop; a running review refreshes its status every two seconds through HTMX polling and the tab reloads when it ends), the summary, the areas as a filter, each finding with its severity, location, snippet, why and effect, and its suggested comment with edit, dismiss, restore and mark-posted, a post form (pending review by default, or comment, approve or request changes) that sends the comments you tick, an Add comment form on each finding that has a location and no suggested comment (it creates your own comment, shown as "Your comment", with the same edit, dismiss and include-in-review controls; the all-sources view has no such control), an Add note form on each finding (it sends the note to the review session like review message --finding; the note shows under the finding as queued, delivered, picked up or answered, with the agent's reply below it and a cancel button while it is queued), the question box with the log of free questions, and the PR's checks, reviewers and conversation. From the second revision of a lineage on, the report header counts what the revision added, resolved, reopened and changed against the one before it, and each finding shows new in rev N, changed in rev N (with the fields that differ and, when it was renamed, its previous title) or resolved/reopened in rev N; the marks are computed from the stored revisions on every read and never stored, so the status you set and the comments you edited are untouched. The actions call the same services as the CLI and are same-origin-only POSTs. The follow-up form queues messages, accepts an optional finding target, and offers cancellation while queued; running turns are delivered by their existing detached process. Agent and GitHub text is shown as escaped Markdown (no HTML, images or javascript: links). Text typed and not yet sent (a question, a note, a comment edit) is kept when the tab reloads after another action or at the end of a run. Not in the tab yet: changing a finding's status, choosing the active revision, grouping findings, and hiding Approve on your own PR (posting it fails with the CLI's message instead); posting needs at least one ticked comment. The tab is tested with Go and jsdom, not in a real browser.
nexus review run my-task # start the agent; returns immediately
nexus review status my-task # status, outdated flag, finding counts
nexus review status my-task --json
nexus review diff my-task --from 1 --to 2 # what revision 2 changed in the findings
nexus review diff my-task --from 1 --to 3 --json
nexus review submit my-task --run run-1a2b3c4d5e6f --file report.md # what the agent runs
nexus review ask my-task "Why is the limiter keyed by remote address?" # free-form PR follow-up
nexus review message my-task "Recheck this edge case" --finding primary/f-3
nexus review message my-task "Run the linked check" --local --check c-1
nexus review cancel my-task n-2 # remove a queued message
nexus review comment list my-task # suggested comments and their state
nexus review comment edit my-task f-1 --body "Key by the user id."
nexus review comment dismiss my-task f-2
nexus review post my-task --dry-run # show the GitHub request, send nothing
nexus review post my-task --comment f-1 # create a pending review with f-1
nexus review post my-task --event request-changes --body-file review.md # submit it with a verdict
- Run.
review runrecords arunningrun in<id>.reviews/pr/runs.mdand starts a detachednexus review execprocess (not listed in--help), so the review survives the terminal and a restart ofnexus serve. The process runs the agent'sstartcommand, orresumewhen the previous review of the task used the same agent, in the task repository's root directory. The prompt carries the embeddedpr-reviewskill, the run ID and the PR's head commit; no checkout is made. - Report. The agent delivers the report with
nexus review submit, which checks it against the format innexus skill show pr-review. Every error is printed asline N: <block>: <field>: <message>(a list with--json), nothing is stored, and the run staysrunningso the agent can fix the report and submit again. A valid report is stored unchanged as<id>.reviews/pr/runs/primary-r<N>.md, except that Nexus adds anid: f-<n>line to each finding that has none. A run takes one valid submission, and a run that is not the currentrunningone is rejected. - End of the run. When the agent exits, the run becomes
doneif a valid report was submitted andfailedotherwise, with the reason inerrorand the last 4 KiB of the agent's output. A non-zero exit code or atimeout(default30m) always fails the run. Either way anagentnote is added to the task history. - Status.
runningwhose process no longer exists reads asinterrupted(the file is not rewritten). A review is outdated when the commit it reviewed differs fromhead_shain<id>.pr.md; runnexus sync <id>to fetch new commits. Running an outdated review again resumes the agent's session and asks for an incremental review that accounts for every unresolved finding of the previous revision. - At most one agent process runs per review session/lineage; second-opinion lineages use separate sessions and may run at the same time. A review that already covers the PR's head commit is not run again unless
--forceis given; the new revision is made on the same commit and must account for the previous findings. - Session messages.
review askis a free-form alias forreview messageon PR reviews. Messages (n-1,n-2, …) can target a finding, or a local-review verification check; they are storedqueued,deliveredoransweredin the matching review'sstate.md. A message sent during an active turn stays queued and the same detached process resumes the same session after the turn, draining queued messages in order; when idle, Nexus starts the follow-up immediately. At most one process per lineage/session is allowed.review cancelremoves only queued notes. “Picked up” is a calculated display state (delivered while the carrying process has a PID), never persisted. The agent answers with onenexus-replyblock per delivered note in a new report revision; a valid answer marks it answered. A failed follow-up discards its unaccepted revision and requeues delivered notes for a later run. Finding statuses, edited comments, posted comments and other user state are not overwritten.review statusshows the chronological note/reply log. See SPEC §§14.16 and 14.19.7. - A run that fails after submitting a valid report (for example, the agent exits non-zero afterwards) does not keep that report: its revision file is deleted, so the next run of the lineage can use the same revision number.
- Web. The GitHub review tab's pull request strip shows the state, repository and number, author, base branch and the head commit (short SHA, full SHA in its tooltip) from the
.pr.mdcopy. When more than one coding agent is configured, Run review and Second opinion have an agent selector: the review offers the default agent first, and the second opinion preselects another configured agent; the chosen agent is sent as--agentis on the CLI. With a single agent there is no selector. - Second opinions.
review run --second-opinionstarts another review of the same PR in a new lineage,second-1,second-2, …, with its own session, its own run records and its own revisions (runs/second-<n>-r<m>.md, idsf-<n>that are independent of the other lineages).--agent <name>picks the agent (default:agent.default). It is blind by default: its prompt carries nothing from any other review. With--see-firstthe prompt also carries the latest accepted report of the primary lineage as data (summary and findings with severity, category, location, why and effect, without the comments written for GitHub), and the run records the revision it saw (sawinruns.md). A second opinion always reviews the PR's current head as a new review, so--forcedoes not apply, and it can run while the primary review runs; there is no primary review needed to start a blind one. - Diff hunks. When
review submitaccepts a report for a PR, Nexus makes one call to GitHub (gh api …/compare/<base>...<reviewed commit>) and, for every finding whoselocationis on a changed line, stores the unified diff around that line in adifffence right after the finding in the revision file. The web tab shows it as code with line numbers,+/−signs and the finding's line highlighted, instead of the agent'ssnippet. It is best effort: if GitHub cannot be read (a note is printed), the file has no patch, or the line is not in the diff, the report is stored as submitted and the tab shows the snippet. Local reviews get no hunks. Agents do not write these blocks. - Agreement. With more than one lineage,
review statuscounts the open findings of each lineage's latest accepted revision: raised by more than one lineage and raised by only one, and lists the lineages that still have a run in progress (agreementwith--json, with the groups). Two findings of different lineages are the same problem when they have the same category and the same file with line ranges that overlap or are at most three lines apart, or, when neither has a location, the same area; the result is computed when read and never stored. Pairs rejected underunlinksinstate.mddo not match and pairs underlinksdo.nexus review link <id> <a> <b>confirms that two findings of different lineages are the same problem (stored underlinks, and removed fromunlinks);nexus review unlink <id> <a> <b>says a matched pair is not (a confirmed link is removed, and a pair that only the rule matched goes tounlinks). Findings aref-<n>(primary) or<lineage>/f-<n>, and both must be open findings of the latest revision of their lineage.review status --jsonlists the effectiveagreement.pairs(viaisruleorlink) and theagreement.rejectedcandidates. Merging matched findings and arbiter runs are not implemented. - Suggested verdict.
review statusalso showscomment,approveorrequest_changes(suggested_verdictwith--json), computed from the findings that are still open in every lineage: any open finding that isblockingmeansrequest_changes; no openmustand no openquestionmeansapprove; anything else meanscomment. A finding is not open when its revision marks itresolvedor whenstate.mdhas it asfixedorwontfix. It is only a suggestion: the verdict the agent writes in its report is ignored, and the verdict that is posted is the one you give withreview post --event. - Stopping a run.
review stoprecords the run asfailedwith the errorstopped, drops a report it had already submitted, returns the notes of a stopped follow-up to the queue and adds anagentnote to the task history. It then sends SIGTERM to the run's detached process, which ends the agent. The recorded process ID could have been reused after the process ended, so Nexus only signals a process whose command line isnexus review exec … --run <that run>; otherwise the run is still recorded as stopped and a warning says the process was not ended. On platforms without signals only the record is changed. - Suggested comments. A finding with a
commentand alocationhas a suggested comment, addressed as<lineage>/<finding id>(primary/f-1; plainf-1meansprimary). Its state is kept instate.mdundercomments, never in the agent's revision:draft(no entry; follows the agent's text),edited,posted(withgithub_id,github_urlandposted_at) anddismissed. The effective text is yours when you edited the comment, so a later revision by the agent never changes an edited, posted or dismissed comment.statuscounts comments by state. Findings that are resolved or have no comment are not listed. - Posting.
review postsends the selected comments as one review created withgh api --method POST repos/<owner>/<repo>/pulls/<n>/reviews --input -(the JSON body goes through standard input; arguments are a list, never a shell string). The body hascommit_idset to the commit of the active revision (the latest accepted one ofprimary, otherwise of another lineage). Nothing is submitted unless you ask: without--event(or with the explicit--pending) the request has noevent, so the review stays pending until you submit it on GitHub. With--event comment,approveorrequest-changes(also writtenrequest_changes) the review is submitted with that verdict (COMMENT,APPROVE,REQUEST_CHANGES);--eventand--pendingcannot be combined. The review text is--body, or the contents of--body-file(-reads standard input), or else the body text ofstate.mdif it has one. A review that only comments or requests changes needs a body or at least one comment; an approval needs neither (with no comments selected, only the verdict is sent, so dismiss or exclude comments you do not want sent together with an approval). Approving your own PR is refused before anything is sent (GitHub does not allow it): Nexus asksgh api userwho you are and compares it, ignoring case, with the PR's author; other verdicts are not checked and GitHub decides. Before sending, Nexus checks thatghis logged in and that you have no pending review on the PR (GitHub allows one per user and PR); if you do, it stops with an explicit error and sends nothing, whatever the verdict. A comment written against another commit than the review's is moved when it can be: Nexus readsgh api repos/<owner>/<repo>/compare/<old>...<new>(once per older commit) and follows the unified diff of the comment's file, so a comment on an unchanged line, or one that only shifted, is sent at its new line (or new path after a rename) and listed inremappedwith a warning to check it. Before sending, every comment is checked against the PR's diff: Nexus readsgh api repos/<owner>/<repo>/compare/<base>...<reviewed commit>once (a dry run does too) and a comment is not sent, and is reported underskippedwith the reason (for exampleline 562 is not in the diff of webapp/x.tsx (RIGHT side) at db5bf8d), when its file is not changed by the PR (a renamed file must use its new path), its line is not in a hunk on its side (RIGHTis added and context lines,LEFTremoved and context lines) or itsstart_lineis in another hunk. GitHub rejects the whole review when one inline comment has an unresolvable line, so the others are posted without it; skipped comments are not added to the review body and stay unposted instate.md, so you can edit the finding's location, dismiss them or post them again. When the diff cannot be read the comments are sent unchecked and the result has awarning; a 422 from GitHub is reported with the list of the comments sent and is never retried.nexus review submitalso records which findings are outside the diff (off_diffinruns.md); the tab shows the reason under the comment, leaves it unticked andreview comment list --jsongives it asnot_postable. A comment is also not sent, and is reported underskippedwith the reason, when its line was removed or rewritten, its file was deleted, the diff is not available (binary or very large files) or the comparison is truncated or fails; comments on the LEFT side (removed code) belong to the base and are never moved. After GitHub accepts the review, Nexus reads its comments back to get each comment's ID and URL, and marks aspostedonly the comments GitHub reports. If GitHub rejects the request, nothing changes; if some comments are missing from its answer, the command exits non-zero, lists them (not_postedwith--json) and leaves them as they were; if the comments cannot be read back, nothing is recorded and the error names the review so you can usemark-posted. A successful post adds anagentnote to the task history. A review submitted with--eventalso moves the task todone(the note says the review has been posted); a pending review leaves the status alone because nothing has been sent yet. When GitHub later asks for your review again,nexus syncmoves the task back totodo(see Sync). With--comment, each named comment must exist and be a draft or edited comment.--dry-runshows the request and sends nothing: it callsghonly for the reads it needs (who you are, for an approval, the PR's diff to check the comments, and the comparisons for older comments), never to write. This is the only operation in Nexus that writes to GitHub; syncing and running agents never do. - Your own finding statuses are written by
nexus review finding(and, for local reviews, by the web tab). They count towards the suggested verdict, butreview statusand the PR tab still count a finding as open unless the agent marked itresolved; the PR tab does not show or set them yet.
Web control-center shell
nexus serve now shows every page inside a shell: a top bar, a sidebar and a status bar. The overview panels and the source view are implemented (below); the screens that SPEC.md §8.16 puts inside the shell that are not built yet are the lists behind Linked and the Run local review button of the source view (start a local review from a task).
- Sidebar. Today has
overviewandagent runs(the number of runs in progress, counting only runs whose process is alive). Sources lists every source of the global registry (nexus registry) with its mode label (FOLDERfor a standalone folder,PRIVATE,TRACKED) and its task count; the repositoryserveruns in is listed first, marked as not registered when it is not in the registry. The selected source expands to its worktrees with their task counts, each linking to the source page filtered to that worktree. Registered sources that are stale or cannot be read are counted ("1 registered source unavailable") and never named, so no local path is shown.+ add sourceopens a small form that registers an existing Nexus store: enter the absolute path of the store root (a folder with.nx-config.yaml, or the top level of a Git repository initialised withnexus init) and the page goes to the new source. It performs the same check and registry write asnexus registry add, answers with the reason when the path is relative, missing, not a store, or a subfolder of one, and never initialises a folder: new stores are created withnexus init --register. The request is aPOST /sources/registerbehind the same cross-site protection as every other web write. Linked shows the number of tasks with a Jira link (<id>.jira.md), a GitHub PR link (<id>.pr.md) and a local review; those three rows do not open anything yet. Below 960 px the sidebar stacks above the content; below 640 px it starts collapsed behind a "Sources and links" button so the board stays within reach. - Routes.
/is the board of the repositoryserveruns in (worktree, repo and global scopes as before)./overviewis the overview page; for now it lists the sources, and explainsnexus init --registerandnexus registry addwhen none is registered./sources/<key>is the source view of a source: for the sourceserveruns in it is writable, for any other registered source it is a read-only board of its tasks (the same board is also reachable at/?scope=global&source=<key>, with its own live sync, search and detail pages); the key is the folder name, with a short hash added when two sources share it. Task writes (status, priority, notes, edits) go to the repositoryserveruns in; to change anything but the status of a task of another source, runnexus servethere (the global scope stays read-only). The exceptions are moving a task between states (the Move menu of its detail, the number keys and drag and drop on a source's board, which save the status in that task's own source) and the GitHub review tab, whose run, stop, ask, comment and post actions act on the task's own source. - Status bar. The mode chip (
GLOBALon the overview and the global board, the mode of the source elsewhere), the counts of the board,gh okorgh !(fromgh auth status),jira !whenacliis not inPATHandjira ?when it is (Nexus does not check the Jira login yet, so it never saysjira ok), the number of agents running and of queued replies. The status is read fromGET /shell/healthwhen a page opens and every 15 seconds while it is visible; the answer aboutghis reused for 30 seconds. - Keys (never while typing in a field):
sor the Sync button refreshes the pull request and Jira ticket copies of every linked task of every readable source (POST /sync, the same asnexus syncin each source, read-only on GitHub and Jira; it never changes a task except to reopen adonepull request task totodowhen a new review of it is requested from you, asnexus syncdoes) and announces the result; with nothing linked it says so without callingghoracli.Ron a task detail is a disabled "Run local review" placeholder until local reviews exist; pressing it says so. The existing keys keep their meaning, and the ARIA tabs work as before, with the tab in the hash of the task view (/tasks/<id>#tab=<tab>; an unknown tab falls back to details). - Design system.
run-ok,run-activeandrun-failedtokens (agent runs, CI and verification state, separate from task state) were added todesign/design-system/tokens.json, and the control-centercc-components (navigation, tabs, panel, list, chip, severity, code excerpt, attachment viewer, drop zone, message thread, evidence, meter and the run glyph colours) were promoted tonx-incss/nexus.css, documented inDESIGN.md§9 and shown on/design-system. - Tests. The Go handler tests, the jsdom tests (
web-tests/shell.test.js) and, in real Chromium,mise run test:browser:shell(sidebar placement and stacking, phone menu, touch targets, no horizontal scroll, contrast of the run tokens and the promoted pairs at 390, 960 and 1440 px in both themes). Every test uses its own registry (XDG_CONFIG_HOME) and a fakegh. The sidebar counts are computed when a page loads and are not live-synced; only the board is.
Overview
/overview is the landing page of the control center and always covers every registered source (plus the repository serve runs in); there is no mine / all sources switch. It is read-only: it has no forms or buttons that write, and every row is a link to the source view of its own source to the task view of the task (/tasks/<id>?…, the same address the board uses when you open a card; the tab, when the row asks for one, is in the hash), inside the same shell, and Esc or the Back link return to the list it was opened from (or to that source's page). The task opens in the writable repo scope for the source serve runs in and in the source's read-only scope for the others (where the write rules of the review tab apply). Rows about a pull request (Review requested, the PR review runs in Agent runs, and the Pull requests panel of the source view) open on the GitHub review tab; the other rows open on the details. A source that cannot be read is counted ("1 registered source unavailable") and never named.
- In progress: every
in-progresstask with its priority, source and worktree, and chips for its Jira key (jira MM-1) and PR (pr #9). - Ready: tasks that are
todoand not blocked (dependencies are resolved inside each source), most urgent first (priority, then creation time, then ID, the order ofnexus ready); the first five, then show N more, a link to the all-sources board filtered withis=ready. - Review requested: tasks whose
<id>.pr.mdsays review is requested from you and is not resolved:#number title,owner/repo, CI summary (ci N failing/ci running/ci passing), approvals and requested changes, whether it is a draft, and one chip per review lineage of the agent (claude · running,pi · done,outdated,failed) with the number of draft comments;no agent reviewwhen none ran. There is no run review button here (the overview is read-only): open the task. - Agent runs: the runs that are running (a run whose process is gone counts as
interrupted) followed by the five most recent finished ones, from the PR review runs (pr review,second opinion) and local review runs (local review,verification) of every source, with agent, revision, start or finish time (UTC) and the first line of an error. The elapsed time, current activity and progress bar of the design are not shown because Nexus does not record them. - JIRA: tickets from
<id>.jira.mdwith key, summary, status, task, type and priority; the panel is hidden when no task has a ticket. Ticket text is escaped.
The status bar shows GLOBAL, the number of sources, tasks and ready tasks. The page is live: it subscribes to /events?scope=overview, which watches the task directories, the review run records and the registry of every source and replaces the panels and counts when anything changes (the same events apply across the sources shown, and an update is skipped when the version is unchanged). The times of running runs are the start time, not a ticking clock. jsdom tests cover the panels, scopes of the links, read-only behaviour and event application; mise run test:browser:shell loads /overview in Chromium at 390, 960 and 1440 px (no horizontal scroll, touch targets, contrast) with that script's seed data; the layout with every panel populated has not been inspected visually.
Source view
/sources/<key> is the page of one source. It shows, from top to bottom:
- Header: the source name (the folder name), its mode (
FOLDER,PRIVATE,TRACKED), its folder (the home folder is shortened to~), the store where the tasks live (.git/nexus/tasksfor a private repository, the configuredpathfor tracked ones and standalone folders), the GitHub repository (owner/repofrom theoriginremote when it points at github.com, otherwise from the pull requests linked to its tasks) and whether the source is in the registry. A worktree / repo switch picks the scope of the board:repo(the default) shows the tasks of every worktree,worktree(?scope=worktree) only those of the current one; only the sourceserveruns in has a worktree scope, so the other sources offerrepoonly. sync prs refreshes the pull request copies of that source only (POST /syncwithsource=<key>&kind=prs; it reads from GitHub, writes only the local<id>.pr.mdcopies and never changes a task). Run local review is shown disabled: starting a local review from the web is not available yet (usenexus review local <id>). - Panels: Worktrees lists each worktree with its task count,
currentfor the oneserveruns in, and the pull requests linked to its tasks (pr #41, orno pr; a worktree has no pull request of its own, so this is derived from its tasks), each linking to the source page filtered to that worktree. Pull requests lists the distinct pull requests linked to the tasks of the source (newest number first, eight at most and then+N more) with state (open,draft,merged,closed), the CI summary, who it belongs to (review requestedor the author), the time of the last sync and a link to the task; titles are shown as escaped text. Agents shows the coding-agent section that Coding-agent configuration describes: where it comes from (repository or global config), the default agent, the other agents, the timeout of the default one and how many runs are running in the source; when no agent is configured (or the section is invalid) it shows the same error as the CLI. Labels shows the label catalog of the source (its configuration and that of its worktrees) with the configured colours. - Filter line:
Showing N of M tasks in <scope>, one chip per term of the filter (a negated term is prefixed withnot), clear, theis=readyandis=blockedcounts of the whole source (each is a button that adds the term to the filter) and the board / list switch. In list view the four status columns follow each other instead of sitting side by side. The filter, the scope and the view are kept in the links of the switches and in the address. - Board: the same cards as the main board (drag-and-drop status changes and creating tasks only in the writable source), with chips for the objects linked to each task:
pr #41(withmergedorclosedwhen it is),ci running/ci passing/ci N failingwhile the pull request is open,attach Nfor the number of attachments, andlocal review done|running|failed|…once a local review run is recorded. The chips are shown on every board, not only on this page. They are display only; the filter line understands the matchinglink,review,ciandsourcekeys.
The board and its link chips are live like the main board (the page watches the same scope); the header, the panels and the is= counts of the first load are not replaced by live sync, so reload the page to refresh the panels (the is= counts are recomputed from the cards in the browser). Tests: Go handler tests (internal/web/source_test.go), jsdom tests (web-tests/shell.test.js) and mise run test:browser:shell, which loads the source page and its list view in Chromium at 390, 960 and 1440 px in both themes (no horizontal scroll, panels within the screen, touch targets on the header and filter line, stacked columns in list view, contrast of the filter chips) with that script's seed data. It has not been checked in other browsers, on a real device or with a screen reader, and not with many worktrees or very long names.
Local review tab
Every task of the board that nexus serve runs in (and of a registered source's board) has a local review tab (/tasks/<id>#tab=local-review, with &view=findings|report|diff and &finding=<id>). It reads and writes the same files as nexus review … --local and nexus verify; nothing it does is not available from the CLI. The all-sources view shows the stored report read-only. The tab is there even before the first review, so R works from any tab of a task.
- Run banner. Status of the newest run (
[~] running,[x] done,[!] failed,interrupted), the agent, the revision, the worktree against its base (main@<sha>), the reviewedHEAD, whether uncommitted changes were included, the session and the start time, and stop (review stop --local) and new run (review local). New run is disabled with the reason when a run is in progress, when a report already exists (more revisions come from messages to the session; starting a fresh review after an accepted report is not supported) or when the task has no git worktree (a folder source). A failed or interrupted run shows its reason and the tail of the agent output; a run you stopped says so. There is no second opinion here: it exists for PR reviews only. The banner does not show the number of changed files or the size of the diff, because runs do not record them. - Views.
findings,report.md(the stored report as written) anddiff(what the findings did between two revisions, with the notes the newer revision answered, likereview diff --local), and group by severity, file or status. A Report updated to rev N line with the resolved / new / changed counts links to the diff from the previous revision. - Verification. The checks of the task, drawn as in the design prototype (
design/views/control-center/task-local-review.html): a header with N of M passed, failed count and where the checks come from; one row per check with a check box, a status glyph ([x]passed,[!]failed,[~]running,[ ]pending,[-]skipped), the check text, a source / result line (who marked it and when, the note, the finding it links to) and a red-tinted row for a failed check. Ticking the box marks the check passed and unticking puts it back to pending (web only;verify markhas nopending). Per-row actions: ask agent (pending) or re-run (verify runfor that check, disabled while a verification run is live), stop on a check that a live run includes (the same stop as the banner), I'll check and add note (open the row's marking controls), and the mark · attach · edit fold with a status select (passed, failed, skipped), note and files, Attach evidence (adds uploaded files to the check and keeps its status and note), Link evidence (existing attachments) and edit/remove. The evidence fold lists the note and the evidence links of the check, each through the normal attachment viewers with Unlink, closed by default and open for a failed or running check; its open state and unsent notes survive a reload of the tab. A running check shows who runs it and since when: Nexus does not capture an agent's output while it runs, so there is no live execution log yet, and code/execution-log sections exist only as attached files. The footer has ask agent to run pending and add check. Styles are ininternal/web/assets/app.css(light/dark tokens, phone layout under 640 px). Tests: Go handler tests, jsdom tests (web-tests/local-review.test.js; the file upload itself is covered by the Go tests, not jsdom) andmise run test:browser, which checks the block in Chromium at 390, 960 and 1440 px in both themes (no horizontal scroll, buttons inside the panel, 44 px actions on a phone, failed row tint, glyph contrast) with that script's seed data. - Findings. Severity, marks (new, changed with the previous title, reopened, resolved in rev N), location, why, effect and code, your status and the thread: your messages with their delivery state (
queued · delivered on the agent's next turn,delivered · picked up,answered) and the agent's reply for each. Actions: reply (queues a message about the finding), mark fixed, won't fix (with a reason), reopen, make task (a task that depends on this one,review make-task --local) and cancel queued reply. Fixed and won't-fix findings, and findings a revision resolved, are folded under Resolved. - Side column. Session (every message with its state and reply, and a composer to message the session, optionally about one finding), Report history (revisions with their resolved/new/changed counts, commit and a link to the rendered report when attachments are enabled) and Run details (run, agent, session, working directory, base, head, context, times).
- Keys (never while typing in a field):
Rstarts a local review from any tab (or says why it cannot); in the tabj/kmove between findings,ropens the reply to the current one,fmarks it fixed andwasks for the won't-fix reason. - Live updates. While a run is in progress or a message is queued, the tab polls a small endpoint every two seconds and reloads when a run or a message changed state. Text you have typed and not sent is never replaced: the reload waits until the fields are empty or sent, a reload you ask for (a form, a view or grouping change) puts every draft back in its field and keeps its
<details>open, and the board's live sync does not reload the open task while a form of the tab has unsent changes. Agent text is rendered as escaped Markdown. Every action is a POST protected as described in Web cross-site protection, and agents are started only through the same services as the CLI.
Tests: Go handler tests (internal/web/localreview_test.go), domain tests for review stop --local and review finding (internal/localreview/stop_test.go, cmd/nexus/review_finding_test.go) and jsdom tests (web-tests/local-review.test.js) with no real agent. jsdom proves DOM behaviour, not layout in a real browser.
Server log
nexus serve logs to stderr (text, one event per line, log/slog). --log-file PATH also appends the same lines to a file, created with mode 0600. --log-level is debug, info (default), warn or error; the web board refreshes itself often, so --log-level warn keeps only problems. Other commands print no log.
- Startup: address, repository root, registered sources, whether
--terminalis on, the level, and whether a log file is used. - HTTP requests: one
requestline each with method, path (no query string), status, duration and a correlation idid=<8 hex>; 5xx are logged as errors and failing write requests as warnings. - External commands (
gh,acli,git, the shell of--terminal, and the detachednexus review execandexec-localprocesses when they are launched): executable, arguments, working directory, start time, duration and exit status, with the request'sidwhen a web request launched it. A failure is logged as a warning with the stderr and, forgh api, the stdout (GitHub's error response), each cut to 2 KB. The coding agent that a detached review process runs is not logged by serve; its outcome stays in the run record. - Levels of external commands: a successful command is logged at
info, except the read-only repository lookups the web board repeats on every refresh (git rev-parse,git symbolic-ref,git worktree listandgit remote get-url origin), which are logged atdebugand so appear only with--log-level debug.gh,acli, the shell, and thegitcommands of local reviews keepinfo. A failed command is always a warning with its stderr, whichever kind it is, so a failingrev-parseis never hidden. The web's own refresh requests are still logged asrequestlines atinfo; this is unchanged. - What is never logged: the environment, tokens (values after
--token-like flags,ghp_…/github_pat_…/Bearer …strings and the values ofGH_TOKEN-style variables are replaced by[redacted]), and, below--log-level debug, long texts (any argument or standard input of 120 bytes or more, such as agent prompts and review comment bodies): only their length and a short hash are logged. Atdebugthey are logged in full, cut to 2 KB. Control characters in command output are escaped, so an event stays on one line. - Errors in the web: when a GitHub review post or a Jira action fails, the message shown includes GitHub's error (for a 422 its
messageanderrors) and(ref <id>); the log lines of that request and its commands carry the sameid=<id>. The CLI shows GitHub's error in its message too, without an id.
Tested with Go against fake gh and acli executables; real GitHub and Jira are not exercised.
Web cross-site protection
Any web page open in your browser can make the browser send a request to 127.0.0.1:8080. To stop other sites from changing tasks, the web server rejects every state-changing request (anything other than GET, HEAD or OPTIONS) with 403 unless the browser says it came from a page served by Nexus itself:
- If the request has a
Sec-Fetch-Siteheader (all current browsers send it), it must besame-originornone.same-siteandcross-siteare rejected. - Otherwise its
Originheader must name the request's ownHost. A missing,nullor different origin is rejected. - A request with neither header, such as one from
curlor a script, is rejected. To script the web endpoints, sendOrigin: http://<host>:<port>; the CLI is the supported way to change tasks from scripts.
This needs no token and no setup. It does not defend against other local users or processes that can reach the port, and GET requests are never changed by it.
Web terminal
nexus serve --terminal adds a Terminal tab next to Details and History in the task detail. It runs a real shell in a pseudo-terminal, rendered in the browser with xterm.js, so you can work on a task without leaving the board.
nexus serve --terminal # 127.0.0.1:8080
nexus serve --terminal --host localhost --port 9000
The terminal is off by default. Without --terminal the endpoints do not exist and the tab is not rendered.
Security model
A terminal is a full shell running as the user who started nexus serve. There is no authentication: anyone who can reach the port and load the page gets a shell with your privileges. Nexus limits exposure like this:
serve --terminalrefuses to start unless--hostis a loopback address (localhost,127.0.0.0/8or::1). Use--host 0.0.0.0or a LAN address only without--terminal.- Every terminal request must use a loopback
Host(a guard against DNS rebinding), the WebSocket upgrade must carry anOriginthat matches theHost, and requests need a random per-process token that is rendered into the page. The token changes on every start. - These checks keep other websites and other machines out; they do not protect against other local users or processes that can open the port, so do not use
--terminalon a shared machine. - Unix only. Other platforms report that the terminal is not supported.
What a session is
-
Shell:
$SHELL -l, or/bin/sh -lwhenSHELLis unset. -
Working directory: tracked tasks use the checkout of the task's worktree. Private tasks use the checkout of the branch they belong to, falling back to the main worktree root when the branch is not checked out or the task is repo-wide. Tasks in a standalone repository use the repository root. If the directory cannot be resolved, the terminal reports an error without exposing paths.
-
Scopes: worktree and repo views only. The global scope stays read-only; its Terminal tab explains this and links to the repo view.
-
Environment: a snapshot taken when the session starts, added to the inherited environment (inherited
NEXUS_*task variables are removed first; variables with an empty value are left unset, andTERM=xterm-256colorandCOLORTERM=truecolorare set):Variable Value NEXUS_TASK_IDtask ID NEXUS_TASK_TITLEtitle NEXUS_TASK_STATUSstatus NEXUS_TASK_PRIORITYpriority NEXUS_TASK_TAGStags, comma separated NEXUS_TASK_DEPSdependency IDs, comma separated NEXUS_TASK_FILEpath of the task's Markdown file NEXUS_REPOSITORYrepository root NEXUS_WORKTREEworktree branch (unset for repo-wide and standalone tasks) The task body is not exported. Later edits to the task do not change a running shell's environment.
Lifecycle and limits
- One persistent shell per task instance. It keeps running when you leave the task or reload the page; reopening the Terminal tab reattaches and replays recent output (the last 256 KiB).
- A session ends when the shell exits, when you press Kill, after 60 minutes with no browser attached, or when
servestops (Ctrl+C or SIGTERM end the shells first). Sessions are not kept acrossserverestarts. - After the shell exits the tab shows its exit status and a Start new session button.
- At most 16 sessions run at once; starting another reports that the limit is reached.
- Cards and the Terminal tab show a
>_marker while a session is running. - The terminal captures every key, including Esc. Press Ctrl+Shift+Esc to return focus to the Terminal tab.
- A replay is the raw output; if the window width changed since it was produced, old output can look garbled until the shell redraws.
Dependencies
Go: github.com/coder/websocket (WebSocket) and github.com/creack/pty (pseudo-terminals). Web: @xterm/xterm and @xterm/addon-fit, pinned and vendored into the embedded assets (no CDN, no bundler at runtime). See mise run vendor-xterm below. The transport and protocol are described in internal/web/README.md.
Development
This repository uses mise to declare tools and project tasks. Run checks through mise:
mise run build
mise run test
mise run vet
mise run test runs Go tests and then runs the jsdom web-interaction files serially; these tests start isolated temporary servers and can interfere under concurrent load. Optional real-browser testing is available through mise run test:browser after installing the pinned browser with mise run browser-install (it also runs web-tests/verification-browser.cjs, the layout check of the Verification block). Browser/jsdom checks do not replace manual acceptance on real devices or assistive technologies. mise run test:browser:shell checks the control-center shell layout and the contrast of the new tokens and components in Chromium. mise run test:browser:attachments runs a hostile HTML prototype against nexus serve in Chromium to check attachment isolation (sandbox, origin, CSP, cookies, storage, navigation, network, path traversal); web-tests/attachments-acceptance.md maps every attachment acceptance criterion to its checks and states what they do not prove.
mise run vendor-pdfjs copies the pinned pdfjs-dist library, worker and standard fonts from node_modules into internal/web/assets/vendor/pdfjs/ (committed and embedded, Apache-2.0, no CDN); run it after npm ci only when changing the pinned version.
mise run vendor-xterm copies the pinned @xterm/xterm and @xterm/addon-fit files from node_modules into internal/web/assets/vendor/xterm/; the copies are committed and embedded in the binary. Run it (after npm ci) only when changing the pinned versions.
Documentation maintenance
When changing CLI commands or flags, installation/build requirements, task workflows, storage behavior, or other user-facing functionality, update this README in the same change. Update docs/ and the embedded skill topics in internal/skill too; TestDocsCommandsExist checks that the nexus examples in docs/ use real commands and flags. Check every command and option against nexus --help and its command-specific help; do not document planned/spec-only behavior as available. Keep the implementation status clear, and link to the relevant specification or detailed documentation when useful.
Project status
The CLI, terminal Kanban, and web interface are implemented. nexus skill show [topic] prints agent instructions for workflow, task commands, search, storage scope, attachments, recipes, PR review reports, and troubleshooting. nexus skill install <directory> writes a minimal pointer SKILL.md without overwriting an existing file. The broader MVP and its remaining acceptance checkpoints are tracked as work in progress; see SPEC.md for requirements and current scope. The specification is not evidence that every described feature is implemented.