ng CLI¶
monitor/ng is the nexus's GitHub helper. It wraps the common bot
operations — reacting on comments, opening issues and PRs, posting
replies, uploading assets, wrapping up reports — into tight
subcommands so an agent can write to GitHub without juggling
gh + jq + raw REST URLs in every shell call.
Every write uses the bot's installation token (minted by
monitor/mint-token.sh from the GitHub App private key), never
the user's gh token. GitHub mutes notifications for actions taken
by the recipient's own account; routing through the bot is what
keeps the operator's notification surface alive. The only GitHub
interactions that should still use the user's identity are
git commit and git push (for accurate commit authorship). See
Security for the full identity boundary.
Conventions¶
Identity — github.repo and github.user_login come from
config/nexus.yml via config/load.sh (see Config).
MONITOR_REPO / MONITOR_USER_LOGIN override.
Repo argument — most verbs accept --repo <owner>/<name> to
target a non-default repo. Unset, the cwd-derive default depends on
whether the verb writes or reads:
- Read verbs (
issue view,pr view,show,stranded-branches) prefer the cwd'sgit remote get-url originif it is agithub.comURL (with a one-line stderr warning when different from$REPO), else fall back to$REPO. - Write verbs (
process,process-issue,react,react-issue,reply,comment,comment-edit,close,close-set,issue create,issue comment,pr create|edit|merge,wrap-up,interactive-sessions --upsert-overview,nexus-identity --upsert-overview) prefer$REPOfrom config. If the cwd has agithub.comorigin that differs from$REPO, the verb refuses to write — closing the soon-to-be-public nexus-code leak risk — and emits a structured error asking for an explicit--repo.
Both rules live in one place, _resolve_repo <read|write> in
monitor/ng. ng upload is the exception and does
not use it: its destination is the asset repo, resolved from
github.asset_repo with its own default-deny rules, and on that verb
--repo means something different. See its section.
Body input — verbs that POST a body (reply, issue create,
issue comment, pr create, pr edit) accept the body via
--body-file <path> or stdin. Pass long markdown in a file; piping
short replies via echo "thanks" | ng reply 7 works too.
Output discipline — verbs print just their primary result
(URL, SHA, comment ID, …) on stdout. Stderr carries warnings and
errors. This makes pipelines (url=$(ng pr create …)) trivial.
Exit codes — 0 on success and 1 on the general refusal path
(die), plus 64 = EX_USAGE for one specific condition: a flag
given no value, or an empty value where the flag's contract requires
one (_die_usage / _argloop_stuck; <your-org>/nexus-code#990
aligned ng with the sixteen other scripts in this repo that already
used 64). So ng pr merge 42 --sha and ng pr merge 42 --sha ""
both exit 64 rather than merging unpinned — a caller that branches
only on 1 will read that as success. Beyond those, verbs carry
their own codes, documented per verb (e.g.
ng wrap-up 3,
ng fetch-asset 4,
ng upload 4/7,
ng report-check 2,
ng watcher-status 0–4).
Quick reference¶
This page is not the complete verb list — ng verbs is. ng
dispatches 59 operational verbs; the sections below cover 35 of
them. The other 24 are mostly pass-through facades over a standalone
monitor/*.sh (whose own --help is the reference), plus narrow
state queries and aliases. ng verbs — the categorized index printed
by ng --help / ng help / ng -h, generated from _verb_index in
monitor/ng — gives each a one-liner. It is hand-maintained and
nothing asserts it against the dispatch table, so 11 of the 59
are currently missing from it (see Maintenance
burden). The table below is derived from the
dispatch table itself and covers every verb ng dispatches that is
not one of the 35 with its own section above — plus the handful whose
section is new, pointed at with [→]. Most are a thin exec over
a standalone script that is the real reference; the right-hand column
says where to look.
| Verb | What it is | Reference |
|---|---|---|
ci-attempts <sha\|branch\|PR#> |
was this head green, and green on the first attempt? | monitor/ci-head-attempts.sh; nexus.ci-triage |
close-set --bodies <dir> <issue>… |
bulk close with per-issue verification | → |
comment <n> |
alias of ng reply |
→ |
comment-edit <id> |
compare-and-swap edit of an existing comment | → |
declare-wait / declare-no-wait |
worker self-declares (or releases) an async external wait | monitor/declare-wait.sh, monitor/declare-no-wait.sh |
guards-for-diff |
which construct-keyed guards read the files you changed | → |
obligation <sub> |
the who-owes-whom ledger (open\|settle\|note\|state\|show\|list\|gate\|pairs\|preserve-closures) |
monitor/obligations.sh |
pane-state <window> |
classify a worker pane | monitor/pane-state.sh; Worker states |
paste-followup <window> |
the canonical follow-up paste into a window | monitor/paste-followup.sh; nexus.tmux-spawn |
remote <sub> |
confined remote SSH endpoint | monitor/remote-enroll.sh; nexus.remote-access |
report-grep <pattern> |
ignore-file-blind search of the reports corpus | → |
reports-for-window <window> |
which reports name this window | monitor/ng; nexus.report |
reports-roll [--dry-run] |
archive pre-buffer reports into reports/YYYY-MM/ |
monitor/reports-roll.sh |
request <sub> |
worker→orchestrator request inbox (file\|await\|fetch\|show\|reqfile\|dir\|list\|reply\|ack\|fail) |
monitor/request-channel.sh |
retire-preflight <window> |
synchronous go/no-go gate before a kill | monitor/retire-preflight.sh; nexus.window-cleanup |
retire-window <window> |
preflight + kill + prune every state surface + verify | monitor/ng; nexus.window-cleanup |
send <agent> … |
harness-neutral agent delivery with a receipt | monitor/send.sh; nexus.agent-delivery |
session-id <window> |
the Claude Code session-id behind a window | monitor/window-session-id.sh |
skeptic-arm · skeptic-disposition · skeptic-evidence · skeptic-obligations · skeptic-orphans |
the skeptic bookkeeping verbs (distinct from the ng skeptic channel) |
→ |
stranded-branches |
pushed branches that never got a PR | → |
token / user-pat |
print the bot installation token / the user PAT | monitor/mint-token.sh, monitor/user-pat.sh |
usage [--json] |
which ng mechanisms are used, how often, what is cold |
monitor/usage-report.py |
write-probe <path> |
pre-flight that a deliverable path is writable | monitor/write-probe.sh |
Most verbs also answer ng <verb> --help with their own usage line
(_usage_for in monitor/ng, or — for a facade — the target
script's own usage). monitor/watcher/test-ng-usage-flag-coverage.sh
is a ratchet over that: it fails CI on any new flag the parser
accepts but the usage line omits, and equally when a recorded known
drift is fixed and left in its manifest, so that gap can only shrink.
It makes the per-verb --help the flag surface least able to
drift. It does not guarantee the verb has a --help at all:
measured at a3177ef6, ng skeptic-arm --help and
ng skeptic-obligations --help die unknown flag, and
ng skeptic-disposition --help prints the whole file header instead
of a usage line.
| Verb | One-line | Section |
|---|---|---|
ng process <id> |
eyes-react + fetch body for a user comment | → |
ng process-issue <n> |
eyes-react + fetch body for a user issue | → |
ng react <id> <kind> |
post a reaction on a comment | → |
ng react-issue <n> <kind> |
post a reaction on an issue | → |
ng show <id> |
read a comment body (no eligibility filter) | → |
ng reply <n> |
post a comment on an issue/PR | → |
ng close <n> |
close an issue, optionally with a comment | → |
ng comment-edit <id> |
compare-and-swap edit of an existing comment | → |
ng close-set <issue>… |
bulk close with per-issue verification | → |
ng issue <n> |
view issue (one-liner; flags expand) | → |
ng issue create |
open a new issue | → |
ng issue comment <n> |
alias of ng reply under issue namespace |
→ |
ng pr create |
open a PR with auto-requested reviewer | → |
ng pr edit <n> |
patch a PR title or body | → |
ng pr merge <n> |
merge a PR via REST | → |
ng pr view <n> |
one-line PR summary | → |
ng preflight <repo> |
is the bot installed on this repo? | → |
ng stranded-branches |
pushed branches that never got a PR | → |
ng guards-for-diff |
which guards read the files you changed (pre-push) | → |
ng upload <file> |
push asset to asset repo, print pinned URL | → |
ng wrap-up <n> <report> |
end-of-task hand-off (upload + comment + rocket + log + retain) | → |
ng wrap-up-check <window> |
verify a worker's wrap-up obligations before closing | → |
ng skeptic <sub> |
worker↔skeptic comms channel + nudge (delegator) | → |
ng skeptic-arm\|-obligations\|-evidence\|-orphans\|-disposition |
skeptic pending-marker bookkeeping | → |
ng respawn <window\|sid> |
resume a wrapped/closed worker's session in a tmux window | → |
ng service-incident <svc> |
assemble a service incident report from recorded state | → |
ng spawn-decision <window> |
advisory continue-vs-spawn classifier for a window | → |
ng engaged-done |
release an operator-engaged interactive window | → |
ng interactive-sessions |
list resumable interactive sessions (+ overview upsert) | → |
ng suppress-emit <id> |
operator-side manual emit-suppression of a comment | → |
ng report-init <slug> |
create a frontmatter'd report skeleton | → |
ng report-check <path> |
validate a report against the schema | → |
ng report-grep <pattern> |
search the reports corpus without a silent zero | → |
ng fetch-asset <url> |
fetch a user-attachments/... URL via user PAT |
→ |
ng dashboard get |
read the overview-issue dashboard middle | → |
ng dashboard put |
splice + PATCH the dashboard middle | → |
ng dashboard scaffold |
print the canonical dashboard section skeleton | → |
ng dashboard validate |
strict check: required sections present? | → |
ng nexus-identity |
render + upsert the auto-generated identity block | → |
ng watcher-status |
heartbeat age + target + liveness | → |
ng decision-ack <w> <fp> |
durably ack a --- pending decisions --- row |
→ |
ng log-action <agent> |
append a JSONL event to the action log | → |
ng mint-jwt |
print an App-level JWT (for /app/* endpoints) |
→ |
ng lit search "<q>" |
content-relevance paper discovery (S2 + ASTA + OpenAlex) | → |
ng lit add <doi> |
fetch metadata + add a paper to the library | → |
ng lit status |
keys / library / setup readiness | → |
Comment + issue triage¶
ng process¶
Eyes-react on a user-authored comment and print its body. The unified "surface a comment for the agent" verb.
Usage
Behaviour
- Fetch comment metadata from
/repos/$REPO/issues/comments/<id>. - Verify the comment author matches
github.user_login— refuse otherwise (security boundary). - Verify the comment is not already marked processed (no
rocket, no non-selfeyes). - POST an
eyesreaction (bot identity). - Cache
<id>inmonitor/.state/processed-comments.txtso the watcher suppresses the comment during GitHub's propagation lag. - Print:
Exit codes — 0 on success; 1 if the comment doesn't exist,
isn't authored by the configured user, or is already marked processed.
Example
ng process-issue¶
Same as ng process but for an issue itself (not one
of its comments).
Usage
Behaviour — mirrors ng process, reading /repos/$REPO/issues/<n>
and acting on the issue's own reactions. Caches under issue:<n> in
the propagation-lag file.
ng react¶
React on a comment (no eligibility check; for explicit closing actions).
Usage
Behaviour — POST the reaction to
/repos/$REPO/issues/comments/<id>/reactions. Cache the comment-id
under the processed-comments file (both eyes and rocket count as
processed markers; the watcher elides them on subsequent polls).
Example
ng react-issue¶
Issue-level analogue of ng react.
Usage
ng show¶
Read-only comment fetch. Bypasses the
ng process eligibility filter so agents can quote
prior bot-authored output in their replies — the security boundary
is in the processing path, not the read path.
Usage
Output — the comment body, raw. --meta switches to a
key=value + heredoc block instead:
That updated_at is the compare-and-swap snapshot
ng comment-edit --expect-updated-at <ts> compares against, so the
safe read-modify-write loop is ng show <id> --meta → edit →
ng comment-edit <id> --expect-updated-at <ts> (exit 4 if the
comment changed underneath you).
Replies and closes¶
ng reply¶
Post a comment on an issue or PR.
Usage
Body comes from --body-file or stdin. Prints the new comment URL.
Examples
echo "done — see follow-up issue" | monitor/ng reply 7
monitor/ng reply 7 --body-file response.md
monitor/ng reply 42 --repo <your-org>/nexus-code --body-file response.md
ng close¶
Optionally post a comment and then close an issue.
Usage
Prints CLOSED on success.
Example
ng comment-edit¶
Compare-and-swap edit of an existing issue/PR comment. Prefer
ng reply (append) on a contended thread — editing is
the operation that can destroy somebody else's words.
Usage
ng comment-edit <comment-id> (--expect-updated-at <ts> | --force)
[--repo <owner>/<name>] [--body-file <path>]
One of --expect-updated-at / --force is required and they are
mutually exclusive: the verb is compare-and-swap by design and will
not guess. The safe loop:
monitor/ng show 4272693242 --meta # 1. read body + updated_at
$EDITOR new.md # 2. modify
monitor/ng comment-edit 4272693242 \
--body-file new.md --expect-updated-at 2026-09-08T17:41:02Z
Exit codes — 0 prints the comment URL; 4 the comment's
updated_at no longer matches what you passed, so a concurrent edit
would have been clobbered and nothing was written; 1 on the usual
failures. --force is deliberate last-writer-wins and warns loudly
on stderr before writing.
ng close-set¶
Close a set of issues, each with its own body, verifying every
one individually. Built for the end of a batch — a PR that closes
eleven issues — where a partial success reported as success is the
failure that matters (<your-org>/nexus-code#1444).
Usage
ng close-set --bodies <dir> [--pr <n>] [--merge <sha>]
[--repo <owner>/<name>] [--dry-run | --only-verify]
<issue>...
Behaviour, per issue N:
- Read
<dir>/<N>.md. No file ⇒ that issue is NOT-CLOSED and the run continues to the next. - Substitute
__PR__(from--pr) and__MERGE__(from--merge). Any surviving__TOKEN__refuses that issue — including a__PR__/__MERGE__you did not supply a value for. The guard matches the placeholder SHAPE (__[A-Z][A-Z_]*__), not any two-underscore substring, so ana__bin an evidence table does not trip it. An unfilled placeholder posted to a thread is worse than no comment. - POST the comment, then
assert-bot-author.shit. A comment whose author cannot be vouched for leaves the issue NOT-CLOSED and says so — repair by hand, do not re-post. - Close, then re-read the issue and require
state=closedfrom that direct read. The close PATCH's own status is deliberately discarded (|| true): the read is the property, and a PATCH that reports success proves nothing the read does not.
Output — one ISSUE <n>: CLOSED … / ISSUE <n>: NOT-CLOSED — <why>
line each, then both sets with their counts:
Exit codes — 0 iff the NOT-CLOSED set is empty; 1
otherwise. So the verb is safe to && against.
--dry-run runs steps 1–2 (so a missing body or an unfilled token is
still reported), prints ISSUE <n>: DRY-RUN — would post <bytes> and
close instead of writing, and returns 0 regardless.
--only-verify skips steps 1–4's writes entirely and does nothing
but the per-issue read — --bodies is not required with it.
Issues¶
ng issue¶
Dispatcher for the issue namespace. A bare numeric argument falls
through to ng issue view.
Usage
ng issue <n> # numeric → view
ng issue create … # see below
ng issue comment <n> … # see below
ng issue close <n> … # delegate to ng close
ng issue view <n> … # see below
ng issue view¶
One-line issue summary, with optional body and comment expansion.
Usage
Output
--with-body appends --- body ---\n<body>.
--with-comments appends a chronological dump of every comment with
its [created_at] <author>: header.
Example
ng issue create¶
Open a new issue under the bot account. Body required (file or stdin).
Usage
Prints the new issue's HTML URL.
Examples
monitor/ng issue create --title "tracking: follow-up" --body-file b.md
monitor/ng issue create --title bug --body-file b.md --label bug --label triage
ng issue comment¶
Alias of ng reply under the issue namespace, kept for
discoverability.
Usage
Pull requests¶
ng pr create¶
Open a PR. Body required (file or stdin). The branch must already be
pushed to origin; this is pure API and does not push for you.
Usage
ng pr create --head <branch> [--base main] --title <t>
[--body-file <path>] [--repo <owner>/<name>]
[--reviewer <login> | --no-reviewer]
Reviewer behaviour — bot-opened PRs auto-request review from
github.user_login. GitHub mutes notifications for actions taken by
the recipient's account, so without this the dashboard surface goes
cold for any repo missing CODEOWNERS. Opt-out with --no-reviewer;
override with --reviewer <other-login>.
Cross-repo PRs — when --repo targets a non-nexus repo, the verb
runs ng preflight first so a missing bot install
fails with a clear remediation instead of the GraphQL
"Resource not accessible by integration" error.
Prints the new PR's HTML URL.
Example
git push -u origin <your-handle>/<branch>
monitor/ng pr create \
--head <your-handle>/<branch> \
--title "docs: reference — config and ng CLI" \
--body-file pr-body.md \
--repo <your-org>/nexus-code
ng pr edit¶
Patch a PR title and/or body.
Usage
At least one of --title / --body-file is required. Prints the
PR's HTML URL.
ng pr merge¶
Merge a PR via REST. Prints the merge commit SHA.
Usage
ng pr merge <n> [--squash|--merge|--rebase] [--sha <verified-head>]
[--base-sha <verified-base>|--verify-base]
[--delete-branch] [--repo <owner>/<name>]
Default merge method is --squash. --delete-branch removes the
head ref after a successful merge (logs a warning if the branch is
already gone).
Pinning what you verified. A green describes a <head> merged into
a <base>, and both can move between the check and the merge.
--sha <verified-head>pins the HEAD you verified. GitHub rejects the merge (409) if the head moved (#628). Absent it, the freshly fetched head is pinned, which closes the fetch→PUT window but does not vouch for a head verified earlier.--base-sha <verified-base>pins the BASE that head's green was computed against.ng ci-attemptsprints that sha in full on itsMerge-ref base: VERIFIEDline for exactly this purpose.--verify-basedoes the same without a sha: it re-runs the merge-ref base check inside this verb, milliseconds before the PUT. Prefer it — measured on this repo, the window between a verdict and the merge has been as short as 4 seconds (#899), which is shorter than the round trip a caller needs to carry a sha.
Why either is needed: GitHub 409s a moved head, and 409s a base that
moved conflictingly. It accepts a base that advanced and still merges
cleanly — which is the whole hazard (#823), and measured at
5 of 17 merges on this board (#880). Both are opt-in: a base
check that fired on every merge would block the board every time the
base moved during a review, and a gate that always fires is a gate
somebody disables.
A missing flag value is refused, not defaulted — ng pr merge 42 --sha
exits 64 (EX_USAGE) rather than merging unpinned, and so does
--sha "": an empty value is the same user-visible condition as a
missing one (<your-org>/nexus-code#990).
ng pr view¶
One-line PR summary.
Usage
Output
--json switches to scriptable output instead: bare, the whole
/pulls/<n> object (jq -S .); with a comma-separated field list
(--json number,state,mergeable_state), only those keys, each
null when absent. Field names are validated against
^[A-Za-z0-9_.]+$ — anything else is refused rather than
interpolated into the jq filter.
Repo introspection¶
ng preflight¶
Check whether the bot's installation can write to a given repo.
Usage
Exit codes — 0 yes, 1 no (with a remediation line on stdout
naming the bot and the target).
Why — cross-repo PR/issue verbs would otherwise surface a confusing GraphQL "Resource not accessible by integration" error. This turns that into one explicit yes/no line front-loaded.
Example
monitor/ng preflight <your-org>/sibling-repo
# → bot installed: yes (<your-org>/sibling-repo)
# or: bot installed: NO — ask org admin to install <bot-name> on <your-org>/sibling-repo
ng stranded-branches¶
List remote branches that are not merged into the integration
branch — pushed work that no surface an orchestrator reads can see
(<your-org>/nexus-code#1399). Open issues, live windows, open PRs and
cross-reference events all miss a branch that was pushed and never
PR'd; this enumerates them directly from refs.
Usage
ng stranded-branches [--since <date>] [--base <ref>] [--remote <name>]
[--with-prs] [--repo <owner>/<name>]
Must run inside a git checkout. It fetches first — a stale
negative is the failure mode here — and warns on stderr if the fetch
failed rather than silently answering from an old tree. --base
defaults to the repo's integration branch resolved via
monitor/_integration_branch.sh, falling back to dev; --remote
defaults to origin; --since <date> drops branches whose last
commit is older.
Output — a base: <remote>/<branch> @ <short-sha> header, then
one tab-separated row per stranded branch, then a total:
<branch> <n> ahead <YYYY-MM-DD> <pr-col> <last commit subject>
stranded: 72 branch(es) ahead of origin/dev; 65 with NO open PR [fetched origin just now; …]
--with-prs adds the pr= column by asking GitHub (a read) which
branches have an open PR. A failed PR listing prints pr=?, never
pr=NONE — an empty list would mark every branch un-PR'd, which is
the silent-zero direction — and a warning goes to stderr.
Always exits 0 when it could look; 1 (via die) when the ref or
--since date is unusable, or when the cwd is not a checkout.
ng guards-for-diff¶
Before you push a change under monitor/: which construct-keyed
guards read the files you changed? A PR's green covers the suites
its diff touches, not the suites its change affects
(<your-org>/nexus-code#803). Facade over
monitor/guards-for-diff.sh; CLAUDE.md carries the canonical
invocation block.
Usage
It is a pass-through facade: every flag monitor/guards-for-diff.sh
accepts works here too (--base, --quiet, --changed-files,
--suites-from), and that script's header is the authority for the
flag set and the exit codes — the table below is a reading aid, not a
second source.
It asks each guard for its population via a --population protocol,
prints what it considered and excluded as well as what it
selected, and states how many suites declare nothing and are
therefore invisible to it. --run additionally runs the selected
guards and reports each verdict.
Exit codes — none of them is a clearance:
| Code | Meaning |
|---|---|
0 |
at least one registered guard reads a file in your diff (under --run, they all passed). Says some declaring guard matched — never that the guard that matters ran (#1078) |
1 |
--run: a selected guard FAILED |
2 |
REFUSED — a guard's population probe errored, or the diff could not be computed. Fail-closed on purpose |
3 |
no registered guard reads your diff. A measured answer, not a green light |
4 |
--run: no selected guard failed, but at least one did not verify anything: a green that is UNVERIFIED — a population guard enumerates git ls-files, so it cannot see an untracked file in your diff (#1054; git add and re-run) — or a guard that printed SKIP because it exited 77 and asserted nothing (#1860). A FAIL still exits 1 |
5 |
--run: the --timeout deadline expired with selected guards left without a verdict |
Read the tool's own per-run blind-spot count, not the exit code:
that line prints byte-identically at 0 and at 3. And git add
before you trust it — selection counts your whole working set
including untracked files, while a population guard enumerates
tracked files only, so a guard can be selected, run, and return a
confident green about a tree that does not contain your new file.
This is not a substitute for the full suite. Worked exit-code
detail: nexus.self-fix.
Asset upload¶
ng upload¶
Push a local file to the asset repo and print a browser-renderable
URL pinned to the resulting commit SHA. Thin wrapper around
monitor/upload-asset.sh.
Usage
ng upload <local-path>
[--issue N]
[--repo-path <path>]
[--shape pin|latest]
[--message <msg>]
[--asset-repo <owner>/<name>]
Exactly one positional (the local path). A second bare positional
is refused — a bare number used to bind to --repo-path and write
assets/<N> as a file where that issue's asset directory lives,
at exit 0 with a resolving URL (<your-org>/nexus-code#858).
URL shapes
- Images, PDFs, CSVs, …:
https://github.com/<asset-repo>/raw/<sha>/<path>— embed-friendly, redirects to a viewer-session-bound signed CDN URL sorenders inline even on private repos. - Markdown (
.md) / Jupyter (.ipynb):https://github.com/<asset-repo>/blob/<sha>/<path>— GitHub renders the content as a page.
--shape pin (default) pins to the commit SHA — permalinks survive
overwrites. --shape latest emits …/main/<path> for a "latest"
link.
Asset tree layout under the asset repo root:
| Path | When |
|---|---|
assets/<N>/<basename> |
--issue N is passed (e.g. assets/13/fig.png) |
assets/reports/<basename> |
uploading from reports/ |
assets/general/<basename> |
everything else |
<free-form> |
--repo-path <path> overrides the default placement |
A --repo-path without a leading assets/ gets one — every push
lands under assets/.
Destination repo — this verb pushes to the asset repo, and
that is the one place on the ng surface where --repo does not
mean the issue repo:
- The default is
github.asset_repo, falling back togithub.repo. Env override:NEXUS_ASSET_REPO. The branch ismain, and is not configurable. --asset-repo <owner>/<name>is the flag that says what it means. Use it when you genuinely mean a different asset repo.--repois the deprecated spelling of--asset-repo. It is accepted only when it restates the configured asset repo; any other value is refused (exit 7), because every otherngverb that takes--repomeans the ISSUE repo by it, and a worker who typed it that way pushed a report onto the implementation repo (<your-org>/nexus-code#1173).- Pushing to the repository this nexus checkout itself came from is
also refused (exit 7) unless
NEXUS_ALLOW_ORIGIN_ASSET_REPO=1.
Defaults
--shape—pin.--message—"Add asset <basename> via upload-asset.sh".
Exit codes — 0 URL on stdout; 1 bad usage; 2
mint-token.sh failed; 3 asset-repo clone/pull/push failed; 4
REFUSED, the nexus primary root could not be established or
<root>/assets is not a git repository rooted at itself
(<your-org>/nexus-code#1077 — distinct from 3 on purpose: 3 means
the asset repo was reached and the operation failed, 4 means the
script declined to act because it could not prove where it was
acting); 7 REFUSED, wrong destination repository (the two rules
above). Both 4 and 7 are decided before anything is staged and
before any git verb runs.
Example
monitor/ng upload diagram.png --issue 13
# → https://github.com/<your-org>/<your-instance>-nexus-assets/raw/<sha>/assets/13/diagram.png
Reports and wrap-up¶
These three verbs form the canonical hand-off flow. See Reports for the workflow narrative.
ng report-init¶
Create a frontmatter'd report skeleton at the canonical path. Captures session-id + tmux window automatically.
Usage
--repo is rejected with a structured error, not accepted and
ignored: reports/ is always written under $NEXUS_ROOT, including
from a worktree or a secondary clone.
Known divergence.
ng report-init --helpadvertises a[--skeptic-role]the verb does not parse: it takes theunknown flagarm and exits1. Deliberately absent from the synopsis above, which describes what the parser accepts.test-ng-usage-flag-coverage.shchecks parsed-flag → usage-line, not the reverse, so this direction is unguarded.
Path shape
Resolution order
<project>—--project, else thework/<project>parent if cwd is under one, else the literalnexus.<reports-dir>—--reports-dir, else$NEXUS_ROOT/reportsif the dir exists, else the nearestreports/walking up from cwd, elsepwd/reports.<session-id>—$CLAUDE_PROJECT_DIR/sessions/current_session_idif set; else the most recently modified jsonl under~/.claude/projects/<slugified-cwd>/; else the empty string (the caller substitutes"unknown", whichng report-checkthen rejects).<tmux window>—tmux display-message -t $TMUX_PANE '#{window_name}'if inside tmux; empty otherwise.
Output — absolute path of the new file on stdout.
Example
ng report-check¶
Validate a report against the schema enforced by
ng wrap-up.
Usage
Schema — defined in the nexus.report skill; this verb is the
machine check:
- Frontmatter delimited by
---lines, with required fieldsproject,date,session-id,status. session-idmust not be literal"unknown"(the capture heuristics failed; resume becomes impossible).status∈ {completed,partial,blocked}.- All five canonical sections present:
## Summary,## What Was Done,## Current State,## What Remains,## How to Resume. - Body length ≥
monitor.report_min_chars(default 500;MONITOR_REPORT_MIN_CHARSoverrides it for one run), counting everything after the frontmatter. - No literal
TODO,FIXME,<...>,_(fill in)_, or_(later)_placeholder text.--allow-todobypasses these last four checks for intentional in-progress checkpoints. - A frontmatter
disposition: no-further-passmust not contradict a skeptic gate for the report'swindow:(<your-org>/nexus-code#1363): it is refused when a skeptic-pending marker (monitor/.state/skeptic/pending/<window>) is live for that window, or the window record is spawn-stampedskeptic_mode: require.retire-preflight.shreads this field as its first gate andno-further-passis the permissive value, so the contradiction can only ever make retirement easier than the marker says it should be. Statesecond-pass, or settle the gate first. Skeptic-role windows are exempt (their verdict path clears their own marker); a report with nowindow:is not compared (ng wrap-uprepeats the check keyed on the invoking pane, and adds--skeptic-decision require, which is not on disk until the wrap-up records it).--skeptic-gate-settlingis passed by a--skeptic-waive/--skeptic-satisfiedwrap-up, whose run is the settlement rather than a contradiction; it is not an authoring override.
Exit codes
| Code | Meaning |
|---|---|
0 |
Report is complete |
1 |
Incomplete (specifics on stderr) |
2 |
File missing or unreadable |
Example
monitor/ng report-check reports/nexus_2026-05-11_142233_docs-w4.md
# → report-check: nexus_2026-05-11_142233_docs-w4.md OK (3247 body chars)
ng report-grep¶
Search the reports corpus. Use this, not grep -r reports/.
reports/.gitignore is a bare *, so an ignore-file-honouring
grep — which the operator's interactive grep is — matches
nothing across thousands of files and returns a confident,
wrong "no" (<your-org>/nexus-code#618). This verb searches with
command grep -r — the command builtin bypasses that shell
function — recurses (so it also sees reports/YYYY-MM/ archives),
and refuses a zero it cannot vouch for. The residual vector the
guard exists for is an ignore-file-aware grep executable ahead of
/bin/grep on PATH, which command grep would still honour.
Usage
Default search root is the resolved reports dir; explicit paths
override it. Options before the pattern are forwarded to grep
(-l, -n, -i, -c, -E, --include=…); -- terminates
options so a pattern may start with -. Grep options taking a
separate argument (-m 5) are not supported — use --opt=value
form, or command grep directly.
Exit codes — this is the whole point of the verb:
| Code | Meaning |
|---|---|
0 |
matches printed |
1 |
genuine no-match — and the corpus was proven visible |
3 |
the corpus is INVISIBLE to the search; the visibility guard fired and printed a diagnostic. Not a no-match |
| ≥2 (other) | the underlying grep's own error, passed through |
The 1/3 split is the contract: nothing else in this workspace
distinguishes "searched and found none" from "could not search". The
guard keys on a sentinel — a root holding at least one standard
ng report-init report is reliable; a root of only non-standard
files is a 3 ("don't know"), never a false zero.
The guard is one-directional: it converts an untrustworthy zero
inside this verb into a loud refusal. It does not neutralise a bare
grep -r reports/ typed elsewhere — that still returns a silent
zero.
ng wrap-up¶
End-of-task hand-off folded into one verb.
Usage
ng wrap-up <issue> <report-path>
[--trigger-comment <id>]
[--repo <owner>/<name>]
[--trigger-repo <owner>/<name>]
[--comment-body-file <path> | --no-comment]
[--allow-stub]
[--guards | --no-guards-preflight] [--strict-guards]
[--retain <reason> | --no-retain]
<skeptic flags — see below>
ng wrap-up --reply-to <request-id> <report-path>
[--issue <n>] [--answer-file <path>]
[--repo <owner>/<name>] [--allow-stub]
[--retain <reason> | --no-retain]
ng wrap-up --last [<window>]
ng wrap-up --explain [<window>]
Skeptic flags — producer side (the worker being reviewed):
[--skeptic-decision require|deny --skeptic-rationale <why>]
[--skeptic-waive <reason>]
[--skeptic-satisfied --skeptic-satisfied-by <report-path|sha256>]
[--skeptic-contradicted <what>]
[--skeptic-delta <what changed since a DECLINED prior request>]
[--skeptic-rearm <what changed>]
…and the skeptic's own wrap-up:
[--skeptic-role --skeptic-verdict credible|check|suspect|refuted
[--skeptic-target <window>] [--skeptic-depth <n>]
[--skeptic-findings <n>] [--skeptic-orig <window>]]
[--skeptic-subject <report-path|sha256>]
[--not-a-skeptic-verdict <why>]
--skeptic-subject is what you actually READ, and it is the one
field on this path that is your assertion rather than an inference:
--skeptic-target is a spawn-time stamp and goes stale the moment a
retained reviewer is re-tasked (<your-org>/nexus-code#963).
Channel delivery — --reply-to (ng request's inbox). Delivers
the answer over the request/reply channel instead of a GitHub issue:
no issue comment, no rocket, unless --issue <n> is also passed,
which does both. --answer-file <path> supplies the reply body.
Re-reading a run — --last / --explain (read-only)
Every ng wrap-up invocation persists its stdout, stderr (prefixed
[stderr]) and exit code under
monitor/.state/wrap-up-output/<window>/, and --last prints the most
recent record verbatim; --explain appends the window's pending-marker
state and its ng skeptic-evidence line. Both are read-only: no
upload, no comment, no skeptic step, no ledger row, no action-log event.
Use them to re-read output a pipe truncated. Do not re-run the verb
for that — every run re-arms the skeptic step (Step 0b), and a re-run whose
only purpose was to read the diagnostic appended discharge rows that bind
to nothing (<your-org>/nexus-code#1370). <window> defaults to the
invoking tmux window; off-tmux runs are recorded under _no-window.
A run that exits from inside a parse refusal leaves a record without an
rc: footer, and --last says so.
Steps
- Pre-flight
ng report-check— refuse stubs (--allow-stubforwards--allow-todoto the check for intentional in-progress checkpoints). 0b. Skeptic gate (skills/nexus.skeptic). Runs before the GitHub hand-off, so a refused wrap-up never announces "done" on the issue or rockets the trigger. The verb reads the worker's spawn-stamped skeptic mode (require/auto/deny) frommonitor/.state/windows/<window>.jsonand acts on it. The gate can refuse the whole wrap-up (return non-zero before any upload/comment/rocket/log) when: - an
auto-mode worker reaches wrap-up undecided andmonitor.skeptic.enforce_auto_decisionis on — record a choice with--skeptic-decision require|deny --skeptic-rationale "<why>"; - a
require-mode worker tries--skeptic-decision deny(the requirement is mandatory; only an operator may release it); --skeptic-waiveis attempted from inside a worker context (NEXUS_WORKER_WINDOWset) — the waive is an operator override only;--skeptic-roleis passed without a valid--skeptic-verdict.
On require (or an auto-mode --skeptic-decision require), the gate
sets a skeptic-pending marker and prints the await-loop guidance; the
window then cannot retire until a skeptic returns a verdict
(retire-preflight.sh enforces the marker). --skeptic-role marks a
skeptic's own wrap-up: it logs the --skeptic-verdict and applies
bounded recursion (--skeptic-depth / --skeptic-findings, chain
root via --skeptic-orig). --skeptic-waive is the operator override
that releases a required skeptic.
--skeptic-findings is optional, and its absence is recorded AS
absence (<your-org>/nexus-code#881). Omitted, it prints
new findings : not stated, logs findings-stated=false with no
findings key, and never reaches the threshold comparison — where it
used to be defaulted to 0, making "measured zero new issues" and
"never said" the same record. There is no sentinel number. With
neither a count nor a readable frontmatter disposition: in the
report, wrap-up refuses (<your-org>/nexus-code#1095, superseding
#881's escalation) rather than terminating on a value nobody
supplied. Stating either one — --skeptic-findings 0 or
disposition: no-further-pass — lifts the refusal. Whether the chain
then terminates depends on the verdict, not on the statement: at
credible / check either statement ends it; at suspect /
refuted neither does — the verdict sets the severity signal on
its own, which #678 makes non-overridable by a disposition, so a
second pass is recommended regardless, and the refusal text says so
rather than promising a termination it cannot deliver
(<your-org>/nexus-code#914).
--not-a-skeptic-verdict "<why>" is for a window whose provenance
carries skeptic_role: true but whose current wrap-up is ordinary
worker work (<your-org>/nexus-code#879). Windows are stamped for their
lifetime, not per task, so a retained skeptic that later authors a
patch would otherwise have to supply a --skeptic-verdict on its own
diff — a self-review indistinguishable downstream from an independent
clearance. The flag takes the ordinary producer path and records
skeptic-role-not-asserted with the reason (mandatory, >= 20 chars).
It clears no skeptic marker — including the opting-out window's
own: an opt-out is a statement about this hand-off, not a release
of a pending obligation (#879 F1; the three producer branches that
would otherwise clear it are guarded, and say so loudly). It is
mutually exclusive with --skeptic-role and --skeptic-verdict, and
refused outright on a window that was never stamped.
0c. Async-wait pre-flight (<your-org>/nexus-code#1181). Reads
monitor/.state/orphan-async-state.tsv for the invoking window. A wait
with a parsed handle (asyncrun:ar-…, slurm:<jobid>) refuses
the wrap-up, naming it — it is answerable (async-run.sh --status-line,
sacct -j) and the agent still has the context to settle it with
monitor/declare-no-wait.sh <kind> <id> once it is confirmed dead. A
syn- wait (the launcher retained no handle; possibly a phantom) only
warns. A token the pre-flight cannot parse refuses. Nothing is ever
auto-cleared. Runs before Step 0b, so a refusal arms nothing.
0d. Disposition-vs-gate check (<your-org>/nexus-code#1363), the
report-check rule above repeated for the invoking window plus this
invocation's --skeptic-decision require. Skipped when the wrap-up
itself settles the gate (--skeptic-waive, --skeptic-satisfied).
0f. Guards-for-diff pre-flight (<your-org>/nexus-code#1459). Runs
ng guards-for-diff's selector and names every
selected guard the report never mentions. Advisory by default;
--strict-guards turns an unreported selection into a refusal
(return 1). It is expensive (~80 s measured), so the automatic arm
fires only for a wrap-up that plainly describes nexus-code work —
cwd inside a repository carrying monitor/guards-for-diff.sh
and the report under the primary's reports corpus. --guards
forces it; --no-guards-preflight or NG_WRAPUP_GUARDS_PREFLIGHT=0
skips it. Every non-selector outcome (selector exit 2/3, the
NG_WRAPUP_GUARDS_TIMEOUT expiry, a missing binary) prints
UNMEASURED / not a clearance and returns 0 — it never converts
"could not look" into "looked and it was fine".
1. Upload the report via monitor/upload-asset.sh →
assets/<issue>/<basename> on the asset repo.
2. Post the link comment on <issue> in --repo:
- --comment-body-file <path> supplies the prose. {{REPORT_URL}}
is substituted with the SHA-pinned asset URL; if no token is
present, a Full report: <URL> footer is appended.
- --no-comment skips this step (caller will ng reply later).
- Default: a templated body built from the report's H1 + Summary.
- Post-once: a re-run for the same issue + report reads the earlier
wrap-up comment. Identical content (ignoring only the asset-link
move) posts nothing; different content POSTs a new comment
ending Supersedes the earlier wrap-up comment: <url>. An earlier
comment is never edited or re-pointed — its SHA-pinned link
already cites the report as it was (<your-org>/nexus-code#1637).
3. Rocket-react the trigger comment on --trigger-repo (defaults
to --repo) if --trigger-comment <id> is supplied.
4. Append a wrap-up event to monitor/.state/action-log.jsonl
with per-step status, asset URL, comment URL, repo, trigger-repo,
and source tmux window.
5. Auto-retain the source window — ON BY DEFAULT. A successful
wrap-up logs a window-retain event for the source tmux window so
the watcher's idle-worker probe suppresses the wrapped row into a
footer for monitor.retain_ttl_seconds (default 24 h). The auto-tag
is wrap-up-<YYYY-MM-DD>. Pass --retain <reason> for a custom tag,
or --no-retain to close-immediately (a short-lived helper that is
truly done, or a context-pressure wrap-up that won't be resumed).
--retain and --no-retain are mutually exclusive. Step 5 is
silently skipped when wrap-up runs outside the worker's tmux pane
(no source window to retain), and a retain-logging failure does
not flip the exit code — the hand-off already succeeded.
Exit codes — three, and the third is the one callers get wrong:
| Code | Meaning |
|---|---|
0 |
published — every attempted step succeeded |
1 |
a step FAILED. Retry it |
3 |
nothing failed and NOTHING WAS PUBLISHED |
On a 1, the verb still attempts every reachable step (rocket and
log-action do not depend on the upload), then emits a structured
stderr report naming which steps ok/failed so the caller can retry
just the failed ones.
A 3 is not a retryable failure: no step failed, so re-running
the command unchanged reproduces it exactly. Either the composed
teaser carried no claim (<your-org>/nexus-code#1114), or your report
changed outside its ## Summary section while the composed comment
body did not (#862) — the comment is built from the first ~200
characters of ## Summary alone, so a correction anywhere else
leaves it byte-identical and there is nothing to publish. stderr
names which and lists the ways out. A re-run is not free: the
upload runs again and so does the skeptic step (0b), which arms —
"no step failed" is not "no step had an effect"
(<your-org>/nexus-code#1230). To re-read the diagnostic, use
ng wrap-up --last, which replays it read-only.
Cross-repo trigger — pass --trigger-repo when the trigger
comment lives in a different repo from the issue thread (e.g. a
worker on a nexus-code clone wrapping up against
<your-instance>-nexus-assets#1 with the trigger comment on
nexus-code#4).
Stdout — one machine-readable line per step:
uploaded: <asset-url>
posted comment: <comment-url>
rocketed comment <id>
logged action: wrap-up issue=<n> report=<basename>
retained window: <window> (reason=<tag>; ttl=monitor.retain_ttl_seconds)
The retained window: line prints only when step 5 ran (in-pane,
--no-retain not passed). When the source window carries a live
operator-engagement mark, a trailing note explains that the wrap-up
does not release the window — see ng engaged-done
to signal the finished state.
Example
monitor/ng wrap-up 1 reports/nexus_2026-05-11_142233_docs-w4.md \
--trigger-comment 4422219722 \
--repo <your-org>/<your-instance>-nexus-assets \
--trigger-repo <your-org>/nexus-code \
--comment-body-file wrap-up-comment.md
Worker lifecycle¶
Window-lifecycle verbs the orchestrator uses to validate, recover, and
classify worker windows, plus the worker↔skeptic comms channel. See
skills/nexus.window-cleanup, skills/nexus.skeptic,
and skills/nexus.tmux-spawn for the policies these
implement.
ng wrap-up-check¶
Verify a worker has met every wrap-up obligation before the
orchestrator closes its window. Independent of ng wrap-up's
own Step 0 pre-flight (which fires before the worker has uploaded
anything); this checks the post-conditions.
Usage
Checks
- The action log carries a
wrap-upevent for<window>in the current lifecycle (itstsis newer than the window's most-recentspawnevent). - The cited report exists on disk (under
$NEXUS_ROOT/reports/orreports/) and passesng report-check. - The trigger comment was rocketed, or the rocket step was legitimately
skipped at wrap-up time — both normalise to
rocket=ok. Afailedrocket becomesmissing; a rocket withheld because nothing was published becomeswithheld; any other recorded value becomesunknown. - The link comment was actually published — a wrap-up that exited
3(degenerate-teaser/nothing-published) reportscomment=unpublishedand is not ok.
Output — five fields, on one line:
status=<ok|incomplete> wrap_up=<present|missing> report_check=<ok|fail|missing> rocket=<ok|missing|withheld|unknown> comment=<ok|unpublished|failed>
status=ok requires all four to read positively — wrap_up=present
and report_check=ok and rocket=ok and comment=ok.
Each is compared for equality against its ok-value, not against a
denylist of bad ones, so withheld and unknown — every state
nobody thought of — read as incomplete rather than slipping through
a permissive default (<your-org>/nexus-code#1116 F1).
Exit codes
| Code | Meaning |
|---|---|
0 |
status=ok — safe to close |
1 |
status=incomplete — at least one obligation missing; do NOT close yet |
ng skeptic¶
Thin delegator to monitor/skeptic-channel.sh, the worker↔skeptic
comms channel and nudge. Gives the skeptic protocol a discoverable home
on the ng surface; every argument is forwarded verbatim, and the exit
codes are the channel's own.
Usage
ng skeptic <init|dir|reqfile|ask|poll|status|list|await|answer
|await-answer|reconcile|close|resolve|reset|defer
|nudge|notify-delta> ...
Subcommands
| Sub | Who runs it | What it does |
|---|---|---|
ask <task> <slug> … |
skeptic | write a request (<slug>.open.md) |
await <task> |
worker | block for *.open.md, ack each (→.ack.md), exit 0; DONE sentinel → exit 10; counterpart finished without closing → 11; displaced by a newer await → 12; deferred → 13; timeout → 4 |
answer <task> <req> … |
worker | reply to a request (→.answered.md) |
await-answer <task> <req> … |
skeptic | block for the worker's answer |
reconcile <task> … |
skeptic | ensure every open request was acked |
close <task> |
skeptic | drop the DONE sentinel that ends the worker's await loop |
resolve <task> --reason … |
orchestrator only | clear a skeptic-pending marker a returned verdict failed to clear, with a mandatory rationale + audit event — the sanctioned replacement for a hand-rm (#577). --disposition additionally releases retire-preflight check 1c and is the one form that succeeds with no marker present (#813) |
defer <task> --reason … |
skeptic/orchestrator | release the worker's current await (exit 13) while the requirement stands — the pending marker is untouched and retire-preflight keeps gating (#845 H) |
reset <task> |
skeptic | open a new round: archive the prior DONE + .answered.md under skeptic/.archive/<task>.reset-<ts>/ |
poll \| status \| list <task> |
either | inspect channel state (open/ack/answered) |
nudge <window> … |
orchestrator/skeptic | wake an idle worker |
notify-delta <skeptic-window> --target <w> … |
target side | the reverse of nudge: wake a pinned skeptic that a new round exists. Its absence is why a parked target and its idle skeptic could each wait on the other for hours (#845) |
init \| dir <task> |
either | create / print the channel directory |
reqfile <task> <req> |
either | resolve a req id/stem/filename → path |
The rename is the signal at every step. Run ng skeptic --help for the
full surface and exit-code contract.
Skeptic bookkeeping verbs¶
Five top-level verbs — not ng skeptic sub-verbs — read and write
the pending-marker bookkeeping that gates retirement. They are
read-only except skeptic-arm. Each exit code is stated because the
whole family exists to keep "nothing outstanding" distinguishable
from "could not look":
| Verb | What it answers | Exit codes |
|---|---|---|
skeptic-arm <window> --report <path> [--issue N] [--state-dir D] |
records what an arm is about, for the writers of armed state that live outside ng (spawn-worker.sh --skeptic-role) |
0 an armed row was appended · 2 usage · 3 this artefact is already outstanding, so nothing was appended (a second arm for one artefact is ambiguous-N-arms-outstanding, #1156 pole B) · 4 could not name a subject — a marker may still exist |
skeptic-obligations <window> [--state-dir D] |
what this window owes, by task — one line per artefact still outstanding, naming its issue and report | 0 nothing outstanding · 1 one or more · 2 usage · 3 no marker store. The pending marker cannot answer this: its content is 1, so presence cannot say which obligation (#961) |
skeptic-evidence <window> [--state-dir D] |
does a verdict exist, and could the bookkeeping match it — separates "no verdict exists" from "a verdict was delivered and could not be matched to an arm", opposite instructions printed identically until #1156 |
0 looked · 3 could not look |
skeptic-orphans [--state-dir D] |
every pending marker, classified by whether its window is present in tmux — the only discriminator separating a live pairing from an orphan (age does not, measured). Report only; reaps nothing | 0 looked · 3 no marker store |
skeptic-disposition <window> [--reports-dir D] |
the window's disposition: as read from its reports, emitted key=value (state= source= report= report_mtime= blocking= reports= detail=) |
0 read · 2 could not determine (state=unknown) |
Per orphan the remedy is ng skeptic-evidence <window>, then
ng skeptic resolve <task> --reason "<why>". While a reviewer's obligation
edge is live, add --reviewer <w> (or --all-reviewers), or the resolve is
refused with rc 9 (<your-org>/nexus-code#1848).
Known divergence at
a3177ef6.ng skeptic-arm --helpandng skeptic-obligations --helpdieunknown flag— neither function calls_help_check, though both have a_usage_forarm.ng skeptic-disposition --helphas the inverse defect: it calls_help_check, has no_usage_forarm, and so falls back to printingmonitor/ng's whole file header. Use the table above until that is repaired.
ng respawn¶
Resume a wrapped or closed worker's Claude Code session in a fresh
tmux window with full spawn parity (env exports, worker-settings hooks,
window options, lifecycle anchors). Thin wrapper over
monitor/spawn-worker.sh --resume.
Usage
ng respawn <window | session-id>
[--window <name>]
[--workdir <path>]
[--replace]
[--nudge | --no-nudge]
[--dry-run]
Pass a tmux window name (the common case) or a session-id UUID. The
session-id and workdir auto-resolve from the report frontmatter, the
action log, or ~/.claude/projects/; pass a UUID positionally with
--window to override resolution. --replace reuses an existing
window of the same name; --dry-run prints the resolved spawn command
without launching. A worker whose last heartbeat was mid-turn (busy)
gets a continuation prompt automatically — claude --resume alone does
not restart an interrupted turn; --nudge / --no-nudge force or
suppress that.
Why not hand-roll it — never tmux new-window … claude --resume
directly: that loses the NEXUS_ROOT / NEXUS_WORKER_WINDOW exports
and every worker hook breaks.
ng spawn-decision¶
Advisory continue-vs-spawn classifier for a retained worker window.
Inspects the window's pane state, its most-recent spawn and
window-retain action-log timestamps, and the retain TTL, then emits a
recommendation. Mirrors the policy in
skills/nexus.window-cleanup (Continue-vs-spawn). Always
advisory — the orchestrator decides what to do with the advice.
Usage
--topic names the topic of the prospective new task; when omitted the
helper assumes topic-match (the caller already decided the routing
applies to this window).
Output
decision=<continue|spawn|ambiguous> reason=<short> pane_state=<state> retain_age_s=<n|none> spawn_age_s=<n|none>
Exit codes
| Code | Meaning |
|---|---|
0 |
a decision was rendered (any class) — including decision=spawn reason=window-absent, which is a genuine verdict about a missing window, not a failure |
1 |
the classifier could not run: reason=no-tmux or reason=enumeration-failed |
The no-tmux line is the one short row — it carries decision= and
reason= only. Parse defensively, or key on the exit code first.
ng engaged-done¶
The interactive session's explicit FINISHED-signal. A wrap-up from an
operator-engaged window keeps the window engaged by default (follow-up
inquiries are expected), so it does not follow the typical
wrapped-window cleanup. This verb appends the engaged-done event that
invalidates the engagement mark, dropping the window back onto the
normal retain-footer → retire-eligibility path. A later operator prompt
re-engages — the release is never a lock-out.
Usage
Window resolution mirrors wrap-up's: the calling pane's tmux window via
$TMUX_PANE; pass --window when invoking from outside the pane.
ng interactive-sessions¶
Enumerate recently paused or closed interactive windows from their
provenance records (monitor/.state/windows/<window>.json, written by
spawn-worker.sh --kind interactive) and render a markdown "Resumable
Interactive Sessions" table with per-session resume commands. The block
is bounded by HTML-comment delimiters and idempotently upserted into
the overview issue body on --upsert-overview (bot identity, REST
PATCH), so repeated runs replace it in place.
Usage
ng interactive-sessions [--limit N] [--days D] [--upsert-overview]
[--dry-run] [--repo <owner>/<name>]
--limit N— include at most N sessions (default 20).--days D— only sessions active within the last D days (default 30). Both must be positive integers; anything else is refused.--upsert-overview— PATCH the rendered block into the overview issue.--dry-run— print the block; skip the GitHub PATCH.--repo <owner>/<name>— which repo holds the overview issue (write-verb resolution; only consulted for the PATCH).
Exit codes — 0 on success (rendered, and patched when
--upsert-overview without --dry-run); 1 if a write failed.
User-attachments fetch¶
ng fetch-asset¶
Download a github.com/user-attachments/... URL via the user's
PAT (the bot's installation token returns 404 silently on this
surface; user-OAuth credentials are required). Both URL forms are
accepted: user-attachments/assets/<uuid> (pasted images) and
user-attachments/files/<id>/<name> (uploaded file attachments).
Usage
Why it exists — when the user pastes an image into a GitHub
comment, the link is https://github.com/user-attachments/assets/<uuid>.
Pointing a sub-agent or the Read tool at that URL produces a 404
followed by a 400 from the image fetcher, which silently disables
every subsequent image fetch in the conversation. This verb is the
workaround: read the user's gh auth token PAT (NOT the bot's
installation token — bot tokens 404 silently), follow the 302 from
github.com/user-attachments/... to the 5-minute signed S3 URL, and
write the bytes to disk. The agent then reads the local file.
Defaults
--out—monitor/.state/assets/<asset-id>.<ext>(extension inferred from responseContent-Type).--image-only— refuses non-image content types (exit 4).
Output — three lines on stdout:
Exit codes
| Code | Meaning |
|---|---|
0 |
Success |
1 |
User input error (not a user-attachments URL, missing arg, unknown flag) |
2 |
Auth failure (gh auth token empty, or PAT rejected at stage 1) |
3 |
Fetch failure (stage 1 missing Location, stage 2 non-200, network) |
4 |
--image-only set and content-type is not image/* |
Example
monitor/ng fetch-asset \
https://github.com/user-attachments/assets/01234567-89ab-cdef-0123-456789abcdef \
--image-only
# → path=/.../monitor/.state/assets/01234567-89ab-cdef-0123-456789abcdef.png
# content_type=image/png
# bytes=84219
Dashboard¶
ng dashboard get¶
Fetch the overview-issue dashboard middle. Caches to
monitor/.state/dashboard.md.
Usage
Output — the content between the <!-- NEXUS_DASHBOARD_START -->
and <!-- NEXUS_DASHBOARD_END --> markers in the overview issue's
body.
Overview-issue resolution — three layers of robustness against the GitHub labelled-issue index's eventual consistency:
github.overview_issue_numberinconfig/nexus.ymlif set — skip the lookup entirely.- Per-process cache at
monitor/.state/overview-number. - Live API call with 3 attempts (1 s + 2 s backoff) when the label
index returns
[]transiently.
ng dashboard put¶
Splice a new middle into the overview issue's body and PATCH.
Usage
Behaviour
- Read the new middle from
--body-file(or stdin;-asks for stdin explicitly). Refuse if empty — the diagnostic names the INPUT, not the dashboard. - Refuse if the supplied body contains a dashboard marker.
puttakes the INNER region;gh issue edit --body-filetakes the whole body. Accepting the wrong one nests the markers and every later put duplicates the content (#959). - Fetch the overview body; refuse unless it carries exactly one START
and exactly one END marker. Presence is not uniqueness (
#1058,#1118): a body with two ENDs used to pass this check and then no-op. - Replace the middle between the markers (header + footer preserved).
- Refuse before sending if the merged body exceeds GitHub's 262,144-byte issue-body cap; warn from 90%.
- PATCH, then verify the write by reading the body back with an
INDEPENDENT GET. A 200 is not evidence of a write — and neither is the
PATCH response, which echoes the body you SENT even when the store keeps
the old one (measured on scratch issue
#1128). The response is compared too, but that only catches corruption on our side of the wire. - Only once verified: cache the new middle to
monitor/.state/dashboard.mdand writemonitor/.state/dashboard-updated.ts(the freshness timestamp the watcher reads).
Prints the overview issue's HTML URL, and only when the write is verified.
Exit codes
| rc | meaning |
|---|---|
0 |
verified applied (or #1010 UNCHANGED — nothing to do) |
1 |
refused before sending, PATCH failed, or verified NOT applied |
4 |
the PATCH may have applied but could not be verified — a refusal to claim, not a claim of failure |
On any non-zero rc no cache is written and no freshness stamp is advanced.
put runs the section-schema check in warn-only mode: if the body
is missing any required section (see
ng dashboard validate) it prints the gaps
to stderr but still PATCHes — the operator must never be blocked from
updating the dashboard in a hurry.
ng dashboard scaffold¶
Print the canonical dashboard skeleton: the six required H2 sections,
each with a one-line hint. Seed a new dashboard by piping into
dashboard put.
Usage
The sections — the single source of truth is DASH_REQUIRED_SECTIONS
in monitor/ng:
| Section | Populated by |
|---|---|
## Identity |
pointer to the ng nexus-identity block (never a copy) |
## Infra |
operator-narrated: watcher / orchestrator / CC-pin health |
## Services |
auto-populatable from monitor/services.registry |
## In-flight |
active worker windows |
## Awaiting operator |
threads/decisions blocked on the operator |
## Recent landings |
last few merged PRs / completed tasks |
ng dashboard validate¶
Strict schema gate: exit 0 if every required section heading is present
exactly once, exit 1 otherwise (missing and duplicated sections listed
on stderr). The hard-failure counterpart to put's warn-only check — use it
for CI, pre-commit, or a deliberate conformance check.
Usage
ng dashboard validate # the LIVE dashboard (default)
ng dashboard validate --body-file <path> # a candidate, before pushing
ng dashboard validate --body-file - # explicitly stdin
With no --body-file it validates the LIVE dashboard, which is what the
verb name implies. It used to read stdin — empty in every non-interactive
shell — and report that emptiness as empty dashboard body, a verdict about
the dashboard derived from the caller's empty pipe (#958). A healthy 15 kB
dashboard failed that way, and the message's obvious consequent action
(regenerate and push) would have overwritten it.
What it checks
- every required section present — searched in the whole issue body, not just
the region, so a
## Identitypointer whose generated block sits above the START marker is not reported missing; - each required section appears once — a duplicate is always a defect on a routing surface;
- exactly one START and one END marker, when given a whole body;
- near-miss headings (
## 🛑 Infra — …) are named rather than silently counted as missing; - duplicate names that differ after their separator are reported as a NOTE
and the verdict stops calling itself an unqualified OK (rc stays
0); - body size and section counts accompany every verdict.
Given only a region it says so, rather than asserting a section is absent when it cannot see the rest of the body.
Identity¶
ng nexus-identity¶
Render the auto-generated Nexus identity block — the working
directory front and centre — and idempotently upsert it into the
overview issue body between <!-- nexus-identity:start --> /
<!-- nexus-identity:end -->. The point is auto-generation: every
field is DERIVED from this nexus's own environment/config, so a second
operator running the same code gets their own correct block with zero
edits, and repeated runs replace the block in place (no drift, no
duplication) — the same upsert pattern as the dashboard markers.
Usage
ng nexus-identity --dry-run # render to stdout, no GitHub write
ng nexus-identity --upsert-overview # PATCH the block into the overview issue
ng nexus-identity --upsert-overview --repo <owner/name>
Derived fields (nothing hardcodes one operator):
| Field | Source |
|---|---|
| Working directory (headline) | $NEXUS_ROOT, else script-relative root |
| Host | hostname |
| Asset + issue repo | github.repo + overview issue number |
| Implementation clone | the nexus root's git origin remote + current branch |
| Watcher pidfile / log | <root>/monitor/.state/watcher.{pid,log} |
It is identity, not status — a stable "where things live" record.
Live status is the dashboard's job. See the
nexus.dashboard skill for the cross-nexus convention.
--dry-run renders without the PATCH (use it to preview the block).
The GitHub write uses the bot identity (installation token, REST
PATCH).
Watcher + audit¶
ng watcher-status¶
One-shot watcher liveness report.
Usage
--scheduler appends a per-task v2 scheduler summary read from
monitor/.state/watcher-scheduler.jsonl — one line per task
(compose_emit, snapshot_local, deliveries_poll, github_poll, …)
with its most recent phase, rc, elapsed_ms, and the ISO ts of
that fire. The cron-supervisor passes it to verify v2-migration
completeness; a missing JSONL prints scheduler: telemetry absent.
Output — a key=value block (human-friendly, also easy to grep):
heartbeat: <ISO ts> (age=<s>s) # or "missing"
pid: <pid> (alive|DEAD|unknown)
target: <window-name>
lock: pid=<pid> started=<ts> # or "absent"
instance-lock: held|free|absent # state-dir-scoped flock singleton
hosting: headless # or "legacy tmux window 'watcher' present (headless expected)"
archived_diffs: <n>
instance-lock: reports the state-dir-scoped flock singleton — the
cross-sandbox / cross-host guard the pid lock: can't provide. held
appends an assessment + host/pid/started detail (a stale lock whose
recorded host rebooted is flagged); free means stale metadata is
present and will be reclaimed on next start; absent means no lock
file. Full inspection: monitor/watcher/launcher.sh --instance-status.
Exit codes (from _watcher_alive in monitor/watcher/_lib.sh,
shared with the bootstrap script so the two paths can't drift —
the watcher is headless, so the buckets are pid identity +
heartbeat age, no window check):
| Code | Meaning |
|---|---|
0 |
Fresh: heartbeat age ≤ 2× + 15 s of monitor.interval_seconds, pid identity-validated alive |
1 |
Stale: heartbeat age within (2× + 15 s, 5×] of monitor.interval_seconds |
2 |
Very stale (> 5×), or heartbeat pid dead / recycled to a non-watcher process |
3 |
No heartbeat file |
4 |
Wedged: pid alive and the heartbeat is fresh, but the progress/cycle signal has stalled — the liveness ticker is beating for a loop that no longer moves (<your-org>/nexus-code#491). A distinct bucket precisely so a caller cannot conflate it with DEAD or with merely-slow |
The DEAD threshold (5×) can be raised by a third argument to
_watcher_alive — the supervise-tick passes a cutoff above the
watcher's own async hang-watchdog floor so it does not race a
self-recovery in progress. It is never lowered. ng watcher-status
itself passes no override.
A pid that fails the identity check is not immediately 2: the
check reads /proc/<pid>/cmdline and is therefore pid-namespace
local, so a live watcher across a sandbox boundary would false-read
as dead. Before declaring 2, _watcher_alive consults the
state-dir flock; a held instance lock falls through to the
heartbeat-age buckets instead.
ng decision-ack¶
Durably ack a --- pending decisions --- row (#790).
Usage
ng decision-ack <window> <fingerprint> [--reason <text>]
ng decision-ack <path-to-decision-json> [--reason <text>]
ng decision-ack <window> --all [--reason <text>]
The emit row cites the exact invocation on its ack= line, and the
file= path is accepted verbatim so nothing has to be re-split by hand
(a mistyped fingerprint tombstones nothing while reporting success).
Why not just rm the file. That was the documented ack for a year
and it is a no-op. The fingerprint is sha1(window | kind | message),
and idle_prompt's message is the constant Claude is waiting for your
input — so for that kind the fingerprint is a pure function of the
window. Remove the file, let the worker sit idle another minute, and the
hook writes back a byte-identical name. Measured on a live nexus:
civerdict.bb0f332f628a.json removed at 15:41, present again at
16:09:32 with the same fingerprint; guardgap.672915acaad8 acked three
times over one session. The reader could not distinguish "fired again
because something changed" from "fired again because I deleted a file".
This verb performs the durable move instead — <w>.<fp>.json →
<w>.<fp>.handled.json — which both the hook's write path and the
watcher's reader have always honoured as terminal. It then verifies the
property (tombstone present and live file gone) rather than trusting
mv's exit status, and logs a decision-ack action-log event.
Scope of the suppression. Terminal for that fingerprint until the
window is retired (ng retire-window prunes decisions/<w>.*) or reaped
(the watcher drops decisions for windows absent from tmux past
MONITOR_PENDING_REAP_MIN_AGE_SECONDS, default 900 s). For
idle_prompt that means this window's idle pings stop being decision
rows — which is correct: --- idle workers --- is the surface that
tracks idle workers, it cannot be tombstoned, and two channels nagging
about the same fact is what made this one skimmable. A
permission_prompt embeds the tool and its arguments in the message, so
a different prompt is a different fingerprint and surfaces normally;
only the identical prompt is muted.
Exit codes: 0 acked (or already acked — idempotent); non-zero on a
malformed fingerprint, a missing decision, or a tombstone that could not
be written.
ng log-action¶
Append one line to monitor/.state/action-log.jsonl. Used by
ng wrap-up and by agents recording orchestration
events.
Usage
Line shape — JSONL with ts, agent, event, optional note,
plus any --extra k=v pairs folded in as additional string fields.
Values are preserved as strings; pass quoted JSON in --extra when
you need structure.
Example
monitor/ng log-action orchestrator \
--event window-closed \
--note "wrapped + idle > 20m" \
--extra window=docs-w4-config-cli \
--extra reason=window-cleanup
ng service-incident¶
Assemble a structured service incident report for a registered service
from the recorded state — the per-service files the watcher's
service-health task writes under monitor/.state/service-health/
(<svc>.state current-incident record + <svc>.events append-only
history), plus the service logfile. Machine facts (down→restored
timestamps, the failing healthcheck, each restart attempt + outcome,
the verbatim event timeline, a 40-line logfile tail) are filled from
the state so they can't drift from what actually happened; the
root-cause / how-to-undo sections are templated placeholders the
dispatched fix-worker completes. See
skills/nexus.service-recovery.
Usage
--state-dir overrides the default monitor/.state/service-health/
lookup directory. The verb dies if neither <svc>.state nor
<svc>.events exists there (the name must be a registry service that
has had an incident).
Output — markdown to stdout with the required sections (Failure
report / Immediate response / Root-cause fix / References / Worker
report). Pipe it to ng issue create:
monitor/ng service-incident jupyter \
| monitor/ng issue create --title "incident: jupyter down" --body-file -
ng suppress-emit¶
Operator-side manual emit-suppression — the backup lever for when the
reactions/dedup filters fail to exclude a comment that re-surfaces
every poll cycle. Appends a comment:<id> line to
monitor/.state/emit-suppression.lines; compose_emit's
_filter_suppression stage drops any matching id=<N> line from the
eligible-comments stream on the next compose tick.
Usage
The comment-id must be numeric. The file is append-only and persists
across watcher restarts (the operator clears entries by editing or
truncating it directly). A monitor action-log emit-suppress event
is recorded so the meta-review can see when and why the backup channel
was used; --reason is folded in as the event note, --repo as an
extra field. Prints suppressed: comment:<id> (or already suppressed:
… if the entry was present).
ng mint-jwt¶
Print an App-level JWT (no installation-token exchange). Thin shim
over monitor/mint-token.sh --jwt-only.
Usage
Why — /app/* endpoints (most importantly
/app/hook/deliveries, the App's webhook delivery log) authenticate
with the App-level JWT, not an installation token. Most agents will
never call this directly; the watcher's deliveries path uses it
internally.
Literature research¶
ng lit¶
Content-relevance literature discovery for scientific work, native to the
nexus (plain curl + jq; no bip install required). Backed by
monitor/lit.sh. Full reference, key acquisition, and the report-citation
convention: Literature research and the
nexus.lit skill.
$ ng lit status # readiness
$ ng lit search "<query>" [--source s2|asta|openalex|both|all] [--limit N] [--year A:B] [--human]
$ ng lit add <DOI|S2-id|openalex:Wid> [--human] # grow the library
$ ng lit setup # key-acquisition refs
searchqueries Semantic Scholar, ASTA, and OpenAlex by relevance (default--source all— the three are complementary, not redundant), dedups against the reference library, and annotates each hitin_library. A keyed backend (S2/ASTA) with no key is skipped with a note (never a hang); OpenAlex needs no key so it's never skipped for that reason. Default output is JSON;--humanis readable. Every response leads withstatus—ok(every requested backend searched; acountof 0 means nothing matched the query as asked, which is not the same as a verified absence),partial(a backend failed or was skipped; a 0 establishes nothing), orerror(the query itself failed — exit 2, and nocount/resultskey at all, so a broken search can never be read as an empty literature). See Literature research.addfetches a paper by DOI, S2 id, or OpenAlex work id and appends a schema-compatible record to the library (<nexus.root>/.bipartite/refs.jsonlby default, orlit.library_path). Dedup-checked by DOI. Works with zero keys configured via the OpenAlex DOI fallback; an S2 key is only needed for non-DOI ids.status/setupreport configured backends (env /config/nexus.ymllit.*/ legacybipconfig — never the key itself). OpenAlex always reports available (no key required), so the tool is never fully "unconfigured";statusprints key-acquisition references as a hint when S2/ASTA are both unconfigured, but only exits non-zero when a specific--sourcerequest (s2/asta/both) has no matching key.
What about archived diffs?¶
Listing and dumping archived snapshot diffs is intentionally not a
subcommand. The archive under monitor/.state/diffs/ is a directory
of sortable filenames; the bootstrap snippet that loads them does:
No CLI wrapper saves enough to justify the surface area.
Maintenance burden¶
The code is canonical; this page is a narrative over it. Four
surfaces describe ng. The first is ground truth — it is what
dispatches. Of the other three, exactly one is machine-checked:
| Surface | Meant to be canonical for | Guarded by |
|---|---|---|
the case "$sub" in main() |
the verb set — the only surface that decides what dispatches | the shell |
_verb_index in monitor/ng |
the browsable index (ng verbs) |
nothing |
_usage_for in monitor/ng |
each verb's flag set | test-ng-usage-flag-coverage.sh (parsed flag ⇒ must appear in the usage line) |
| this page | why a verb behaves as it does — exit-code semantics, refusal rationale, worked examples | nothing |
Whenever you add, rename, or remove a verb, or change a flag or an exit code:
- Update
monitor/ng's top-of-file usage block, its_usage_forarm, and_verb_index. - Update the section for the verb on this page and the Quick reference table — including the subset list, if a verb gained or lost a section here.
Three known gaps, so you know what the guards do not buy you.
test-ng-usage-flag-coverage.sh checks parsed-flag → usage-line and
not the reverse — a usage line may advertise a flag no parser
accepts (see ng report-init). Nothing compares
this page against either source. And nothing asserts _verb_index
against the dispatch table, so a verb can ship without ever
reaching the index that exists to make verbs findable. Measured at
a3177ef6 (monitor/ng is unmodified at that ref), the case in
main() dispatches 59 verbs and _verb_index names 48 of
them. The 11 it omits: ci-attempts, close-set,
reports-for-window, send, session-id, skeptic-arm,
skeptic-disposition, skeptic-evidence, skeptic-obligations,
skeptic-orphans, stranded-branches. All 11 dispatch; --help
support among them is uneven — see the measured examples in the
Quick reference note above, which is a separate
gap from this one. Add a verb to _verb_index in the same commit
that adds its case arm. See
Development.