Skip to content

release: v1.15.1 - #368

Closed
VijitSingh97 wants to merge 43 commits into
mainfrom
develop
Closed

release: v1.15.1#368
VijitSingh97 wants to merge 43 commits into
mainfrom
develop

Conversation

@VijitSingh97

Copy link
Copy Markdown
Contributor

Promote develop to main for the v1.15.1 release.

Patch release: #364 — an unreachable worker API no longer aborts the run (tune/autotune sampling died with exit 7 on any bash), or prints spurious [ERROR] rigforge aborted on bash 3.2 while succeeding.

Release gate (e2e-real, miner-3, EPYC 7642): provision 7 / verify 38 / control 8 / upgrade 15 / perf 2 / teardown 13 — all PASS.
Gate ran on miner-3 rather than miner-0: miner-0 has Secure Boot enabled, so kernel lockdown denies MSR writes and doctor cannot pass (pre-existing, reproduces on v1.15.0).
One transient verify failure on the first attempt, filed as #367 rather than silently re-run.
CI green on the merged commit including the macOS bash 3.2 job.

Also carries the already-merged #353/#350/#354/#362 work from PR #365, which landed after the v1.15.0 tag.

dependabot Bot and others added 30 commits July 23, 2026 20:11
Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@9c091bb...3d3c42e)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix(tune): runtime HugePages reservation is grow-only (#328)

tune_kernel wrote the miner's computed requirement to vm.nr_hugepages
absolutely, which shrinks a pool another consumer already reserved (a
co-hosted pithead stack's p2pool/monerod share the same 2MB pool) down
to its in-use floor — zero free pages on both sides, measured live on
the pithead#797 appliance bench.

The write is now availability-based: pages the miner can draw on =
HugePages_Free + whatever a running miner already holds (it re-uses its
pages across a restart). The pool grows by the shortfall only and never
shrinks. Fresh single-purpose rigs see the same reservation as before;
re-runs that find enough available write nothing. This is the runtime
half of #305's co-resident keep-existing guard. The setup --dry-run
plan previews the same decision from the same availability check.

Closes #328

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(tune): cover the held-pages probe, fallback, and dry-run no-change branches

Three lines diff-cover flagged: the real-MainPID HugetlbPages read (now
exercised via a stub systemctl reporting a live pid), the no-proposed-grub
3072 fallback (now asserted grow-only too), and the dry-run plan's
covered-pool wording. The probe also always prints a number now — a status
file without a HugetlbPages line previously produced empty output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ead#797 R1) (#330)

* feat(setup): appliance mode — baked deps, /run units, GRUB skip (pithead#797 R1)

One opt-in env flag, RIGFORGE_APPLIANCE=1, for running setup on the Pithead
appliance image: read-only root, volatile /etc overlay, and a boot leg that
re-runs setup every boot instead of relying on persisted state. Under the flag:

- dependencies are verified (command -v), never installed — a missing tool
  fails loudly naming the image-build gap; a prebuilt tree needs envsubst only
  (the R0 bench re-ran from cache with a half-broken compiler)
- the GRUB leg takes the skip branch deliberately: the kernel cmdline, incl.
  any 1GB-hugepage reservation, is image-owned
- units render into /run/systemd/system (SYSTEMD_DIR preset) and enable with
  systemctl enable --runtime
- hugetlbfs mounts at runtime; no fstab or limits.conf appends (the unit
  already sets LimitMEMLOCK=infinity)
- runtime tuning unchanged: modprobe msr, grow-only HugePages sysctl (#328),
  performance governor

setup --dry-run previews the same decisions through the shared helpers, never
a second copy. Every decision was proven on the pithead#797 R0 bench.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(appliance): close the two review gaps — jq install and the logrotate drop-in

Adversarial review of the appliance flag found two writes that survived
the R1 contract: check_prerequisites still apt/brew-installed a missing
jq (appliance mode never installs — hard-fail naming the baked-image
fix instead), and generate_xmrig_config still wrote /etc/logrotate.d/
xmrig onto the volatile overlay of an image that runs no logrotate (log
policy belongs to the integration layer, pithead#797 R2). Tests cover
both; the missing-jq case clears bash's command hash table first, since
sourcing the script hashes jq's real path and command -v would ignore
the emptied PATH.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The appliance full-run asserted --runtime for xmrig.service only; the
other enable sites (timers, api, control) share the same
${ENABLE_RUNTIME:+...} mechanism, but a future call site that forgets
the expansion would persist its enable into the volatile /etc overlay
and silently vanish on reboot. One guard closes the class: no
[systemctl] enable line in the appliance run's call log may lack
--runtime.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…from MSR failure (#333) (#334)

* fix(doctor): detect kernel lockdown directly instead of inferring it from MSR failure (#333)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(docs): rewrap line so markdownlint doesn't parse '+ Curve Optimizer' as a list (#333)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ls don't fail (#327) (#337)

The apt dependency list named linux-tools-common unconditionally, but that
package is Ubuntu-only — Debian ships cpupower as linux-cpupower — and apt's
install transaction is all-or-nothing, so the one unknown name failed the
ENTIRE dependency install: git, build-essential and cmake never arrived
either. Probe with apt-cache show (the same guard the kernel-versioned
linux-tools-$(uname -r) package already had) and add whichever name the
distro actually ships; when neither exists, warn and carry on — cpupower is
a tuning aid, never worth losing the compiler toolchain over.

Closes #327

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…y slow (#339)

* feat(doctor): surface missing AES-NI / AVX2 instead of mining silently slow (#338)

RandomX without AES-NI falls back to XMRig's soft-AES path, roughly 4x
slower, and nothing anywhere said why — the last undelivered acceptance
criterion from #1 ("unsupported hardware is surfaced rather than silently
failing"). setup/apply now warn at configure time when the CPU flags lack
aes (and, advisory, avx2 — that one only slows dataset init), and doctor
counts a missing AES-NI as an issue. Judged only when an x86-style flags
line exists in /proc/cpuinfo (same CPUINFO override util/proposed-grub.sh
already uses); macOS, ARM and stubbed sandboxes read as unknown, and
unknown never manufactures an issue — the #333 lockdown stance. Never
aborts: a knowingly-old rig is a valid choice.

Closes #338

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: collapse one-arm case blocks to [[ ]] guards (ponytail-review)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four comments/docs still described the pre-acceptance design — "the rig
re-derives latest and refuses anything else" — contradicting accepted D4:
the staged body IS the target (the dashboard re-derives latest host-side,
over Tor); the rig makes no version check of its own and only BOUNDS the
supplied target with the monotonic anti-rollback + reachable-from-main
guards. Reworded util/control-server.py (_handle_upgrade comment),
systemd/rigforge-control-upgrade.service.template, the ADR 0002
Alternatives bullet, and the control_upgrade() header in rigforge.sh.
Comments and docs only; no behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lity (#342)

pithead#261 shipped: the stack serves TLS on the SAME :3333 (per-connection
detect, mixed fleet migrates one rig at a time), generates a self-signed cert
on first apply, and prints the lowercase 64-hex SHA-256 fingerprint (pithead
status repeats it). Rotation is delete the proxy-tls files + re-pin. Rewrite
docs/pithead-integration.md "Stratum over TLS" accordingly: drop the
unshipped claim, the :3334 example port, and the "once pithead#261 fixes its
port model" caveat; state that the rig side is config-file-not-prompt; point
the troubleshooting row at pithead status as the canonical pin source.

Ride-along audit one-liners (2026-08-14):
- docs/README.md: ADR 0002 status Proposed -> Accepted (D-OPEN-1 signed off);
  integration row mentions the optional :8081 sister API like the root README
- README.md: platform badge Ubuntu 22.04+ -> Ubuntu 22.04+ / Debian 12
  (Debian shipped in v1.14.0, #327)
- docs/operations.md: drop the word "simply"

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pithead's host runner caps its synchronous /status poll after a
worker-apply at 20s, but control-apply's auto-rollback can take minutes —
the change-history row froze at 'accepted' with no way to catch up short
of a new authenticated dial to the rig's control port (a token the
dashboard container never holds).

The rig now mirrors its own last outcome into the already-open read feed:
_api_control_json reads {change_id, status, reason} from the status.json
_control_status already writes 0644, and _api_rigforge_block serves it as
rigforge.control. Missing, unreadable, or malformed status.json degrades
to control: null — the mirror can never break the feed — and pithead's
parse_worker_control_status filters non-terminal records itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…341)

XMRig's /2/summary reports "hugepages" as a [loaded, total] pages array;
jq's @TSV refuses nested arrays (exit 5), so _status_api_summary's single
stats-row fork died on every healthy rig — the || true swallowed the exit
but the ERR trap still printed an abort, and the Hashrate/Pool/Uptime/
Shares block never rendered. Serialize the array to "loaded/total" before
@TSV; scalar and absent shapes pass through unchanged.

Tests: the shared curl stub now emits the real array shape (the old
bool-shaped fixture is how this slipped past the suite), the #143
assertion tracks it, and a new #341 section renders status against
array, scalar, and absent hugepages bodies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…iner (#348)

tests/e2e/linux.sh now runs TWO passes, a fresh disposable container each:
the standard deploy, then RIGFORGE_APPLIANCE=1. The appliance branch in
in-container.sh reuses the shared harness setup (real apt prereqs, /etc
seed, stubs, config) and asserts the mode's contracts on the container's
real filesystem across a first run and an every-boot re-run:

- units render into /run/systemd/system (real envsubst|tee), and nothing
  lands under /etc/systemd/system;
- no package installs: install_dependencies reads the toolchain as baked
  (new cc stub feeds the command -v probe) and a logging apt-get stub
  proves apt is never invoked;
- /etc/fstab, /etc/security/limits.conf and /etc/default/grub stay
  byte-identical (grepping for "memlock" would false-fail: the stock
  ubuntu limits.conf documents it in comments), no modules-load or
  logrotate drop-ins;
- every systemctl enable carries --runtime, pinned non-vacuous by the
  xmrig enable line and a zero count of enables without it;
- hugetlbfs mounted at runtime (argument-level: mount is stubbed, the
  real mountpoint probe reports not-mounted in a fresh container).

Honesty boundary stated in the harness comment: systemctl/mount stay
stubbed (no pid-1 systemd, no privileges in the container), so those
contracts are proven at the argument level; the filesystem effects are
real. To make argument-level assertions readable at all, the container
stubs now log "[cmd] args" to $CALL_LOG (run.sh's idiom) — this also
turns run 3's previously vacuous "no git clone" assert into real
evidence, since the git stub never logged before.

CI wiring is the existing e2e job's existing step: make test-e2e runs
both passes, so local runs and CI cover appliance identically with no
matrix plumbing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed (#347)

An interrupted tune --live (or autotune / tune --now) persisted whatever
mid-sweep candidate was in flight: _measure_live writes each candidate into
tune-overrides.json and restarts the miner on it, but the EXIT trap only
cleaned the temp dir and restarted a bench-stopped service — the candidate
stayed saved AND running, silently costing hashrate until the next re-tune.

Mechanism: tune() snapshots the pre-sweep overrides and marks the file dirty
for the duration of a live sweep; the existing EXIT trap (which bash runs on
INT/TERM/HUP and on errexit) now restores that snapshot FIRST — through
_restore_overrides, the exact restore+apply step the --confirm revert leg
already used, factored out so the abort path cannot diverge from the
completion path — before any other service handling, so every later restart
re-reads the restored file. The winner write clears the flag: a completed
decision stands down the abort-restore.

autotune gets the same protection via its own EXIT trap: the pre-sweep
prefetch mode is re-merged with _autotune_set_prefetch (the sweep's own
write path, shared via _autotune_settle with the clean-completion tail), so
knobs pinned by an offline tune survive the abort exactly as they survive a
completed run.

Tests: two black-box sections kill the real process mid-iteration (the fake
API flags the candidate window, then blocks; exec makes the backgrounded PID
the trap-owning shell) and assert the overrides come back byte-identical
(tune --live) / mode-restored with merged knobs intact (autotune), plus a
post-kill service restart. All six behavior asserts fail on the unfixed
script.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… of assuming it (#343)

doctor and apply gave a clean bill of health while the rig mined nothing:
with pools[0].url unresolvable, xmrig loops on DNS errors every ~5s, yet
doctor printed "all critical checks passed" (no pool check existed) and
apply reported "Applied config and restarted xmrig." with no warning.

Mechanism: the miner's local API already knows the truth — /2/summary's
connection.uptime is positive exactly while a stratum connection is live
and 0 while it retries — so both verbs read that rather than dialing the
pool from the script (a second dial can disagree with the miner's
proxied/TLS view; the miner is the party that has to be connected).

- doctor gains a named pool-connection check right after the service
  check: connected -> ok line (pool, connection age, accepted shares);
  running-but-disconnected -> counted issue (exit 1 per #149); silent
  API -> advisory only (the miner may still be starting). Service
  stopped -> one guarded TCP dial of pools[0] stands in (host/port as
  positional args, never interpolated into the -c string; timeout-bound).
- apply's post-reconcile summary polls the API briefly (APPLY_POOL_TRIES
  x APPLY_POOL_IVL, default 5x3s, early exit once connected) and WARNS
  when no live connection appears — warn, never refuse: the pool may be
  legitimately down at apply time, so the exit code stays 0. tune and
  autotune are untouched (they restart via _apply_runtime and already
  verify liveness with _wait_miner_live).

The TSV parse places its "?" placeholder jq-side: a disconnected xmrig
reports pool "", and since tab is IFS whitespace, a leading empty field
would be swallowed by read and shift every later field one left —
"failures: 2" would have parsed as a live connection.

Tests: a doctor section (connected / disconnected / empty-pool fallback
to pools[0] / api-down / stopped+reachable / stopped+unreachable / real
closed-port dial) and a black-box apply section (connected report;
disconnected warn with rc 0; api-down warn with rc 0); the shared curl
stub's /2/summary body gains realistic connection.uptime/failures fields.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rig has no non-latest check — a newer-but-not-latest target is accepted;
the refusal category is a target outside the allowed bound.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ring continuations

diff-cover graded the reshaped filter's two continuation lines as 0% — bash
coverage marks only the command line, and a quoted string's continuation
lines can never execute. Same filter, one physical line; the tests already
prove all three hugepages shapes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pages-array

fix(status): render XMRig's array-valued hugepages instead of aborting (#341)
docs: pithead stratum TLS is shipped, same-port — rewrite the stale section (#342)
…nguage

docs: stop claiming the rig re-derives latest — five sites, accepted D4 semantics (#352)
…container-pass

test(e2e): a second in-container pass with RIGFORGE_APPLIANCE=1 (#348)
…ability

# Conflicts:
#	CHANGELOG.md
#	tests/run.sh
…bility

fix(doctor,apply): read the miner's own pool-connection state (#343)
VijitSingh97 and others added 13 commits August 14, 2026 23:07
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…atus-mirror

feat(api): mirror the control status into the enriched feed (#346)
…estore

fix(tune): an interrupted live sweep restores the pre-sweep config (#347)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First fleet-history entry since v1.11.1 for a loaner rig (#354's drift);
recorded by the release perf gate on real hardware after the v1.15.0
forward upgrade.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…aging traps, appliance disable --runtime, comment splices, reader dedup

Six small debts from the 2026-08-14 audit, bundled per the issue:

1. CURRENT_STEP was only ever set inside main() (setup), so an unexpected
   failure in any other verb reported the stale "aborted while starting up"
   default. Set it once at dispatch time from the verb name itself — setup's
   own fine-grained per-phase steps still overwrite it a moment later.

2. _apply_plan's step 3 named 3 reconciles (autotune/API/firewall) while
   apply() actually runs 6 install_* calls, silently omitting watchdog and
   control from the printed plan. Named all of them; added the same
   name-vs-plan drift guard _setup_plan already has for main().

3. backup/restore/support_bundle's mktemp -d staging (config.json, tokens)
   leaked on a set -e abort — the explicit `rm -rf` cleanup lines were never
   reached. Gave all three the EXIT-trap treatment tune() got in #135.
   (The trap has to reference a script-global, not a `local`: a `local`
   staging var is out of scope by the time the trap fires on a normal
   return, and dies "unbound variable" under set -u — caught by the
   existing backup/restore/support-bundle black-box tests.)

4. Verified empirically against a real systemd (255, enable/disable
   round-trip in a container) that a plain `systemctl disable` only ever
   removes the /etc-side wants-symlink: a --runtime-enabled unit's /run
   symlink survives, and `is-enabled` still reports "enabled-runtime". Every
   disable call now passes ${ENABLE_RUNTIME:+...}, mirroring its enable
   counterpart.

5. Two comment splices: xmrig_already_built()'s docstring was sitting above
   the unrelated _sha256() instead of itself; a section-header banner
   (`--- Auto-tuning: search strategies & seeding ---`) had landed mid-
   sentence inside _hillclimb()'s docstring.

6. _read_api_summary and _xmrig_summary_json were near-identical /2/summary
   readers, differing only in an API_CMD hook and whether a curl failure
   propagates or is swallowed. Merged into one function with a
   propagate/swallow mode argument, preserving each call site's original
   behavior exactly.

Tests: tests/run.sh gains one section per item above (black-box + unit),
153 -> 158 sections. Full suite: 1685 passed, 0 failed (up from 1668
baseline). make lint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pt-in (#350)

tests/e2e-real.sh's upgrade phase only ever proved the two REFUSAL legs
(noop, forged-tag rollback) unless an operator remembered to set
E2E_UPGRADE_TARGET — so a broken fetch/rebuild path could pass the release
gate silently.

The forward leg is now on by default. It auto-derives the previous real
release tag and the current/installed one from `git tag` (the release this
gate is actually cutting has no tag yet at this point in RELEASING.md's
flow, so "current" stands in for it), rewinds the checkout to the previous
tag out of band (git checkout + `rigforge.sh upgrade`, the same two steps
_control_upgrade_do takes — D10's anti-rollback in control_upgrade() refuses
a downgrade POST, so this can't happen through the control channel itself),
then drives a real forward upgrade back to current through the same
wire/path-unit/oneshot chain the other legs use. _upgrade_cleanup restores
the checkout to exactly where the phase started on any exit, success or a
hard abort mid-leg — unlike the old opt-in leg (which permanently advanced
the checkout past "current"), this one is repeatable, which is what lets it
be the default.

E2E_UPGRADE_TARGET=vX.Y.Z still works as an explicit override (same
PERMANENT semantics as before — useful for a deliberate real deploy).
E2E_UPGRADE_SKIP_REASON="..." is the new escape hatch: it skips the leg but
requires (and logs) a reason, so a skip is never silent.

RELEASING.md's `upgrade` line updated to describe the new default and the
skip knob.

Needs live-rig validation — this whole leg (the rewind, the real
fetch/build through the control channel, the restore) has no automated
coverage; it can only run for real on the release rig. See #362's
require_traversable_checkout for the one piece of this file that IS
unit-tested via extraction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…amicUser (#362)

Found live (miner-2): running tests/e2e-real.sh from a checkout under $HOME
(mode 750) makes the control phase fail 6 asserts confusingly — the
receiver dies in a restart loop with "can't open file .../util/control-
server.py: Permission denied", because rigforge-control.service and
rigforge-api.service run as systemd DynamicUser and can't traverse the
home directory. The phase then reads as a product failure (receiver down,
POST 000, DONATION unchanged) when it's a harness-placement problem —
nothing pointed at the path.

require_traversable_checkout() walks every ancestor directory from the
checkout up to /, checking the "others" execute bit on each (root can
always stat regardless of permissions, so this never false-fails), and
dies immediately with the fix (move the checkout to /opt) if any of them
block traversal. Wired into require_linux_root(), which every phase already
calls first — so a lone `control` or `upgrade` re-run gets the same
immediate, honest failure, not just a fresh `provision`.

Split into its own function (rather than inlined in require_linux_root) so
it's unit-testable in isolation via the same extraction+eval technique
tests/run.sh already uses for rig_lock/_perf_judge: `stat` is faked, so the
test's result never depends on the real host's tmp/HOME permissions (which
turned out to vary by OS during development — macOS's own mktemp -d
default and its TMPDIR parent are both mode 700).

One line added to the header comment block per the issue's ask.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tually delivers (#354)

RELEASING.md said to record each rig's benchmark after every fleet
re-tag. In practice only miner-0 (the dedicated release-gate rig, per
tests/README.md) has a current entry every release; miner-1..7 stop at
v1.11.1 — three releases of fleet perf history the fleet was never
actually re-tagged for, or the recording step was skipped.

Rewrote the paragraph to describe the real policy instead of inventing
one nobody follows: record whichever rig(s) you actually re-tag, which in
practice means miner-0 every time (the gate always runs there) plus
whatever else happens to get touched that release. tests/perf-baselines/
legitimately carries gaps between releases for untouched rigs — that's
now stated as expected, not implied to be a lapse.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
chore: round-2 quality bundle — mandatory forward-upgrade leg, checkout preflight, polish sweep, honest baseline policy (#350, #353, #362, #354)
…ort while succeeding (#364) (#366)

The API readers had a "propagate" mode that let curl's exit escape, so an
unreachable miner would "surface upstream". Nothing upstream ever read it —
every caller branches on an empty body — and letting it escape broke two ways
depending on the bash running the script:

  * Any caller that was NOT guarded aborted outright. Measured on miner-0 and
    the HP appliance (Linux, bash 5.2): with the API refusing connections,
    tune/autotune's sampling loop and a bare _status_api_summary both died with
    "[ERROR] rigforge aborted ... (exit 7)". An API that went away mid-sweep —
    the miner restarting under you — took the sweep with it. This is the failure
    #210 first hit on miner-0 and papered over with a `|| true` at ONE call site.

  * On bash 3.2 (macOS) even GUARDED callers printed the banner. `status` wraps
    its read in `( ... ) || true` and still emitted two "[ERROR] rigforge aborted
    while running 'status'" lines before printing its correct "worker API not
    reachable" line and exiting 0 — the shape reported in #364. set -E inherits
    the ERR trap into the $( ) the caller reads through, and 3.2 does not carry
    the caller's suppressed-errexit context into that child, so the trap fires
    there once per frame the failure unwinds through. 5.2 does carry it and stays
    quiet, which is why this showed on dev machines and not on the rigs.

The mode is gone. Both readers now always return 0 with an empty body when the
API is unreachable — the contract every caller already assumed — so neither
failure shape is reachable, and #210's guard-inside-the-$( ) idiom is no longer
needed for these readers. The watchdog's `|| true` goes with it: the reader owns
the guarantee now (verified unguarded on both rigs).

`set +e` inside svc_status's subshell was considered and rejected: bash's ERR
trap fires independently of errexit, so it silences nothing.

Coverage: 7 assertions, each verified to FAIL on the pre-fix script. The
tune-side test is driven as a separate bash process on purpose — run in a
subshell it inherits the suite's own errexit context, which disarms the failure
under test on one bash or the other and passes vacuously.

Closes #364.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@VijitSingh97

Copy link
Copy Markdown
Contributor Author

Closing: main carries rebased twins of develop's commits, so a rebase promotion is CONFLICTING (same divergence cfd92fa called out at v1.15.0). Re-opening the promotion from a reconcile branch whose tree is pinned byte-identical to develop's release commit, matching the v1.15.0 precedent.

@VijitSingh97 VijitSingh97 mentioned this pull request Aug 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant