Configuration¶
The keys an operator sets on a normal install — what each does, what it defaults to, the environment variable that overrides it, and a runnable example.
This page is a curated subset, not the full key set. At
a3177ef6 the tree reads at least 207 distinct dotted keys and
this page documents 63 keys — 53 of them inside that measured
set, the rest either read through a wrapper the command below cannot
see or, in three flagged cases, read by nothing at all. The two
exhaustive surfaces are config/nexus.example.yml (the canonical
template, with the long-form rationale in YAML comments) and the code
itself. Re-derive
the read set with (git grep -o does not exist at git 2.17.1, this
host's default, so the walk goes through xargs):
git ls-files -z | xargs -0 grep -hodskip -E \
'(load\.sh|_cfg)"? +"?(github|monitor|nexus|notifications)\.[a-z0-9_.]+' \
| grep -oE '(github|monitor|nexus|notifications)\.[a-z0-9_.]+' \
| sed 's/[.]$//' | sort -u | wc -l
That number is a LOWER bound, and saying which direction it errs in is the point: it sees only keys passed as a literal argument to the loader, so a key composed at runtime is invisible to it. It under-counts; it never over-counts.
The single source of truth is config/nexus.yml in your nexus
checkout. Copy config/nexus.example.yml to config/nexus.yml,
chmod 600, and edit each value to match your environment.
See Install for the bootstrap walkthrough.
How config is loaded¶
Values flow through a small ladder; the first match wins:
1. Process-local env var (e.g. MONITOR_INTERVAL, NEXUS_ROOT)
2. config/nexus.yml (the canonical site config)
3. Script default (each script names its own fallback)
config/load.sh <dotted.key> [default] resolves a single value;
every script in monitor/ calls it for each configurable knob.
Env-var overrides are intentional escape hatches — they let an
operator restart the watcher with a snappier MONITOR_INTERVAL for
a debugging session without editing the file. The mapping between
env vars and config keys is listed against each key below.
Paths starting with ~ expand against $HOME. Secret files
(.pem, Pushover keys, webhook secrets, …) must be chmod 600;
the scripts refuse to read group/world-readable secret files.
Variables index¶
| Key | Type | Env override | Default |
|---|---|---|---|
nexus.root |
path | NEXUS_ROOT |
— (required) |
nexus.node_module |
string | NEXUS_NODE_MODULE |
nodejs |
github.user_login |
string | MONITOR_USER_LOGIN |
— (required) |
github.repo |
owner/name |
MONITOR_REPO |
— (required) |
github.asset_repo |
owner/name |
NEXUS_ASSET_REPO |
falls back to github.repo |
github.bot_app_id |
int | NEXUS_BOT_APP_ID |
— (required) |
github.bot_installation_id |
int | NEXUS_BOT_INSTALLATION_ID |
— (required) |
github.bot_pem_path |
path | NEXUS_BOT_PRIVATE_KEY_PATH |
— (required) |
github.bot_token_cache |
path | NEXUS_BOT_TOKEN_CACHE |
~/.claude/.nexus-bot-token.json |
github.bot_git_name |
string | — | legacy fallback (see entry) |
github.bot_git_email |
— | legacy fallback (see entry) | |
github.bot_login |
string | MONITOR_BOT_LOGIN |
empty |
github.bot_webhook_url |
URL | — | empty |
github.bot_webhook_secret_path |
path | — | empty |
github.overview_issue_number |
int | — | unset (live-resolved) |
notifications.pushover.user_key_path |
path | NEXUS_PUSHOVER_USER_KEY_FILE |
~/.claude/.nexus-pushover-user-key |
notifications.pushover.app_token_path |
path | NEXUS_PUSHOVER_APP_TOKEN_FILE |
~/.claude/.nexus-pushover-app-token |
notifications.ntfy.topic_url_path |
path | NEXUS_NOTIFY_TOKEN |
~/.claude/.nexus-notify-token |
notifications.email.address |
NEXUS_EMAIL_TO |
— (required for email tier) | |
notifications.email.probe_address |
email|null |
— | null (probes go to address) |
notifications.email.smtp_host |
host | NEXUS_SMTP_HOST |
— (smtp.example.org placeholder) |
notifications.email.smtp_port |
int | NEXUS_SMTP_PORT |
25 |
monitor.interval_seconds |
int (s) | MONITOR_INTERVAL |
60 |
monitor.target_window |
string | MONITOR_TARGET |
orchestrator |
monitor.diff_retention_days |
int (d) | DIFF_RETENTION_DAYS |
7 |
monitor.idle_threshold_seconds |
int (s) | MONITOR_IDLE_THRESHOLD_SECONDS |
60 |
monitor.idle_close_hours |
int (h) | MONITOR_IDLE_CLOSE_HOURS |
24 |
monitor.report_min_chars |
int | MONITOR_REPORT_MIN_CHARS |
500 |
monitor.agent_dead_threshold |
int | AGENT_DEAD_THRESHOLD |
3 |
monitor.respawn_loop_limit |
int | MONITOR_RESPAWN_LOOP_LIMIT |
3 |
monitor.respawn_loop_window_seconds |
int (s) | MONITOR_RESPAWN_LOOP_WINDOW |
120 |
monitor.deliveries.asset_enabled |
bool | MONITOR_DELIVERIES_ASSET_ENABLED |
true |
monitor.deliveries.bot_mention_enabled |
bool | MONITOR_DELIVERIES_BOT_MENTION_ENABLED |
true |
monitor.mentions_enabled |
bool | MONITOR_MENTIONS_ENABLED |
false |
monitor.graphql_threshold |
int (pts) | MONITOR_GRAPHQL_THRESHOLD |
200 |
monitor.skeptic.enforce_auto_decision |
bool | MONITOR_SKEPTIC_ENFORCE_AUTO_DECISION |
true |
monitor.skeptic.findings_threshold |
int | MONITOR_SKEPTIC_FINDINGS_THRESHOLD |
1 |
monitor.skeptic.max_depth |
int | MONITOR_SKEPTIC_MAX_DEPTH |
3 |
monitor.skeptic.await_timeout_seconds |
int (s) | MONITOR_SKEPTIC_AWAIT_TIMEOUT_SECONDS |
900 |
monitor.skeptic.await_interval_seconds |
int (s) | MONITOR_SKEPTIC_AWAIT_INTERVAL_SECONDS |
5 |
monitor.skeptic.await_hang_seconds |
int (s) | MONITOR_SKEPTIC_AWAIT_HANG_SECONDS |
600 |
monitor.watcher.auto_unstick |
bool | MONITOR_AUTO_UNSTICK |
true |
monitor.watcher.ratelimit_probe |
bool | MONITOR_RATELIMIT_PROBE |
false |
monitor.watcher.probe_model |
string | MONITOR_PROBE_MODEL |
claude-haiku-4-5-20251001 |
monitor.watcher.ratelimit_heuristic_minutes |
int (min) | MONITOR_RATELIMIT_HEURISTIC_MIN |
30 |
monitor.watcher.ratelimit_ack_timeout_s |
int (s) | MONITOR_RATELIMIT_ACK_TIMEOUT_S |
60 |
monitor.watcher.paste_response_grace_seconds |
int (s) | MONITOR_ORCH_PASTE_RESPONSE_GRACE_S |
120 |
monitor.watcher.unstick_window_seconds |
int (s) | MONITOR_ORCH_UNSTICK_WINDOW_S |
150 |
monitor.watcher.orchestrator_dead_threshold_seconds |
int (s) | MONITOR_ORCH_DEAD_THRESHOLD_S |
300 |
monitor.watcher.stale_paste_ceiling_seconds |
int (s) | MONITOR_ORCH_STALE_PASTE_CEILING_S |
1800 |
monitor.watcher.dead_threshold_floor_margin_seconds |
int (s) | MONITOR_ORCH_DEAD_THRESHOLD_FLOOR_MARGIN_S |
60 |
monitor.watcher.idle_pane_override_max |
int | MONITOR_ORCH_IDLE_OVERRIDE_MAX |
5 |
monitor.watcher.liveness_log_throttle_seconds |
int (s) | MONITOR_ORCH_LIVENESS_LOG_THROTTLE_S |
30 |
monitor.version_restart.enabled |
bool | MONITOR_VERSION_RESTART_ENABLED |
true |
monitor.version_restart.interval_seconds |
int (s) | MONITOR_VERSION_CHECK_INTERVAL_SECONDS |
60 |
monitor.version_restart.settle_seconds |
int (s) | MONITOR_VERSION_SETTLE_SECONDS |
45 |
monitor.version_restart.cooldown_seconds |
int (s) | MONITOR_VERSION_RESTART_COOLDOWN_SECONDS |
600 |
monitor.version_restart.self |
bool | MONITOR_VERSION_SELF_RESTART |
true |
monitor.version_restart.services |
bool | MONITOR_VERSION_SERVICE_RESTART |
true |
monitor.version_restart.self_loop_limit |
int | MONITOR_VERSION_SELF_LOOP_LIMIT |
3 |
monitor.version_restart.self_loop_window_seconds |
int (s) | MONITOR_VERSION_SELF_LOOP_WINDOW_SECONDS |
3600 |
The worker-lifecycle / engagement keys (monitor.retain_ttl_seconds,
monitor.operator_engaged_*, monitor.paste_confirm_grace_seconds,
monitor.idle_pool_spawn_grace_seconds, monitor.over_limit.*, …)
are tabulated with their defaults and exact semantics in the
agent state machine
"Switching conditions and times" table — the single source of truth
for lifecycle thresholds, deliberately not duplicated here.
One env var has no corresponding YAML key:
| Env var | Purpose |
|---|---|
ANTHROPIC_API_KEY |
Anthropic API key for the case-B rate-limit reset probe. Read only from the environment; never persisted to config. Required when monitor.watcher.ratelimit_probe: true. |
Retired: NEXUS_ASSET_BRANCH is inert — setting it does nothing
This page listed it as an env-only knob for the asset-repo push branch.
Nothing has read it since 5dc735cd (2026-04-18), which deleted
BRANCH="${NEXUS_ASSET_BRANCH:-main}" from monitor/upload-asset.sh;
main is now hardcoded at every branch-touching site there
(monitor/upload-asset.sh:639,659-661,840,915,947). If you have it
exported, unset it — it is not honoured, and an asset push goes to
main regardless.
The tombstone stays rather than the row simply vanishing, because the
failure mode of a phantom knob is an operator setting it, observing no
effect, and concluding the whole config surface is unreliable. Verified
at a3177ef6: the name appears nowhere in the tree outside this
paragraph —
nexus¶
nexus.root¶
Type path · Env NEXUS_ROOT · Default — (required)
Absolute path to the nexus workspace root (the directory holding
monitor/, work/, reports/, …). Scripts use it to resolve
monitor/.state/, the assets clone, the reports dir, and the
per-project work/<project>/ checkouts.
nexus.node_module¶
Type string · Env NEXUS_NODE_MODULE · Default nodejs
Environment-module name that provides Node.js, for HPC hosts where
node is not on the default PATH but is available via Lmod / Tcl
environment-modules (e.g. module load nodejs).
monitor/install-claude-local.sh consults this only when node
is absent: it sources the module init, module loads this name (then
falls back to discovering the highest >=18 versioned module if the
site has no default), and re-checks. Leave as the default nodejs
for node is
already on PATH. Env var NEXUS_NODE_MODULE wins over this key.
github¶
github.user_login¶
Type string · Env MONITOR_USER_LOGIN · Default — (required)
GitHub login of the single authorised operator. The watcher only surfaces issues and comments authored by this user; the bot rejects everything else as a security boundary (see Security).
ng pr create also auto-requests this login as the reviewer on
bot-opened PRs, so it must resolve to a real GitHub collaborator on
github.repo — otherwise the request_reviewers POST returns 422
and every PR open emits a warning naming the unfilled placeholder.
github.repo¶
Type owner/name · Env MONITOR_REPO · Default — (required)
The asset+issue repo the watcher polls and the bot writes to — issues,
overview body, dashboard reactions. This is your per-operator
state repo, not the canonical implementation repo
<your-org>/nexus-code (which is read-only for non-maintainers).
Convention: <your-org>/<your-handle>-nexus-assets (private). One
asset+issue repo per operator; nothing in the implementation repo
hard-codes its name.
github.asset_repo¶
Type owner/name · Env NEXUS_ASSET_REPO · Default falls back to github.repo
Repo where monitor/upload-asset.sh pushes uploaded assets (reports,
PNGs, PDFs, notebooks …). Defaults to github.repo; leave commented
unless you want assets in a separate repo.
monitor/upload-asset.sh clones this to <nexus.root>/assets/
(gitignored) and pushes new files to its main branch under
assets/<issue-N>/… or assets/general/…. URLs returned by
ng upload are pinned to the post-push commit SHA, so subsequent
overwrites do not change what an old URL resolves to. See
ng upload.
github.bot_app_id¶
Type int · Env NEXUS_BOT_APP_ID · Default — (required)
Numeric App ID of the GitHub App that authors bot writes. Visible on the App's settings page. See GitHub App setup for App creation, permission grants, and installation.
github.bot_installation_id¶
Type int · Env NEXUS_BOT_INSTALLATION_ID · Default — (required)
Numeric installation ID for the App on github.repo. Pinning the
installation ID lets monitor/mint-token.sh skip a discovery call
on every token mint.
github.bot_pem_path¶
Type path · Env NEXUS_BOT_PRIVATE_KEY_PATH · Default — (required)
Path to the App's RSA private key (PEM). chmod 600; scripts refuse
to read it otherwise. Used by monitor/mint-token.sh to sign the
App-level JWT exchanged for an installation token.
github.bot_token_cache¶
Type path · Env NEXUS_BOT_TOKEN_CACHE · Default ~/.claude/.nexus-bot-token.json
Cache file for the minted installation token (+ its expiry). Tokens
live ~1 h; mint-token.sh reuses the cached token until ~5 min
before expiry, then mints a fresh one. Safe to delete to force a
fresh mint on the next call.
github.bot_git_name¶
Type string · Default legacy fallback (a pre-cutover bot name)
Git author name used on bot-side commits — currently the asset
pushes in monitor/upload-asset.sh. GitHub's convention for App
commits is <app-slug>[bot]. Find the slug in
https://github.com/apps/<slug>. Leaving this unset falls back to
a legacy default baked into upload-asset.sh for back-compat with
pre-cutover installs; new deployments should always set this key
explicitly.
github.bot_git_email¶
Type email · Default legacy fallback (matching bot_git_name)
Git author email for bot-side commits. GitHub's noreply form
(<slug>[bot]@users.noreply.github.com) keeps the commit attributable
to the App in the GitHub UI without exposing a real mailbox.
github.bot_login¶
Type string · Env MONITOR_BOT_LOGIN · Default empty
The bot's GitHub login slug (no [bot] suffix). Informational
since issue #86 retired the bot-mention dispatch in
_deliveries.sh — the watcher now restricts every comment path to
github.user_login-authored content via the
_filter_to_user_author chokepoint, so non-user @bot mentions
are an accepted loss. The slug is kept for operator reference.
github.bot_webhook_url¶
Type URL · Default empty
GitHub App webhook URL. The watcher does not POST to this URL —
GitHub does. Setting any reachable URL on the App settings page is
what makes GitHub retain every delivery in /app/hook/deliveries,
which the watcher reads with the App JWT.
Any inert sink works: a smee.io channel, a Cloudflare Worker returning 200, eventually a self-hosted receiver. This key is documentation-only today; the slot exists so the URL is recorded in the same place as everything else.
github.bot_webhook_secret_path¶
Type path · Default empty
Path to a file holding the webhook HMAC secret (matching the value
pasted into the App settings' "Webhook secret" field). chmod 600.
Generate with openssl rand -hex 32.
The watcher does not read this file today — it trusts GitHub's deliveries log over an authenticated channel. The slot is here so secret rotation becomes a config change rather than a code change once a self-hosted receiver verifies HMAC signatures locally.
github.overview_issue_number¶
Type int · Default unset (live-resolved)
Pin the overview issue number to avoid the labelled-issue lookup that
ng dashboard and friends would otherwise run on every call. GitHub's
label index is eventually consistent and returns empty results during
high write activity, so pinning is the cheapest robustness fix. Set
to the integer (no #) of your overview issue.
Unset, ng does a 3-attempt retry on the live label query and caches
the result in monitor/.state/overview-number for the life of the
state dir.
github.overview_issue_number is the canonical key — it is what
config/nexus.example.yml documents and what the resolver in ng
reads (the _overview_number lookup and the dashboard scaffold).
One ng dashboard --upsert-overview code path reads a differently
spelled github.overview_issue (no _number suffix, default 1)
instead; that second spelling is not a documented key and falls
through to its hard-coded 1 default for every operator whose
overview is not issue #1. Set overview_issue_number (the correct
key) and treat any bare overview_issue as inert.
notifications¶
Both Pushover and ntfy are optional; with neither configured,
monitor/notify.sh silently no-ops so scripts can call it
unconditionally.
notifications.pushover.user_key_path¶
Type path · Env NEXUS_PUSHOVER_USER_KEY_FILE · Default ~/.claude/.nexus-pushover-user-key
File whose first line is the Pushover user key from
pushover.net. chmod 600. The key is
optional because monitor/notify.sh falls back to the path above
(monitor/notify.sh:173) — drop the file there and Pushover works
with no config entry at all; with neither key nor file the tier
silently no-ops.
notifications.pushover.app_token_path¶
Type path · Env NEXUS_PUSHOVER_APP_TOKEN_FILE · Default ~/.claude/.nexus-pushover-app-token
File whose first line is the Pushover application token from
pushover.net/apps/build. chmod 600.
Same fallback shape as the user key (monitor/notify.sh:174).
notifications.ntfy.topic_url_path¶
Type path · Env NEXUS_NOTIFY_TOKEN · Default ~/.claude/.nexus-notify-token
Fallback push channel; the code fallback is monitor/notify.sh:175. File's first line holds the full ntfy topic
URL (e.g. https://ntfy.sh/<unguessable-topic>). The topic name is a
bearer secret on ntfy.sh's public instance — choose something
unguessable.
notifications.email.address¶
Type email · Env NEXUS_EMAIL_TO · Default — (required for email tier)
Where emergency-tier emails go (production address). Used by
monitor/notify.sh when push is unavailable or the alert tier is
"emergency".
This is the ONLY address the nexus may email (operator rule, 2026-09-28,
config/nexus.yml, never through NEXUS_CONFIG: a NEXUS_CONFIG naming a
different recipient is refused (rc 5). Unset means no email, at rc 0. NEXUS_EMAIL_TO can only narrow it: set it to
the same address, or to empty for no email. Anything else (another
address, a list, a display-name form) is refused at rc 5. A placeholder or
malformed value here is refused too, rather than mailed.
notifications.email.probe_address¶
Type email | null · Default null
Optional disposable alias for liveness probes (the daily watcher
probe email, …). Set to null to send probes to address instead.
Reserved; not yet consumed. No script reads this key today —
monitor/notify.sh routes every tier (probes included) to
address. The slot is recorded here and in the example config so
that when a separate probe channel lands it is a config change, not
a code change.
notifications.email.smtp_host¶
Type host · Env NEXUS_SMTP_HOST · Default — (smtp.example.org is the example-config placeholder, not a code fallback)
Outbound SMTP relay. Must accept mail from this host without auth.
Set to whatever your institution's relay is. monitor/notify.sh has
no built-in default — the smtp.example.org shown here is the
placeholder shipped in config/nexus.example.yml, which will not
deliver mail; replace it before relying on the email tier.
notifications.email.smtp_port¶
Type int · Env NEXUS_SMTP_PORT · Default 25
SMTP port. Adjust if your relay uses submission (587) or implicit TLS
(465). monitor/notify.sh does not negotiate STARTTLS today — pick
an unauthenticated relay your host can reach.
monitor¶
monitor.interval_seconds¶
Type int (s) · Env MONITOR_INTERVAL · Default 60
Seconds the watcher sleeps between snapshots. Lower values surface events faster at the cost of GraphQL points and CPU.
monitor.target_window¶
Type string · Env MONITOR_TARGET · Default orchestrator
Name of the tmux window the watcher pastes snapshots into. Default
matches the workspace convention for the orchestrator window
(self-enforced by the orchestrator's session-pin hook, which renames
the hosting window to orchestrator and disables tmux's
automatic-rename on first turn). The watcher respawns a fresh
orchestrator session (the claude CLI) in this window if it goes
missing for monitor.agent_missing_respawn_delay consecutive
confirming polls (see
monitor.agent_dead_threshold).
Stack recovery (bootstrap-recover.sh) additionally pins this
window to the canonical tmux index monitor.target_window_index
(default 2) — non-destructively: an occupied slot is logged, never
stolen.
monitor.diff_retention_days¶
Type int (d) · Env DIFF_RETENTION_DAYS · Default 7
Days before archived snapshot diffs under monitor/.state/diffs/ are
pruned. Set higher to retain a longer audit window; lower to bound
disk use.
monitor.idle_threshold_seconds¶
Type int (s) · Env MONITOR_IDLE_THRESHOLD_SECONDS · Default 60
A worker tmux window is "really idle" when its #{window_activity}
age crosses this threshold AND monitor/pane-state.sh reports
idle / autosuggest-only / empty. The threshold is independent
of the tmux bell flag (which clears on view, so bell-clear ≠ idle).
monitor.idle_close_hours¶
Type int (h) · Env MONITOR_IDLE_CLOSE_HOURS · Default 24
Hard-close threshold for the same probe: a window idle for ≥ this
many hours surfaces as idle-too-long and the orchestrator defaults
to closing it. Retention overrides (recent user engagement, loaded
kernel cost) still apply. Raise for long-running observational
workers.
monitor.report_min_chars¶
Type int · Env MONITOR_REPORT_MIN_CHARS · Default 500
Minimum body length (chars, excluding frontmatter) for
ng report-check to accept a report.
Guards against stub asset uploads by gating
ng wrap-up on a substantive report. Lower
for noisier projects; the default catches placeholder
# title \n_(later)_ skeletons.
monitor.agent_dead_threshold¶
Type int · Env AGENT_DEAD_THRESHOLD · Default 3
Reserved for the slow-path "window present, agent silent" detector
(not yet wired in). The fast path — target window confirmed absent
— is governed by monitor.agent_missing_respawn_delay (env
AGENT_MISSING_RESPAWN_DELAY, default 3): that many consecutive
absent observations (~8 s at the probe cadence), plus a pre-launch
re-verification, before the watcher launches a fresh orchestrator
(claude CLI) session in target_window.
The window-present-but-inert case is handled by the hook-driven
liveness state machine (monitor.watcher.* liveness keys; see
monitor/README.md § Mutual-liveness contract).
monitor.respawn_loop_limit¶
Type int · Env MONITOR_RESPAWN_LOOP_LIMIT · Default 3
Crash-loop guard for orchestrator respawns. If more than this many
respawns happen within
respawn_loop_window_seconds,
the watcher stops respawning until the sliding window empties or a
paste-to-target succeeds (which clears the history). One
sandbox-notify fires on the ok → tripped transition, then quiet.
Set to a large value (e.g. 999) to effectively disable.
monitor.respawn_loop_window_seconds¶
Type int (s) · Env MONITOR_RESPAWN_LOOP_WINDOW · Default 120
Sliding-window length (seconds) for the
respawn_loop_limit guard.
monitor.deliveries.asset_enabled¶
Type bool · Env MONITOR_DELIVERIES_ASSET_ENABLED · Default true
Gate the in-github.repo (asset-nexus) deliveries emit path — the
issue=/pr=/pr_review=/issue_new= shapes that surface operator
comments on the asset repo within ~15 s, no @bot-mention required. The
near-real-time complement to the always-on snapshot_github poll (the
~600 s asset baseline, untouched by this flag). Set false to opt out.
The deliveries-polling source reads /app/hook/deliveries (the App's
webhook delivery log) on a separate auth bucket (App-level JWT). It is
active when this flag OR
deliveries.bot_mention_enabled
is on, and degrades to a clean no-op when the App has no webhook URL
configured. Effective surfacing needs:
- The App has a webhook URL configured (any inert sink — smee.io channel, Cloudflare Worker returning 200, …). GitHub records every delivery in the log regardless of receiver outcome; the watcher reads from the log, not from the URL.
- The App is subscribed to the events the watcher folds into its
line shapes:
issue_comment,pull_request_review_comment,pull_request_review,issues,pull_request.
Only USER_LOGIN-authored events surface (issue #86's universal
user-author filter via the _filter_to_user_author chokepoint).
A stale
monitor.deliveries_enabledkey (the removed pre-split umbrella) is silently ignored — set the twodeliveries.*flags instead.
See Watcher protocol for the line-shape vocabulary and dedup rules between this path and the GraphQL search.
monitor.deliveries.bot_mention_enabled¶
Type bool · Env MONITOR_DELIVERIES_BOT_MENTION_ENABLED · Default true
Gate the cross-repo mention= deliveries emit path — @bot-mentions
the operator posts on App-installed repos other than github.repo,
discovered via the App webhook delivery log. Set false to opt out.
This is the webhook-based channel; monitor.bot_mentions_enabled is the
webhook-FREE GraphQL-poll equivalent for the same concern — running both
is safe (cross-source id= duplicates collapse in the dedup hop). If the
App has no webhook URL configured, the deliveries path 404s and this flag
is moot; the poll channel covers it.
monitor.mentions_enabled¶
Type bool · Env MONITOR_MENTIONS_ENABLED · Default false
Enable the mentions-search fallback path. When true, the watcher
runs an additional GraphQL search each cycle for issues/PRs that
@-mention github.user_login (NOT bot_login —
that qualifier does not index [bot] accounts).
Scope: cross-repo activity in repos where the App is NOT installed — the gap that the deliveries path can't reach. Complementary to the deliveries path; safe to enable both.
Routing logic should treat cross_repo= events as read-only
context until the bot is installed on the referenced repo (the bot
cannot comment back from a repo it isn't installed on). Cost: one
GraphQL search call per poll cycle (50 results) plus one
/installation/repositories call per 24 h to refresh the cache at
monitor/.state/bot-installed-repos.txt.
monitor.graphql_threshold¶
Type int (points) · Env MONITOR_GRAPHQL_THRESHOLD · Default 200
Minimum GraphQL remaining points before the watcher will issue any
GraphQL search. Below this, GraphQL polling is skipped for that
cycle (per-surface — both issue_comments and mentions are gated).
The deliveries path keeps surfacing events on its separate bucket
while the GraphQL bucket replenishes. The default leaves headroom
for orchestrator + worker writes during the cycle.
monitor.skeptic¶
Knobs for the universal skeptic protocol (skills/nexus.skeptic) —
the independent, adversarial validation pass a worker's result may
require. The wrap-up enforcement and the recursion bound live in
monitor/ng;
the worker↔skeptic comms channel (await/answer) lives in
monitor/skeptic-channel.sh;
the await-hang exemption lives in the idle probe. Each key resolves
through the standard env → config/load.sh → inline-default ladder.
| Key | Type | Env override | Default | Governs |
|---|---|---|---|---|
enforce_auto_decision |
bool | MONITOR_SKEPTIC_ENFORCE_AUTO_DECISION |
true |
When truthy, ng wrap-up refuses to complete an auto-mode hand-off until the worker has recorded a skeptic decision — the wrap-up gate. |
findings_threshold |
int | MONITOR_SKEPTIC_FINDINGS_THRESHOLD |
1 |
Minimum substantive new findings in a skeptic pass to warrant a further recursive pass. Floored at 1 (a 0 would recommend a second pass on a clean run, defeating the diminishing-returns rule). |
max_depth |
int | MONITOR_SKEPTIC_MAX_DEPTH |
3 |
Recursion cap on the skeptic chain. Depth strictly increments and is capped, so the chain terminates after at most max_depth + 1 passes. |
await_timeout_seconds |
int (s) | MONITOR_SKEPTIC_AWAIT_TIMEOUT_SECONDS |
900 |
Per-call ceiling for skeptic-channel.sh await; a single blocking call is bounded by this, then exits (the worker re-enters await, which re-heartbeats). |
await_interval_seconds |
int (s) | MONITOR_SKEPTIC_AWAIT_INTERVAL_SECONDS |
5 |
Poll cadence inside the await loop. |
await_hang_seconds |
int (s) | MONITOR_SKEPTIC_AWAIT_HANG_SECONDS |
600 |
Idle-probe exemption window: a worker parked in await is not treated as hung until its pane has been static past this threshold. |
monitor.watcher¶
monitor.watcher.auto_unstick¶
Type bool · Env MONITOR_AUTO_UNSTICK · Default true
Auto-resolve stalled agent prompts. The watcher captures every non-watcher tmux pane each cycle; if a recognised wedge is showing, the watcher pastes the appropriate recovery keystroke:
- Case A — permission prompt (
"Do you want to proceed?"): paste Enter to accept the default-highlighted option. - Case B — rate-limit prompt: every stuck non-watcher window
receives Enter + a "please continue" follow-up once the limit has
reset, and the orchestrator gets a separate heads-up paste.
Case C (an API-error Enter-nudge) was retired in
/nexus-code#1670, and with it the api_error_backoff_minutesknob; a turn killed by an API error is reported through theStopFailuremarker instead.
Set to false to opt out of these branches and require manual
confirmation. Action + pre-action pane captures are recorded under
monitor/.state/watcher-unstick.log and monitor/.state/unstick/
for post-hoc audit.
monitor.watcher.ratelimit_probe¶
Type bool · Env MONITOR_RATELIMIT_PROBE · Default false
Probe the Anthropic API on rate-limit detection (case B) to discover
the reset timestamp via response headers
(anthropic-ratelimit-unified-reset / -tokens-reset). The probe
costs a few hundred input tokens + 1 output token per rate-limit
incident (cached for the duration).
Requires ANTHROPIC_API_KEY in env. The API key is read from the
environment only — never from config — to keep secrets out of the
file. With the probe disabled (or when it fails: no key, network
error, missing header), the watcher falls back to
ratelimit_heuristic_minutes.
monitor.watcher.probe_model¶
Type string · Env MONITOR_PROBE_MODEL · Default claude-haiku-4-5-20251001
Model used for the rate-limit probe. The cheapest available Anthropic
model is the right pick; max_tokens is hard-coded to 1 in the probe
call, so token cost is dominated by the model's input pricing.
monitor.watcher.ratelimit_heuristic_minutes¶
Type int (min) · Env MONITOR_RATELIMIT_HEURISTIC_MIN · Default 30
Fallback wait time (in minutes) used when the probe is disabled or
fails. The watcher schedules the cascade for now + this many
minutes and waits, logging once every 5 min. Tune to your
subscription's typical reset cadence (5 h limits sit on a different
schedule than per-minute throttles).
monitor.watcher.ratelimit_ack_timeout_s¶
Type int (s) · Env MONITOR_RATELIMIT_ACK_TIMEOUT_S · Default 60
After cascading the unstick, how many seconds the watcher waits for
the orchestrator's ratelimit-resume-ack action-log entry before
logging orchestrator-unresponsive. Allows for one poll-interval
round-trip plus a small margin.
monitor.watcher.paste_response_grace_seconds¶
Type int (s) · Env MONITOR_ORCH_PASTE_RESPONSE_GRACE_S · Default 120
Orchestrator-liveness grace window. After the watcher pastes to the orchestrator, it waits this long before declaring "pasted-without-response" — multi-step tool turns legitimately take this long before any liveness signal advances. Also the response window granted to the one-shot re-submit rescue. See Orchestrator liveness.
monitor.watcher.unstick_window_seconds¶
Type int (s) · Env MONITOR_ORCH_UNSTICK_WINDOW_S · Default 150
Budget for the auto-unstick cycle (e.g. AskUserQuestion-Escape) to resolve a wedge before the watcher fires
the one-shot re-submit rescue. grace + unstick_window must stay
below orchestrator_dead_threshold_seconds so the rescue retains a
verification window before the absolute deadline (enforced as a
startup WARN).
monitor.watcher.orchestrator_dead_threshold_seconds¶
Type int (s) · Env MONITOR_ORCH_DEAD_THRESHOLD_S · Default 300
Absolute deadline: if the orchestrator was pasted to this many
seconds ago and no liveness signal (heartbeat / paste-received /
jsonl / tool-results) has advanced past the paste, respawn
unconditionally (subject to cooldown). The re-submit rescue cannot
defer this ceiling. Clamped up at startup when it would sit at or
below the maximum compose_emit gap (the full-state heartbeat,
ceil(full_state.safety_floor_seconds / full_state_emit_interval_seconds)
cadence ticks, plus one interval_seconds loop tick — 1260 s with
defaults) — see
dead_threshold_floor_margin_seconds
and Orchestrator liveness. Must stay below
stale_paste_ceiling_seconds.
monitor.watcher.stale_paste_ceiling_seconds¶
Type int (s) · Env MONITOR_ORCH_STALE_PASTE_CEILING_S · Default 1800
Upper bound on how old a last-paste timestamp may be and still serve
as evidence of wedging. Once the paste age crosses this, the state
machine treats it as too old to be a wedge signal and returns healthy
(paste-too-stale) — a quiet workspace with no eligible pastes for
half an hour is not a wedge. Must satisfy
orchestrator_dead_threshold_seconds < stale_paste_ceiling_seconds,
else the ceiling masks the dead-threshold check and the detector
never fires.
monitor.watcher.dead_threshold_floor_margin_seconds¶
Type int (s) · Env MONITOR_ORCH_DEAD_THRESHOLD_FLOOR_MARGIN_S · Default 60
Margin added when the watcher clamps the effective
orchestrator_dead_threshold_seconds up to
heartbeat + interval_seconds + margin, where heartbeat is
full_state_emit_interval_seconds rounded up to cover
full_state.safety_floor_seconds. The clamp closes the
static-workspace false-positive respawn race at the source (the
2026-06-15 incident): with defaults it lifts the deadline from 300 to
ceil(900/600)*600 + 60 + 60 = 1200 + 60 + 60 = 1320 s, so a
static-workspace full-state paste always resets the clock before the
deadline. The clamp declines
(leaving the configured value, with a WARN) if it would cross
stale_paste_ceiling_seconds.
monitor.watcher.idle_pane_override_max¶
Type int · Env MONITOR_ORCH_IDLE_OVERRIDE_MAX · Default 5
Budget for the runtime idle-pane guard — the defense-in-depth backstop
that suppresses a dead-threshold respawn when pane-state.sh reports
the orchestrator alive and idle/empty. The guard suppresses at most
this many consecutive times (the counter resets on any genuine
healthy verdict, a non-idle pane, or budget exhaustion); after the
budget it escalates and honors the respawn, so a hung-but-idle pane
cannot suppress its own respawn forever. Set to 0 to disable the
bound (unconditional suppression — not recommended, reintroduces
the dead-forever risk). See Orchestrator liveness.
monitor.watcher.liveness_log_throttle_seconds¶
Type int (s) · Env MONITOR_ORCH_LIVENESS_LOG_THROTTLE_S · Default 30
Throttle for the repeated waiting verdict log lines emitted while the
liveness task polls (~every 5 s). State entries, transitions, and
resubmit/respawn/idle-pane-override events always log regardless.
monitor.version_restart¶
The version-aware component auto-restart (issue #186,
monitor/watcher/_version_restart.sh)
— what makes git pull the entire update story. The running watcher
hashes, per component, the source set each running instance loaded at
start against the same files on disk; on confirmed drift it restarts
the component (itself via a detached launcher.sh --replace,
registered services via svc.sh restart <name>) or asks the
orchestrator (the cockpit, whose TUI window the watcher never kills).
Operator-facing walkthrough: Operating → Upgrading.
| Key | Default | Governs |
|---|---|---|
enabled |
true |
Master switch. false (or interval_seconds: 0) restores the fully manual pull-then-restart discipline. |
interval_seconds |
60 |
Drift-evaluation cadence (a scheduler task inside the watcher loop). |
settle_seconds |
45 |
A changed hash must hold unchanged this long before any action — outwaits a mid-pull torn tree (a source set with a missing file is torn and never acted on). |
cooldown_seconds |
600 |
Per-component minimum spacing between actions; a persistent mismatch retries slowly instead of tight-looping. |
self |
true |
Watcher self-restart channel. false degrades a confirmed self-drift to an emit advisory — never silent. |
services |
true |
Registry-service restart channel. Same advisory degradation when false. |
self_loop_limit |
3 |
Max watcher self-restarts per window before the loop guard trips (auto self-restart suspends; an advisory emit asks for manual intervention). |
self_loop_window_seconds |
3600 |
The loop-guard window; re-arms after a full quiet window. |
Bootstrap caveat: only a version-aware watcher can auto-restart
anything — the first-ever deploy of this module is itself still a
manual git pull + monitor/svc.sh restart watcher.
Maintenance burden¶
This page mirrors config/nexus.example.yml and the env-var table in
monitor/README.md. Whenever you add, rename, or remove a config key,
or change its default:
- Update
config/nexus.example.ymlfirst (this is the canonical doc, with the long-form rationale in YAML comments). - Update this page's index table and the section for the key.
- If a new env-var override is introduced, update both this page and
monitor/README.md's env-var table.
The two pages drift quietly when only one is changed. See Development for the standing convention.