No description
Find a file
Miguel de la Cruz 0940fdca6f Add bug task for the Move menu options not working on click in the task detail
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.
2026-11-12 13:27:25 +01:00
.agents/skills/integrate-design docs: adapt design integration skill to repository 2026-11-04 23:45:24 +01:00
cmd/nexus Validate review comment lines against the PR diff before posting 2026-11-12 13:08:30 +01:00
design Show the task detail as its own view instead of a dialog 2026-11-12 01:42:38 +01:00
docs Document special Markdown blocks and file conventions 2026-11-12 13:17:45 +01:00
internal Validate review comment lines against the PR diff before posting 2026-11-12 13:08:30 +01:00
nx Add bug task for the Move menu options not working on click in the task detail 2026-11-12 13:27:25 +01:00
web-tests Validate review comment lines against the PR diff before posting 2026-11-12 13:08:30 +01:00
.gitignore docs: add Markdown block task records 2026-11-10 10:34:53 +01:00
.nx-config.yaml docs(control-center): integrate the control center design into the spec, design and tasks 2026-11-07 00:05:20 +01:00
AGENTS.md docs(agents): note that pushes to origin may need a larger http.postBuffer 2026-11-09 15:02:47 +01:00
go.mod feat(cli): add attachment prune with confinement to the task's attachments folder 2026-11-09 14:25:51 +01:00
go.sum feat(cli): add attachment prune with confinement to the task's attachments folder 2026-11-09 14:25:51 +01:00
mise.toml Match the web Verification block to the local review prototype 2026-11-12 12:51:37 +01:00
package-lock.json feat(web): show PDF attachments in an embedded, sandboxed pdf.js viewer 2026-11-09 14:25:51 +01:00
package.json feat(web): show PDF attachments in an embedded, sandboxed pdf.js viewer 2026-11-09 14:25:51 +01:00
prompt.md docs: add complete plan-to-Nexus migration prompt 2026-11-05 10:17:37 +01:00
README.md Document special Markdown blocks and file conventions 2026-11-12 13:17:45 +01:00
SPEC.md Validate review comment lines against the PR diff before posting 2026-11-12 13:08:30 +01:00

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.

  • add copies; 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-hidden is 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 --force replaces 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-replaced when --force overwrote at least one file, or attachment-removed) listing the affected paths, with the size and SHA-256 of each file for additions, and updates the task's updated time. Files and event are written together: if the task cannot be saved, the files are restored.

  • remove deletes 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).

  • prune works on one task instance. It keeps every file linked as [text](attachment:path) or ![text](attachment:path) (or as a reference definition) in the task body or in a note, and the resources an HTML attachment linked that way references directly: src of script, img, audio, video, source, track, iframe and embed, href of link and a, each srcset candidate, poster and object data. 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 prune touch 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 single attachment-pruned event lists the deleted files. --dry-run changes nothing: no file, folder, updated time or event.

  • nexus serve also starts a second listener on 127.0.0.1 with 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/revoke invalidates 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 the Host header, sends a closed Content-Security-Policy (no network access, forms, frames or plugins), nosniff, no-referrer and no-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) and ![text](attachment:path) work 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"> marked SANDBOXED, or a PDF in the embedded pdf.js viewer (pages, text layer, page navigation and zoom; n/p, +/-/0 keys), 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 (Esc closes 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 by remove or prune shows. 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 agent mark 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 as user events, 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.
  • init outside Git uses the current directory as the root unless a parent already holds .nx-config.yaml (then that parent is used and init is idempotent). A failed init never overwrites an existing config.
  • There are no worktrees: task files carry no worktree, and --repo writes (which need --worktree) fail with a clear error.
  • --register adds the absolute directory to the same global registry file (see nexus registry); list --global then includes it. JSON task objects from standalone repositories carry "kind": "standalone" (omitted for Git-backed repositories), and their worktree is 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 run and nexus review local use 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.

  • start and resume are 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: stdin sends the prompt on standard input ({prompt} must not appear). prompt: arg requires exactly one {prompt} in start and in resume.
  • resume must contain {session_id}. If start contains {session_id}, Nexus generates the ID and passes it in. Otherwise set session_id_from to the JSON field of the agent's output that holds it.
  • timeout is a Go duration such as 30m (default 30m).
  • default is required when several agents are defined and optional with a single one.
  • The local-review skill is embedded in Nexus and can be inspected with nexus skill show local-review. agent.local_review_skill selects 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 repository agent section replaces the global section, including this key. Run local reviews with nexus review local; the agent submits reports with nexus 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.md copy is overwritten in full on every sync. Its front matter holds url, owner, repo, number, title, author, state (open, merged, closed), draft, base, head_sha, review_requested, resolved, synced_at, checks, reviews and comments; the body is the PR description. The task file itself is not changed, and a sync adds no history events.
  • checks, reviews and comments come from the same gh pr view call (statusCheckRollup, reviews, reviewRequests, comments) and are left out when empty. checks lists each CI check with name, state (passed, failed, running, queued) and the url of its log; a neutral or skipped check counts as passed and anything that is not a known success counts as failed. gh does not say which tests failed, so none are stored. reviews has one entry per reviewer with user, state (approved, changes_requested, commented, pending), the commit it was made on and stale: true when 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 as requested. comments keeps the latest 10 comments of the conversation (oldest first) with their text cut at 1000 characters, and checks at 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_requested is true when the PR appears in gh search prs --review-requested=@me --state=open, which includes requests made through a team. resolved is 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 tags review/github-pr and repo/<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 results array with --json) and the command exits non-zero. If gh is missing, logged out, or the review-request search fails, the command stops before writing anything.
  • Without --global, sync and sync --discover only refresh and discover pull requests of the GitHub repository gh repo view reports for the current directory (so it follows gh repo set-default and prefers upstream on a fork); linked tasks whose PR is in another repository are skipped. If gh cannot 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 ## Description and ## 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 sync gives the same content apart from synced_at.
  • Comments. When the paginated listing is needed its output carries no dates, so those comments show unknown and the result has the warning comments_partial_dates; if that call fails the comments received are kept and the warning is comments_truncated. The paginated output is plain text; Nexus reads it as blocks separated by a blank line, using Author: text when a block has that form. This parsing is verified only against a fake acli, not a real one.
  • Errors (--json prints {"error":{"code","message"}} on stderr): invalid_jira_key (must match ^[A-Z][A-Z0-9_]+-[0-9]+$, checked before acli runs), acli_not_found, acli_not_authenticated, jira_not_found_or_forbidden (acli gives the same answer for both), acli_malformed_output, acli_failed, already_linked, not_linked. In sync --json the tickets are in a jira array, each with id, jira (key, url, status, synced_at) and warnings, or id, key and error; one failing ticket does not stop the others and the exit code is non-zero. sync only needs gh when there are pull requests to refresh or --discover.
  • Web (nexus serve). A task linked to a ticket has a jira <KEY> tab (/tasks/<id>#tab=jira) with the read-only copy: a banner KEY · 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 (like nexus 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 ↗ (key o on the tab, never while typing) and Unlink… (asks for confirmation; like nexus 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 named Acceptance Criteria, Acceptance, AC or Definition 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 a body-edited event by user; 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 as nexus link --jira and opens the new tab. The write endpoints are POST /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: the Linked counts 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 run records a running run in <id>.reviews/pr/runs.md and starts a detached nexus review exec process (not listed in --help), so the review survives the terminal and a restart of nexus serve. The process runs the agent's start command, or resume when the previous review of the task used the same agent, in the task repository's root directory. The prompt carries the embedded pr-review skill, 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 in nexus skill show pr-review. Every error is printed as line N: <block>: <field>: <message> (a list with --json), nothing is stored, and the run stays running so 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 an id: f-<n> line to each finding that has none. A run takes one valid submission, and a run that is not the current running one is rejected.
  • End of the run. When the agent exits, the run becomes done if a valid report was submitted and failed otherwise, with the reason in error and the last 4 KiB of the agent's output. A non-zero exit code or a timeout (default 30m) always fails the run. Either way an agent note is added to the task history.
  • Status. running whose process no longer exists reads as interrupted (the file is not rewritten). A review is outdated when the commit it reviewed differs from head_sha in <id>.pr.md; run nexus 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 --force is given; the new revision is made on the same commit and must account for the previous findings.
  • Session messages. review ask is a free-form alias for review message on PR reviews. Messages (n-1, n-2, …) can target a finding, or a local-review verification check; they are stored queued, delivered or answered in the matching review's state.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 cancel removes only queued notes. “Picked up” is a calculated display state (delivered while the carrying process has a PID), never persisted. The agent answers with one nexus-reply block 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 status shows 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.md copy. 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 --agent is on the CLI. With a single agent there is no selector.
  • Second opinions. review run --second-opinion starts 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, ids f-<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-first the 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 (saw in runs.md). A second opinion always reviews the PR's current head as a new review, so --force does 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 submit accepts a report for a PR, Nexus makes one call to GitHub (gh api …/compare/<base>...<reviewed commit>) and, for every finding whose location is on a changed line, stores the unified diff around that line in a diff fence 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's snippet. 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 status counts 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 (agreement with --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 under unlinks in state.md do not match and pairs under links do. nexus review link <id> <a> <b> confirms that two findings of different lineages are the same problem (stored under links, and removed from unlinks); 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 to unlinks). Findings are f-<n> (primary) or <lineage>/f-<n>, and both must be open findings of the latest revision of their lineage. review status --json lists the effective agreement.pairs (via is rule or link) and the agreement.rejected candidates. Merging matched findings and arbiter runs are not implemented.
  • Suggested verdict. review status also shows comment, approve or request_changes (suggested_verdict with --json), computed from the findings that are still open in every lineage: any open finding that is blocking means request_changes; no open must and no open question means approve; anything else means comment. A finding is not open when its revision marks it resolved or when state.md has it as fixed or wontfix. 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 with review post --event.
  • Stopping a run. review stop records the run as failed with the error stopped, drops a report it had already submitted, returns the notes of a stopped follow-up to the queue and adds an agent note 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 is nexus 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 comment and a location has a suggested comment, addressed as <lineage>/<finding id> (primary/f-1; plain f-1 means primary). Its state is kept in state.md under comments, never in the agent's revision: draft (no entry; follows the agent's text), edited, posted (with github_id, github_url and posted_at) and dismissed. 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. status counts comments by state. Findings that are resolved or have no comment are not listed.
  • Posting. review post sends the selected comments as one review created with gh 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 has commit_id set to the commit of the active revision (the latest accepted one of primary, otherwise of another lineage). Nothing is submitted unless you ask: without --event (or with the explicit --pending) the request has no event, so the review stays pending until you submit it on GitHub. With --event comment, approve or request-changes (also written request_changes) the review is submitted with that verdict (COMMENT, APPROVE, REQUEST_CHANGES); --event and --pending cannot be combined. The review text is --body, or the contents of --body-file (- reads standard input), or else the body text of state.md if 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 asks gh api user who you are and compares it, ignoring case, with the PR's author; other verdicts are not checked and GitHub decides. Before sending, Nexus checks that gh is 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 reads gh 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 in remapped with a warning to check it. Before sending, every comment is checked against the PR's diff: Nexus reads gh api repos/<owner>/<repo>/compare/<base>...<reviewed commit> once (a dry run does too) and a comment is not sent, and is reported under skipped with the reason (for example line 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 (RIGHT is added and context lines, LEFT removed and context lines) or its start_line is 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 in state.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 a warning; a 422 from GitHub is reported with the list of the comments sent and is never retried. nexus review submit also records which findings are outside the diff (off_diff in runs.md); the tab shows the reason under the comment, leaves it unticked and review comment list --json gives it as not_postable. A comment is also not sent, and is reported under skipped with 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 as posted only 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_posted with --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 use mark-posted. A successful post adds an agent note to the task history. A review submitted with --event also moves the task to done (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 sync moves the task back to todo (see Sync). With --comment, each named comment must exist and be a draft or edited comment. --dry-run shows the request and sends nothing: it calls gh only 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, but review status and the PR tab still count a finding as open unless the agent marked it resolved; 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 overview and agent 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 (FOLDER for a standalone folder, PRIVATE, TRACKED) and its task count; the repository serve runs 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 source opens 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 with nexus init) and the page goes to the new source. It performs the same check and registry write as nexus 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 with nexus init --register. The request is a POST /sources/register behind 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 repository serve runs in (worktree, repo and global scopes as before). /overview is the overview page; for now it lists the sources, and explains nexus init --register and nexus registry add when none is registered. /sources/<key> is the source view of a source: for the source serve runs 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 repository serve runs in; to change anything but the status of a task of another source, run nexus serve there (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 (GLOBAL on the overview and the global board, the mode of the source elsewhere), the counts of the board, gh ok or gh ! (from gh auth status), jira ! when acli is not in PATH and jira ? when it is (Nexus does not check the Jira login yet, so it never says jira ok), the number of agents running and of queued replies. The status is read from GET /shell/health when a page opens and every 15 seconds while it is visible; the answer about gh is reused for 30 seconds.
  • Keys (never while typing in a field): s or the Sync button refreshes the pull request and Jira ticket copies of every linked task of every readable source (POST /sync, the same as nexus sync in each source, read-only on GitHub and Jira; it never changes a task except to reopen a done pull request task to todo when a new review of it is requested from you, as nexus sync does) and announces the result; with nothing linked it says so without calling gh or acli. R on 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-active and run-failed tokens (agent runs, CI and verification state, separate from task state) were added to design/design-system/tokens.json, and the control-center cc- components (navigation, tabs, panel, list, chip, severity, code excerpt, attachment viewer, drop zone, message thread, evidence, meter and the run glyph colours) were promoted to nx- in css/nexus.css, documented in DESIGN.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 fake gh. 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-progress task with its priority, source and worktree, and chips for its Jira key (jira MM-1) and PR (pr #9).
  • Ready: tasks that are todo and not blocked (dependencies are resolved inside each source), most urgent first (priority, then creation time, then ID, the order of nexus ready); the first five, then show N more, a link to the all-sources board filtered with is=ready.
  • Review requested: tasks whose <id>.pr.md says 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 review when 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.md with 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/tasks for a private repository, the configured path for tracked ones and standalone folders), the GitHub repository (owner/repo from the origin remote 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 source serve runs in has a worktree scope, so the other sources offer repo only. sync prs refreshes the pull request copies of that source only (POST /sync with source=<key>&kind=prs; it reads from GitHub, writes only the local <id>.pr.md copies and never changes a task). Run local review is shown disabled: starting a local review from the web is not available yet (use nexus review local <id>).
  • Panels: Worktrees lists each worktree with its task count, current for the one serve runs in, and the pull requests linked to its tasks (pr #41, or no 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 requested or 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 with not), clear, the is=ready and is=blocked counts 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 (with merged or closed when it is), ci running/ci passing/ci N failing while the pull request is open, attach N for the number of attachments, and local 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 matching link, review, ci and source keys.

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 reviewed HEAD, 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) and diff (what the findings did between two revisions, with the notes the newer revision answered, like review 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 mark has no pending). Per-row actions: ask agent (pending) or re-run (verify run for 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 in internal/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) and mise 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): R starts a local review from any tab (or says why it cannot); in the tab j/k move between findings, r opens the reply to the current one, f marks it fixed and w asks 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 --terminal is on, the level, and whether a log file is used.
  • HTTP requests: one request line each with method, path (no query string), status, duration and a correlation id id=<8 hex>; 5xx are logged as errors and failing write requests as warnings.
  • External commands (gh, acli, git, the shell of --terminal, and the detached nexus review exec and exec-local processes when they are launched): executable, arguments, working directory, start time, duration and exit status, with the request's id when a web request launched it. A failure is logged as a warning with the stderr and, for gh 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 list and git remote get-url origin), which are logged at debug and so appear only with --log-level debug. gh, acli, the shell, and the git commands of local reviews keep info. A failed command is always a warning with its stderr, whichever kind it is, so a failing rev-parse is never hidden. The web's own refresh requests are still logged as request lines at info; this is unchanged.
  • What is never logged: the environment, tokens (values after --token-like flags, ghp_…/github_pat_…/Bearer … strings and the values of GH_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. At debug they 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 message and errors) and (ref <id>); the log lines of that request and its commands carry the same id=<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-Site header (all current browsers send it), it must be same-origin or none. same-site and cross-site are rejected.
  • Otherwise its Origin header must name the request's own Host. A missing, null or different origin is rejected.
  • A request with neither header, such as one from curl or a script, is rejected. To script the web endpoints, send Origin: 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 --terminal refuses to start unless --host is a loopback address (localhost, 127.0.0.0/8 or ::1). Use --host 0.0.0.0 or a LAN address only without --terminal.
  • Every terminal request must use a loopback Host (a guard against DNS rebinding), the WebSocket upgrade must carry an Origin that matches the Host, 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 --terminal on a shared machine.
  • Unix only. Other platforms report that the terminal is not supported.

What a session is

  • Shell: $SHELL -l, or /bin/sh -l when SHELL is 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, and TERM=xterm-256color and COLORTERM=truecolor are set):

    Variable Value
    NEXUS_TASK_ID task ID
    NEXUS_TASK_TITLE title
    NEXUS_TASK_STATUS status
    NEXUS_TASK_PRIORITY priority
    NEXUS_TASK_TAGS tags, comma separated
    NEXUS_TASK_DEPS dependency IDs, comma separated
    NEXUS_TASK_FILE path of the task's Markdown file
    NEXUS_REPOSITORY repository root
    NEXUS_WORKTREE worktree 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 serve stops (Ctrl+C or SIGTERM end the shells first). Sessions are not kept across serve restarts.
  • 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.