Every shipped surface audited against a named comparator with the file or screen: the pause of 6 October had no cause on any surface for two hours (the node reports no frozen-table reason, the observer copies the flag alone, /live computes the silent share from the sliding table, Ember has no pause state); a fresh machine meets two warnings before the first screen (ad hoc codesign, no notarisation, unsigned Windows installer); every update has been urgent since the 0.3.14 manifest (fork_is_close treats any passed activation as close). Rows Q1 to Q105 with severity, hours, owner and gate; cross-cutting: one formatter table for every number, the five 6 October rule rows without a tools/ci check, the forbidden-string scope gaps. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
108 KiB
Horizon lane 6: polish. Every shipped system against the best in its class
Date: 6 October 2026, evening UK (written 20:00 to 21:00Z, while the live devnet's finality was still paused). Lane 6 of the Horizon programme. Worktree /Users/joshm/Projects/igneum-wt-horizon (branch horizon, HEAD c3aa502). Output: this file only. No file outside it was edited; nothing was built, deployed, posted or started; the installed app and every live service were read, never touched.
the project lead's bar: "a level of polish that has not been seen before." This file is the honest audit: what each shipped system does, the named comparator and the exact screen or feature it has that we lack or do worse, the ledger rows that close the gap (95 rows, ids Q1 to Q105 with gaps, each with a file or screen, a severity, hours of agent work, an owner and a gate), and what is already better than the comparator. The first ten rows are what the project lead will notice first when he opens each product tomorrow.
0. What was read and run
Read, in the worktree: app/igneum-app (src, ui, tests), app/mac/IgneumMiner.swift, app/windows/host.cpp, docs/plans/miner-ui-2.md, miner-ui-3.md, miner-ui-3-audit.md, ember-tune.md, release-0.3.14.md, docs/design/app-screens/, docs/design/miner-tuning.md, docs/fud-ledger.md (X17, X19, X21 to X28, M25 to M29, G7, G10, G13), site/ (every page, api/, lib/, verify/, build.mjs read not run, scrub.mjs, forbidden-strings.txt, downloads.json, journey.json, vercel.json, sitemap.xml), docs/design/site-polish-2026-10-04.md, docs/plans/site-ui-3-shots/, docs/plans/site-miner-2026-10-06.png, docs/review/round-4-reddit-2026-10-06.md, docs/plans/explorer.md and explorer/, relay/ (ui, api, lib, clients, playbooks, tests), tools/relay.mjs, tools/console.mjs, tools/jobs.mjs, docs/design/relay/, docs/design/console/, pool/ (src, web, tests, README), docs/plans/pool.md, docs/spec/09-pool-protocol.md, tools/community/discord-hooks.mjs and its tests and fixtures, docs/community/discord-hooks.md, tools/fleet/, tools/build-job.mjs, tools/build-remote.sh, tools/ci/, docs/plans/build-job.md, build-server.md, hands-on-build-1.md, node-changes.md, packaging/README-ship.md, packaging/ota/README.md, packaging/mac/README.md, packaging/windows/README.md, tools/observer/ (observer.mjs, README), docs/spec/03 section 3.9, and this lane's siblings finality-and-weight.md (3.1, 6) and frontier.md (3.7).
Read, read-only, in other agents' worktrees: /Users/joshm/Projects/igneum-wt-ship0315/docs/plans/release-0.3.15.md (the newest release notes; release-0.3.15.md does not exist on horizon or master), /Users/joshm/Projects/igneum-wt-wallet* (wallet 0.1.4 source, UI 3 branch), /Users/joshm/Projects/igneum-wt-gpu-fleet/tools/fleet/, /Users/joshm/Projects/igneum/vendor/igneum-node-0310/consensus/src/processes/finality.rs (the node's finality_reason).
Run (nothing live, nothing built): node --test app/igneum-app/ui/*.test.mjs (35 pass), node --test relay/test/*.test.mjs per file (24 pass), node --test tools/community/discord-hooks.test.mjs (31 pass, every live poster stubbed), one curl -s https://igneum.network/ (the allowed single fetch; saved to the scratchpad), and one read-only SQL pass over the observer's Neon tables (live_checkpoints, live_events, live_state) through the same HTTPS SQL endpoint site/api/_neon.mjs uses, to see what every surface had to render during tonight's pause. No browser automation.
Comparator claims are from memory unless a repository or page is named, and are labelled approximate.
1. The ten rows the project lead will notice first
| Rank | Id | Row | File or screen | Severity | Hours | Owner | Who it hits | Gate |
|---|---|---|---|---|---|---|---|---|
| 1 | Q1 | The pause has no cause anywhere. The node reports finality_reason (active, window filling, N of M, paused) but not the frozen-table share that held tonight's pause; the observer drops even the reason; the API has no reason field; /live computes "N% of weight silent" from the sliding table, which read 11 percent silent at 20:05Z while finality was paused (held by the table frozen at lock 6842, 57.3 percent signing). Add finality_reason, held_by (frozen index, its signing share, its expiry DAA) and lane 3's finality_provisional end to end |
vendor/igneum-node-0310/consensus/src/processes/finality.rs:1794-1807 (reason exists, no frozen share), tools/observer/observer.mjs:720 (copies finalityActive only), site/api/live.mjs:114-122 (no reason), site/live.html:520 (the silent percentage) |
the project lead-visible | 4 (node 1.5, observer and API 1, /live and /api/stats copy 1, spec 3.9 row 0.5) |
consensus-engineer (node), site owner (observer, API, page) | holder, exchange: the one line that says whether a pause is a silent third, a split, or a table waiting to expire; miner: nothing lost, but the app can finally explain itself; operator: a pause with a cause and an end time | A forced pause on Devnet 2 (one slice over a third stopped): /api/live.finality carries reason, held_by, provisional; /live reads "finality paused since 18:40Z: 89% of the sliding weight is signing, held by the table frozen at lock 6842 (57% signing) until DAA 216,402 (about 20:40Z)"; the exchange guidance in spec 3.9 carries the provisional row (lane 3 proposal 4) |
| 2 | Q2 | Ember never says "paused". The engine keeps only last_lock, last_lock_at, age_s, votes; the node line showed "#6842 · 1 h ago" under "a point the miners agreed can never be undone" for the whole two-hour pause. No finality_active anywhere in the app |
app/igneum-app/src/state.rs:247-248, src/engine.rs:3495-3497, 3896-3902 (lock parsed from the miner's LOCK checkpoint line only), ui/app.js:1350-1351 |
the project lead-visible | 3 | app owner (engine reads getFinalityCheckpoints or the miner's FINALITY line, spec 3.9 row; UI state and view test) |
home miner and rig: the app tells them finality is paused and why, instead of a lock age that climbs; pool user: the same on the pool page later | Mock state with finality_active:false, reason:"paused" renders "Finality paused since 18:40Z, 89% of the 30-day weight signing, held by the frozen table until about 20:40Z" on the node line and in the pill; view.test.mjs case; seen on the Devnet 2 forced pause |
| 3 | Q3 | A fresh machine's first 60 seconds start with a warning. macOS: ad hoc signature, no Developer ID, no notarisation; the engine strips com.apple.quarantine from its own bundle on start; the README's "Right-click > Open" bypass is gone on macOS 15 (approximate). Windows: installer and exes unsigned, SmartScreen "Windows protected your PC", then a UAC prompt for the firewall rule 20 to 50 s in before any screen explains it |
packaging/mac/build-dmg.sh:118-122, packaging/mac/README.md:51-53, app/igneum-app/src/main.rs:78, packaging/windows/README.md:76, 111, packaging/windows/build-installer.ps1:188, app/igneum-app/src/ota.rs:981-1015 |
the project lead-visible | 6 (Developer ID signing plus notarytool and stapling in build-dmg.sh 3; Authenticode signtool step in windows.yml and build-installer.ps1 2; firewall step after setup_done with a sentence on the Cards screen 1), plus the project lead: an Apple Developer account and an OV or EV code-signing certificate (purchases, hours of the project lead's time, approximate) |
app owner; the project lead (the certificates) | every new miner on every tier, Windows and macOS: Signal's and Tailscale's installers open with no warning (approximate); today ours open with two | Fresh macOS 15 VM: the DMG's app opens on double-click, spctl -a -vv says accepted and notarised; fresh Windows 11 VM: no SmartScreen interstitial, signtool verify /pa passes; the first UAC prompt appears after the Cards screen names it |
| 4 | Q4 | Every update is "urgent" once an activation height is behind the node: fork_is_close(Some(198000), 209000) is true, so the manifest's stale activation 198000 turned the 0.3.14 update into "the node is 0 blocks away. Installing now", stripped Later and skipped every safe-moment guard (the PC 1 install under a measurement job at 17:52:54Z). The same path has no finality input: an update applies through a pause |
app/igneum-app/src/manifest.rs:350-355 (daa + 1800 >= h), :322-347 (safe_to_apply, no finality), src/ota.rs:484, ui/app.js:217, docs/plans/release-0.3.14.md:79, release-0.3.15.md section 6 |
the project lead-visible | 3 (close means within 1,800 blocks ahead and not behind 1; the publisher drops a passed activation_height 0.5; Moment.finality_paused holds a non-urgent update 1.5) |
app owner | home miner, rig: no surprise restart mid-measurement or mid-pause; operator: an activation that has passed is not an emergency | Unit tests: behind by any amount is not close; publish-manifest.sh refuses a passed height; a paused mock holds the update with the words "finality is paused; installing when it resumes"; the 0.3.16 manifest carries no stale height |
| 5 | Q5 | The homepage says "final" while finality is paused, in a 90-word hero, under a share card that renders as a thumbnail. The chain scene takes src.locked and draws the dashed line labelled "final" at the newest locked block with no finality.active check (R4.6.3 was fixed on /live only); the hero is one 90-word paragraph with two bench deep links and "ships when the packaging row lands"; the miner section still says "a 24 GB NVIDIA card proves as well" while the hero and litepaper say 8 GB; home, litepaper, live and explorer share a 256 px og-small.png with summary cards |
site/index.html:810, 838 (final label), :345 (hero), :494 and site/miner.html:7, 13, 21, 265, 382 (24 GB), site/index.html:15-19 (OG) |
the project lead-visible | 3 (final label 0.5, hero 1, 24 GB sentence 0.5, four 1200x630 cards 1) | site owner | every visitor; a holder or exchange reading "final" during a pause is the worst of them | No "final" label while finality.active is false (the /live rule); hero under 40 words with one link; one proving-tier sentence on every page; every page's shared card is 1200x630 |
| 6 | Q6 | The hub shows stale data as live, said "active" for the first 20 unlocked checkpoints, says "paused" with no since or cause, and buried the pause under 401 miner_quiet and miner_back events. After the first load an API failure only changes the header to "offline: ..."; tiles, cards and "Refreshed" keep the old values. finality_active flips only after presence_window (20 on the devnet) indices without a lock, so 18:42 to 18:50Z read "active" in white with no lock forming. No finality_paused or finality_resumed event exists; live_events between 18:25Z and 21:30Z holds 203 miner_quiet, 198 miner_back, 55 difficulty, 30 checkpoint_locked (the last at 18:42:10Z) and no pause row. The checkpoint table also showed "% of active" above 100 (102.4 to 113.4 percent on indices 6900 to 6930) |
relay/ui.html:564 (stale), :367-368 (tile), relay/api/console.mjs:214, vendor/igneum-node-0310/consensus/src/processes/finality.rs:1794 and vendor/igneum-node/consensus/core/src/finality.rs:63, 75 (presence_window 240 mainnet, 20 devnet), tools/observer/observer.mjs:652 (the only finality event), live_checkpoints.fraction_active rows 6900 to 6930 |
the project lead-visible | 4 (dim every panel with "last data N min ago" 1; amber on the first under-2/3 checkpoint 0.5; finality_paused and finality_resumed events, and collapse quiet/back pairs under 5 min 1.5; clamp or explain the active fraction 1) |
fleet agent (hub), consensus-engineer (observer events, the fraction) | operator: the hub is the project lead's first screen; a holder reading the public API gets the same events | Cut the network: every hub panel dims within 15 s; a forced Devnet 2 pause turns the tile amber on the first checkpoint under two thirds, posts one finality_paused event with the cause and one finality_resumed with the duration; no fraction above 100 percent on any row for 24 h |
| 7 | Q7 | Discord said nothing. The bot has no credentials file and its timer is not installed, so tonight's pause produced zero posts; had it been live, a condition already firing at the watcher's first look is marked preexisting and never opens an incident; the pulse hides the pause in its description with no colour or title change |
docs/community/discord-hooks.md:81-82, tools/community/discord-hooks.mjs:481-485, :252-255, :158, infra/build-server/discord-hooks/install.sh |
the project lead-visible | 3 (install and one live pulse 1; a pre-existing condition opens with "since at least " 1; paused pulse in a distinct colour with a title suffix 1) | miner-community-lead (community owner) | every Discord reader, which on launch day is every miner | check prints three "set"; one live pulse in #numbers; a fixture where the pause predates the first tick opens an incident; the paused pulse renders amber with "(finality paused)" in the title |
| 8 | Q8 | The relay's security fixes are not on master. Commit 28c028b (X23 to X28: relay/lib/guard.mjs, handler.mjs, run signatures, retention) is on fud-close and the ledger-* branches only; git merge-base --is-ancestor 28c028b master is false. On this tree any of the three intake keys (one ships in every miner app) can post, sync and delete console items, read the whole feed, and every client puts the token in the URL path |
relay/api/console.mjs:249, 283-301, relay/lib/relay.mjs:34-46, relay/clients/agent.sh:10, igneum-agent.ps1:13, 72, 154, tools/relay.mjs:24 |
operator-visible (security) | 2 (merge or cherry-pick with the 47 relay tests 1; handler test that an intake key gets 403 on post, sync, delete 1) | fleet agent (relay owner) | operator: the console is the control plane for every PC and the fleet; a miner app's intake key is in every install | 28c028b is an ancestor of the deploying branch; 47 relay tests pass; the handler test above passes; relay/README.md names the deployed commit |
| 9 | Q86 | The fleet page is blind to the standing fleet, to death and to finality. page.py:43-47 publishes standing and devnet2 and the page references neither (grep 0); a dead box reads "running" for up to 20 minutes because state comes from boxes.json and standing.jsonl is never read; no fleet or console script reads finality_active, so tonight's pause was found by hand. The night the project lead ruled that 22 boxes stay up permanently, the page that shows them cannot show them |
igneum-wt-gpu-fleet/tools/fleet/page.py:38-47, dlsite index.html:243, lib/standing.py:54-56, tools/console.mjs:124 |
the project lead-visible | 6 (standing block with USD per day and the Devnet 2 gate line 2; "unreachable since HH:MM" within 2 minutes 2; finality on the page and in console.mjs chain 2) |
fleet agent | operator (the project lead reads this page every evening); every tier indirectly: a dead standing box is weight that left silently, the class of tonight's pause | the page shows the standing count and spend; a box killed by hand reads "unreachable since" within 2 minutes; a forced Devnet 2 pause reads on the page and in the console |
| 10 | Q9 | The wallet page describes 0.1.4 while UI 3 (five state words, light mode, pounds line) sits unmerged on wallet-ui-3; the MetaMask guide's testnet RPC is still labelled a placeholder, its devnet RPC port is the miner's node (26790) while a wallet-only Mac runs 26800, and wallet_addEthereumChain carries blockExplorerUrls: [] and no iconUrls, so MetaMask shows no explorer link and a blank icon |
site/wallet.html:261-266, 312, igneum-wt-wallet-ui/docs/plans/wallet-ui-3.md:93, 112, site/metamask.html:197, 225, 233, 285-286, igneum-wt-wallet/app/igneum-wallet/src/node.rs:24-26 |
the project lead-visible | 6 (wallet 0.1.5 with UI 3 published and the page rewritten 4; guide: both ports, the placeholder line removed, explorer URL and icon in the request 2) | app owner (wallet), site owner (guide) | holder: the first non-miner product; a wrong port or a placeholder RPC is a dead end at the first step | downloads.json wallet-mac 0.1.5; the page names pending, included, executed, proven, finalised; eth_chainId returns 0x116e from the guide's URL; MetaMask shows the explorer link after the add-chain button |
Next in line, outside the ten: Q10 (the explorer knows nothing about finality and swallows failures after first load, 3 hours), Q80 (the Windows installer pipeline is dead on GitHub billing) and Q12 (Ember's light mode fails contrast on the private key and every live state).
Hours for the ten: 40 agent hours, plus the project lead's certificate purchases for Q3.
2. Tonight's pause on every surface (the ledger row the project lead asked for)
Times UTC. The chain-side facts are lane 3's (finality-and-weight.md 3.1) and the observer's own rows, read tonight: last lock 6842 at about 18:39:40Z; 6843 proposed at 53.1 percent of total; finality_active false from about 18:50Z (index 6862, twenty indices without a lock, presence_window 20); no lock after 6842 as of 20:05:03Z (live_state.updated_at), total_weight 7,167, active_weight 6,346 (88.5 percent signing on the sliding table), voters 85, and no finality_reason key in the stored JSON. Sampled live_checkpoints rows:
| Index | First seen | State | Signed, percent of total | Signed, percent of active | Votes |
|---|---|---|---|---|---|
| 6820 | 18:28:30 | locked | 88.8 | 100.0 | 80 |
| 6840 | 18:42:10 (ingested after the hub came back) | locked | 79.5 | 98.6 | 83 |
| 6850 | 18:43:41 | proposed | 55.2 | 76.8 | 75 |
| 6880 | 19:00:22 | proposed | 50.1 | 81.2 | 49 |
| 6910 | 19:13:55 | proposed | 67.4 | 107.4 | 55 |
| 6920 | 19:19:07 | proposed | 74.8 | 113.4 | 78 |
| 6960 | 19:40:19 | proposed | 86.0 | 100.0 | 78 |
| 7010 | 20:04:32 | proposed | 87.7 | 99.1 | 73 |
From 6910 (19:13:55Z) every proposed checkpoint carried over two thirds of the sliding total and did not lock: the frozen table of Q5 held it (lane 3, 3.1). No surface could say so, because no field carries it. The "percent of active" above 100 on 6900 to 6930 is a display bug on the hub's checkpoint table (the active denominator lags the signers).
| Surface | What it showed 18:40Z to 20:05Z (from the code path and the rows above) | Honest? | Fix row |
|---|---|---|---|
Ember (app.js:1350) |
"last lock #6842 · 1 h 25 min ago" with "a point the miners agreed can never be undone; locks at 2/3 of the 30-day weight". No pause word anywhere | No: the age climbed and the note contradicted the state | Q2 |
Site /live (live.html:512-520) |
Status LIVE, blocks flowing, finality bar grey, "finality paused: 45% of weight silent" at 18:50Z falling to "11% of weight silent" by 20:05Z, no final region, no checkpoint ticks; "last lock #6842, 1 h ago" in the stats (:404, no active check); a hovered checkpoint block still read "locked" (:450) |
Half: the pause is named (R4.6.3) but the percentage says the opposite of why it is paused once the stayers passed two thirds | Q1 |
Homepage (index.html:810, 838, 879) |
Counter "last lock: paused"; the chain scene still drew the dashed line labelled "final" at the newest locked block | No: "final" and "paused" on one screen | Q5 |
Explorer (explorer.html, block.html) |
Identical to a normal hour; stars on locked checkpoint rows; block pages of ordinary blocks "not a checkpoint" | No: nothing marked the pause | Q10 |
Hub (relay/ui.html:367-368) |
18:30 to 18:42Z: "observer STALE N s" in red, tiles frozen, finality "active" (the hub itself was down with the Mac). 18:42 to about 18:50Z: "active" in white, no lock forming. About 18:50Z onward: "paused" in amber, "last lock #6842" in amber, eight rows "proposed" at 53 to 89 percent, Events card full of miner_quiet and miner_back |
Half: paused, but no since, no cause, no end; active for ten minutes of no lock | Q6 |
| Discord | Nothing: the bot is not live. Had it been: the 20:00Z pulse would have read "Finality paused since 2026-10-06 18:4x UTC: nothing is final until two thirds of the 30-day weight signs again; mining and proving are paid as usual" in the description, and the watcher would have marked the pause preexisting and never opened an incident |
No | Q7 |
Wallet 0.1.4 (igneum-wt-wallet app.js:185, 340) |
Node card "paused" when a local node reports finality_active false, or "not checkpoint-checkable without a node" on the public RPC; transaction rows stay "in block N" with no mention |
Half | Q35 |
Pool page (pool/web/index.html) |
Not deployed; the code has no finality concept; it would have confirmed and paid on blueness throughout | n/a (a design choice, stated in Q50) | Q50 |
The row for the ledger: Q1 plus Q2, Q5, Q6, Q7, Q10: every public and operator surface either hid the pause, named it without a cause, or contradicted it, for two hours, during the first finality pause the project has had in public. The gate across all of them is one forced pause on Devnet 2 with a screenshot of each surface, before the public testnet opens.
3. Per system
3.1 Miner app (Ember)
What it does today. A Rust engine (app/igneum-app/src/) runs igneumd and one GPU worker per card, serves a tokenised dashboard on 127.0.0.1 (src/server.rs:11-33, routes :184-359: api/state, api/log, detect, setup, key/saved, key/reveal, start, pause, resume, cards, tune/goal, settings, prove, update/check|install|auto|open, jobs/allow|check, power/apply|control, sweep/*, clock/*, quit). Screens (ui/index.html): Welcome (line 71), Cards (102), Address (127), the one-time key sheet (438), then a rail with Mine, Earnings, Prove, Settings (164 to 413), a log drawer (460), a status strip (Notices, ui/app.js:7-140) and an update card (UpdateCard, app.js:154-243). Ember Tune (src/ember.rs, src/sweep.rs, docs/plans/ember-tune.md) tunes power and clocks per card against a goal. Updates are Ed25519-signed manifests (src/ota.rs, src/manifest.rs). Hosts: app/mac/IgneumMiner.swift (WKWebView, menu bar), app/windows/host.cpp (WebView2, tray). The shipped build is 0.3.14 with miner-ui-3; 0.3.15 (miner-ui-4: the big-number hero and the chain scene) is staged, its canary failed on block version 1026 and was rolled back (release-0.3.15.md section 5). UI tests: 35 pass.
Comparators and the exact gap (all approximate, from memory of the products).
| Comparator | Their screen or feature | Ours, and where it falls short |
|---|---|---|
| NiceHash QuickMiner | Profitability per day in fiat on the main screen; Rig Manager web view of your own rigs | Earnings reads "£0.00 earned: nothing is bought or sold on devnet" (index.html:250); mined IGN is never shown, only proving paid_wei (app.js:1364); no owner-facing remote view (the console is the operator's) |
| HiveOS | Hash rate and temperature charts over hours; Telegram or Discord alert on a GPU fault or a rig offline; per-GPU fan and memory clock | A 10-minute blocks strip only (app.js:1052-1127); no outbound alert; manual control is the power slider (app.js:1255); the memory knob exists in the engine with no control |
| lolMiner, T-Rex consoles | Per-GPU accepted, rejected, invalid and faults on every status line | Per card only "N blocks" (app.js:306); rejected_session a total in the hero sub-line (app.js:1290); mismatched= and faults=, which X21 made the engine read, never reach the row |
| Nanominer | Restart-on-hash-drop watchdog with a visible threshold | src/watchdog.rs exists; the row says "stopped after repeated faults" (app.js:296) with no count or threshold |
Rows.
| Id | Row | File or screen | Severity | Hours | Owner | Gate |
|---|---|---|---|---|---|---|
| Q2 | Finality pause invisible (section 1) | state.rs:247, engine.rs:3495, app.js:1350 |
the project lead-visible | 3 | app owner | section 1 |
| Q4 | Every update urgent once the activation is behind; no finality hold (section 1) | manifest.rs:322-355 |
the project lead-visible | 3 | app owner | section 1 |
| Q11 | Offline start says "The update failed." A check error with no held manifest calls set_error; the cached manifest is loaded for the floor but never into self.manifest; the notice offers "Try again", which POSTs install |
ota.rs:649-654, 195-200, app.js:29, 1198 |
user-visible | 1 | app owner | notices test: a check-class error renders "Could not check for updates" with no install action |
| Q12 | Light mode fails contrast on every live text: molten #B8731F on white 3.81:1, on bone 3.38:1; ember #E04A14 on bone 3.61:1; molten carries the private key, the address and the live states. miner-ui-3.md section 9 claims 4.5:1 everywhere |
app.css:29, 263, 338, 431 |
user-visible | 1 | app owner | a node script over the token pairs asserts 4.5:1 for every text token in both schemes; CI runs it |
| Q13 | Per-card rejects and faults never reach the row (the lolMiner line) | app.js:291-309, 1290 |
the project lead-visible on PC 1 | 2 | miner-community-lead | row meta "N rejected · M faults" when nonzero; view test |
| Q14 | Windows first run raises a UAC prompt (firewall rule) before any screen explains it (part of Q3) | ota.rs:981-1015, index.html:71-99 |
user-visible | 1 | app owner | the firewall step runs after setup_done; the Cards screen names it |
| Q15 | Mac first-run instruction stale: "Right-click > Open" (part of Q3) | packaging/mac/README.md:51, site/miner.html download copy |
the project lead-visible on a fresh Mac | 1 | app owner | the download page and README name the Privacy and Security step until the certificate lands |
| Q16 | No rate history: a 10-minute strip against HiveOS's hours | app.js:1052-1127 |
user-visible | 4 | app owner | one-hour per-card sparkline from an engine ring buffer; view test |
| Q17 | Hill climb hidden until tune_climb is on the state |
index.html:341, app.js:1407 |
operator-visible | 1 | app owner | the row shows whenever the engine answers tune/goal |
| Q18 | Earnings never shows mined IGN; "£0.00 earned" is the first number on the tab | index.html:250, app.js:1364 |
user-visible | 2 | miner-community-lead | the tab shows blocks mined and the subsidy they earned in IGN, with the devnet line under it |
| Q19 | 10 px type in five places, 9 px ruler | app.css:196, 271, 272, 302, 491, 531 |
cosmetic | 1 | app owner | no font-size under 11 px except the ruler |
| Q20 | Design screens in docs/design/app-screens/*.png are a UI 1 app (v0.3.0 tiles, a Finality card the shipped UI lacks) |
docs/design/app-screens/ |
cosmetic | 1 | app owner | screens regenerated from ?screen= on the current build |
| Q21 | AMD step line prints a percent as watts ("owed: a unit word for AMD") | ember-tune.md:278 |
operator-visible | 1 | miner-community-lead | the line reads "70% (an offset)"; unit test |
| Q22 | A code comment naming the founder ships in the UI bundle (// The list is ordered by performance (the project lead, 6 October 2026)), and the ui bundle is outside every forbidden-string check |
app/igneum-app/ui/app.js:270, tools/ci/identity-check.sh |
cosmetic (identity rule) | 0.5 | app owner | tools/ci forbidden-string check covers app/igneum-app/ui/; the comment reads "(ruling of 6 October 2026)" |
| Q23 | Two copy-law slips: "Votes lock the chain; leave it on." (aphorism), "The window can close. The miner keeps going" (two-beat) | index.html:404, 81 |
cosmetic | 0.5 | app owner | reworded; the grep below stays at 0 em dashes |
Error states, exact strings. Engine down (four failed polls): pill "engine away" (app.js:1437); node line "Node: no answer", sub "the engine is not answering; the window reconnects by itself"; Mac host "The engine stopped. Quit and open Igneum Miner again." (IgneumMiner.swift:233); Windows "The engine stopped. Close this window and open Igneum Miner again." (host.cpp:301). Node down: "the node could not start" or the engine message, "the node is not running", "the external node went away" (app.js:452-453, engine.rs:3163); pill "node failed" (app.js:1428); digest box "the node is not running" (app.js:1341); canvas "waiting for the node to sync" (app.js:1099). Finality paused: nothing (Q2). Update server unreachable: "The update failed. Manifest signature: ." (app.js:1360, Q11). GPU lost: "Card removed: . Its worker stopped." (app.js:95), row word "removed", sub "unplugged; its worker stopped. The row goes in five minutes." (app.js:303); Code 43: ": not usable (Code 43). No worker runs on it." with "reboot with the card attached; if it persists, reinstall the driver with the card attached" (app.js:100, detect.rs:55).
Already better than the comparators. Automatic efficiency tuning against a goal with the money consequence on screen (app.js:408-419; 37 to 41 percent more hashes per watt measured on PC 1, ember-tune.md:254, 271). Signed, hash-checked, rolled-back updates with a safe-moment rule and machine slots (ota.rs, manifest.rs). A reason word on every zero rate and hot-plug words (ember-tune.md 6a). A virtualised log drawer with time jump and error marks (app.js:880-1050). Confirmations in place, no dialogs. A tokenised local API with origin checks (server.rs:102-125). Dark-scheme contrast is good: bone 17.4:1, ash 7.0:1, ember 5.6:1, molten 11.0:1 on obsidian (app.css:19). Focus rings, aria-live strip, labelled switches and sliders, reduced motion honoured on every animation (app.css:67, 113, 142, 148, 157, 420, 437, 466; app.js:1137). None of the four comparators ship the first two.
3.2 Wallet
What it does today. One desktop wallet (Igneum Wallet, Rust engine plus a local window; igneum-wt-wallet/app/igneum-wallet/: engine.rs, vault.rs, hd.rs, tx.rs, finality.rs, node.rs, updater.rs, server.rs; ui index.html, app.js, lock-screen.js, update-card.js), shipped as 0.1.4 (site/downloads.json wallet-mac, 19.6 MB; release-0.3.15.md:90). 0.1.5 (bundled 0.3.14 node, RPC on localhost) has a DMG and is unpublished (release-0.3.14.md:82). UI 3 is commit ab8dfdc on wallet-ui-3 and wallet-0.1.5, not on master. No wallet inside the miner app (it holds the payout key, wallet.json, and a button "Open the wallet" to the site, app/igneum-app/ui/app.js:826). No web wallet. A MetaMask guide (site/metamask.html), an address page (site/address.html, noindex, balance plus blocks mined), a marketing page (site/wallet.html). Keys: BIP-39 (12 to 24 words, hd.rs:23), BIP-44 Ethereum path; vault Argon2id 64 MiB, 3 passes, XChaCha20-Poly1305 (vault.rs:14-81); key zeroised on lock (engine.rs:392); 24 words shown once, three typed back (engine.rs:321); raw key export behind password or Touch ID (ui/index.html:271-275). Chain ids 4461, 4462, 4463 (metamask.html:233). Signing: EIP-1559 value transfers only; no EIP-712, no personal_sign, no contract calls, no dapp provider, no hardware wallet (spec 8.5 item 2 decided, not built; docs/fud-ledger.md:2058).
Comparators and the exact gap (approximate unless a standard is named).
| Comparator | Their feature | Ours |
|---|---|---|
| MetaMask | Account list and HD derivation; address book; speed up and cancel a pending tx; window.ethereum provider; eth_signTypedData_v4 |
One account (engine.rs:343 refuses a second import); an address field only (ui/index.html:177); no replace-by-fee; no provider; no EIP-712 |
| Rabby | Pre-sign simulation and recipient risk flags | Review shows amount, fee and total (app.js:293-295) |
| Frame | Ledger, Trezor, GridPlus signers; injected provider for any browser | Neither (spec 8.5 "MUST offer", no code) |
| Monero GUI | View-only wallet; subaddresses; a settable node address with status | Node source engine-chosen, no settable endpoint (wallet-ui-3.md:112) |
| KDX, Kaspium | Sync bar with peer count; per-transaction DAA score | "reading the chain, N of M blocks" (app.js:175), no peer count |
| EIP-1193 | request provider |
Used on the guide (metamask.html:294) |
| EIP-6963 | Multi-provider discovery | Absent; the guide falls back to whichever extension owns window.ethereum (:292) |
| EIP-3085 | wallet_addEthereumChain with blockExplorerUrls, iconUrls |
blockExplorerUrls: [], no icon (:225, 285-286) |
Rows.
| Id | Row | File or screen | Severity | Hours | Owner | Gate |
|---|---|---|---|---|---|---|
| Q9 | Page vs UI 3; guide placeholders; empty explorer URL (section 1) | site/wallet.html, site/metamask.html |
the project lead-visible | 6 | app owner, site owner | section 1 |
| Q30 | Private key export copies to the clipboard with no clear and no timer | igneum-wt-wallet/.../ui/app.js:129, 463 |
user-visible | 2 | app owner | clipboard cleared after 60 s by the host; unit test on the timer |
| Q31 | Wallet OTA trusts one key; no revoked_keys, unlike the miner's updater |
docs/plans/consequences-2026-10-05.md:36, updater.rs:7, 729 |
operator-visible | 3 | app owner | updater carries OTA_PUBLIC_KEYS [K1, K2] and honours revoked_keys; the three-key test of the rig installer |
| Q32 | No hardware wallet path despite spec 8.5 "MUST offer" | docs/spec/08-client-security.md:36 |
the project lead-visible | 24, or 0.5 to relabel | app owner (cryptographer reviews) | a Ledger signs one devnet transfer through the Ethereum app at chain id 4463; or the spec row reads "Designed, not shipped" |
| Q33 | Single account, no second address, no address book | engine.rs:343, ui/index.html:177 |
user-visible | 6 | app owner | two derived accounts, a saved recipient reused in Send |
| Q34 | confirm() on Remove wallet never renders in the Mac host |
app.js:598, wallet-ui-3-audit.md:189 |
user-visible | 1 | app owner | in-page two-step card; snapshot test |
| Q35 | Finality pause: node card "paused" or "not checkpoint-checkable without a node", rows stay "in block N"; public-RPC mode never says what to do | wallet app.js:185, 321, 340 |
user-visible | 2 | app owner | the row reads "in block N; finality is paused on the network" and the public-RPC line names the fix with a button |
| Q36 | Reorged-out transaction: "in a block" then the row vanishes; no resend hint | app.js:340, docs/fud-ledger.md:2154-2158 |
user-visible | 3 | execution-engineer (node side), app owner (the row) | a transfer unwound in the P23 conformance run shows "reorged out, resending" and ends executed |
| Q37 | The address page shows no finality state and "n/a" with a bare reason for balance | site/address.html:205-209, 242-252 |
cosmetic | 2 | site owner | the page shows the latest locked checkpoint and whether finality is active |
| Q38 | The permanent phishing sentence differs between surfaces ("words or your key" on the wallet, "your key" in the miner) against spec 8.5's one verbatim sentence | site/wallet.html:366, wallet ui/index.html:54, app/igneum-app/ui/index.html:97, 08-client-security.md:39 |
cosmetic | 0.5 | site owner, app owner | one sentence, grep-identical on all three |
| Q39 | Vault file permissions not set explicitly (no 0o600 in vault.rs) |
vault.rs |
operator-visible | 0.5 | app owner | the file is created 0600; test |
Error states. Node down: pill "no node", balance note "waiting for a node" (app.js:160, 176), the node line's raw engine message (:186; UI 3 replaces it with "the node stopped answering; trying again", wallet-ui-3.md:199). Pending versus final: "PENDING, waiting for a block", "IN A BLOCK, chain block N; final once a verified checkpoint covers it", "FINAL, under checkpoint N, certificate verified by this wallet", "FAILED, the execution failed; the fee was still paid" (app.js:340). Address page: "The API did not answer: " (address.html:270), "Not found." (:247). MetaMask page: "No Ethereum wallet found", "Waiting for the wallet...", "Cancelled in the wallet.", "The wallet refused: " (metamask.html:292-296).
Security notes. Plaintext key only in engine memory, zeroised on lock and after each sign (engine.rs:392, 563); the window never holds the key (nonce-gated reveal, server.rs:108); local API on 127.0.0.1 with a random port, per-run token path and a host token (server.rs:1, 39, 71); chain id checked at quote and send (engine.rs:549). Testnet RPC is https (testnet-go.md:13); devnet is http to 127.0.0.1. Chain ids 4461 to 4463 were absent from chainid.network on 3 October (docs/design/execution-layer.md:354), not yet registered (frontier.md:645); from memory no listed chain uses them (approximate).
Already better. The wallet verifies finality certificates itself with the node's consensus code (finality.rs:101-121) and never trusts the RPC's "locked"; MetaMask and Rabby trust the RPC's block tag (approximate). Touch ID is bound to the exact quote (address, value, nonce, chain id) with a 30 s one-use nonce (engine.rs:539). Argon2id at 64 MiB is above MetaMask's PBKDF2 default (approximate). The QR is drawn locally as SVG with the ethereum: URI and a checksummed address (server.rs:140). The MetaMask page says plainly that nothing on it asks for a seed (metamask.html:172).
3.3 Hub and relay
What it does today. One Vercel project (igneum-relay, Neon database, Blob store; relay/README.md:3-5). relay/ui.html (583 lines) is the console at /r/<token>/ (relay/vercel.json:5-8); relay/index.html is a placeholder. Functions: relay/api/relay.mjs (feed, items, files, inbox, register, name, role), relay/api/console.mjs (machines, jobs, builds, chain, log, results, tuning), relay/api/wake.mjs (public long-poll for the apps' jobs file, 30 a minute per IP, relay/lib/wake.mjs:15, 42-54). Auth: the 20-character token in the path or x-relay-token, or any of RELAY_KEY, LOG_INTAKE_KEY, LOG_INTAKE_KEY_NEXT in x-igneum-key (relay/lib/relay.mjs:34-46); only task, a run or task drop, name, role, delete demand the token (relay/api/relay.mjs:114-115). Two job models: relay run items polled every 20 s by relay/clients/igneum-agent.ps1:165 and executed as administrator; and signed app jobs in igneum-jobs.json, whose status the console derives from miner_logs rows (relay/api/console.mjs:172-188). Seven tabs (relay/ui.html:186-192): Machines, Jobs, Builds, Chain, Work log, Results, Relay; refreshed every 15 s when visible (:577), server cache 10 s (console.mjs:21). Chain tiles: blocks, identities, hash estimate, difficulty, last lock, finality, peers, blocks per second (:363-370). Mac tools: tools/relay.mjs, tools/console.mjs, tools/jobs.mjs. Tests: 24 pass per file (node --test relay/test as a bare directory fails under Node 22.23.2 with MODULE_NOT_FOUND; the README's command needs the glob).
Comparators and the exact gap (approximate).
| Comparator | Their screen | Ours |
|---|---|---|
| Tailscale admin console, Machines | Last seen, OS and version, key expiry with "Disable key expiry", tags; an ACL editor with tests | Last seen and a red card after 180 s (relay/ui.html:270, 273); no credential state (one static token, relay/lib/relay.mjs:36), no per-machine key, no revoke; roles are labels never checked (relay/api/relay.mjs:191-195) |
| GitHub Actions job view | Live streaming log with line anchors, per-step timing, re-run, artefacts, annotations at the top | One text blob in a modal (relay/ui.html:319); duration_s and the error lines that tools/jobs.mjs:41-44 already parses are discarded by console.mjs:184-187; no re-run; results are separate items |
| Vercel deploy log | Build steps, deployment list, instant rollback, diff between deployments | Builds tab shows the current manifest, the last CI fetch, 40 posts and a folder listing (relay/ui.html:323-341); no rollback, no manifest history, no diff |
Rows.
| Id | Row | File or screen | Severity | Hours | Owner | Gate |
|---|---|---|---|---|---|---|
| Q6 | Stale shown as live; "active" for 20 unlocked indices; "paused" with no since or cause; event flood (section 1) | relay/ui.html:564, 367-368 |
the project lead-visible | 4 | fleet agent, consensus-engineer | section 1 |
| Q8 | X23 to X28 fixes off master; intake key can write and delete; token in URL (section 1) | relay/api/console.mjs:249, relay/lib/relay.mjs:44 |
operator-visible | 2 | fleet agent | section 1 |
| Q40 | A hung job reads "running" forever: no elapsed time, no timeout, no "last line N min ago" | relay/api/console.mjs:182-187, relay/ui.html:317 |
the project lead-visible | 2 | fleet agent | a killed job reads "no report for 12 min" in red within one refresh |
| Q41 | Jobs tab trusts the unsigned igneum-jobs.json while the apps verify the signed file |
relay/api/console.mjs:170 vs tools/jobs.mjs:57-60 |
operator-visible | 1.5 | fleet agent | the tab shows the envelope's signature state per file |
| Q42 | Job result modal: no error lines first, no duration, no anchors (the Actions view) | relay/ui.html:319, console.mjs:184-187 |
the project lead-visible | 3 | fleet agent | a failed run opens with its five error lines and its duration first |
| Q43 | Machines: no per-machine key state, no revoke, roles never enforced (the Tailscale view) | relay/lib/relay.mjs:14, relay/api/relay.mjs:191-195 |
operator-visible | 3 (after Q8) | fleet agent | a revoked PC's register is 403 and its card says "key revoked" |
| Q44 | Builds: no rollback, no manifest history (the Vercel view) | relay/ui.html:328-341 |
operator-visible | 4 | fleet agent | one tap republishes the previous manifest and the fleet's OTA state shows it |
| Q45 | "live feed unreachable: live feed unreachable" doubled; the whole Chain tab empties, losing Events and the infra card | relay/api/console.mjs:210, relay/ui.html:357 |
cosmetic | 0.5 | fleet agent | live feed down: tiles grey, Events and infra card still render |
| Q46 | Replay: register and task have no nonce; a captured task POST re-queues a run |
relay/api/relay.mjs:153-167 |
operator-visible (security) | 1 | fleet agent | a replayed task is refused; test |
Error states. Relay API unreachable: header "offline: Failed to fetch" and a grey dot (relay/ui.html:564, :32); the panel is replaced only while it still shows "loading". Node down: a machine card goes red after 180 s with "silent 4h" (:270, 273; SILENT_S, console.mjs:35); a deliberate stop reads "stopped (quit) 2h ago" (relay/lib/parse.mjs:134-140); the live API down reads "live feed unreachable: " (:357); the observer stale reads "observer STALE N s" in red after 30 s (:361, site/api/live.mjs:8) while every tile keeps its last value. Finality paused: tile('finality', f.active ? 'active' : 'paused', ...) in amber #FFB35C (:368, :17, :65); the "last lock" tile amber too (:367); eight checkpoint rows with state and fraction (:374). Job hang: "running" until a SUMMARY with finished_at arrives (console.mjs:179-182); relay run items show "read" and never "done" (tools/relay.mjs:58).
Security, X23 to X28 on this tree. X23: the tier split exists (relay/api/relay.mjs:114-115), no run signature, no per-machine HMAC, the intake key still writes the console (console.mjs:249). X24: every client builds /r/$TOKEN/api (agent.sh:10, send.sh:11, igneum-agent.ps1:13, tools/relay.mjs:24, tools/console.mjs:27). X25: Arm-Restart at top level and /RL HIGHEST (igneum-agent.ps1:154, 72). X26: feed to 500 with no retention; delete leaves blobs (relay.mjs:48, 148-152); __DL_BASE__ substituted into bodies (tools/relay.mjs:79). X27: from is free text, register for any hostname (relay.mjs:30, 153-167). X28: constant-time compare and HSTS present (relay/lib/auth.mjs:5-11, relay/vercel.json:38-41); still a GET that acks (relay.mjs:85), RELAY-REBOOT anywhere in output (igneum-agent.ps1:133), no rate limit, NOPASSWD:ALL (wsl-setup.ps1:40), sudo -S with the password (prover-setup.ps1:21). All of this is what Q8 merges.
Already better. One phone-first page, no login flow, seven tabs, 15 s refresh (relay/ui.html:5, 577); Tailscale, Actions and Vercel each take several screens on a phone (approximate). The machine card fuses miner STATUS, node log, app telemetry and OTA state and remembers an OTA line after it scrolls out (console.mjs:61-84); no comparator shows workload telemetry. Stale marking at 180 s drops the card from the total; stopped and silent are distinct words (parse.mjs:16-21, 134-140). Drop-anywhere upload direct to Blob with a progress bar and a 50 MB cap (ui.html:506-534). The wake long-poll is dependency-free and tested with a fake clock (relay/lib/wake.mjs). Headers: nosniff, frame DENY, noindex, no-referrer, no-store, HSTS on every path (relay/vercel.json:21-43).
3.4 Site
What it does today. Thirteen static pages plus 404 (site/index.html, litepaper.html, live.html, bench.html 611 KB rendered from docs/bench-log.md, evidence.html, ledger.html 312 KB generated, miner.html, miners.html from miner-bench.json, wallet.html, metamask.html, faucet.html, explorer.html, block.html, address.html). site/build.mjs:49-50, 357-372 injects partials/head.html (self-hosted fonts, tokens), nav.html, footer.html, stamps download links from downloads.json, inlines journey.json. scrub.mjs runs on bench and miners only (build.mjs:171, 401). Live data: index polls /api/live every 2 s (index.html:704; 10 s while off); the light client fetches /api/checkpoint and imports two noble libraries from cdn.jsdelivr (verify/verify.js:5-6); live.html polls every 2 s (:665-666); faucet POSTs /api/faucet. APIs (site/api/): live, stats, supply, explorer, checkpoint, faucet, log, all reading Neon tables written by tools/observer. Edge cache: live 1 s, stats and supply 10 s, explorer 5 s (vercel.json). The live HTML (one curl, 20:02Z) is 60,009 bytes, served with HSTS (preload), X-Frame-Options: DENY, nosniff, Referrer-Policy: strict-origin-when-cross-origin, no CSP; download buttons stamped "v0.3.14 · 45.4 MB"; "Public testnet: not yet open; the devnet build is here for people who want to look."
Comparators and the exact gap (approximate unless a page is named).
| Comparator | Their page or element | Ours |
|---|---|---|
| kaspa.org, ethereum.org | A Developers or Docs nav item | Nav is Litepaper, Live devnet, Engineering log, Miner, Wallet, Evidence, GitHub (partials/nav.html:14-21); builders land on a litepaper section (index.html:335) |
| getmonero.org/downloads | Every platform, hashes and signing keys on one page | Split across index.html:363-365, /miner#get, /wallet; the homepage's Linux and HiveOS button links /miner#get, not a file; sha256 shown only for the HiveOS package (miner.html:558), none beside the Windows and Mac buttons, no signing key, no verify line |
| z.cash, aztec.network | Named people, a transparency page | "one founder, pseudonymous" once in the litepaper; the Reddit review's key-powers table (round 4, section 3 row 8) absent |
| kaspa.org, getmonero.org | Media kit, press kit, language switch, newsletter | None (site/ has no /press or /brand) |
| ethereum.org, kaspa.org | Light and dark | Dark only; only the litepaper honours prefers-color-scheme |
| succinct.xyz, aztec.network | 1200x630 share cards | Home, litepaper, live, explorer use 256 px og-small.png with summary (index.html:15-19); miner, wallet, bench, evidence use the large card |
Lighthouse, estimated from source (approximate; no run). Common: lang="en", a skip link (partials/nav.html:1), one :focus-visible ring (head.html:22), self-hosted fonts with font-display:swap, two preloads and fallback metrics (head.html:1-19), no render-blocking third-party CSS, canonical, description, OG and twitter tags on every page.
| Page | Performance | Accessibility | Best practices | SEO | What holds it down |
|---|---|---|---|---|---|
| index.html | 78 to 85 | 88 | 95 | 92 | 89 KB HTML, 35.0 KB inline JS, 24.0 KB CSS; two rAF canvas loops at display rate with no visibility or intersection stop (:790, 853, 857); 2,880 px webp with no srcset; h3 before the first h2 (:344, 359); ember on bone 3.08:1 at 15 px in the light section (:119-120), eyebrow #7A776F on bone 3.97:1 at 12 px (:130); 256 px OG card |
| explorer.html | 88 to 93 | 90 | 100 | 85 | 32 KB, no images, no third party; aria-live="polite" on an 8-tile strip refreshed every 10 s (:222); not in the sitemap |
| live.html | 85 to 90 | 88 | 100 | 85 | 64 KB, 35.6 KB JS; the canvas loop is capped at 30 fps and stops off screen (:605-612), good; aria-live="polite" on the 8-cell strip every 2 s (:225); canvas aria-hidden with no text alternative (:241) |
Rows.
| Id | Row | File or screen | Severity | Hours | Owner | Gate |
|---|---|---|---|---|---|---|
| Q5 | "final" during a pause; 90-word hero; 24 GB contradiction; thumbnail OG (section 1) | index.html:345, 494, 810, 838, 15-19 |
the project lead-visible | 3 | site owner | section 1 |
| Q50 | No downloads page: hashes only for HiveOS, no signing key, no verify line, the Linux button goes to an anchor | index.html:363-365, 516, miner.html:549-558 |
user-visible | 2 | site owner | /download lists every artefact with version, size, sha256, the OTA key fingerprint and a verify command per OS; the Linux button links the file |
| Q51 | /miners has 6 rows, two card models, "not measured" MH/W on every row; the eleven-card fleet table is not ingested |
miners.html:183, miner-bench.json |
user-visible | 2 | miner-community-lead | rows carry W and MH/W for every measured card |
| Q52 | dl\.igneum in forbidden-strings.txt matches the public download host on three built pages; the scrub guards only bench and miners |
forbidden-strings.txt:25, build.mjs:171, 401 |
operator-visible | 0.5 | site owner | the pattern becomes dl\.igneum\.network/dl/(?!public/) or /dl/[0-9a-f]{12,}, and the check runs on every built page |
| Q53 | Sitemap misses /miner, /explorer, /faucet, /ledger; every lastmod is 4 or 5 Oct; 404 says "These four pages are everything the site has" |
sitemap.xml, 404.html:138 |
cosmetic | 0.5 | site owner | the sitemap is generated by the build from the page list |
| Q54 | aria-live="polite" on strips refreshed every 2 s and 10 s (a screen reader hears eight values every refresh) |
live.html:225, explorer.html:222 |
user-visible | 0.5 | site owner | aria-live="off" on the strips plus one polite status line on state change |
| Q55 | Homepage canvases never stop off screen or cap their rate | index.html:790, 853, 857 |
user-visible | 1 | site owner | 30 fps cap and IntersectionObserver as live.html:605-612 |
| Q56 | Light-section contrast fails (3.08:1 links, 3.97:1 eyebrow) | index.html:119-120, 130 |
cosmetic | 0.5 | site owner | AA on every token pair; the same node script as Q12 |
| Q57 | "devnet v0" eyebrow on the live page; the chain is v4 | live.html:219 |
cosmetic | 0.1 | site owner | matches the app's network word |
| Q58 | Heading order (h3 before the first h2) | index.html:359 |
cosmetic | 0.2 | site owner | h2 or a styled div |
| Q59 | No team, custody or key-powers page (Reddit round 4 artefact 8); "0 admin keys in consensus" tile unchanged | index.html:611; no file |
user-visible | 1 (plus the project lead's policy call) | site owner | the key-powers table published; the tile links it |
| Q60 | Roadmap dates versus the testnet: the journey says "Public testnet, Aug to Oct 2027" and the litepaper "Pools and the public testnet are August 2027", while docs/plans/testnet-go.md has igneum-testnet-1 seeds up and a go checklist dated 5 October 2026 |
index.html:679, litepaper.html:719, docs/plans/testnet-go.md:1-4 |
the project lead-visible | 0.5 (after the project lead decides which is true) | site owner | one date for the public testnet on the journey, the litepaper and the download notice |
| Q61 | No Content-Security-Policy header on the site (the relay has none either); inline scripts throughout, two pinned CDN modules on the homepage | site/vercel.json:7, relay/vercel.json:31-43, verify/verify.js:5-6 |
operator-visible (security) | 2 | site owner | a CSP with hashes or nonces for the inline scripts and script-src limited to self and cdn.jsdelivr; every page renders with no console violation |
| Q62 | Copy-law borderline headlines: "Mined by GPUs. Proven by fire." (the brand line, the project lead's call), "GPUs are back · for good", "Last hour's chip is already obsolete.", "Your coins. Final means final.", "Dates slip. Gates do not.", "Install. Start. The card mines and proves.", "Nothing is mined here." | index.html:343, 344, 424, wallet.html h1, litepaper.html:678, miner.html h1, 404.html h1 |
cosmetic | 1 | site owner (the project lead rules on the brand line) | each either kept by the project lead's word or reworded |
Reddit round 4, still open in the HTML. Finding 6 (24 GB): half fixed (Q5). Finding 7 (eleven-card table): open (Q51). Finding 8 (admin keys): sentence added to the litepaper (:659), tile unchanged, no table (Q59). Finding 15 ("Monero's idea, finished for GPUs"): open (index.html:444, litepaper.html:429). Finding 16 ("devnet v0"): open (Q57). Finding 22: /ledger present, no nav item. Findings 2, 3, 5, 9, 10, 12, 13, 17, 19: fixed in the current files. "0% anyone else in the protocol" legend still live (index.html:604).
Error states, exact strings. API unreachable: /live after three failed polls shows "OFFLINE", "api unreachable", the canvas frozen with "api unreachable, scene frozen" (live.html:665, 600); /explorer writes the fetch error into the table on first load and is silent after (:342-345); /block "The API did not answer: " (:328); the homepage "simulated preview · live feed unavailable" and "Live feed unavailable. This is a simulation and its counters count simulated blocks." (index.html:870-872). Observer stale over 30 s: "OFFLINE", "observer updated N min ago", "observer offline, last update N min ago" (live.html:663); explorer eyebrow "observer offline, last known" (:318); homepage "simulated preview · observer offline, last update N s ago" (:708). Finality paused: section 2.
Already better. Self-hosted latin subsets with preloads and computed fallback metrics (head.html:1-19); ethereum.org and kaspa.org load heavier bundles (approximate). Zero third-party scripts except the two pinned noble modules; no analytics, no cookie banner. Honest live states: "simulated preview · observer offline", "finality paused: N% of weight silent", "api unreachable, scene frozen"; Etherscan shows nothing when its indexer lags (approximate). A browser-side BLS certificate verifier on the homepage (verify/core.js), which no comparator homepage has. A public API with a CI field contract (api/stats.mjs:10-14) and supply derived from the emission rule with an hourly coinbase check. HSTS with preload, frame DENY, nosniff, immutable font caching, skip link, reduced-motion rule on every page. A dated journey with pass or fail gates inlined at build, and a ledger page of every criticism; none of the six site comparators publish the equivalent.
3.5 Explorer
What it does today. explorer.html polls /api/explorer?blocks=50 every 5 s and /api/stats plus /api/supply every 10 s (:342-343); block.html fetches ?block= or ?height= once (:276); address.html fetches ?address= once; /block/:id and /address/:addr are Vercel rewrites. Search classification is client-side (lib/explorer.mjs:19-27: hash, 0x address, bech32, height) with a server fallback for tx hashes (api/explorer.mjs:49-59). The data window is 24 hours of observer rows; balance needs EXPLORER_EVM_RPC, unset on the devnet deployment (docs/plans/explorer.md section 7).
Comparators and the exact gap (approximate).
| Comparator | Their feature | Ours |
|---|---|---|
| Etherscan, Blockscout | A transaction page: decoded input, logs, status, fee | None; a tx hash lands on /block/<hash>#tx-<hash> (api/explorer.mjs:57); block.html:310 lists hash, from, to, value, bytes, with from and to "needs an EVM RPC" when unset |
| Etherscan, Blockscout | Address page with transactions, token transfers, nonce, contract tab | Blocks mined, 24-hour earnings, balance only with an RPC (address.html:209, 281) |
| Blockscout | Hosted API reference (Swagger), rate-limit statement | A card on /explorer (explorer.html:232-240) and docs/api/public-stats.md in the repo |
| explorer.kaspa.org | DAG view inside the explorer; merge-set order per block | DAG view lives on /live; block page has blue work, parents, children, mergeset blues and reds (block.html:285-325), no prev and next navigation |
| mempool.space | Mempool as the product, fee charts | Mempool count is one tile on /live (live.html:233); no charts on the explorer |
| All four | Per-block status (final, confirmations) | None for an ordinary block (Q10) |
Rows.
| Id | Row | File or screen | Severity | Hours | Owner | Gate |
|---|---|---|---|---|---|---|
| Q10 | No finality state; failures swallowed (named under the ten in section 1) | api/explorer.mjs:16-25, explorer.html:222, 309, 342-345 |
user-visible | 3 | site owner | during a forced pause every block row carries "paused" and the strip tile reads the Q1 reason; cut the API: the table dims with "api unreachable, last update N s ago" within 10 s |
| Q63 | No tx page, no hosted API reference | api/explorer.mjs:57, explorer.html:232-240 |
user-visible | 4 | site owner | /tx/<hash> with status, fee, from, to, input bytes, shard and proof state; /api page with every field and the cache times |
| Q64 | Address page: no transaction list, no finality (Q37 covers the finality half) | address.html |
user-visible | 3 | site owner | the last 50 transfers in and out, each with its block and state word |
| Q65 | No prev and next block navigation; no block-by-height neighbours | block.html |
cosmetic | 1 | site owner | two links from selected_parent and the first child |
| Q66 | Blocks per second inverted against the other pages ("1.2 s per block" vs "0.9 blocks / s") and difficulty "126.81 M" with a space against "126.81M" on /live |
explorer.html:330-331, live.html:329 |
cosmetic | 0.5 | site owner | section 4.3's one formatter |
Already better. Block page depth for a DAG: parents, children, mergeset blues and reds, selected parent, shards with prover and lag, checkpoint weight fractions and certificates (block.html:285-325); explorer.kaspa.org shows less of the mergeset (approximate). Client-side search that accepts a height, a hash, an 0x or bech32 address with no round trip (lib/explorer.mjs:19-27). Five-second edge cache with a stale flag from the observer, so a stale page says so on first load.
3.6 Pool
What it does today. igneum-pool, a Rust crate (pool/Cargo.toml, 12 source files) speaking the spec 09 protocol in its v0 form: newline JSON over plain TCP (pool/src/server.rs:1-2, protocol.rs:1-9), the project's own message set (hello, welcome, authorize, seeds, template, job, share, share_result, solution, stats; protocol.rs:21-170). Every share is CPU-verified on the node's warp verifier (verify.rs:52-64; 1.35 ms isolated, 2.1 ms under load, docs/plans/pool.md section 5). PPLNS only, window in blocks of weight, default 2 (pplns.rs:1-5, config.rs:67); fee 1 percent, minimum payout 1 IGN (config.rs:65-66); EIP-1559 transfers from the pool's coinbase key, at most 16 per round, failed receipts re-credited (payout.rs:197-299). One page, pool/web/index.html, baked into the binary (api.rs:166-174): a stat strip, connect card, address lookup, blocks, payments, API card. Stats API /api/stats, /api/blocks, /api/payments, /api/miners/<addr>, /api/pool-stats, /health (api.rs:185-200) with fixture-pinned field contracts (api.rs:255-272). Operator surface: a STATUS line every 30 s on stdout (main.rs:152-174). Deployment: a written, unexecuted systemd unit behind Caddy (pool/README.md:75-116; "not executed", docs/plans/pool.md section 8). Nothing is deployed. The horizon tree equals igneum-wt-pool-rebase HEAD 269364a.
Comparators and the exact gap.
| Comparator | Their feature | Ours |
|---|---|---|
| 2miners | Per-worker hash rate charts over time; payout history per miner; worker-offline alerts by email or Telegram; estimated earnings per day; luck | A single 10-minute and 1-hour number, samples kept 15 minutes and not persisted (state.rs:704-705, 779-781); the API returns 50 payments and the page renders none of them (api.rs:148, web/index.html:181-194); no contact field (protocol.rs:50-60); no earnings estimate; luck defined inverted against MiningPoolStats' convention (api.rs:72-74) |
| WoolyPooly | Solo mode; worker offline alerts | scheme hard-coded "pplns" (server.rs:53, api.rs:80); no alerts |
| Kaspa acc-pool (approximate) | Prometheus and Grafana | No /metrics; stdout only |
| Stratum v2 reference (github.com/stratum-mining/stratum, not cloned, approximate) | Noise-encrypted transport, job negotiation, binary framing, header-only mining | Plain TCP while spec 9.3 demands TLS 1.3 and binding is sent empty (server.rs:1-2, protocol.rs:58-60, 09-pool-protocol.md:37, 43, 49); mode A only; JSON lines, and the template ships the whole RpcRawBlock to every member every second (node.rs:147-164) |
Rows.
| Id | Row | File or screen | Severity | Hours | Owner | Gate |
|---|---|---|---|---|---|---|
| Q67 | Member transport is plaintext TCP against spec 9.3's TLS 1.3 with session binding; binding ignored |
server.rs:1-2, 115, protocol.rs:58-60 |
user-visible (hijack risk) | 8 | consensus-engineer | rustls listener; a replayed authorize on a second connection is refused (O-9.7) |
| Q68 | Node down shows "difficulty 0, DAA 0" and a ramp-day-0 reward; no "node unreachable" state | node.rs:350-368, web/index.html:163-164 |
the project lead-visible (once deployed) | 2 | miner-community-lead | the page renders "Node unreachable since |
| Q69 | Raw integers (DAA, block count, shares) with no separators; difficulty uses toLocaleString() with no locale, so it varies by browser; hash rate 2 decimals against the site's 1 |
web/index.html:152, 163, 172, 190 |
the project lead-visible | 1 | miner-community-lead | every number formatted as section 4.3 says |
| Q70 | Ledger is a JSON file rewritten every 15 s; hash-rate samples and check cost not persisted, so a restart zeroes every rate tile and the 24-hour luck | state.rs:704-710, main.rs:131-144 |
user-visible | 4 | miner-community-lead | restart test: tiles recover within one sample window; README names the loss window |
| Q71 | Address lookup never shows the payment history the API returns | api.rs:148, web/index.html:181-194 |
user-visible | 1 | miner-community-lead | payments table under the lookup |
| Q72 | No per-miner history, no charts, no worker-offline notice, no /metrics; /health always ok |
state.rs:779-781, web/index.html:187, main.rs:152-174, api.rs:195 |
user-visible, operator-visible | 4 + 4 + 2 + 1 | miner-community-lead | hourly buckets per address kept 7 days with a sparkline; opt-in webhook after 10 min silence; Prometheus text endpoint; health reflects net.synced and template age |
| Q73 | Finality: the pool confirms and pays on blueness and has no finality concept; the page says nothing during a pause (a design choice to state) | node.rs:279-345, server.rs:9 |
user-visible | 1 | miner-community-lead | the page carries the network's finality state and the sentence "payouts follow blue confirmation, not finality" |
Already better. Every share CPU-verified at the node's own engine, no sampling (verify.rs:52; spec 9.8 item 5); most pools trust the miner's claim for low-difficulty shares (approximate). The member's vote key rides in every header it hashes (node.rs:47), so pool concentration does not become vote concentration; no Stratum pool does this. PPLNS split snapshotted at find time, credited only on blue confirmation, orphans pay nobody (state.rs:821-852). Failed payout receipts re-credit the balance (payout.rs:288-292). Exact binary share weights (vardiff.rs:138-140). Field contracts pinned by fixture tests.
3.7 Discord
What it does today. tools/community/discord-hooks.mjs (798 lines, Node 22, no dependencies). Webhook URLs in ~/.config/igneum/discord (:18-19, 41, 520); state in ~/.config/igneum/discord-hooks-state.json. Posts: network pulse to #numbers at 03:00, 09:00, 15:00, 21:00 UK (shapePulse :236-273); daily digest 09:00 (:276-299); weekly numbers Monday 09:00 (:302-352); release to #announcements by the shipper (:394-409); incident open and resolve to #incidents by hand or by the watcher (:412-433). Watcher conditions: finality_paused (5-minute hold), proof_lag (900 s), observer_silent (180 s) (WATCH :37, :437-472); one incident per condition, resolved after 120 s clear (:490-503). Idempotency keys per post (:562, 569); 429 backoff (:534-551); a forbidden-string guard before every post (:47-96); allowed_mentions: {parse: []}. Runner: a systemd timer on igneum-build-1, tick --live every minute (infra/build-server/discord-hooks/). Per docs/community/discord-hooks.md:81-82, no live post has been made and the install has not run. Tests: 31 pass; every live poster is stubbed.
Comparators and the exact gap (approximate).
| Comparator | Their feature | Ours |
|---|---|---|
| Ethereum, Kaspa, Monero Discords | Ticker bots renaming channels with height, hash rate, price | None; a webhook cannot rename a channel, a bot token is needed |
| The same | Embed colour by severity; a pinned current-status message; "resolved" as an edit of the open message | Single ember colour for every kind (:33, 158); no pin; resolve posts a second message (:706-712) |
| The same | Explorer links per block or lock | Only /live and /miner links (:271, 297, 350, 420, 432) |
| The same | Reorg or stall alerts | No condition for a block-production stall (the fixture shows 11 zero minutes at 18:30 to 18:41Z) or a reorg (:437-472) |
Rows.
| Id | Row | File or screen | Severity | Hours | Owner | Gate |
|---|---|---|---|---|---|---|
| Q7 | Bot not live; pre-existing never opens; pulse hides the pause (section 1) | discord-hooks.mjs:481-485, 252-255, 158 |
the project lead-visible | 3 | miner-community-lead | section 1 |
| Q74 | No block-production stall condition, the actual first signal of tonight's 18:30Z gap | :437-472, fixtures/discord/live.json |
user-visible | 2 | miner-community-lead | blocks_stalled condition, 3 minutes, fixture test |
| Q75 | Resolve posts a new message instead of editing the open one | :706-712 |
cosmetic | 2 | miner-community-lead | PATCH /messages/<id> with the resolve fields |
| Q76 | Embeds link only /live and /miner; the last lock could link /block/<hash> |
:271, 297, 350 |
cosmetic | 1 | miner-community-lead | last lock links its block page |
| Q77 | "Last lock" names 93 voters while the hash-rate field says 56 vote keys active; readers will ask | :259, 262 |
user-visible | 1 | miner-community-lead | one clause explains the two counts, or one number |
| Q78 | Four semicolon-balanced two-beat sentences in the shipped incident texts ("Blocks keep being produced; the chain is not locking checkpoints", "finality pauses rather than locks on a minority", "Vote keys are not machines; the machine count is owed", "Blocks are produced and final as usual; their proofs arrive late") | :347, 448, 452, 458 |
cosmetic | 0.5 | miner-community-lead | reworded as single statements |
Already better. Release embeds carry the full sha256 of every artefact plus the exact verify commands (:399-406); most project bots post a link. Every post passes a guard that refuses the founder's name, hosts, IPs, paths, webhook URLs and mentions (:47-96). Every number names its API field and the posts link the raw JSON (:314-341). Incident texts state who is affected per tier (:449, 458, 467). A dry-run HTML preview renders the exact card before anything goes live (:602-634).
3.8 Fleet tooling
What it does today. On the gpu-fleet branch (/Users/joshm/Projects/igneum-wt-gpu-fleet/tools/fleet/, read-only; the horizon tree holds only publish-fleet.sh): providers vast.py (console.vast.ai v0: filtered bundle search :47-54, rent with optional onstart :61-72, instances, destroy, a price-stamped ledger.jsonl :88-102) and runpod.py (:27-44); the orchestrator fleet.py (rent-card, wait, setup, status, run, pull, destroy, tail, sh, dn2-version, list; :5-13, 183-187); the shared library the 6 October rule demands, lib/box.py (Box.run/put/alive, install_payload sha-checked, stop_node/start_node, height/daa/peers/synced/wait_synced, exec_status/exec_tip/proving_status/paid_segments/paid_shards/state_root, node_version, rejects_since, max_reorg_since, start_miner/stop_miners/start_prover/stop_all, destroy; Registry) and lib/standing.py (roster, install, check, update, rerent, loop, weight_check); lib/test_box.py runs against dn2-3 with one known-failed case (:10-12) and a PASS line (:37), by hand, not in CI. Box side: 20 box-*.sh scripts (setup, matrix, ember, prover, dn2, standing supervisor, node-swap, exec-snapshot, wave, wave-pool, pool, rig, rig-prover, rehearsal, kill, datadir, floor-v5, segal-host) plus dn2-kill.sh, dn400-worker.sh, rehearsal-kill.sh. Collectors: collect.py, bps-collect.py, night.py (60 s probe, 15-minute rows, exec-reset re-run :45-55), autorun.py, restage.py, swap.py. Gates: devnet2-gate.sh (PASS = zero rejects, max reorg under 4, version equal, exec roots equal at a common height, one paid segment; :92-100), canary.sh, canary-next.sh. Page: page.py builds fleet.json; publish-fleet.sh copies it and runs a full Vercel production deploy (:10); publish-0315.py is the first standing publish. PC job runner tools/build-job.mjs (run, publish, watch, fetch, verify; targets ae432dc7 and 1ccfe586; signed kinds run, fetch, collect, restart, update-now, shard-benchmark, build; --stop-miners becomes stop_miners_first, consumed at app/igneum-app/src/jobrun.rs:563). Console tools/console.mjs (post, log, machines, chain, jobs, builds, tuning, results, sync, url). CI checks in tools/ci (22): bash-body, check-workflow-shell, commit-string, copied-sources, identity-check, install-hooks, kit-path, link-check, no-conflict-markers, no-foreign-tree-writes, no-secrets, override-json, pinned-guests, playbook-quit, prover-socket, ps-drive-ref, public-api-check, second-engine, signer-pipe, windows-spawn, plus fixtures; pgrep-self-match-check.sh exists on gpu-fleet only (its ci.yml:82-83), not on horizon or master.
Comparators and the exact gap (approximate unless cited).
| Comparator | Their feature | Ours |
|---|---|---|
| Vast CLI | search offers with any filter expression; create instance --onstart; logs without ssh |
A fixed filter set (vast.py:47-52); rent-card never passes onstart (fleet.py:79), so install is a second ssh step (:102-109); tail needs ssh (:180) |
| SkyPilot | Declarative task YAML, autostop -i, spot recovery, sky status, sky logs |
Imperative; one-shot boxes have no autostop (the caps at page.py:45 are display only; 59 boxes running at 20:02Z); recovery is standing.loop after two dead checks at 600 s (lib/standing.py:126-140); check prints raw dicts (:148) |
| Nomad | Restart policy: attempts, interval, delay, mode; allocation health in the UI | box-standing.sh restarts every 60 s with pkill -9, no cap, no backoff, no failure count (:33, 46-49, 70) |
| HiveOS farm view | Per-rig cards: hash, temps, fan, accepted and rejected, last seen, flight sheets, bulk actions | The fleet page card shows card, state pill, "doing", USD/h, hours only (dlsite index.html:176); standing.py:56 strips the miner= field the supervisor writes (box-standing.sh:64 has MH/s, accepted and rejected, GPU util, watts); console.mjs:114-116 has the HiveOS-style row for the app machines, not for rented boxes |
Rows.
| Id | Row | File or screen | Severity | Hours | Owner | Gate |
|---|---|---|---|---|---|---|
| Q86 | The fleet page is blind to the standing fleet, to death and to finality. page.py:43-47 publishes standing and devnet2, and the page's index.html references neither (grep 0; it reads boxes, results, phases, night, log, :243); a dead box reads "running" for up to 20 minutes because state comes from boxes.json (page.py:38) and standing.jsonl is never read; no fleet or console script reads finality_active (only bps-collect.py:30 tails "Finality:" log lines), so tonight's pause was found by hand (CLAUDE.md, the standing-fleet rule) |
page.py:38-47, dlsite index.html:243, lib/standing.py, tools/console.mjs:124 |
the project lead-visible | 6 (standing block, USD per day, Devnet 2 height and last gate line 2; "unreachable since HH:MM" from standing.jsonl within 2 minutes 2; getFinalityCheckpoints in the standing check and "finality paused since, reason" on the page and the console 2) |
fleet agent | the page shows the standing count and spend; a box killed by hand reads "unreachable since" within 2 minutes; a forced Devnet 2 pause reads on the page and in console.mjs chain |
| Q87 | rerent does not set up or supervise the new box: the docstring promises both (lib/standing.py:14-16), the code rents and patches rows (:78-91); a re-rented card bills idle |
lib/standing.py:78-91 |
the project lead-visible (spend) | 2 | fleet agent | the new label shows synced and mining on the page within the install time |
| Q88 | One-shot boxes have no autostop; cap_vast 1000 and cap_runpod 500 enforce nothing (page.py:45; vast.py:61-72 has no cap check) |
page.py:45, vast.py:61-72, autorun.py |
the project lead-visible (spend) | 1.5 | fleet agent | autorun destroys a one-shot box N minutes after its done line; rent refuses above the cap |
| Q89 | Supervisor restart loop without backoff or a failure line | box-standing.sh:46-49 |
operator-visible | 1.5 | fleet agent | after N failures a standing_node_failed line and a page flag |
| Q90 | Dropped miner and GPU facts: standing.py:56 strips miner=; gpu= never parsed |
lib/standing.py:56, box-standing.sh:64 |
operator-visible | 1 | fleet agent | MH/s, accepted and rejected, watts per box on the page (the HiveOS card) |
| Q91 | A transient ssh drop counts as death: Box.run returns 124 on timeout (lib/box.py:49), check_one marks dead on any rc (standing.py:54), the loop never asks the provider (fleet.py:48-60 refresh_ssh unused); Vast proxy drops are known (canary.sh:41) |
lib/box.py:49, lib/standing.py:54 |
operator-visible | 1 | fleet agent | provider status consulted before a dead check counts |
| Q92 | lib/test_box.py pins a stale binary and digest (:31 igneumd-0313, :34 digest 4a0b8726; 0.3.15 is 06211d55, publish-0315.py:15) and is not scheduled |
lib/test_box.py:31-34 |
operator-visible | 1.5 | fleet agent | a nightly PASS line on the page |
| Q93 | The Devnet 2 gate reads no peers, no finality, no exec blocked (read_box, devnet2-gate.sh:51-53; night.py:15 reads blocked, the gate does not) |
devnet2-gate.sh:51-53 |
operator-visible | 2 | fleet agent | FAIL on 0 peers or finality paused at step 4 |
| Q94 | publish-0315.py hard-codes one worktree and release (:14-18); page.py:13 points at the master checkout's publish script; every publish is a full Vercel deploy |
publish-0315.py:14-18, page.py:13, publish-fleet.sh:10 |
cosmetic | 2 | fleet agent | publish.py reads paths and sha from the release manifest; the page deploys as one file |
| Q95 | Five 6 October rule rows have no tools/ci check and are therefore OPEN by the project's own rule (table below) |
CLAUDE.md 6 October rules, tools/ci/ |
operator-visible | 5 (1 each) | fleet agent (consensus-engineer for the gate check) | each check fires on a known-failed fixture and passes a known-good one |
The 6 October rules against their checks.
| Rule (CLAUDE.md, 6 October 2026) | Check today | What the check is |
|---|---|---|
pgrep -f with a literal pattern is banned |
pgrep-self-match-check.sh on gpu-fleet only; absent from horizon and master |
merge it; wire it in ci.yml on master |
| Box operations live in one tested library | none; devnet2-gate.sh:40-41 and canary.sh:32-33 define their own SSH() and SCP() past lib.box |
fail any file outside tools/fleet/lib that runs ssh -i ~/.ssh/igneum-fleet |
| A watcher verifies the chain-side fact and is trusted only after a known-finished and a known-failed case | none; devnet2-gate.sh and canary.sh have no --self-test |
a --self-test flag on every gate with both fixtures |
| A standing box never leaves the live chain for an experiment; never remove over 10 percent of weight in an hour | weight_check exists (standing.py:108-125) but nothing enforces its call; Box.destroy (box.py:136) has no standing guard |
destroy refuses standing rows; CI greps that scripts calling stop_miners on a label call weight_check first |
| No live build before the Devnet 2 PASS line | none; devnet2-status.json last_gate reads "not run during the block-rate experiment" |
the ship script requires PASS <sha16> in devnet2-status.json, as commit-string-check runs inside build-remote.sh |
| Have checks | commit-string, override-json, ps-drive-ref, no-foreign-tree-writes, playbook-quit, second-engine |
Error states, exact strings. Console machines (tools/console.mjs:114): STOPPED (<reason>) <ago> ago, SILENT, live, then seen <ago> ago, synced or not synced; cards (stale, not in the total) and | <fault> (:115). Console chain (:124): last lock #N (blue N) | lag Ns STALE; no "paused" word in console.mjs (the web tile has it). fleet.py: no ssh (:116), ssh timeout (:118), scp failed (:106, 133), pull failed: ... (:145). lib/box.py: (124, "", "timeout") (:49), SshError("<label>: rc N: <err>") (:53). standing.py: re-renting <label> -> <new> (:134), behind: <label> <sha> wanted <sha> (a publish script moves it; the loop only reports) (:137; "behind" is the binary sha, never height), standing check: N boxes, N alive, N synced, N behind (:138). Fleet page: the state pill (renting, installing, running, done, failed) and doing text only; no dead, behind or finality string exists. devnet2-gate.sh: FAIL <box>: N rejected blocks, FAIL <box>: a selected-chain reorg of depth N, FAIL <box>: version V, FAIL <box>: exec root at height H ... differs, FAIL no segment record paid on any box during the run (:92-100).
Already better. Every rent and destroy writes a price-stamped ledger line (vast.py:43-45, runpod.py:25-26), which Vast's CLI does not (approximate). The Devnet 2 gate compares exec state roots across every box at one height (devnet2-gate.sh:98-99), beyond any SkyPilot or Nomad health check. Gates read chain facts (height, paid segments, reorg depth), not process names. The supervisor runs the exec-recovery recipe by itself (box-standing.sh:66-67). The 10 percent weight rule is a real function (standing.py:108-125). The console distinguishes stopped-on-purpose from silent (parse.mjs:134-140).
3.9 The node's operator surface
What it does today. Fork-added flags (igneum-wt-ship0315/vendor/igneum-node-0315/kaspad/src/args.rs): --devnet-suffix, --evm-rpclisten (default 26790), --evm-disable, --override-params-file, --igneum-exec-snapshot (:297), --ua-rule (:440), --rocksdb-wal-dir (:502), --devnet alias --igneum-devnet. Kept from kaspad: --loglevel with per-subsystem <subsystem>=<level> (kaspad/src/args.rs:262-271), --perf-metrics (:446, debug lines only), --utxoindex, --rpclisten-borsh, --rpclisten-json, --externalip, --appdir. Env knobs: IGNEUM_PROOF_VERIFIER, IGNEUM_PROOF_VERIFY, IGNEUM_DEVNET_GENESIS_BITS, IGNEUMD_DEVNET_BPS, IGNEUM_POW_STRIKES and IGNEUM_ATTACK_TS_OFFSET_MS (feature-gated after X19). Finality logs (consensus/src/processes/finality.rs): Finality: checkpoint {i} determined: block {h} (blue score, daa) (:625), Finality: certificate at index N received: X of Y voters, weight A of B (:1127), Finality: CONFLICTING certificate at index N (:1023), Finality: no body tip passes through locked checkpoint ...; fork choice falls back to depth finality (:1448). No pause or resume log line; "paused" exists only as the polled RPC reason (window filling, N of M, active, paused; igneum-node-0310 finality.rs:1794-1807); the conflict reason is "O-3.17, not reported yet". RPC getFinalityCheckpoints returns finality_active, finality_reason, window_filled_daa, window_full_daa, latest lock (rpc/core/src/model/finality.rs:293-299); no provisional field, no frozen-table share. Exec JSON-RPC on one listener: igneum_getExecStatus (executedTip, blocked, startedFrom), igneum_getProvingStatus, igneum_getFinalityView, igneum_submitProofRecord (igneum/exec/src/rpc.rs:858-979). Metrics: no Prometheus endpoint (no hit in the fork's Cargo files). X19 (docs/fud-ledger.md:1936-1946): a slow clock disconnecting peers silently, time_offset unused, attack env compiled in, a dead override field; fixed on branch ledger-fixes-0311 fbb0082a (one WARN per minute clock-skew line, attack env behind attack-switches, override refuses the field), pending merge; not done: the local-ahead mirror case, the faketime run, the skew field in getBlockDagInfo (docs/plans/node-changes.md:18-19).
Comparators and the exact gap. kaspad parity holds (vendor/rusty-kaspa/kaspad/src/args.rs:241 loglevel, :259 rpclisten-borsh, :348 utxoindex, :392 externalip, :401 perf-metrics) but the fork's own subsystems (finality, exec, proving) got no new knobs. geth (approximate): --metrics with Prometheus or InfluxDB, --log.format json, --http.api allowlist; the fork has none, and writes on the EVM listener share it with reads (rpc.rs:920). reth (approximate): --metrics, RUST_LOG env filter, reth db stats, Grafana dashboards in the repo; the fork has no DB tool or dashboards. monerod (approximate): status, runtime set_log, limit_rate, sync_info; the fork's log level is start-only, no bandwidth limit, status is igneum-miner watch plus getConnectedPeerInfo.
Rows.
| Id | Row | File or screen | Severity | Hours | Owner | Gate |
|---|---|---|---|---|---|---|
| Q96 | No log line on a finality flip: an operator tailing the log never sees the pause begin or end | consensus/src/processes/finality.rs (no pause line) |
operator-visible | 1.5 | consensus-engineer | fast-time simnet prints Finality: paused (<reason>) and Finality: resumed at index N, one line per flip |
| Q97 | finality_reason says paused and nothing else: no share that left, no frozen-table hold, no expiry, no conflict reason (O-3.17) (the node half of Q1) |
finality.rs:1794-1807 |
the project lead-visible (through every surface) | 1.5 (counted in Q1) | consensus-engineer | tonight's pause reads "paused: 42.7 percent of weight left the window; held by the table frozen at lock 6842 until DAA 216,402" |
| Q98 | No /metrics: finality_active, latest lock, exec tip, blocked, paid segments, peers, DAA as Prometheus gauges (the axum listener exists) |
igneum/exec/src/rpc.rs |
operator-visible | 3 | execution-engineer | curl :26790/metrics scraped by a Grafana dashboard committed under infra/ |
| Q99 | Log level is start-only; no runtime change (monerod set_log, reth env filter) |
kaspad/src/args.rs:262-271 |
operator-visible | 2 | consensus-engineer | an RPC or SIGHUP changes the level without a restart |
| Q100 | Clock-skew field in getBlockDagInfo and the local-ahead mirror case still owed after X19 |
docs/plans/node-changes.md:18-19 |
operator-visible | 2 (after the ledger-fixes-0311 merge) |
consensus-engineer | faketime -60s gives one WARN and the field |
| Q101 | No --evm-rpc-api allowlist: submit and export share the listener with eth_ reads |
rpc.rs:920 |
operator-visible (security) | 2 | execution-engineer | a public listener answers eth_ and refuses igneum_submitProofRecord |
| Q102 | No JSON log format: collectors grep four reject patterns (lib/box.py:119) |
kaspad logger |
operator-visible | 2 | consensus-engineer | --log-format json; the gate reads a field, not a pattern |
| Q103 | No igneumd db subcommand (column sizes, exec snapshot tip, finality blob layout) |
none | operator-visible | 3 | execution-engineer | igneumd db stats on a devnet datadir |
| Q104 | Merge ledger-fixes-0311 (X19) into 0.3.16 |
docs/fud-ledger.md X19 |
operator-visible | 1 | consensus-engineer | the faketime WARN on the shipped binary |
| Q105 | Eleven fork .rs files carry an em dash, one inside a log or help string (upstream origin not verified, approximate) |
igneum-node-0315 (a grep for the character) |
cosmetic | 0.5 | consensus-engineer | 0 in any string the operator can see |
Already better. finality_active ships with a reason string, which kaspad, geth and reth have no analog for. --ua-rule version admission (args.rs:440-445) is beyond kaspad. A fixed-height activation is refused by rule; the override file is refused on a dead or duplicate field. Every build binary carries its commit or fails CI.
3.10 Downloads, install and the update flow
What it does today. site/downloads.json (updated 2026-10-06T17:48:20Z) lists four artefacts: Windows installer 0.3.14 (45.4 MB), Mac DMG 0.3.14 (41.9 MB), HiveOS package 0.3.14 (24.7 MB), Mac wallet 0.1.4 (19.6 MB), with sha256 and size, under https://dl.igneum.network/dl/public/ and four stable aliases under /public/ (packaging/README-ship.md, "The public downloads path"). tools/ship-app.mjs cuts a version in eleven checked, resumable steps (preflight, bump, inputs, commit, ci, fetch, dmg, copy, manifest, deploy, verify, console; README-ship.md). The Windows installer is built only by windows.yml on GitHub-hosted runners; since 19:24Z tonight every hosted job dies at start ("recent account payments have failed or your spending limit needs to be increased", release-0.3.15.md 6a), so no Windows installer can be cut until billing is fixed. 0.3.15 is staged with --until manifest; its Devnet 2 canary failed (block version 1026 on the thirteen-field file) and was rolled back. The ship tool's commit step pushed the release tree to master although the run had --branch release-0.3.15 (release-0.3.15.md 6a).
Signing and notarisation state. macOS: codesign -s - (ad hoc) on every binary and the bundle (build-dmg.sh:118-122); no Developer ID, no notarisation, no stapling; the engine strips com.apple.quarantine from its own bundle on start (main.rs:78, packaging/mac/README.md:51-53). Windows: unsigned installer and exes; SmartScreen "Windows protected your PC" (packaging/windows/README.md:76, 111; build-installer.ps1:188); the installer is per-user (PrivilegesRequired=lowest) so no UAC for install; the firewall rule asks once, 20 to 50 s in (ota.rs:981). Comparators: Signal and Tailscale ship Developer ID-signed, notarised DMGs and Authenticode-signed installers; both open with no interstitial (approximate).
The update flow today, precisely (the baseline for frontier 3.7, reproducible-build attestations with N of M).
| Step | What happens | Where |
|---|---|---|
| 1. Build | Node fork and workers built on igneum-build-1 (Linux, Windows cross) and the Mac (macOS); Windows exes reproducible (-Wl,--no-insert-timestamp); a binary whose strings lack its commit fails tools/ci/commit-string-check.sh. The Windows installer and payload zip are built by windows.yml on GitHub-hosted windows-latest from payload-inputs.zip, whose payload-inputs.json (sha256 and size of the zip and every file, fork commit, repo commit) is signed on the Mac with the OTA key and verified in CI against the key compiled into the app and the pin packaging/windows/node-source.pin before anything is built (G13, fixed 5 October; 16 local tests pass) |
tools/build-remote.sh, cross-remote.sh, packaging/windows/push-inputs.sh, .github/workflows/windows.yml, docs/fud-ledger.md G13 |
| 2. Sign | One Ed25519 key, generated once on the Mac (4 October 2026), seed in ~/.config/igneum/ota-signing-key (0600), never in the repo or CI; public key the constant OTA_PUBLIC_KEY_HEX in app/igneum-app/src/manifest.rs:28; igneum-ota-sign sign over the canonical manifest bytes (64-byte detached signature, igneum-app-latest.json.sig); publish-manifest.sh refuses to sign when the embedded key is not the one in ~/.config/igneum. Rotation = a bridge build with the new constant signed by the old key. The wallet's updater trusts one key with no revocation list (Q31). Key not in hardware (G7 says it will be) |
packaging/ota/README.md "Keys", publish-manifest.sh |
| 3. Publish | Manifest (version, per-platform url, sha256, size, kind, min_supported_version, notes, activation_height) and .sig to dl.igneum.network/dl/<token>/igneum-app-latest.json, public copy under dl/public/; the apps' long-poll on the relay's /wake fetches it within seconds; the igneum-jobs.json channel uses the same key |
packaging/ota/publish-manifest.sh, publish-public.sh, relay/api/wake.mjs |
| 4. Check | On start (20 to 50 s in) and hourly with jitter; both files through curl; signature verified over the manifest bytes before parsing; a bad signature reads "manifest signature does not verify" and the manifest is never parsed; a version not newer, or a manifest without this platform, ends the round | ota.rs:144, 628, 1038-1048, manifest.rs:114-120, 175-178 |
| 5. Download and verify | Into <app data>/app/updates/ with resume and --retry 3; size and sha256 from the manifest ("sha256 mismatch: the file is not what the manifest signed"); the Mac re-hashes before mounting |
ota.rs:1054-1079, 1124 |
| 6. Stage | macOS: mount, copy to .Igneum Miner.app.new, run its engine with --version and demand the manifest's version; an unwritable folder gives manual and "Open the download". Windows: the installer is the staged file |
ota.rs:1096, packaging/ota/README.md "Stage" |
| 7. Safe moment | Not urgent: network has not lost over 30 percent of identities in 10 minutes, this machine's minute of the hour, node synced, no hourly boundary within 180 s, no worker starting; a ready update applies anyway after 6 hours. Urgent (fork within 1,800 blocks, unsupported version, Install now) skips every guard. Q4: a passed activation counts as close; no finality input; 0.3.15 adds "never while a remote job is active" | manifest.rs:322-355 |
| 8. Apply | The engine writes update-pending.json, starts the helper and exits through its quit path (miners 8 s, node 30 s, last log upload). macOS helper swaps bundles, strips quarantine, reopens, restores .previous on failure. Windows helper runs the per-user installer /VERYSILENT ... /IGNOTA=1 first, with the engine still mining, then the installer stops the engine and relaunches |
packaging/ota/README.md "Apply", ota-apply.sh, ota-apply.ps1 |
| 9. Rollback | The new engine counts starts in update-pending.json; a third start without 90 healthy seconds restores the previous version ("rolled back" in Settings); the first Windows update from 0.3.0 has no rollback target |
ota.rs:371 |
| 10. What the user sees | "Igneum Miner X is available.", "Downloading X: N%.", "X is ready.", "Installing X. The app restarts itself. Mining continues until then.", "X did not stay up and was rolled back.", "updated to X from Y" in the event feed; Settings: Check now, Install now, automatic switch, the key fingerprint under "Allow remote jobs from Igneum (signed)" | ui/app.js:29-52, index.html Settings |
| 11. Second engine, paused finality | A sweep or IGNEUM_APP_NO_OTA=1 engine never runs the updater (engine.rs:601, 778; the 5 October rule). Nothing in the update path reads finality (Q4) |
So the trust today is one key, one builder (the box and the Mac, both the project's), one manifest, no attestation by anyone else, no on-chain record, and the client installs whatever that one key signs at the moment the safe-moment rule allows. That is the baseline frontier 3.7 moves from: N of M attestations in a registry contract, the updater refusing under N, and no install while finality is paused.
Rows.
| Id | Row | File or screen | Severity | Hours | Owner | Gate |
|---|---|---|---|---|---|---|
| Q3 | Signing and notarisation; first-run prompts (section 1) | build-dmg.sh:118-122, windows.yml |
the project lead-visible | 6 plus certificates | app owner, the project lead | section 1 |
| Q80 | The Windows installer pipeline is dead: every GitHub-hosted job fails at start on billing; the installer and payload zip have no other builder, so 0.3.15 and every hotfix are Mac and HiveOS only until the project lead fixes Billing & plans, or the installer step moves to igneum-build-1 (Inno Setup under Wine, or a self-hosted Windows runner on PC 1) | release-0.3.15.md 6a, .github/workflows/windows.yml, packaging/windows/build-installer.ps1 |
the project lead-visible | 0 (the project lead: billing) or 4 (installer step on the box or PC 1 as a signed job) | app owner; the project lead | a green windows.yml run, or tools/ship-app.mjs --dry-run showing the fetch step satisfied from the new builder |
| Q81 | The ship tool pushes master whatever --branch says (the 19:30Z push of the release tree to master) |
tools/ship-app.mjs commit step |
operator-visible | 1 | app owner | --branch X pushes X; a test on a scratch repo |
| Q82 | The download page shows no hash for Windows and Mac, no OTA key fingerprint, no verify command; the stable aliases are not explained (Q50 covers the page; this row is the artefact side) | miner.html:549-558, packaging/ota/publish-public.sh |
user-visible | 1 | site owner | igneum-downloads.json carries the fingerprint; the page prints it with shasum -a 256 and certutil -hashfile lines |
| Q83 | The update key is one software key on one Mac, not in hardware, against G7's "held in hardware and its policy published" | packaging/ota/README.md "Keys", docs/fud-ledger.md G7 |
operator-visible (security) | 3 (a YubiKey or Secure Enclave signer behind igneum-ota-sign, the policy page) |
cryptographer, app owner | a release signed with the hardware key installs; the key policy is a public page; the software seed is retired |
| Q84 | An activation height in the manifest outlives its activation and turns every later update urgent (the Q4 class, publisher side) | publish-manifest.sh --activation-height |
operator-visible | 0.5 | app owner | the publisher refuses a height at or below the live DAA and drops a stale carried-over one |
First 60 seconds, fresh Windows PC (from the code and the READMEs): download Igneum-Miner-Setup-0.3.14.exe from /miner, SmartScreen interstitial "Windows protected your PC" (More info, Run anyway), per-user install with no UAC, "Start Igneum Miner now" (Igneum-Miner.iss:86), tray "Igneum Miner: starting", Welcome, Cards (detect runs on entry, app.js:641), Address, the one-time key sheet, "Start mining"; between 20 and 50 s in, a UAC prompt for the igneumd.exe inbound firewall rule arrives on top of whatever screen is up (ota.rs:981; declined = the node dials out, never asked again); closing the window gives "Igneum Miner keeps mining" (host.cpp:354). Fresh Mac: DMG, drag to Applications, Gatekeeper refuses the ad hoc app (macOS 15 has no Right-click > Open; the user must find Privacy and Security > Open Anyway, approximate), "Starting the engine" (IgneumMiner.swift:146), quarantine cleared from the bundle (main.rs:78), the same three screens, a menu-bar item with "Pause mining". Both tiers of card see the same screens; an 8 GB card gets two vote identities and a 24 GB card eight (detect.rs), which no screen explains (the "a card runs several" line lives on the site, not the app).
4. Cross-cutting
4.1 Accessibility (estimated from HTML and CSS)
| Surface | Contrast | Keyboard | Screen reader | Motion |
|---|---|---|---|---|
| Ember dark | bone 17.4:1, ash 7.0:1, ember 5.6:1, molten 11.0:1 on obsidian: passes | :focus-visible ring (app.css:66), rail arrow keys, Escape on the card and quit (app.js:681-688, 979) |
aria-live="polite" strip (index.html:50), role="dialog" aria-modal (:419), radiogroup with aria-checked (:194), labelled switches and sliders; gaps: the blocks canvas aria-hidden with no text alternative (:241), the drawer a div with no landmark, user-select:none on body (app.css:36) |
reduced motion honoured everywhere (app.css:67, 113, 142, 148, 157, 420, 437, 466; app.js:1137) |
| Ember light | molten #B8731F on white 3.81:1 and on bone 3.38:1; ember #E04A14 on bone 3.61:1: fails AA on the key, the address and every live state (Q12) |
same | same | same |
| Site | dark passes (ash 6.97:1, ember 5.64:1 on obsidian); the homepage light section fails on links 3.08:1 and the eyebrow 3.97:1 (Q56) | skip link and one focus ring on every page (nav.html:1, head.html:22); explorer rows clickable with the hash link as the keyboard target |
aria-live="polite" on strips refreshed every 2 and 10 s (Q54); aria-live="off" on the homepage live strip, correct; the DAG canvases aria-hidden with no alternative |
reduced-motion rule on every page (head.html:62); live.html caps 30 fps and stops off screen; the homepage does not (Q55) |
| Hub | amber #FFB35C and red on graphite pass (approximate from the tokens, relay/ui.html:17, 65) |
buttons and tabs are native elements; no skip link | no live regions; the console is an operator tool | none needed |
| Pool page | default light tokens, not audited beyond the formats | native | none | none |
4.2 Copy law
The em dash character, counted per shipped file: 0 in every site/*.html and site/partials/*.html, 0 in app/igneum-app/ui/index.html and app.js, 0 in relay/index.html and relay/ui.html, 0 in pool/web/index.html, 0 in tools/community/discord-hooks.mjs, 0 in app/mac/*.swift and app/windows/host.cpp, 0 in every app/igneum-app/src/*.rs user-facing string, 0 in the wallet's ui/. The rule holds everywhere that ships.
Forbidden strings (site/forbidden-strings.txt, 25 patterns) per shipped file, with what each hit is:
| File | Pattern | Count | What it is |
|---|---|---|---|
site/index.html |
dl\.igneum |
2 | the public download aliases (:514-515); the pattern is meant for the token path (Q52) |
site/miner.html |
dl\.igneum |
5 | the same aliases and the HiveOS installation URL (:549-557) |
site/wallet.html |
dl\.igneum |
1 | the wallet alias (:371) |
site/downloads.json |
dl\.igneum |
1 | the base field |
relay/ui.html |
Hetzner |
1 | the "Hetzner network" card title (:377); private console, rule does not apply, but one word |
app/igneum-app/ui/app.js |
the project lead |
1 | a code comment (:270); the ui bundle is outside every check (Q22) |
tools/community/discord-hooks.mjs |
MacBook, \+0100, tailscale, ts\.net, dl\.igneum |
1, 1, 1, 1, 2 | the guard's own regexes (:51, 59, 61), the read-only fleet URL (:31), and ts\.net matching "stats.net" in a template (:255): false positives, none posted |
pool/README.md |
Hetzner, /opt/igneum, CLAUDE\.md |
5, 2, 1 | the deploy section; not a site page, but it would fail the scrub if copied |
Every other pattern is 0 on every file. Two-beat antithesis and aphorisms in shipped copy: the headlines listed in Q62, the two Ember lines in Q23, the four Discord incident sentences in Q78.
4.3 Consistency of numbers across surfaces
| Quantity | Site home | Site live | Explorer | Ember | Hub | Discord | Pool page |
|---|---|---|---|---|---|---|---|
| Hash rate | fmtHash: 1 decimal, MH/s or GH/s only, "pending" when null (index.html:719) |
1 decimal, kH to TH, unit in <small> (live.html:329, 652) |
1 decimal, kH to PH, integer under 1 kH/s (lib/explorer.mjs:62-67) |
1 decimal MH/s (app.js:1450) |
2 decimals GH/s, 1 decimal MH/s (relay/ui.html:246) |
1 decimal MH/s, 2 decimals GH/s (discord-hooks.mjs:101-109) |
2 decimals once scaled (web/index.html:152) |
| Blocks per second | toFixed(1) "blocks / s" (index.html:375, 722) |
toFixed(2) (live.html:228, 650) |
inverted: "1.2 s per block" (explorer.html:331) |
not shown | toFixed(2) (relay/ui.html:370) |
6-hour window | not shown |
| Height, counts | toLocaleString('en-GB') "chain block" (index.html:377) |
compact() "110.0k blocks" (live.html:229, 651) |
full digits "Height" (explorer.html:212, 328) |
withCommas |
nf() separators |
en-GB separators (:99) |
raw integers, no separators (web/index.html:163, 172) |
| Difficulty | not shown | compact() "126.81M" |
"126.81 M" with a space | compact() |
nf() |
6-hour delta | toLocaleString() with no locale |
| Miner count | "vote keys active in 10 min (a card runs several)" (index.html:374) |
"Identities" (live.html:232), table heading "Miner" (:254) |
not shown | "ids" per card | "identities 10 min" | "56 vote keys active" and "93 voters" in one post (Q77) | "Miners / workers" |
| IGN | not shown | not shown | reward 4 decimals, supply integer | 4 decimals (app.js:1364) |
2 decimals (fmtIgn) |
4 on tiles, 6 in tables | |
| Price | none anywhere; "pounds a day" once (index.html:510); Ember has a priceNow() with a devnet zero |
||||||
| Finality | "#N" or "paused" (index.html:879) |
"#N, 3 min ago" plus the bar (live.html:404, 512) |
none | "#N · 1 h ago" (app.js:1350) |
"active" or "paused" | "checkpoint 6,842 at 55.x% of weight, 1 h ago, 93 voters" | none |
Row Q85 (cross-cutting, 2 hours, site owner with the app owner): one shared formatter module (site/lib/format.mjs) for hash rate (1 decimal, unit ladder kH to PH), counts (en-GB separators, never compact() for the same quantity shown in full elsewhere), blocks per second (2 decimals, never inverted), difficulty (one spelling), IGN (4 decimals on tiles, 6 in tables), and one word for a vote identity ("vote key" on every surface); the app, hub, Discord and pool copy the same table as a Rust and a JS constant, with a test that renders the fixture values identically. Gate: the fixture renders byte-identical on all seven surfaces. Severity: the project lead-visible (he reads three of these side by side every evening).
4.4 Error states, one table
| Condition | Ember | Site home | Site live | Explorer | Hub | Discord | Wallet | Pool |
|---|---|---|---|---|---|---|---|---|
| Node down | "the node is not running", pill "node failed", "waiting for the node to sync" | "simulated preview · observer offline" after 30 s | "OFFLINE", "observer offline, last update N" | "observer offline, last known" | card red "silent 4h"; "observer STALE N s" | observer_silent after 180 s (not live) |
"no node", "waiting for a node" | "difficulty 0, DAA 0", no word (Q68) |
| API or relay unreachable | "The update failed." (Q11) | "live feed unavailable" | "api unreachable, scene frozen" | error on first load, silent after (Q10) | "offline: Failed to fetch", stale panels (Q6) | n/a | "The API did not answer: ..." (address page) | "stats unavailable" |
| Finality paused | nothing (Q2) | "paused" counter, "final" label (Q5) | bar "finality paused: N% silent" (Q1) | nothing (Q10) | tile "paused", no cause (Q6) | nothing (not live), preexisting (Q7) |
"paused" or "not checkpoint-checkable" | nothing (Q73) |
| GPU lost | "Card removed: . Its worker stopped." | card telemetry drops | member drops from the count | |||||
| Job hung | "job running" notice | "running" forever (Q40) |
4.5 First-run and update flow
Section 3.10 carries both in full. The two facts to carry forward: a fresh machine on either OS meets two warnings before the first screen (Q3), and the update flow is one software key, one builder, one manifest, with a stale activation height that makes every update urgent (Q4, Q84) and no attestation by anyone outside the project (the frontier 3.7 baseline).
5. Already best in class (honest both ways)
- Ember Tune: automatic per-card efficiency tuning with the money consequence on screen, measured 37 to 41 percent more hashes per watt on PC 1 (
ember-tune.md:254, 271). No miner console ships it. - The update path: Ed25519-signed manifest verified before parsing, sha256 of the artefact, staged swap, safe-moment rule, machine slots, automatic rollback after two failed starts (
ota.rs,manifest.rs). HiveOS and NiceHash update without a safe-moment rule (approximate). - The wallet verifies finality certificates itself with the node's consensus code (
finality.rs:101-121); Touch ID is bound to the exact quote (engine.rs:539). No mainstream EVM wallet does either (approximate). - The site: self-hosted fonts with fallback metrics, zero third-party scripts beyond two pinned modules, no analytics, no banner, HSTS preload, honest live states, a browser-side BLS verifier on the homepage (
verify/core.js), a dated journey with gates and a public ledger of every criticism. - The block page: parents, children, mergeset blues and reds, selected parent, shards with prover and lag, checkpoint fractions and certificates (
block.html:285-325). - The pool: every share CPU-verified at the node's own engine; the member's vote key in every header it hashes (
node.rs:47), so pool share never becomes vote share. - Discord: full sha256 and verify commands in every release post; a forbidden-string guard on every post; incident texts that say who is affected per tier.
- The hub: one phone-first page with every PC's miner, node, app and OTA state fused on one card, stopped and silent as different words, drop-anywhere uploads.
- The relay's wake long-poll: dependency-free, fake-clock tested, 30 a minute per IP.
- The ship pipeline: eleven checked, resumable steps with secrets scrubbed from every line and a signed inputs manifest verified in CI before any build (G13).
6. Open questions and what could not be run
- The pause's end was not observed: at 20:05:03Z (
live_state.updated_at) no lock after 6842 had formed. Lane 3 expects the frozen table to expire at DAA 216,402, about 20:40Z. Whoever reads this after 21:00Z should add the resume time and whether any surface showed it. - Which commit the deployed relay runs is unknown from this tree (
relay/README.mddoes not name it); Q8 assumes master. The deployed site is master by the GitHub integration. - Lighthouse numbers are estimates from source; no browser run was allowed. The gate for every site row is a real run.
- The comparator claims are from memory; where a repository or page is named it is cited, otherwise approximate. The Stratum v2 reference was not cloned.
- The fleet and node sections depend on the gpu-fleet worktree and the vendor forks, read tonight; the fleet's own page was not opened.
- Hours are agent hours (the project lead's rule); Q3 and Q80 also need the project lead (certificates, GitHub billing).
7. Summary for the coordinator
Every shipped system was audited against named comparators from the code and the design docs, with 95 ledger rows (ids Q1 to Q105), each with a file or screen, a severity, hours, an owner and a gate; the top ten total 40 agent hours; every row summed is about 230 agent hours. The three findings that matter most: (1) during tonight's two-hour finality pause no public or operator surface could say why, because the node's finality_reason is dropped at tools/observer/observer.mjs:720, the frozen-table share that held the pause is reported by nothing, Ember has no pause state at all, the homepage drew "final", the explorer showed nothing and Discord was not live; one forced pause on Devnet 2 with a screenshot per surface is the gate (Q1, Q2, Q5, Q6, Q7, Q10). (2) A fresh machine meets two warnings before the first screen (ad hoc Mac signature with a quarantine strip, unsigned Windows installer plus a UAC prompt 20 to 50 s in), and the Windows installer cannot be rebuilt at all tonight because GitHub-hosted runners are blocked on billing (Q3, Q80). (3) The update path treats any passed activation height as "within 1,800 blocks", so every update since 0.3.14's manifest has been urgent, skipping the safe-moment rule (the PC 1 install under a measurement job); the same path has no finality input and is one software key, one builder, one manifest, which is the baseline frontier 3.7 moves from (Q4, Q83, Q84). Rules for main: the observer and every API carry finality_reason, held_by and finality_provisional before the public testnet; the tools/ci forbidden-string check covers app/igneum-app/ui/; one formatter table for every number on every surface (Q85).