The plan's top now says it was built, the same day it was written: the
ten commits in four phases at the dry run's numbers exactly, Phase 6's
two native items done, and its other three -- pushing the removal, the
two reverse-pulls, the fork decision -- named as the user's. Phase 6
gains its own Built paragraph: what the refusal changed and did not
change in the DAO's welcome branch, the four re-pointed anchors, and a
measurement the plan had only asserted -- that a Mantra commit offers
itself to the fork cheaply. git format-patch on the refusal commit, four
inverse rules over the patch text, and git apply --check on a scratch
worktree of curated/curated at 29027f2b: it applies cleanly, five files.
That is the shape a normaliser running the other way would take, and it
is recorded so the fork can take the commit in seconds rather than by
hand. The README's reading-order sentence and the first plan's pointer
say "built" where they said "planned".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The upstream moved the day the first pull landed: ten commits on
curated/curated, 86cb876b..29027f2b, that let a device hold several
profiles and move between them, and with them the fork's first schema
migration, version 20. The first plan named them the next pull and said
the schema change earned a decision of its own. This is that plan, and
unlike the first it was written after the dry run rather than before it,
so its numbers are measured: rewrite from the fork point in 103 s, ten
commits replayed onto 776455ec with nine clean and one resolved by rule,
both compilers clean, jvmTest 826 -> 868 and testDebugUnitTest 413 -> 420
with no failures, every audit budget met, and 20.json regenerated
byte-identical after a full build.
The verdict is all ten, because the line is one feature and seven fixes
braided together and four of the fixes are live on Mantra today with one
profile: the relay observer that is never all cancelled, the read-only
inbox that stays closed after its nsec is pasted, the DataStore race on
desktop, and the startup screen's wallet-worded literals. Four decisions:
take the whole line rather than carve the fixes out of five commits; take
upstream's version 20 verbatim and adopt the rule that whichever tree
migrates first owns the number; the one conflict is Mantra's own import
from 39fb64b6, resolved theirs, which closes one of the first plan's owed
reverse-pulls; and two of the device's profiles in one group is a Mantra
follow-up before release, since ChatRoom is keyed by the group id alone.
What changed in the method: Mantra's tree is no longer a superset of the
fork's, so the exactness check becomes "the residual between the trees is
the same before and after the pull, file for file", and the plan gives the
commands. The replay driver gains the one rule the dry run needed, with
its reason. The README gets the row and a reading-order sentence, and the
first plan points forward from the paragraph that predicted this one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Lines B, D and H of docs/curated-to-mantra.md, pulled from the Curated fork this
morning, go again this afternoon: the group's nostr profile (kind 0), its four relay
lists (NIP-65, NIP-17, NIP-50, NIP-51), its posts (kind 1), the curated schemas it
publishes (31889), the suggestions it reads (31888) and the entries it accepts
(31890). Eighty-two files, twelve screens, the `nostr/curated/` package, the readings
(`GroupNostrProfile`, `GroupRelayList`, `GroupPost`, `GroupCuratedSchema`,
`GroupCuratedEntry`, `CuratedSuggestion`), the `applyInnerEvent` arms for all eight
kinds, the `ChatRepository` and `NostrRepository` reads that fed them, 226 strings,
and every test that came with them. A translation collective has no list to curate
and no reason to describe itself under a key no member controls, and five rows
saying so on every group's screen were five rows about somebody else's product.
**The pasted-event proposal goes with them, because it was theirs.**
`GroupEventProposal.ACCEPTED_KINDS` was exactly kinds 0, 1, the four relay-list
kinds and 31889 -- "the kinds the group's screen has a place for", in its own words --
so with the five rows gone it would have refused every paste. Keeping it as a
generic "sign any event" was considered and rejected: the screen's whole argument
was that a member goes to the row to see the event landed, and there is no row. The
`ProposedEvent` summaries for the same kinds go too; the one test of its pre-existing
fallback ("Event of kind N") is kept, as the only line of `ProposedEventTest` that
was about code this repository had before the pull.
**Broadcast stays, and nothing opens it.** `BroadcastGroupSignedEventScreen`, its
route, view model, state and `BroadcastButton` are kept, on the decision that a way
to send a group-signed event to relays is worth having against the day something
wires a button in -- the button's only call sites were the five removed screens.
Two things had to change for it to compile against a tree with no relay lists:
`defaultRelaysFor(kind)` is now the app's own publish set for every kind, since the
General list it preferred, the Blocked list it filtered by and the schema relays it
widened to no longer exist; and `relayUrlOrNull` moves into the view model's
companion from the deleted `GroupRelaySet`, unchanged. The hint on the screen says
"the relays this app publishes to" rather than "where the group has said it lives".
Its two tests are rewritten around the new seed: the screen test answers for the
first two seeded relays and counts the rest as asked, and empties the list by hand
to see the empty state, since the seed always has something in it. Deleting
broadcast outright -- the tidier tree -- was the recommendation and was declined.
**Every pre-existing file is back at its pre-pull content plus the kept lines'
hunks, and nothing else.** Eleven files -- `ChatMessage.kt`, `NostrEventDao.kt`,
`NostrEvent.kt`, `LocalChatRoom.kt`, `Member.kt`, `ProposedEvent.kt`,
`ChatTranscript.kt`, `ProfileAvatar.kt`, the group screen's view model and state,
and `m3-title-case.py` -- were touched by no kept commit and are restored from
ba0830a3 byte for byte, so `inComparableGroups` is a private helper of the group
screen again rather than a shared extension one deleted screen needed, and
`ProfileAvatar` has one overload again. The rest were restored and had the kept
hunks re-applied: the back button's three on `ChatRoomDetailScreen`, broadcast's
`groupSignedEvent` read on the two chat repositories, and on the two nostr
repositories the sign-in reads, by reverse-applying the queue commit's hunk. The
check is `git diff ba0830a3 -- <file>`, which shows only those. Reverting the eight
commits was rejected because the back button and the read-only identity work landed
on top of them and would have conflicted in every one of the shared files; editing
the current files by hand was rejected because it leaves residue that a diff against
the base cannot distinguish from a decision.
**The strings that went are exactly the ones nothing references any more and that
the pull added.** Six strings were unreferenced before the pull and stay; the header
sentence and one capitalised "Mantra" that the queue commit's join carried are prose,
not feature, and stay too. The seven section comments that described removed blocks
go; the broadcast block's stays.
**The plan's third decision said "hide the two rows behind a constant".** That
covered the curated rows and not the profile, relays and posts beside them, and the
call was to take the whole identity block out. The record of what went and what
stayed is the paragraph after the built table in docs/curated-to-mantra.md, with the
seam it leaves for the next pull: an upstream commit that touches the identity block
conflicts at `ChatRoomDetailScreen`, `MantraNavHost` and `strings.xml`, and is
dropped. docs/README.md says the same in a sentence. The nsec and npub notes still
name `EditGroupCuratedSchemaViewModel`; they are records of what was built upstream
and are left as written.
Verified with :composeApp:compileDebugKotlinAndroid, :composeApp:compileKotlinJvm,
:composeApp:jvmTest (826 tests, from 1,039), :composeApp:testDebugUnitTest (413,
from 530) and :composeApp:m3Audit, every budget met, 12 adaptive uses at the floor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phases 0 to 5 of docs/curated-to-mantra.md are landed on this branch: thirty
commits pulled from curated/curated, each carrying a `Pulled-From` trailer
naming the commit it came from, on top of Phase 0's two native ones. The plan
is kept as written and its status line now says what happened, with the table
the house keeps for a plan that has been built -- what it said against what
it turned out to be -- and the README's sentence about it follows.
**Seven differences, and none of them are corrections to the plan's
decisions.** The three decisions -- drop the brand line but keep its
reasoning, keep Mantra's sections by dropping 808a3459, take the curated
lists -- all held, and the measured predictions about them (the screen's
order, the tests, the audit) came true to the number. What changed was
mechanics: the phased replay is a cherry-pick per commit rather than a rebase
of each cut, because a recreated merge cannot reach a parent that was replayed
in an earlier phase; the library pin follows the commit rather than staying
put, because the compiler said the nsec line does not build against 84cc44c;
the join files are taken from upstream's own merges rather than unioned, for
three reasons that each took a failed attempt to learn; and the exactness
residual is seventeen files rather than sixteen plus a logo, because Mantra's
own side had moved too. Each is written in the table with the reasoning
beside it, so the next pull starts from what happened.
**The numbers are per phase, so a regression later can be placed.** jvmTest
736, 864, 905, 1,007, 1,039 and testDebugUnitTest 403, 486, 495, 530, 530
across Phases 0 to 5; both compilers clean and every m3 audit budget met at
each; the final jvmTest executed rather than restored from the build cache,
1,039 tests in 46 seconds of test time, 0 failures. The plan's own dry run
predicted 1,036; the three extra are 39fb64b6's pin of the library's
nostrPublicKey() against NIP-06.
**What is not done is named rather than implied.** Phase 6's two reverse-pulls
-- 39fb64b6, since the fork still carries the app-side WalletManagerExtension.kt
it made redundant, and Phase 0's Torch retirement -- land in the other
repository and are not this branch's to make. Its last item is a decision
about what the fork becomes, and the plan's recommendation stands: converge the
source package so the next pull is a plain cherry-pick. And the upstream has
already moved on -- ten commits for several profiles on one device, one of
them the fork's first schema migration -- which are the next pull, kept
separate because a migration landing on Mantra's database deserves a decision
of its own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 1 of docs/curated-to-mantra.md, run on 2026-09-13 on top of Phase 0, on a
scratch worktree that landed nothing. The plan asked for exactly this -- redo
the dry run the day you land, because the numbers drift -- and it was right to:
the upstream had moved, the pipeline as written could not do phased landings,
and the library pin turned out to have to move backwards. All three are now in
the plan, in the Phase 1 section, as a record beside the plan rather than a
rewrite of it.
**The replay lands by cherry-pick in topological order, not by
`rebase --rebase-merges`, and the reason is a property of git rather than a
preference.** The plan's phases are cuts through a graph with four merges in
it. Rebasing a cut whose merges have one parent in an earlier cut recreates
each such merge against the *pre*-replay parent -- the commit in the rewritten
source branch, not the one already landed -- and drags the unrebased lineage in
beside the rebased one. A single whole-range rebase, which is what the plan's
dry run measured, never meets this because every parent is inside the range.
So: the ordinary commits are picked one at a time, the merge commits land as
nothing, and their content, which is only ever conflict resolutions, is folded
in where the conflicts actually surface. The driver that does it is
docs/scripts/curated-replay.py, added here; every commit it lands carries a
`Pulled-From: curated/curated@<sha>` trailer stamped by the rewrite, and a
paragraph naming any resolution made on the way in.
**At a branch join the join files are taken exactly as upstream's own merge
left them, and a union was tried and rejected three times before that rule
was reached.** A union -- keep both sides of the conflict -- is the obvious
resolution for two branches appending strings to the same file, and it was
wrong three ways, each caught by the exactness check against the rewritten
tip and none of them visible in a passing build. It duplicates lines both
sides already hold when a conflict hunk widens (thirteen string keys, twice).
It never applies the other side's deletions, so the two copy-suggestion
strings that fcc19f95 removes survived. And, the one that took longest to see,
a join resolved in a different *order* from upstream's merge leaves every later
commit that edits the block unable to find its base, so its deletions fail
silently while its additions land -- which is why the second fix still left
the same two strings behind. The rule that survives: files whose lines are
unique by construction (string keys, imports, route registrations, table rows)
get a line-set three-way merge over diff3 hunks -- ours, minus what theirs
deleted from base, plus what theirs added that the file does not already hold
anywhere -- and never at a join, where the file upstream's merge produced is
the answer. The driver's docstring says all of this so the next reader does
not rediscover it.
**The library pin goes back to 59c11ed for Phases 3 and 4 and forward again
in Phase 5, and the compiler was asked before deciding.** The nsec line was
written against lightning-kmp-app 59c11ed and writes through its
`NostrKeyManager`; 84cc44c, Mantra's pin, renamed that class to a read-only
`LegacyNostrKeysFile`. Reasoning said the Phase 3 cut would not compile against
84cc44c; the trial worktree was put at that cut and built against it, and
produced eight `Unresolved reference 'NostrKeyManager'` errors in
IdentityWriter.kt. So 366b0177 moves the pin to 59c11ed, whose nested
lightning-kmp -> bitcoin-kmp -> secp256k1 chain is the same commit as
84cc44c's and rebuilds nothing native, and 5efeae76 brings it to 84cc44c --
not to the 01962f3 it named upstream, which is a branch commit since rebased
onto the library's master and no longer fetchable, but whose tree 84cc44c
reproduces exactly. Every commit on the branch will compile against the pin it
records, which is what upstream's history had and a fixed pin would have
thrown away. Alternatives rejected: adapting the Phase 3 commits to the
renamed class (rewriting upstream's work on the way in, and inventing an
intermediate state nobody built) and landing Phases 3 to 5 as one unit whose
inner commits do not build (bisect would hate it, and so would review).
**The upstream moved, and the new line is the next pull rather than part of
this one.** curated/curated went from 86cb876b, which the plan was written
against, to 29027f2b: ten commits for several profiles on one device, one of
which (759199f2) adds schema version 20, the fork's first migration. They are
recorded and left out on purpose; a migration landing on Mantra's database
deserves its own decision, and the plan's own principle -- pull the whole
non-brand tree so the next pull is a replay -- says how that decision goes
once it is made.
The trial, with the committed driver: 30 commits replayed (4, 8, 6, 12), 21
clean and 9 with a resolution note, 0 stuck; the driver's own rerun from the
Phase 0 tip reproduces the tree and improves on the hand-run trial by the one
README line the old union had wrongly kept; residual diff against the
rewritten 86cb876b exactly the known set -- Phase 0's prose, 39fb64b6's files,
808a3459's three, this document and its scripts, the logo.
:composeApp:compileDebugKotlinAndroid and :composeApp:compileKotlinJvm clean;
:composeApp:jvmTest 1,039 tests, 0 failures; :composeApp:testDebugUnitTest
530 tests, 0 failures; :composeApp:m3Audit all budgets met.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Curated (remote `curated`, curated/curated-kmp) forked from this repository at
ba26c0b1 on 2026-09-09 and has landed thirty-nine commits since: a group's own
nostr identity, curated lists over kinds 31888-31890, sign-in with a recovery
phrase, an nsec or an npub, a back button on every pushed screen -- and two
rebrands, 744 and 765 files each, underneath all of it. docs/curated-to-mantra.md
is which of that Mantra wants, in what order, and how it lands under Mantra's
names. docs/scripts/curated-unbrand.py is the rewrite the plan depends on, and
docs/README.md gets the row.
**The plan is measured, not argued, and that is its whole claim to be read.** The
obvious way to write it was to reason from the commit list about what would
conflict. Instead the method was run to the end on a scratch worktree before a
sentence was written: Curated's history rewritten into Mantra's names, replayed
onto origin/mantra with `rebase --rebase-merges`, built for both targets, and put
through jvmTest, testDebugUnitTest and the m3 audit. Every number in the document
is from that run -- 37 of 39 commits replay, 3 conflicts with known resolutions,
1,036 jvm tests and 530 common tests at 0 failures, all audit budgets met, Room
regenerating nothing -- and the appendix records it so the next reader can tell a
drifted number from a wrong method. A plan whose mechanics had not been tried
would have been a list of hopes about seven hundred renamed files.
**Mantra has not moved since the fork, and that decides the shape of the
problem.** The merge-base of the two branches is origin/mantra itself, so
`git merge curated/curated` is a fast-forward: it merges nothing and makes Mantra
become Curare, icon, package rename and the deletion of Mantra's own library,
dialects and projects sections included. The document says so before anything
else because it is the one thing git does by default. It also means the pull is
not a merge at all but a translation -- a Curated commit written in Mantra's
vocabulary applies to Mantra as if it had been written there -- which is why a
history rewrite followed by an ordinary rebase works, and cherry-picking the
original commits (every path and every import line in every context wrong) does
not.
**808a3459, which drops the translation sections, is dropped rather than
reverted afterwards, because that was measured too.** The first plan was to
replay everything and restore the sections with a forward commit written against
the final row layout. Trying the drop instead showed git merging 930d37c8,
1e52fc8f and 55664cc7's rewrite of ChatRoomDetailScreen around it without a
conflict: the screen comes out as signing key, the five identity rows, Propose
event, then Library, Dialects, Projects, then Subgroups, with `artifacts` and
`dialects` still on the UI state, and the same 1,036 tests pass --
GroupSignedWorkRowsJvmTest only asserts that Subgroups is below the block. The
one conflict is a modify/delete on a test 55664cc7 deletes anyway. A restore
commit would have been Mantra owning a layout upstream never had.
**Curated lists are pulled although they are not Mantra's product, and the
reasoning is written out in full because it is the only real product question.**
Excluding the five commits is not deleting five commits: the paste flow routes
kind 31889 beside 0, 1 and 10002, broadcast seeds an entry's relays from its
schema, the row restructure builds five rows of which two are these, the
back-button sweep touches the suggestion screens, and 165 of the 306 strings
added since the fork are theirs, interleaved. Carving that out means editing five
other commits by hand and then owning a group screen that conflicts with
upstream's on every future pull. Including it costs no schema, no migration and
no query on a screen nobody opens. The principle underneath is stated as such:
the method only stays cheap while Mantra's tree is a superset of Curated's
non-brand tree, so every feature left out is a permanent seam. If product wants
the two rows gone, that is two lines behind a constant, not surgery.
**The brand line lands as one re-messaged commit of comments, and Torch is
retired natively first.** Run through the rewrite, the two rebrand commits
collapse from 744 and 765 files to sixteen and twenty-one, and what survives is
the reasoning the rebrands added about the names they refused to change -- the
comments on TWEAK_TAG, HOST_KEY_DERIVATION_TAG and Relays.ephemeral and the
paragraph in shared-key-derivation.md. Mantra should have those; the next person
to grep "mantra/" here is as likely to finish the rename as anyone upstream was.
The rewritten d26cf6c7 is a different case: Mantra is still Torch in TorchTheme
(116 references), UserAgent, two error strings, and an iOS config that builds
Torch.app under a bundle id from two brands ago. That is Mantra's own debt and
Phase 0 pays it as MantraTheme and "Mantra", so that CurareTheme maps onto a name
that means something rather than onto the brand before last. Then d26cf6c7' is
dropped in the replay, since its work is done -- the plan was made to say this
explicitly after Phase 0 and Phase 2 were found to disagree about it.
**The normaliser is a script in docs/scripts rather than a description, because
three of its rules were wrong before they were right and prose would have hidden
that.** `\b` does not fire inside snake_case, so `\bcurare\b` misses the string
keys that carry the brand and they need look-around rules that treat `_` as a
boundary. File names carry identifiers, so a path rule that only moves
directories leaves MantraNavHost.kt and CurareNavHost.kt side by side with equal
content. And "Curated" is two words -- the brand in generation one, the
protocol's name at the tip (CuratedSchemaEvent, nostr/curated/, the queue's
"Curated" mark) -- so the bare word is deliberately not a rule and only the
generation-one identifiers are mapped, which is exact because the two commits
written in that generation never use the word as a brand in code. Every rule
matches a brand token and none matches a Mantra one, so the twelve Mantra*
entities, the two mantra/ prefixes and ephemeral.mantra.press cannot be touched by
construction; the check is that the entity-token count does not fall (433 before,
435 after, none lost) and that the rewritten tip compiles, which no missed
identifier would survive. It has a tree mode for `filter-branch --tree-filter` and
a patch mode for `format-patch` files; the consolidated script was re-run on a
fresh export of the tip and reproduces the tree that was built and tested, to the
byte, logo aside.
**The phases are cuts through the graph, not lines of work, so no commit is
replayed twice and the merges are recreated where they were.** The natural
grouping is by feature line, but the lines interleave -- the nsec branch and the
queue fork at the same commit and rejoin at c8de3a1f, the back-button branch
joins the accept flow at fedbe724 -- and two of those merges carry hand
resolutions that a flattened replay would silently lose. Each phase therefore
replays every commit reachable from its cut that the previous phase did not,
`--onto <mantra tip> <previous cut> <this cut>`, and the two conflicts and two
reconcile files that the dry run found sit in Phase 4 with their resolutions
written down as a runbook. The exactness check closes each phase: the diff between
the replay and the rewritten tip must be exactly what was chosen to drop, and
anything else is a rule that is wrong or a resolution that is, found there rather
than in production.
**Phase 6 says what the fork should become, because otherwise this document is
needed again next week.** Every future upstream commit is written in to.curare.*,
and each pull costs the dry run, the review and the prose -- affordable once,
corrosive weekly. The options are given in the order they should be taken:
converge the source package (a Kotlin package is not a brand, and the rebrands'
own messages say the domain is Mantra* everywhere that matters), then invert the
relationship so Curated is Mantra plus a thin brand-and-product series, and only
failing both keep the normaliser maintained beside the code it maps. Two things
already flow the other way and are named: 39fb64b6, since Curated still carries
the dead WalletManagerExtension.kt and one caller, and Phase 0's Torch retirement.
**Several counts in the draft were checked against the data and corrected before
they were committed.** The entity-token figure quoted from the rebrand's message
(1,369) was measured with a different pattern and is replaced by this run's own
(433 -> 435); "roughly 150 of 293" strings became 165 of 306; the back-button
commit touches eight of Mantra's translation screens, not nineteen; and the claim
that nothing in the fork added a DAO method was too strong -- three queries were
added, and the statement now says no table and no migration, with 19.json
byte-identical after a build as the evidence.
The dry run, for the record: normaliser at the tip 812 files rewritten and 806
paths moved; filter-branch over 39 commits in 84 s; replay with the logo and
808a3459 dropped 37 commits, 3 conflicts, 2 reconcile files, residual diff
exactly the logo plus the three files 808a3459 touched;
:composeApp:compileDebugKotlinAndroid and :composeApp:compileKotlinJvm clean;
:composeApp:jvmTest 1,036 tests, 0 failures (Mantra alone: 733);
:composeApp:testDebugUnitTest 530 tests, 0 failures; :composeApp:m3Audit all
budgets met with 12 adaptive uses and 2 navigation components. The scratch
worktree and the tmp/ branches were removed afterwards; the script recreates them
in under two minutes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>