Plan of record for Feature #1053, owner approved 2026-11-03. Tagged releases go to Play closed testing or straight to production by dispatch, and closed testing promotes to production. Test builds never upload, so no version codes are spent on them.
Two Gemini 2.5 Pro passes applied after approval: a tighter keyless-identity condition, a provenance attestation on the AAB, a tag-on-main check, and idempotent, non-destructive track updates. Section 12 records each finding.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes the Environment rule that would refuse the first signed run, adds IPA signature and altool validation checks, aligns the privacy goal with the CoreScope choice, and marks rounds 1 to 3 done. Verdicts are on #958.
Feature #200.
Agent: ChartreuseBay (session 425e424d)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Finding 6 and owner round 2 now use app.offband.meshcore.ios, after Apple refused the original ID.
Feature #200.
Agent: ChartreuseBay (session 425e424d)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four Epics: Windows signing toolchain proven on a Mac runner, signed IPA and TestFlight lane, App Review package, Integration Testing. Every finding cites the research on #958.
Agent: ChartreuseBay (session 425e424d)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Builds the owner-approved parity design (docs/initiatives/standalone/975-settings-restructure/tones-screen-parity-mockups.html) against meshcore-firmware 3e788e2a:
- #986: the six event tones (DM, Channel, Room, New contact, Ack, Sent) on Buzzer and tones, one shared picker with a play button per row (0x0F/0x10/0x11). DM is one tone for every direct message.
- #970: all eight custom slots listed; the editor reads the stored tune back (0x12), checks typing with a port of firmware's rtttlProblem(), and saves in transport-sized chunks (0x0A), restarting once if displaced.
- #971: Radio tone in the channel sheet and chat menu, Default naming what it plays.
- #987: Screen (text sizes, Screen off, time zone with a phone-matched suggestion) and Battery (full point, set here, back to auto) under Node Settings, gated on caps2 0x40.
Caps2 0x10 is now TUNES (slots, channel tone, event tones, preview). Also corrects the mockup's zone count: 40 zones plus not set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner-approved 2026-10-25 with four decisions: Screen and Battery under Node Settings, a separate play button, no way back to radio default, and the Menus and lists / Messages labels. Built against meshcore-firmware 3e788e2a. Covers #970, #971, #986, #987.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The command is 0xC5 sub-codes 0x0D/0x0E with caps byte 2 0x20, not a new 0xC7. Power-save availability comes from ps_ok in every GPS reply. The radio raises a sleep interval of 1 to 299 s to 300. The buzzer-only examples are now boards that actually lack a button; the T1000-E has both. Facts confirmed by AmberForge on Agent Mail 2026-10-24 and verified on firmware-base.
Agent: MistyLynx (session 6c0d5797)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner-approved 2026-10-23. Six numbered state cards drawn against the real SettingsShell widgets, with behavior bullets as the build contract and a rationale section, following the firmware GUI mockup convention. Self-contained: inline SVG icons, system fonts, no network references.
Covers GPS off, on, Power save backed off, a build without Power save, the Settings menu before and after, and the button and buzzer split. Serves #969 as well as epic #975.
Agent: MistyLynx (session 6c0d5797)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner asked why there were no UI mock-ups to review. There was no good
answer: this Epic's card was built and installed on both his machines without
him ever seeing the design, and the plan had no step requiring otherwise.
So the plan now has B0: mock-ups first, owner-approved before any of it is
built, as a committed HTML file beside the plan in the format his firmware
initiative already uses. epic-b-storage-mockups.html is that file: eleven
numbered variants with real Material 3 tokens, a 360-wide pane, example values
from his own install, and the behaviour to build as bullets under each.
Owner-approved 2026-10-23.
The confirmation (B1a, variants 8 to 10). It states how many copies, roughly
how much space, how many messages are NOT affected, and that it cannot be
undone, with the count on the action button so the scope is the last thing
read. Cancel, a tap outside and back all decline. Nothing eligible opens no
dialog at all, because a dialog that only says zero is not worth opening.
That needed a preview pass: verify every eligible key and total it without
deleting, so the numbers are real rather than estimated. Verification now runs
twice for a confirmed clear, once to preview and once under the key lock. That
is the cost of saying what will go before it goes. approximateBytes is named
for what it is: the sum of blob lengths, not what VACUUM returns, because page
rounding moves the real figure; the dialog says "about" and the result reports
what actually changed.
Writing the preview found a real bug, and it was about to bite him. The
"skip anything stamped this pass" guard exists so installing an update never
deletes a blob on sight, and it was being applied to the button as well. Every
install that migrated before #962 has an empty __msgrows_migrated_at, so the
first press stamped everything and cleared NOTHING. His install is exactly
that: 47 migrated keys, none stamped. He would have pressed the button on a
70.9 MB database with 16.6 MB of reclaimable blobs and been told "nothing to
clear".
Fixed as eligibleByAge, one static rule both preview and run share, so the
dialog cannot promise what the sweep will not do. An explicit press includes
newly stamped keys; the automatic sweep still refuses them. Three tests on the
rule directly, plus one proving the automatic path refuses while the press
clears.
Label stays "Clear Stale History Now" rather than "Compact", owner's call and
the better argument: the name generalises to clearing stale history for other
reasons later, such as #963's retention limit.
14 widget tests and 34 storage tests. Full suite 1,502 pass, 1 skipped.
analyze clean, format clean.
Task: meshcore-open-gan
Agent Mail: TealGlen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
965 Epic A the timestamp and sweep, 966 Epic B the manual action, 967 Epic C
Integration Testing. All three are sub-issues of #962, with Citadel tasks
meshcore-open-5vt, -gan and -2jd chained A to B to C.
This is a follow-up only because of a sequencing mistake on my part: the plan
merged with epic_chain empty, and the grant validator requires a non-empty
list while the mint path indexes its first and last entries, so the plan as
merged could not have minted anything. The Epic issues need to exist before a
plan PR merges, which is how #853 did it. Noted in section 5 so the next plan
does not repeat it.
No other term changes: budget 3, owner verification, 168-hour expiry, chain
true, no flash.
Task: meshcore-open-8xj
Agent Mail: TealGlen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Owner, 2026-10-22: "X = 30, sweep once per launch, timestamps in separate
blob, button reports refusals, agreed." All four as recommended.
Section 9 becomes a Decisions table carrying the reason for each, so the
trade-offs are on the record rather than only the outcome.
Every X placeholder is now the literal 30 days: at a glance, scope, A3, the
can't-tell row for a key with no timestamp, the acceptance bar and done-when.
A plan that goes to build with placeholders in it invites someone to pick a
number themselves.
Three things sharpened while propagating them:
- A3 states the once-per-launch wiring, since the trigger is a real decision
and not an implementation detail.
- B1 states that the manual action REPORTS refused keys in its result, not
just in the log, which is what decision 4 actually asks for. It previously
only said it shared the sweep's refusal path.
- The acceptance bar tests the boundary at 29 days, not just the middle of
each range.
Noted that 30 days is a named constant, so changing it later is one edit and
the tests can set it directly rather than manipulating clocks.
Task: meshcore-open-8xj
Agent Mail: TealGlen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The owner asked whether VACUUM would wipe out non-migrated blobs. It cannot:
it rebuilds the file to reclaim pages the deletes already freed and never
removes a row that still exists. Measured on a copy rather than asserted.
deleted on purpose : 40 migrated history blobs
kept : 291 keys, of which 98 UNMIGRATED history
missing after VACUUM : 0
byte-changed after VACUUM : 0
unexpectedly present : 0
message rows : 28,389 -> 28,389
file : 74,350,592 -> 54,927,360
integrity_check : ok
Every kept blob was hashed before and after and came through identical.
The consequence is worth more than the reassurance, so it is now diagnosis
row 11: ALL the risk is in the DELETE selection and none of it is in VACUUM.
VACUUM makes a wrong selection permanent but never selects anything itself,
which is why the safety budget sits on the per-key re-verify and the
key-pattern restriction.
Added to the acceptance bar as a test: every kept blob byte-identical after
VACUUM, unmigrated history keys asserted present by name, row count unchanged.
Task: meshcore-open-8xj
Agent Mail: TealGlen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The #917 plan deferred this as a separate owner-gated step. The owner opened
that gate on 2026-10-22 and specified the design: a per-key time marker, an
automatic clear X time after a key migrates, and a manual "Clear Stale History
Now" action.
Ten diagnosis rows, each measured rather than asserted. The two that shape the
design:
- DELETE frees NO disk space. 74,350,592 bytes before deleting all 40 migrated
blobs on a copy of the owner's database, 74,350,592 after. VACUUM is what
shrinks the file: 54,919,168 bytes, -18.5 MiB in 0.6s, integrity ok, all
28,386 rows intact.
- Migration is lazy and per key. His install had 40 keys migrated and 98 not,
because those radios had not been reconnected, so an install-wide clock
would delete nothing for a long time and then a lot at once.
Also records what the retained blob does and does not protect, because
"keeping a backup" is the wrong mental model: it is a frozen snapshot of the
pre-migration history, and everything that arrived since has been single-copy
from the instant it landed, blob or no blob.
Three Epics, merge budget 3, owner verification per Epic because this deletes
data. Five can't-tell branches, four of which stop; the one that proceeds is
a failed VACUUM, which affects file size only and follows deletes that were
each individually verified.
Four choices for the owner, with recommendations: X = 30 days, sweep once per
launch, timestamps in a separate blob rather than reshaping the hot marker,
and the manual action reports keys it refused rather than only logging them.
epic_chain is deliberately empty: the Epic issues do not exist until this plan
merges, and it is filled in before the grant is minted.
Task: meshcore-open-8xj
Agent Mail: TealGlen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4b8f5731 corrected the plan's downgrade claim but explained it with a drift
version check that does not exist. drift compares the stored user_version
against schemaVersion for inequality and routes a downgrade into onUpgrade;
the pre-#917 build simply had no migration getter, so it got the default that
throws.
The conclusion is unchanged and firmer: every shipped build lacks that
strategy, so nothing done now can let one open a schema-2 file.
Also points at the measurements and the per-decision recommendations now on
the issue, and at the test that pins the behaviour.
Task: meshcore-open-3e1
Agent Mail: TealGlen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The plan's Epic B outcome said a downgrade to an older build "still finds its
history where it looks for it". True of the data, false in practice, and the
owner hit it: drift refuses to open a database whose user_version is newer
than the binary's schemaVersion, so a pre-#917 build cannot open a migrated
database at all. It never reaches the blobs it would have read.
Correcting it rather than leaving a wrong statement in a merged plan of
record, because the consequence is a release one: once a user launches a
schema-2 build they cannot go back to an earlier release, so a Play rollback
or a halted staged rollout does not help anyone who already launched it. The
real fallback is an older build PLUS a pre-migration database copy, which
loses everything received since.
#943 carries the open decisions. This commit only fixes the claim.
Citadel: meshcore-open-gq7
Refs #943, #917, #920
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Owner chose A on all three, 2026-10-20. Canon says a plan is rewritten so
the new design IS the plan, rather than leaving answered choices sitting as
options, so section 9 is now a Decisions table and nothing in the document
still asks a question.
1. The legacy untagged data is DELETED, not orphaned: the 2,041
path_history_<contact> keys and the global delivery_observations. Both
regenerate, and here the orphaned data IS the performance problem, which
is why this differs from #754's orphan choice.
2. Path history MOVES into drift rather than gaining a prefix in place, one
migration instead of two.
3. #158 and #190 stay in the Feature as Epic D.
Propagated to the places the decisions change behaviour, not just section 9:
B2 and B3 now state the legacy delete and gate it behind the per-radio
readback, and the two can't-tell rows in section 6 name decision 1 instead
of pointing at an open choice.
grant-terms re-validated: zero errors, chain [936-940], budget 5.
Citadel: meshcore-open-rmr
Refs #853
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#853 had an audit in its issue body and nothing else: no plan doc, no merged
PLAN.md, no grant. That is why #754 and #471 sat written-but-unmerged for
four days with nowhere to go, and why its other children were filed as three
separate bugs against one file.
Written from the canonical FEATURE-PLAN template. Five Epics (#936 to #940),
ordered base-capability first, Integration Testing last per canon.
Three things the research changed from #853's own audit, which was taken at
a69cb998 and has moved since:
- `pending_messages` (#855) is DEAD CODE. Its three methods have no callers
anywhere in lib/ or test/ and the key is absent from the owner's live
prefs. The answer is deletion, not scoping. Three separate bug PRs would
not have caught that.
- #854 and #924 are the same store seen from two directions, not-per-radio
and unbounded-at-2,041-keys. One change fixes both, and because 86% of the
prefs file is that store, it also cuts most of #923's write cost as a side
effect.
- #190's named root cause was already addressed by #194 and #472, so the
plan carries a verdict task rather than a fix, with evidence either way.
Epic A exists because of what tonight taught: #853's acceptance says every
store has "a verified scoping verdict" and the only way to check that today
is to re-read an audit by hand. A1 makes it a test that fails when a surface
is unclassified or a new global key appears. Same lesson as #927, where a
comment saying a flag must never reach dev did not stop it reaching dev.
grant-terms validated against the real parser: five epics, merge budget 5,
owner verification, 168-hour expiry with its basis. Zero errors.
Three owner choices in section 9, each with a recommendation and reasoning,
all three about what happens to existing untagged data or where it lives.
Citadel: meshcore-open-rmr
Refs #853, #854, #855, #856, #507, #158, #190, #923, #924
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The numbers, and the two places the implementation deliberately differs from
what this plan said: the blob is never deleted rather than deleted after a
verified readback, and a row holds the per-message JSON rather than a column
per field. Both are narrower or stricter than the plan, and a plan that does
not say where it was departed from is not a record.
Citadel: meshcore-open-i8t
Refs #920, #917
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The plan gated the smaller stores on measurement. The measurement came back
"no", so the plan says so, with the numbers and with the two findings that
fell out of taking it (#923, #924). A gate that resolves silently is not a
gate.
Citadel: meshcore-open-xxc
Refs #919, #917
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first version was invented YAML in an untagged fence. grant.py requires a single fence tagged grant-terms containing JSON with keys epic_chain, chain, verification, merge, and optionally flash/expires_after_hours.
epic_chain is a list of distinct ISSUE NUMBERS, so the Epics had to exist first: 919 (A, isolate offload), 920 (B, normalise to rows), 921 (C, integration testing). Verified with grant.parse_terms against the file: zero errors.
Refs #334
Agent: TealGlen (session c61ce804)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measured diagnosis: StoredBlobs holds one JSON blob per key; saving one message decodes, rebuilds and re-encodes the whole channel history. 134 ms per message at 9230 records against 0.002 ms for an append, so 67000x more work than needed, scaling ~14 ms per 1k records.
Epic chain: A moves the whole read-modify-write into one Isolate.run (relief, no data risk); B normalises to one row per message (removes the cost); C integration-tests both on hardware.
Carries the cannot-tell branches table and grant terms with the basis for every budget. Also records that #335 did not fix this and that drift already runs SQL off the UI isolate.
Refs #334
Agent: TealGlen (session c61ce804)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Promise table, signal precedence, marker requirements for the firmware D1 decision, known fail-closed limitations.
Agent: GentleGlacier (session f0c92a36)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Initiative plan (4 features, 9-epic chain, grant terms, can't-tell table, owner decisions D1-D6 with recommendations) plus the three-surface contract investigation it derives from. Owner approved the plan in chat 2026-10-17; D-recommendations stand unless overruled on the PR.
Agent: GentleGlacier (session f0c92a36)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Records what shipped (Discover Regions + channel Set Region Scope), the verified wire contract, the D1 hardware pass, the six D1-found issues and their fixes, the region-hierarchy finding (flat whitelist, cosmetic hierarchy), and known limits/follow-ups.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The plan grant was refused: a plan's flash term must name one
registered device, and the radio isn't chosen yet. The flash now gets
its own per-device flash grant before D1. Terms checked with the
grant module's parser and validator: no errors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner direction 2026-10-02: emulate stock and Offband separately; the
firmware publishes a protocol manifest at every tag and merge
(OffbandMesh/meshcore-firmware#1318) and the fake radio consumes it;
client CI checks its constants against the manifest; real-radio
traces (Offband, and one radio flashed to stock by the owner) check
behavior at D1. 55 points, still four Epics.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Dart fake companion radio, in-process and over TCP, so CI can test
connect, sync, messaging and preset flows (including remote nodes)
with no hardware. Four Epics: core and handshake (#756), messaging,
settings, remote nodes and faults (#757), CI suites and a manual
runner (#758), integration testing (#759). Gemini 2.5 review: three
findings, all verified and folded in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Covers the Custom Name override epic and the documentation of what a display name does not prove. Owner approved the plan in chat before this PR was opened, per the review-then-approval-then-execution order in SAFELANE section 3.
Architecture: Contact.name becomes a getter returning customName ?? receivedName, and the constructor takes receivedName. Measured 85 display reads against 22 writes, so renaming the write parameter makes the compiler enumerate every write site while every read stays correct and untouched. The rejected alternative, adding displayName and migrating 85 reads, fails silently when a site is missed.
The field is receivedName rather than advertName because a contact-card name is not advertised. Writing the test matrix caught that; the original naming would have lied for every card-added contact.
One hole the type system cannot see: the five buildUpdateContactPathFrame call sites read .name and would still compile, which would write a private label into the radio's shared contact table. Test T14 reads the source and guards it.
31 automated tests, every behaviour with a positive and a negative. Nine assert absence and exist purely to fail on a future regression.
Adversarially reviewed with Gemini 2.5 Pro. One finding accepted: nothing proved the UI never writes an empty custom name, so T29 and T30 were added. One finding rejected with a citation: the claimed migration crash does not exist because contact_store.dart line 145 already reads json name as String with a fallback to Unknown. Its narrower grain, that the rename could drop that fallback, became T31.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
OUTCOMES.md records what was delivered, the owner's D1 approval, the
evidence, what went wrong and what's left. PLAN.md gains section 10:
Epic A merged before D1 (the plan's gap), and the #748 tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sensor admin path is in scope (C5). Hardware testing happens once at
the end, in Epic D; the chain does not stop between Epics.
Agent: MagentaHawk (session 5a8922d6)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The MESH 500 regional preset system: Liam's public preset list as the
base, an Offband overlay in config-profiles that wins on conflict, a
catalog in the app that works offline and refreshes without a release,
and applying presets to every node type with stock firmware commands.
Four Epics (A config-profiles, B app catalog + Companion/Observer,
C remote nodes, D integration testing) with grant terms for running
B, C and D as a chain, merges deferred to the end.
Feature: #647
Agent: MagentaHawk (session 5a8922d6)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Observapeater web dashboard (repeater variant w/ MQTT+WiFi tabs), the
on-device TFT UI, and the field-terminal concept panels — reference
assets for the NeonPocketMC investigation (issue 542).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reference asset for the NeonPocketMC investigation (issue 542): n30nex's
on-chip WiFi-AP web client UI, shared on the mesh channel 2026-08-06.
Stored under docs/assets so the issue can embed a stable raw URL instead
of the screenshot floating untracked in debug_logs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the DM composer with an "Unblock to message" notice bar when
the contact is blocked, so a block also stops outgoing DMs (previously
only incoming DMs/adverts were suppressed). Inline unblock restores the
composer. Adds block_composerNotice l10n string and documents the
outgoing-DM rule in block-contract-as-built.md §4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Offband rebrand renamed the Windows executable meshcore_open.exe ->
offband_meshcore.exe (verified: the #244 build produced offband_meshcore.exe).
Only the Windows distribution doc was stale; the Android package/namespace,
MethodChannel names, and macOS/iOS bundle names are deliberately still
meshcore_open (those renames are deferred per the rebrand plan).
Remove the invented whole-channel-mute (never specified; already exists in
notification settings). Repurpose §6 to the real spec: blocking a channel sender
who can't be resolved to a pubkey adds a GLOBAL name-fallback (hides them in every
channel, not one at a time), which promotes to a pubkey block once the identity is
learned via advert OR DM (§7). Single/invisible-char rename evasion explicitly out
of scope. A4/#171 closed not-planned; behavior lives in A3/#170 + A5/#172 + A7/#174.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ben's UX call: do NOT hide blocked contacts to a settings-only list. Keep them
in Contacts/Discovery with a red block icon + muted styling + inline unblock.
A 'hide blocked entirely' toggle is a post-implementation follow-on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reply-shape [0xC2][sub][ok] (result byte, not ack/err frame); generic error
frame [0x01][0x06] is NOT 0xC2-prefixed. Overflow at MAX_BLOCKED_KEYS=32 returns
ok=0 (not error) + already-stored returns ok=1 (dedup short-circuit). BLOCK_LIST
truncation detect via START count vs key-frames -> re-request on APP_START
early-END; never derive removals from LIST. DM-drop is silent (no notif/frame) ->
no firmware block counts for UX. No unsolicited pushes. Design confirmed by
PearlMeadow; app half locked to shipped firmware, no open discrepancies.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Align to firmware PR #247 (merged): cap OFFBAND_CAP_BLOCK=0x02, FIRMWARE_VER_CODE
15, 0xC2 CMD_OFFBAND_BLOCK (ADD/REMOVE/LIST/CLEAR), MAX_BLOCKED_KEYS=32, BLOCK_LIST
streamed-dump framing (0xFF count / index / 0xFE end) + APP_START early-END.
Switch sync model to union / always-retain-local (app-local never wiped; portable
across Offband clients, local-only on non-Offband). Global (by-pubkey) app store.
Fold in channel all-matching-pubkeys rule, dual name-key index sourcing (0x8A vs
0x80), promote-and-prune age-out, gaps G1/L1/L2/L3, interop invariant.
Feature #164 / Epic A #165 / Epic B #166; firmware doc #244.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sweep user-facing docs from "MeshCore Open" to "Offband Meshcore":
README, docs/PRIVACY_POLICY, documentation/*, CONTRIBUTING, TESTFLIGHT_GUIDE.
- Prominent README attribution (fork of MeshCore Open by zjs81, MIT) + a
Credits section. LICENSE left untouched (zjs81 copyright retained).
- Removed the upstream donation block (Solana/Monero/BTC) per owner.
- Removed upstream-only pointers that would misdirect (zjs81 Obtainium badge,
meshcoreopen.org); pointed clone/issues at Strycher/meshcore-open; fixed the
stale iOS bundle id -> app.offband.meshcore.
CLAUDE.md (dev-facing instructions) intentionally left as-is.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>