Skip to content

Releases: p2pool-starter-stack/rigforge

RigForge v1.12.0

Choose a tag to compare

@github-actions github-actions released this 19 Jul 21:57
Immutable release. Only release title and notes can be modified.

The pithead#597 producer release: the control-upgrade /status contract a one-click worker
upgrade can poll without string-matching, a fail-closed anti-beacon throttle, corrected
security docs, and the remote-upgrade chain codified as a repeatable real-hardware e2e phase.

Added

  • control-upgrade status contract: started / noop / throttled + version echo (#320).
    Consumer findings from pithead's one-click worker upgrade (pithead#597): the oneshot now writes a
    non-terminal started record the moment it claims a staged intent (so a poller can tell mid-build
    from queued, and a started that outlives the oneshot means the run was lost); the already-on-target
    refusal reports noop and the throttle window reports throttled instead of collapsing into
    failed; a successful upgrade's reason names the landed version. Additive to the ADR 0002 D3/D6
    model — D6 amended in place, contract documented in Operations › Writable control path.
  • tests/e2e-real.sh grows an upgrade phase (#322). Codifies the real-hardware
    control-upgrade run that caught both real bugs in this chain (#308's dubious-ownership silent git
    death, #318's origin/HEAD refusal) — invisible to the unit suite by design, since it stubs git. A
    noop leg proves the wire → path unit → root oneshot → /status round trip; a rollback leg forges
    a local tag unreachable from origin/main and asserts the D10 refusal rolls back with the tree,
    VERSION, and miner intact and the D6 throttle stamped; an opt-in E2E_UPGRADE_TARGET leg drives
    a real forward upgrade. Runs inside all between control and perf, with the same
    snapshot-and-revert guarantees. Also the producer half of pithead#597's cross-repo gate.

Fixed

  • The anti-beacon throttle fails CLOSED on an unusable state dir (#321). It used to proceed
    unthrottled (with a warn) when it couldn't take its lock — inverting the guard's whole purpose on
    exactly the kind of degraded box an attacker might arrange. Now the upgrade is refused with a
    failed status whose reason names the throttle state, distinct from throttled so a consumer
    doesn't retry-later forever against a broken rig.
  • Disabling the control path re-stops the receiver explicitly. On the v1.12.0 pre-tag gate,
    a loaded systemctl transiently dropped the stop half of disable --now: the unit files were
    removed but the running receiver was left orphaned — a live write surface on a rig whose config
    said control was off. The disable path now issues one explicit re-stop (no-op when the stop
    landed). The control e2e phase also gained the polled receiver-up gate the upgrade phase
    ships with — its single-shot check hit the same pinned-CPU cold-start race on the gate run.
  • Security-doc drift on the remote-upgrade trust model (#321). SECURITY.md claimed the remote
    path is "verified by SHA256SUMS hash only" — it never was; the real anchor is the ADR 0002 D5
    design (git checkout of the release tag over TLS, origin/main ancestry, immutable releases), and
    the doc now says so. ADR 0002 D8's "a flock serializes concurrent triggers" corrected in place:
    the only flock guards the throttle stamp; serialization is Type=oneshot + newest-wins spool
    draining.

RigForge v1.11.2

Choose a tag to compare

@github-actions github-actions released this 18 Jul 21:49
Immutable release. Only release title and notes can be modified.

Consistency-audit patch: CLI-surface and docs cleanups, plus a real fix to the remote-upgrade
reachability guard surfaced by the first real-hardware control-upgrade e2e (miner-0).

Fixed

  • control-upgrade's reachability guard pins origin/main (#318). The D10 guard checked the
    target tag against origin/HEAD; on a fresh clone that resolves to develop (the repo default),
    and release tags are merge commits on main that develop doesn't contain — so such a rig
    refused every legitimate remote upgrade with a rollback. Releases are cut from main, so the
    guard now checks ancestry against origin/main explicitly (fail-closed when the ref is absent,
    e.g. a single-branch clone). ADR 0002 D10 amended in place. Deployed rigs were unaffected (their
    clones point origin/HEAD at main).
  • control-upgrade joins the CLI surface (#312). It's now listed in help with the other
    internal verbs, rejects stray arguments like its siblings, and is documented in
    Operations › Writable control path. The unknown-command hint no longer maintains a second,
    drifting verb list (it had silently lost watchdog and both control verbs) — it points at
    help instead.
  • Unknown-option errors name the verb's valid flags (#314). Every Unknown option for <verb>
    message now both lists the flags that verb accepts and points at help; previously some verbs
    did one, some the other. New tests cover the previously untested backup/restore/support-bundle
    error paths, plus a drift guard on the template.
  • Test suite: the control_upgrade unit-test git stub no longer matches subcommands by position.
    v1.11.1's safe.directory fix shifted git's argument positions, silently breaking the stub's
    $3 dispatch — the two rollback tests failed on develop from that commit until now.

Changed

  • README: "What it does" now covers day-2 operation. New-user feature tour extended past
    provisioning: tune/autotune, the watchdog and max_temp_c, doctor/status, the bios
    walkthrough, backup/restore/support-bundle/completion, the opt-in Pithead control path,
    and clean uninstall.

RigForge v1.11.1

Choose a tag to compare

@github-actions github-actions released this 18 Jul 19:26

The first published v1.11 release. v1.11.0 was tagged but not published: its remote worker-upgrade
path (#308) was inert on real rigs — fixed here. Everything under [1.11.0] below (co-resident
HugePages sizing #305, the remote worker-upgrade path #308) ships in this release, with the fix.

Fixed

  • control-upgrade now works when triggered on a real rig (#308). The privileged upgrade oneshot
    runs as root with no $HOME, so git couldn't read root's safe.directory config and fatally
    refused the operator-owned install as "dubious ownership" — every git call in the upgrade failed and
    the upgrade silently died (rolling back to the same, unbuilt state). Each git invocation in the
    upgrade path now pins -c safe.directory="$SCRIPT_DIR", so fetch / reachability / checkout run
    regardless of the unit's environment; a drift guard keeps a future git call from dropping it. Caught
    by the real-hardware (miner-0) upgrade e2e — the stubbed unit suite can't reach it (it stubs git).

RigForge v1.10.0

Choose a tag to compare

@github-actions github-actions released this 17 Jul 18:43

Follow-ups from the post-v1.9.0 re-validation of the 2026-07 repo scan: one operator-facing check
added, three ERR-trap / state-durability edges closed. All four issues (#278, #290, #296, #301) were
filed during the scan and its regression hunt.

Added

  • doctor reports control-receiver health when the control path is enabled (#278). With
    control: enabled, doctor now checks that rigforge-control is active and that its /status
    endpoint answers (an authed probe over loopback; 200/503 both count as up — 503 just means no
    change applied yet), so an operator (or Pithead) can't believe the writable path is live while the
    service is dead, crash-looping, or firewalled. Enabled-but-down is a counted issue with a
    journalctl -u rigforge-control hint; the token is never echoed; doctor stays silent when control
    is disabled.

Fixed

  • An unverifiable numa_nps/smt BIOS item no longer vanishes from the resumable state (#296).
    If a fresh probe comes back unknown at verify time (e.g. lscpu can't read the CPU model, or SMT
    isn't exposed in sysfs) the item is now kept pendingmemory_profile/power_boost already
    resurrected, numa_nps/smt didn't — instead of silently dropping from rigforge-bios.json. The
    "can't verify" hint is also per-item now (the old text named dmidecode, wrong for NPS's
    lscpu/sysfs probe and SMT's sysfs probe).

  • A host with no RAPL power sensors no longer spams the ERR trap during tuning (#290, #301). When
    RAPL watts sampling is on but the powercap sysfs is absent, _rapl_sum legitimately returns
    non-zero; under set -Eeuo pipefail that fired the misleading "[ERROR] rigforge aborted while
    starting up" line at every sample — the same class fixed for the hashrate grep in #277. Both the
    offline bench path (_xmrig_bench, #290) and the live-tune path (_measure_live, #301) now guard
    the sampling the way _autotune_sample already did. Cosmetic on the fleet (all rigs have RAPL);
    it only bit generic/older hardware.

RigForge v1.9.0

Choose a tag to compare

@github-actions github-actions released this 17 Jul 16:49
d353c0b

Fixes and hardening from the 2026-07 full-repo scan (five independent review passes over the whole
tree: security, shell correctness, util/tests/systemd, docs accuracy, and test gaps), plus a
post-merge re-validation pass over the result. All 13 scan issues (#265#277) closed.

Fixed

  • Explicit false pool booleans are honored (#265). jq's // operator treats false like
    null, so "enabled": false (a parked failover pool) and "keepalive": false were silently
    rewritten to the true default — the pool mined anyway. A single-line restore pass (same shape
    as the #115 tls-fingerprint pass) re-attaches explicit falses after normalization, and the tune
    seeds (_seed_g/_seed_wr) keep an explicit base-config false instead of benchmarking with
    the wrong MSR/1G-pages behavior.
  • tune --bench no longer falsely rejects healthy candidates as "throttled to 0 MHz" (#266).
    For an even sample count, awk printed large kHz medians in scientific notation (4.6275e+06),
    which the shell floor mangled into 4 — ~97% of candidates per run were rejected on real
    hardware and the search silently kept the seed baseline. The freq writer now floors to whole
    kHz; a follow-up keeps an empty sample set (no readable cpufreq) as "no reading" instead of a
    literal 0, which would have re-introduced the false trip from the other side.
  • The standalone perf release gate exits non-zero on a regression (#267). perf() counted
    failures but never called summary, so sudo bash tests/e2e-real.sh perf printed a red
    regression and exited 0 — a scripted release check would have passed a slower build. The #214
    unit test now exercises the real exit plumbing instead of reimplementing it.
  • The guided bios flow walks and verifies the EPYC NPS item (#268). numa_nps was detected
    and persisted (#201) but never rendered in the checklist and never verified — a pending-NPS-only
    state printed "Everything's already set" and a verify run silently dropped the item. It now
    guides to NPS4 (mirroring doctor's recommendation) and counts applied/still-pending correctly.
  • restore validates the backup's config before overwriting the live one (#274). A corrupted
    or cross-host archive whose config.json fails parse_config is rejected with the live config
    left byte-untouched, instead of bricking a working rig until the next apply.
  • control-apply writes a terminal status on every exit path (#276). An unreadable rollback
    snapshot used to ERR-trap the root oneshot out with no status written, leaving the receiver
    serving the stale previous outcome forever; it now records failed with the cause (additive
    fourth outcome next to applied/rejected/rolled_back). The two rollback outcomes are also
    distinguishable now: "rolled back and live" vs "rollback re-apply also failed to restore
    liveness".
  • compute_build_jobs caps at 1 job on sub-1GB hosts (#277). Integer truncation made the RAM
    cap vanish on exactly the hosts it protects, compiling with -j$(nproc) — the OOM scenario the
    cap exists for. A zero-hashrate bench iteration also no longer spams the journal with misleading
    [ERROR] rigforge aborted lines from the ERR trap (#277).
  • e2e-pithead cleans up the right rig-lock holder sidecar (#269). The cleanup still removed
    the pre-#244 /run/rig-e2e.holder path, so every run left a stale "miner-0 busy" breadcrumb at
    /var/lock/rig-e2e.lock.holder for the next blocked arrival; a drift guard now fails the suite
    if the old path ever reappears.
  • Docs accuracy (#271 + re-validation). Coverage-scope claims (kcov instruments
    rigforge.sh + util/proposed-grub.sh, not all of util/), diff-cover's base branch, which
    verbs self-elevate vs use inline sudo, the Linux-only command list, the ADR's control-apply
    command name, the failed outcome in the control-status enums, and the NPS item in the
    guided-bios docs.

Security

  • config.json is created mode 600 from the first byte (#270). The fresh-config write went
    through the process umask before its chmod 600, leaving a short window where another local
    user could open the file the operator is about to fill with a wallet and ACCESS_TOKEN. Now
    created under umask 077 like every other secrets-bearing file. (The 2026-07 security pass
    found nothing else: token comparison, control-path fail-closed posture, unit hardening, and
    secrets handling all came back clean.)

Added

  • control phase in the release e2e (#272). tests/e2e-real.sh now drives the writable
    control path (#236) against real systemd for the first time — enable, POST a change, watch the
    path-unit → root-oneshot chain reach applied, assert the config/revision/miner, then revert —
    with an idempotent cleanup trap that guarantees the rig never stays control-enabled. Wired into
    all between verify and perf, and into RELEASING.md's gate list.
  • Test-coverage batch from the scan (#273, #275, #276). uninstall and apply now have
    coverage over the api/control units (a regression leaving the token-authed writable server
    running after uninstall is caught); five can't-fail assertions actually fail now (stratum-user
    label, dry-run drift guard, envsubst NFT_PATH, two-way reference-config key drift, the #reown
    skip is explicit); five failure paths are pinned (rollback double-failure, tune-abort restart,
    config-meta stamping on apply, changes-index prune, api_refresh partial failure). The two
    timing-sensitive tune black-box tests are Linux-gated (#292) — their plumbing is Linux sysfs,
    and the deterministic unit-level halves still run everywhere.

RigForge v1.8.0

Choose a tag to compare

@github-actions github-actions released this 13 Jul 04:43

Added

  • Config revision + last-change provenance in the enriched feed (#254). rigforge.config_meta
    now rides GET :api_port/1/summary: {revision, changed_at, source, last_change_id}. revision is
    a short content hash of the effective writable config — it changes iff that config changes, so a
    polling consumer can tell "same as last time I looked" from "changed under me" (and invalidate a
    stale editor prefill). It's over the unmasked config, so a pool-password change bumps it too. It's
    stamped on every path that rewrites the writable config — local apply, control-apply, restore —
    recording source (control/local/restore) and, for a control change, its last_change_id; a
    no-op apply or an autotune restart (autotune tunes runtime params, not the writable config) never
    false-bumps it. Additive.

  • Query a specific change_id on the control API (#255). GET :control_port/status?change_id=<16hex>
    returns that change's recorded outcome (or 404), so a caller whose confirmation was stepped on by
    a concurrent change can still record its own result — removing the accepted→poll race. The applier
    indexes each outcome under changes/<change_id>.json (last ~20). The no-arg GET /status still
    returns the most recent (backward compatible); auth unchanged (bearer). Additive.

  • Effective writable config in the enriched feed (#253). GET :api_port/1/summary (= /2/summary)
    now carries rigforge.config — the rig's current writable config (pools, DONATION, autotune,
    watchdog, watchdog_interval_min, max_temp_c), exactly the keys the control path accepts, read
    the same way parse_config does (canonical strings). Lets Pithead's Worker Inspect prefill from a
    live rig read and round-trip read→edit→POST /apply. Served even when the miner is down (read from
    config.json). Pool secrets are masked: pools[].pass and any tls-fingerprint are omitted, so a
    round-trip that re-sends pools must re-supply the pool password. Additive — no existing key changes.

  • IPv6 api_allow_from (#243). The API firewall scoping now accepts an IPv6 address/CIDR
    (rendering an ip6 saddr rule in the inet rigforge table) as well as IPv4, and the sister/control
    servers bind dual-stack when api_bind/control_bind is set to ::. On an IPv6-primary LAN you
    can now pin the writable control path to the stack's v6 source instead of falling back to a broad
    LAN CIDR. The per-family charset stays the injection guard; malformed addresses and a bad prefix
    hard-error.

Fixed

  • rig-lock holder write can never drop the flock (#249). The shared rig_lock holder-marker
    write is now strictly display-only and best-effort (plain write → sudo -n tee → || true); under
    set -Eeuo pipefail a failed write to a non-writable holder path used to abort the run after the
    flock was acquired, dropping it and leaving the shared box UNRESERVED — the exact collision the
    lock exists to prevent. The EXIT-trap cleanup gained a sudo -n rm fallback for a root-written
    marker. A new self-test probes the kernel flock directly (a raw flock -n fails while a run holds
    it, succeeds after) so a "reports success but never held" regression can't return; this was also
    re-confirmed live on the rig during the release gate (a raw flock -n failed 91/91 probes across a
    perf run). The read-open from #242 stays the canonical approach — it holds the flock on FD 9 for the
    whole run and survives fs.protected_regular=2, which the sibling pithead#484 exec 9> does not;
    the pithead mirror should adopt the read-open for the two to be byte-identical. Test-infra only.

  • API firewall fails closed on a rejected ruleset (#248, pre-release scan). install_api_firewall now
    checks that nft -f actually loaded the rules and hard-errors if it didn't, rather than apply
    swallowing the failure with || true and still logging "firewall active". A charset-valid but
    structurally-invalid IPv6 api_allow_from could otherwise pass parse_config, fail to load, and
    leave the API ports open while the log claimed they were scoped. parse_config also now warns when
    api_allow_from is IPv6 but api_bind/control_bind are still IPv4 — a silent no-op scope.

  • rig-lock helper: portable timestamp + non-root holder default (#244). The shared rig_lock
    breadcrumb now defaults beside the lock (<lockfile>.holder) instead of root-owned
    /run/rig-e2e.holder (a non-root box couldn't write there — noise, no breadcrumb), and stamps
    date -u +%Y-%m-%dT%H:%M:%SZ instead of the GNU-only date -Iseconds (BSD/macOS rejects -I).
    Mirrors the sibling pithead fix; both e2e helper copies stay identical. Also from a pre-release
    scan: dropped the sudo chmod fallback and added a symlink-path guard on the lock/holder (a
    planted symlink in world-writable /run/lock could otherwise redirect a root-side write).
    Test-infra only.

  • rig-lock survives a non-root reserve (#242). The shared rig_lock helper now opens
    /var/lock/rig-e2e.lock read-only (9<) instead of write (9>). A lock file first created by a
    non-root flock (a manual reserve after a reboot clears the /run/lock tmpfs) is owned by that
    user, and fs.protected_regular=2 then blocked even root's O_CREAT-write of it with EACCES,
    aborting the release perf gate until someone rmed the file. A read-open is never guarded and
    flock works fine on a read fd, so this sidesteps it without ever removing a possibly-held lock.
    The ~/README.md box contracts also now say to reserve as root. Test-infra only.

Security

  • The remote control path can no longer strip thermal protection (#257). A POST :control_port/apply
    that would disable the watchdog, or unset / set an out-of-band max_temp_c (a rig's thermal
    cutoff), is now refused with 400 at the receiver (util/control-server.py), with a mirrored
    applier-side backstop in _control_commit. Before this, such a change passed the liveness rollback
    gate (the miner keeps hashing) and was recorded as a normal successful applied — so Pithead's
    one-click Worker Inspect edit could silently remove a rig's thermal cutoff. A local rigforge.sh apply on the box is unaffected (operator physically present). Defense-in-depth: any allowed change
    touching watchdog/max_temp_c adds a warnings[] entry to the /status outcome (additive to the
    /status shape) so the dashboard can force an extra confirmation.

  • Documented the write-capable control-token posture (#256). With the control path enabled, the
    ACCESS_TOKEN bearer is write-capable and travels in cleartext HTTP (no TLS); api_allow_from scopes
    the source but not the token in flight. SECURITY.md, docs/pithead-integration.md, and ADR 0001
    now state this explicitly and prescribe LAN isolation; on-LAN TLS is deliberately deferred (reverse
    proxy is the escape hatch). No code change — a conscious, documented decision.

RigForge v1.7.0

Choose a tag to compare

@github-actions github-actions released this 11 Jul 19:48
c1619b2

Added

  • Writable control path (#236). An opt-in, fail-closed producer for pithead's Worker Inspect:
    a separate authenticated endpoint (default :8082, control/control_port/control_bind)
    that lets the stack apply config changes through RigForge, so config.json stays the source of
    truth. Enabling it requires both ACCESS_TOKEN and api_allow_from (a writable API with no
    token or no pinned source is refused with a hard error), and the nftables scoping extends to the
    control port. An unprivileged receiver validates the shape of a change and stages it off the
    request path (so a write never touches mining); a path-triggered root oneshot snapshots the old
    config to a timestamped config-backups/, re-validates against RigForge's key allowlist (only
    pools, DONATION, autotune, watchdog(+interval), max_temp_c are writable — never
    identity, trust, path, or auth keys), writes it durably (fsync + atomic rename), applies it, and
    rolls back anything that doesn't come back to a live hashrate. GET /status reports the outcome
    with source: "control". Design and decisions in docs/adr/0001. Linux-only; off by default.

RigForge v1.6.0

Choose a tag to compare

@github-actions github-actions released this 11 Jul 02:01
16a4ccd

Added

  • Thermal sensing works on hwmon-only boards (#208). _read_temp falls back to the CPU hwmon
    (k10temp/coretemp temp1_input) when no thermal_zone exists — the shape of every rig in
    the production fleet, where the watchdog's max_temp_c and tune's temperature sampling were
    silently blind. A thermal zone still wins when present; nothing readable stays empty (a missing
    sensor never stops a miner). Docs carry the Tctl caveat: k10temp reads a control temperature
    that runs high by design, so damage-avoidance cutoffs belong well above the everyday reading.
  • Watchdog state on the wire (#212). The sister API's rigforge object gains a watchdog
    block — mode, thermal_hold, max_temp_c / resumes_below_c, live temp_c, and the strike
    count — and /health carries it too. A thermally-held miner now says "held, resumes below N"
    on the one component still alive instead of looking mystery-dead; a wedge being counted is
    visible before the restart lands. Absent or garbled state files degrade to defaults, never a
    failed refresh.
  • EPYC NPS regression detection (#201). A multi-CCD EPYC reporting a single NUMA node is NPS1
    — a BIOS update or CMOS reset silently eats the NPS4 setting and costs RandomX its per-node
    datasets. doctor's firmware advisory and the guided bios walkthrough now flag it (with the
    AMD CBS menu path) via one shared detector; desktop parts and unverifiable topologies are never
    flagged. The generic power_boost menu text also gains the server-board spellings (cTDP /
    Package Power Limit / Determinism).

Changed

  • Per-rig benchmark recording is now also the per-rig perf gate (#214). E2E_PERF_RECORD=1
    judges against the committed baseline and best-ever history before writing; a regressed
    measurement fails the phase and records nothing (E2E_PERF_FORCE=1 is the conscious
    re-record override). Previously the recording run — the only perf run most rigs ever got —
    skipped the judgment entirely.
  • Perf recording tells you how to finish the job (#206). A recorded run prints the files to
    collect and the post-merge rig reset (git checkout -- tests/perf-baselines/) that keeps the
    next tag deploy from aborting on a dirty checkout; RELEASING.md carries the same step.
  • Tab completion caught up with the CLI (#207). setup/apply --dry-run, upgrade --check,
    and bios --perf/--efficiency now complete, and a flag-level drift guard keeps future flags
    from shipping uncompleted.

Security

  • Release signing removed by decision; checksums stay (#205). The minisign machinery from
    #137 never actually signed a release (no keypair existed until the 2026-07-11 audit), and its
    protection only materializes for users who pin the public key outside the repo — nobody, for a
    project deployed from git tags. Rather than carry key-custody forever for a hypothetical,
    releases now ship SHA256SUMS only, and SECURITY.md says plainly what that does and doesn't
    prove. The signing machinery stays in git history if a download-based user base ever makes the
    trade worth it. Dependabot now watches the SHA-pinned actions, and a weekly workflow opens
    build-verified PRs when upstream XMRig releases (#225).

RigForge v1.5.1

Choose a tag to compare

@github-actions github-actions released this 10 Jul 23:59
105eb12

Fixed

  • watchdog: wedge recovery actually fires on an unreachable API (#210). In v1.5.0 the check
    crashed (curl's exit rode pipefail out of the probe into an unguarded assignment under
    set -e) before the strike logic ran — so the exact condition the watchdog exists for, a live
    process with a dead API, was never recovered, and the failed oneshot spammed the journal every
    tick. Caught by the #139 live wedge test on miner-0. The probe guard now sits inside the
    command substitution (an outer || can't suppress the ERR trap firing in the subshell); the
    same shape made offline upgrade --check print ERR-trap noise ahead of its one warn line,
    fixed identically. Regression tests run both paths through the real dispatch, where the trap
    is live.

RigForge v1.5.0

Choose a tag to compare

@github-actions github-actions released this 10 Jul 22:25
d705fb0

Security

  • Sister API hardening (2026-07-10 audit). The one network-facing process no longer runs as
    root: rigforge-api.service gets DynamicUser=yes, reading the 0600 config.json through a
    systemd LoadCredential instead of owning it. The Bearer check is now constant-time
    (hmac.compare_digest) and a 10 s request-arrival timeout closes the single-connection
    slowloris hang on the deliberately single-threaded server. doctor gains one advisory line for
    the open-AND-unscoped posture (no ACCESS_TOKEN, no api_allow_from) — correct on the designed
    trusted LAN, loud enough to catch before a rig faces anything else.

Changed

  • Consistency sweep (#149). doctor now exits non-zero when critical issues are found
    (cron-friendly, matching Pithead's health verb). Extra arguments to no-arg verbs (doctor foo,
    Pithead's setup --skip-deps muscle memory) error with a pointer to help instead of being
    silently ignored. uninstall gains the same EOF-guarded prompt as every other prompt (a piped
    run aborts cleanly on the default-No path) and the same strict flag loop as restore. All
    unknown-flag errors share one template (Unknown option for <verb>: '<arg>'), the four
    "Aborting." suffixes are gone, and the reboot reminder speaks [WARN] like every other
    instruction. Docs: a Privileges note (which verbs sudo inline, which self-elevate, and that
    status/logs/backup/restore never need root — the docs used to prescribe sudo for
    looking), and a written config-key naming policy (legacy SCREAMING keys frozen; all new keys
    lowercase snake_case, matching Pithead). No key or verb was renamed.

Added

  • Miner watchdog + optional thermal cutoff (#139). "watchdog": "enabled" installs a systemd
    timer (every watchdog_interval_min minutes, default 5) running one health check per tick: two
    consecutive checks seeing 0 H/s or an unreachable worker API restart a wedged miner — the
    process-alive-but-mining-nothing case Restart=always can't see — while a single blip never
    does. max_temp_c (opt-in, empty by default) adds a runtime thermal cutoff: stop above it,
    start again exactly once below max_temp_c - 5 (fixed 5 °C hysteresis). A stopped miner is left
    to systemd; an unreadable sensor never stops a healthy miner; the token never lands in unit
    files or logs. Same timer machinery as periodic autotune; Linux-only; removed cleanly on
    disable and by uninstall.

  • upgrade --check (#148). On-demand "is there a newer RigForge?" — one unauthenticated query
    of GitHub's releases API, compared against the local VERSION with sort -V, printing the new
    version, release URL, and the upgrade recipe when you're behind (an "ahead of the latest
    release" note on develop checkouts, never a downgrade prompt). Runs exactly when you type it —
    nothing scheduled, no other verb calls it, so the "no version ping" promise in SECURITY.md
    stands — and always exits 0, so it's safe inside operator scripts. doctor stays fully offline;
    its all-clear summary now just mentions the command.

  • support-bundle (#147). One verb collects everything a bug report needs — doctor output,
    version, configs, miner-log tail, tuning + unit files, hardware basics — into a mode-600 local
    tarball with secrets redacted structurally in jq (token/pass removed, wallet masked to
    first-4…last-4; a file jq can't parse is skipped, never shipped unredacted). Collects the
    minimum, uploads nothing, and says exactly what it did and didn't take.

  • setup --dry-run / apply --dry-run (#146). A numbered, machine-specific plan of exactly
    what would happen — missing packages by name, build-or-skip, the exact GRUB cmdline
    before → after and whether a reboot follows, fstab/unit/timer actions — computed from read-only
    probes, no sudo, nothing touched. A printer function, not a DRY_RUN flag threaded through
    mutating code (a printer can't mutate by construction); a drift test pins the plan to every
    step in the real pipeline.

  • Shell completion (#145). completion bash|zsh prints a static, zero-dependency tab-completion
    script (nothing auto-installs; source <(rigforge completion bash) opts in). Verbs and per-verb
    flags complete; a suite drift-guard diffs the script's verb list against the dispatch case, so a
    new verb can't ship without completion. zsh rides bash's script via bashcompinit — one list.

  • Live status summary (#143). status now opens with a one-glance block from a single
    worker-API fetch — hashrate, pool, uptime (1d 2h 3m), accepted/rejected shares, huge pages —
    then the unchanged platform detail. Facts only (no ✓/! judgment — that's doctor's), never sudo,
    and a stopped miner or bad config degrades to one explanatory line plus the platform block.

  • Shared-rig lock for the release gates (#183). e2e-real and e2e-pithead take a kernel
    flock on /var/lock/rig-e2e.lock before their first service- or API-touching action, so
    RigForge's rig-mutating gates and Pithead's e2e testing can't collide on the shared miner-0: a
    second arrival exits 75 (EX_TEMPFAIL) naming the holder (project, suite, pid, start time, via
    the /run/rig-e2e.holder sidecar), and RIG_LOCK_WAIT=1 queues instead of failing. The lock
    dies with the holding process — a killed run needs no cleanup. Pithead's harness carries the
    same helper against the same path; the path is the whole contract. Test-harness only:
    rigforge.sh itself stays lock-free.