Commit Graph
951 Commits
Author SHA1 Message Date
thePR0M3TH3ANandClaude Opus 5 cd1ced63e1 Pin nostr-sdk below 0.44, which the client code cannot build against
CI went red where the Tests workflow stayed green, and the reason is that the
two install from different lockfiles which had drifted apart: poetry.lock
held nostr-sdk 0.43.0, requirements.lock had resolved to 0.45.0 under the
unbounded `>=0.43`. Two lockfiles disagreeing about a protocol SDK is not a
CI quirk — it means the code was being tested against one version and shipped
against another.

0.44 removed NostrSigner's constructors, including the NostrSigner.keys the
client calls, and changed Client's constructor. Verified against 0.45.0:
NostrSigner is left with only get_public_key, sign_event and the nip04/nip44
methods, and Client.__init__ takes no arguments. So this is a real migration
touching signing and client setup, not a rename to be done in passing.

Bounded at <0.44 in all three places the constraint is declared, and both
lockfiles regenerated: requirements.lock resolves 0.43.1, poetry.lock keeps
0.43.0, and the relock changed exactly one line — the content hash — with no
version churn anywhere else. Confirmed 0.43.1 still exposes NostrSigner.keys
and a Client that takes an argument, since that is what CI will actually
install. pip-audit reports no advisory against the 0.43 line, so the bound
costs nothing today.

The migration is recorded in TODO.md rather than attempted here: it wants the
sync round-trip re-validated against a real relay, not folded into a CI fix.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
2026-08-20 09:30:26 -04:00
thePR0M3TH3ANandClaude Opus 5 dec2b0f677 Guard the v3 TUI test's textual import, as every other TUI test does
`textual` is not a declared dependency — not in dependencies, not an extra,
not in the dev group — so it is absent from any environment built from the
lockfile. Every other TUI test opens with pytest.importorskip("textual") and
skips. This file did not, so instead of skipping it failed COLLECTION, and a
collection error takes the whole run down: the entire Python suite went red
on Linux and macOS over a module no test here can rely on.

The guard is the immediate fix. The larger finding is recorded in TODO.md:
the whole TUI test surface has been passing by not running, and the step
meant to catch that — `seedpass tui2 --check` — uses find_spec and reports
"unavailable" with a zero exit, so it passes whether or not the TUI can
start. A fresh install from the lockfile ships a TUI that does not work and
nothing in CI says so. Which way to resolve that is a product call, so it is
written down rather than decided here.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
2026-08-20 09:19:58 -04:00
thePR0M3TH3ANandClaude Opus 5 4204a23cab Fix three CI failures: a stale black, my scratchpad path, an impossible check
First push of this branch in a while, and CI found three things.

BLACK VERSION DRIFT. The repo pins black ^24.3 (24.10.0 locked) and the local
environment has 26.1.0, whose stable style differs. An earlier commit titled
"satisfy the black gate" reformatted five tui_v3 screens with 26.1.0, which
UNformatted them as far as CI was concerned — the fix made the gate worse.
Everything is now formatted with 24.10.0, the pinned version, reproducing
CI's six files exactly before and zero after. scripts/mutation_test.py is in
there because I wrote it this session and never ran black on it at all.

MY SCRATCHPAD PATH IN requirements.lock. pip-compile records the command that
generated a lockfile in its header, and when I regenerated it during the
dependency-vulnerability fix I compiled to a temp path and copied the result
in. That baked an absolute path from my machine into a committed file, so
CI's regenerate-and-diff could never match. Regenerated with CI's exact
command on Python 3.11; only the two header lines change, so the dependency
set was never wrong.

AN UNSATISFIABLE LOCKFILE CHECK. Separately, and not caused by me: the step
regenerated the lockfile and diffed it on every matrix entry, including
Windows on 3.10 while Linux and macOS use 3.11. pip-compile resolves
version-conditional dependencies per interpreter, so those two produce
genuinely different files — 135 lines apart here, argon2-cffi-bindings among
them. One committed lockfile cannot equal both outputs, so whichever version
it was built on, the other job failed. Verification now runs once on
Linux/3.11, the canonical generator; every platform still installs from the
lockfile with --require-hashes, which is the part that had to stay everywhere.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
2026-08-20 09:14:19 -04:00
thePR0M3TH3ANandClaude Opus 5 3395aadd1c style: satisfy the black gate that was failing the Tests workflow
The Tests workflow was red on every OS and Python version for one reason:
`black --check` found 28 unformatted files. Part predates this branch (the
tui_v3 screens, core/api.py, core/index0.py); part is mine from this session.

Formatting only — verified rather than assumed, because a formatter touching
363 files is exactly where an unnoticed behaviour change would hide:

  - 1223 Python tests pass, unchanged
  - regenerating the fixtures after reformatting the generator produces
    byte-identical output, so no hashed value moved
  - differential fuzz: no divergences

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
2026-08-19 12:22:19 -04:00
thePR0M3TH3ANandClaude Opus 5 71db3bc498 fix(deps): clear all 100 known vulnerabilities; the CI red was real
`dependency_scan.sh` had been failing CI on the port branch, and it was right
to: pip-audit reported 100 known vulnerabilities across 15 packages, including
`cryptography` itself — the library doing the vault's actual encryption.

Raised the floors to the fixed versions pip-audit named. `pip-audit` on the
regenerated lock now reports zero (one ignored exception remains, the
pre-existing GHSA-wj6h-64fc-37mp).

The one that needed judgement was starlette. It was pinned `<0.48` "must
remain compatible with FastAPI", which kept seven advisories permanently open.
That ceiling predated fastapi 0.141, which requires `starlette>=0.46` with no
upper bound — so the pin was protecting against a constraint that no longer
exists. Lifting it took starlette across a major version (0.47 -> 1.6), which
is why this was verified rather than assumed:

  - 1223 Python tests pass, 171 of them API tests
  - the API app builds all 64 routes and still answers 401 for a missing
    token, a bad token, and 404 for an unknown path
  - cross_impl_check: 41/41
  - differential fuzz: no divergences

poetry.lock was regenerated too. It dated from March, and `poetry check --lock`
reported it inconsistent with pyproject.toml — CI runs `poetry install`, so
that alone would have kept the build red no matter what else was fixed.

None of this touches the TypeScript port, which has an empty production
dependency list. It matters because the Python implementation is still the
normative reference and still what a user runs today.

Also wires the differential fuzzer into ts-parity: three fixed seeds so a
regression is deterministic and bisectable, plus one seed derived from the run
id so the explored input space grows over time rather than freezing at
whatever was chosen today.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
2026-08-19 12:16:05 -04:00
thePR0M3TH3ANandClaude Opus 5 7f095cba86 feat(agent): port job profiles and recovery split; fix Shamir's sub-threshold secrecy
Recovery split is Shamir over GF(257), one polynomial per secret byte, shares
encoded as sprec1:<label>:<threshold>:<total>:<index>:<digest>:<payload>.
Verified interoperable in both directions on real shares, including multi-byte
UTF-8 and payloads containing colons: Python recovers TS-produced shares and
TS recovers Python-produced ones. Confirmed end to end by splitting a live
parent seed 3-of-5 with the TS binary and recovering it in Python.

**Python's coefficients were derived from the secret, and that is now fixed.**
`_coef` HMAC'd the secret to produce every polynomial coefficient, which made
the entire share set a deterministic function of it. Shamir's defining
property is that any sub-threshold set of shares is information-theoretically
independent of the secret; deriving coefficients from the secret destroys it,
because one share becomes an offline verifier — guess a secret, re-run the
split, compare. Demonstrated recovering "hunter2" from one share of five at
threshold three, before changing anything. It also meant re-splitting produced
byte-identical shares, so a leaked share could never be rotated out without
changing the secret.

The determinism was load-bearing in the wrong way: coefficients were
recomputed inside the per-share loop, so they HAD to be reproducible or the
points would not lie on one polynomial. Fixed by hoisting them out and drawing
from `secrets`. The share token format is unchanged and `recover_secret` is
untouched — recovery is Lagrange interpolation over (x, y) pairs and never
recomputes coefficients — so old shares keep recovering and both versions
interoperate. Zero interop cost for a real security property.

TypeScript never had the defect and its test suite pins it: reintroducing
secret-derived coefficients turns three tests red, including the guessing
attack. Coefficients use rejection sampling because 257 does not divide 2^16,
and a modulo draw would bias them.

Job profiles port straight across, with policy_stamp as the security-relevant
field: sha256 of the canonical agent_policy.json at creation, so a policy
tightened afterwards shows as a mismatch instead of leaving the job running
under rules nobody reviewed it against. The canonical form matches Python's
`sort_keys=True, separators=(",",":"), ensure_ascii=True` — including the
ensure_ascii escaping, which is easy to miss and would otherwise make a
non-ASCII policy hash differently in the two implementations. Verified
byte-identical stamps for both an empty policy and one with non-ASCII values.
An unreadable policy hashes to its own distinct value rather than to the empty
policy, so jobs read as stale rather than silently current.

Recovery drills use the same HMAC-chained log design as the audit log, with
tests that an edited record, a removed record and an unsigned appended record
are all detected.

Every Python API endpoint now has a TypeScript equivalent: UNPORTED_PREFIXES
is empty. The 501 mechanism stays, because the honest answer for a dropped
subsystem is 501 naming the feature rather than 404.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
2026-08-19 10:41:45 -04:00
thePR0M3TH3ANandClaude Opus 5 727ddeff62 docs: record that Python's high-risk unlock writes the partition key to disk
agent_high_risk_unlock.json stores the session's partition_key_tag, and the
partition file's encryption key is derived from exactly that value. While a
session is live the partition therefore decrypts from disk with no factor, for
anything that can read APP_DIR — and the session file sits beside the key
envelope, so reading one implies reading the other. That is the guarantee the
partition exists to provide.

Documented at grant_high_risk_unlock, where it happens, rather than only in a
tracking file. The exposure is inherent to on-disk session state in a CLI with
no resident process, and encrypting the session under the master password or
seed does not fix it — that would make the password alone sufficient, which is
what the second factor is meant to prevent.

The two real fixes are product decisions (drop the session model, or give
Python a resident agent), so they are recorded rather than chosen. Interim
guidance: short TTLs, high-risk-lock when finished, treat read access to
APP_DIR as equivalent to holding the factor.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
2026-08-19 10:20:42 -04:00
thePR0M3TH3ANandClaude Opus 5 885cbe5665 fix(semantic): delete indexes written before secrets stopped being indexed
Fixing the writer does not rewrite files already on disk, so a profile that
built an index before today still has one holding stored secrets in plaintext.
Reporting a version string in a status field and expecting the user to notice
is not a remedy for that.

Both implementations now check the manifest's model_id on every read — status,
search, and the API equivalents — and delete records + manifest when it
predates the fix. Reading a stale index would also mean serving results
derived from the secrets it should never have held, so search purges before
it reads rather than after.

Deleting is safe: the index is a derived cache, rebuilt from the vault in
milliseconds, and the user is told to rebuild.

A manifest that cannot be parsed is deliberately NOT treated as stale.
"Cannot tell" must not mean "delete it", or every corrupt-manifest read would
throw away a working index; that case is tested on both sides.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
2026-08-19 10:02:31 -04:00
thePR0M3TH3ANandClaude Opus 5 39c4e45376 feat(semantic): port the retrieval index — and stop it writing secrets to disk
The "semantic (vector search)" entry in not_yet_ported was a misnomer. It is
not vector search and needs no model: MODEL_ID is "seedpass-token-overlap-v1",
and it scores by Jaccard overlap over tokenized metadata. That is why it could
be ported at all — there is nothing to embed.

Porting it surfaced two defects in the Python implementation.

**Stored secrets were written to a plaintext, 0664 file.** `_extract_text`
appended a key_value entry's `value` — the stored secret itself — and
`build()` wrote it to semantic_index/records.json in the clear, along with a
tokenized copy that leaked it just as thoroughly. The file was created at the
process umask, so on a default install it was group- and world-readable, next
to a vault that is encrypted and 0600. Anything that could read the directory
— a backup, a sync client, another user on a shared machine — got the secret
without the master password.

Fixed in Python (index the key NAME, never the value; chmod 0600 on both
files) and never done in TypeScript. The rule is worth keeping simple: nobody
searches for a secret they do not already know, so indexing one is pure
downside. MODEL_ID is bumped to v2 so an index built by the old code is
distinguishable — a v1 index on disk still contains the secrets and should be
rebuilt.

**Entry 0 was silently unsearchable.** The check was
`int(entry.get("id", 0) or 0) <= 0`, conflating "no id" with "id 0" — and
entry ids start at 0, so the first entry any user ever created was absent from
every search result with nothing to indicate why. Found by writing a test that
queried for it. Fixed in both implementations: skip an id that is absent,
unparseable or negative, which is what the check was reaching for.

The TypeScript port is wired into the CLI (`semantic build/status/search`),
the API (the /api/v1/semantic routes are now real rather than 501), and
writes through atomicWrite for a genuine 0600 inode. Fixtures come from
scripts/generate_ts_port_fixtures.py, which asserts the leaked value is absent
from its own output, so the fixture-drift job proves provenance the way it
does for every other fixture.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
2026-08-19 09:54:53 -04:00
thePR0M3TH3ANandClaude Opus 5 276aca65b6 feat(sync): report what a merge resolves silently; stop it editing its inputs (L-2, L-3)
The deterministic merge is frozen for parity -- both implementations must
reach the same vault from the same inputs -- so how it resolves is not up for
negotiation. What was missing is that nobody is told a resolution happened.

L-2: the allocation watermark is per-replica, so two devices working offline
can both allocate id N. The merge then resolves that id by timestamp then
hash, and the loser is replaced with no trace. An entry the user created is
simply absent afterwards. Worse than a plain overwrite, because an id is a
permanent BIP-85 derivation coordinate: the survivor derives from the same
coordinate the discarded entry did (see the L-1 detector).

L-3: past TOMBSTONE_RETENTION_CAP the oldest deletions are forgotten, and
merging a stale replica or old snapshot then resurrects those entries. A
documented protocol limit -- but a user cannot know the cap has actually begun
trimming their history unless something says so.

Both implementations gain an optional MergeReport collector: same-id conflicts
(with the kept and discarded entries named) and a count of evicted tombstones.
`nostr restore` reports both in its summary and on stderr, covering delta
replay as well as the final local merge.

Only genuine divergences are reported. Two replicas holding the same entry
with different timestamps is an ordinary edit and is ignored; flagging it
would bury the case that matters in noise and train the user past the warning.

The collector is write-only and cannot change the merged result -- asserted in
both implementations, because if observing could alter the outcome it would be
a parity divergence rather than an observation.

Two defects found while writing the tests:

  - Python's merge MUTATED its `current` argument. `dict(current)` shares the
    nested entries dict, so the merge wrote its result straight into the
    caller's payload: after merging, "current" had already become the merged
    value. Anything comparing before-and-after, or merging one payload twice,
    silently saw the wrong input. Now deep-copied, matching TypeScript's
    structuredClone. Found because the "reported whichever side wins" test
    failed -- the forward merge had corrupted the input for the backward one.

  - test_merge_replay_same_payload_is_idempotent was passing only because of
    that aliasing: `once` and `twice` were one object. With inputs no longer
    mutated, replaying the same payload is shown to normalize entry SHAPE --
    the equal-timestamp path unions field sets and writes explicit nulls for
    absent optional fields. TypeScript produces byte-identical output for the
    same input, so this is shared designed behaviour rather than a Python
    regression, and it converges on the second pass. The test now asserts what
    is actually true (content unchanged, shape normalized, settles at two) and
    records why the old assertion held.

Mutation-verified: removing the recording turns the reporting tests red.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
2026-08-19 09:10:16 -04:00
thePR0M3TH3ANandClaude Opus 5 75e370d098 feat(vault): detect shared BIP-85 derivation coordinates; bind Python imports too (L-1, L-8)
L-1 — three entry kinds derive from BIP-85 app 32 at the entry's index:

    ssh@N       -> 32 bytes of m/83696968'/32'/N'
    pgp@N       -> 32 bytes of m/83696968'/32'/N'   (the SAME 32 bytes)
    password@N  -> 64 bytes of the same, then PBKDF2

So ssh@N and pgp@N are byte-for-byte the same Ed25519 private key, and
password@N takes that key as its entropy prefix. The password stays safe (256
unknown bits remain, PBKDF2 is one-way) but this is not key separation, and
SSH keys are exactly what gets exported to a server or uploaded to a forge.

Deliberately NOT fixing the derivation itself yet. Domain-separating the three
uses is a versioned protocol change in both implementations, i.e. a new
derivation path -- and the project's own note is that "a derivation bug does
not throw, it quietly produces a secret that cannot be recovered". Adding one
immediately before the first outside review inverts the point of the review.
The v2 design is written up in TODO.md instead, ready to implement after it.

What ships is the audit's short-term recommendation, which needs no derivation
change: notice the state and say so. findDerivationCollisions /
find_derivation_collisions report cross-kind clashes at one app-32 index,
worst first, in both implementations with identical message text -- a user
moving between the two must not be told different things about one vault.
Wired into every path the state can arrive by: vault import (CLI and TUI, both
implementations), nostr restore's merge, `vault import --inspect` before
anything is written, and a new `util check-derivation` for auditing an
existing vault (exit 1 when it finds something, for scripts).

Neither implementation can create the state -- index == vault id, ids unique,
watermark prevents reuse -- so this is about imported, foreign, and merged
data, which is also why it must never be the thing that fails an import: it
reports, never refuses, and tolerates malformed input.

The lead test in each implementation does not exercise the detector at all.
It derives the keys and compares the bytes, proving the property the detector
reports. Without that, the rest only checks that a written-down rule is
applied consistently, and a derivation change could falsify the rule with
everything still green.

L-8 (Python) — while wiring the above it turned out Python's import_backup had
the same unbound-plaintext-import gap the audit found in TypeScript: the
wrapper carries a `fingerprint` and nothing compared it. Now fixed there too,
with a new ProfileMismatchError so the TUI can offer the override instead of
reporting a generic failure.

The comparison is against the fingerprint DERIVED FROM THE SEED, not the
profile directory's name. In production they are the same string, but the
question worth asking is "was this backup made from my seed?", and only the
seed answers it -- comparing directory names refused a legitimate restore of
the same seed into a freshly named profile, which test_account_roundtrip_
restore caught immediately. It is enforced only when the wrapper records
something that is actually a fingerprint: a backup carrying some other label
offers no evidence about its source seed, and refusing on no evidence blocks
restores without making anyone safer.

All guards mutation-verified on both sides.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
2026-08-19 08:58:12 -04:00
thePR0M3TH3ANandClaude Opus 5 8414deff20 fix(sync): order snapshots by signed publication time, in both implementations (M-1)
Restoring the wrong manifest is not an ordering detail -- the manifest that
wins the comparison IS the vault the user gets back. Two defects compounded:

TypeScript ordered manifests by created_at with the event id as tie-break.
created_at is whole seconds, so two snapshots published in the same second
tied, and the id tie-break -- stable and relay-independent, but arbitrary with
respect to time -- returned the OLDER vault about half the time. Entries
created between the two syncs disappeared silently; skippedNewerManifests
stayed empty because nothing was skipped, the wrong manifest simply sorted
first. It surfaced only as an intermittent test failure.

Python never ordered at all: it asked the relay for limit(1) and restored
whatever came back, handing the relay the choice of which snapshot wins.

Both now stamp published_ms -- publication time in milliseconds -- inside the
manifest JSON, so it is covered by the event signature and no relay can steer
it, and both order by it. The value is monotonic per process (max(now,
last+1)), because two back-to-back publishes can share a millisecond and would
otherwise fall back to the same arbitrary id tie-break in a narrower window.
Across machines the clock remains the only shared reference; that case is
unchanged and documented.

Backwards compatible: the field is optional, and a manifest without it orders
at the start of its created_at second. That biases a new-format manifest to
win a same-second tie against an old-format one, which is the right direction
-- old-format manifests are by definition from before the upgrade. Ordering
the unknown one last would let a pre-upgrade snapshot outrank every
post-upgrade one for that second: the same bug from the other side.

Two things found while doing it:

  - Python's publish_delta republishes the manifest with an updated
    delta_since. It needed the new field too: without it the manifest carrying
    the newest delta_since sorted BELOW the snapshot it supersedes, so readers
    fetched deltas from a stale watermark and the update never arrived. Caught
    by the existing roundtrip tests, which is why they are worth having.

  - Raising Python's fetch limit turned its vestigial fallback loop into a
    live downgrade path: a relay withholding one chunk of the newest snapshot
    could walk a client silently backwards through its history. The loop now
    skips unparseable events (relay noise must not deny service) but refuses
    to reach past the newest manifest that genuinely parses, and says why.
    A restore that quietly returns last week's vault is indistinguishable from
    one that worked.

Tests: js relay.test.ts gains a 12-round same-second case -- one pair
reproduces the bug only half the time, which is how it stayed a "flake" --
plus a legacy-manifest case built by hand in the true old format. Python gains
test_manifest_ordering.py (6 cases: order key, both fallbacks, arrival order
irrelevant, id tie-break, unparseable content). Mutation-verified on both
sides: restoring the old comparison turns the matching tests red.

The nostr CLI test now passes 6/6 consecutive runs, having failed roughly 1
in 4 before.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
2026-08-19 08:42:26 -04:00
thePR0M3TH3ANandClaude Opus 5 cdd74f379f feat(cross-impl): Phase L proves foreign data survives a full A->B->A cycle
The round-trip conformance check from spec section 8.6, now enforced in CI:
a vault holding a foreign record (unknown kind, nested foreign data, and
trap fields) plus an unknown top-level index key goes through an interleaved
Python -> TypeScript -> Python read-modify-write cycle, and every foreign
byte must come back exactly as planted. Allocation must skip past the
foreign id, and TS must refuse to reveal the record rather than guess.

Landing the check forced the normalization decision it existed to force.
Python's _normalize_entry_defaults ran its legacy renames on every entry,
foreign ones included -- and those renames are migrations for OUR old
shapes: the negative control shows the old code turning a foreign
"blacklisted" string into our "archived" flag, which is not backfill but
reinterpretation of another application's data. The normalizer now skips
unknown kinds entirely, so both implementations carry foreign records
byte-for-byte untouched and produce identical canonical state -- the
property sync convergence is defined by. Entries with neither kind nor
type are pre-kind legacy Python data and still normalize as before.

The spec's gap ledger entry for the divergence is resolved, and the Python
tolerance tests are tightened from field-subset to exact equality, plus a
trap-field test pinning that blacklisted/website/words on a foreign record
mean whatever the other application says they mean.

Verified: with the normalizer gate stashed, Phase L fails on the exact
mangling ("archived": "foreign meaning, not our archive flag"); with it in
place, 38/38 cross-impl checks and 1183 Python tests pass.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Tkj4rzKBA3uhfgk7kqjsb8
2026-08-18 13:36:46 -04:00
thePR0M3TH3ANandClaude Opus 5 e434b0abc1 feat(schema): carry unknown entry kinds through instead of refusing the vault
Spec section 8.2: a record whose kind this build does not understand -- one
written by a newer build, or by another application sharing the vault, such
as a future BitLogin namespaced kind -- must be carried through untouched,
never dropped, and must not brick the whole index. TS previously failed the
entire parse on the first unrecognized kind, stranding any shared vault on
older SeedPass builds.

The entry schema is now a union: known kinds validate strictly against the
discriminated union exactly as before, and anything else parses as an opaque
loose record. The refine on the opaque branch is the load-bearing detail --
a malformed record of a KNOWN kind must still fail its own schema, never
fall through and escape validation. Records with no kind or type at all
remain rejected as garbage. Statically the entries map stays typed to the
known kinds (exhaustive switches keep their meaning); the module comment
states the runtime widening plainly.

At the CLI surface, foreign records list with unrecognized fields redacted
to has_* flags -- another application's data is not ours to hand out -- and
reveal refuses with "unsupported entry kind" instead of guessing. Allocation
counts foreign ids, so neighbours never collide with them.

Python's natural tolerance is now pinned by test, which surfaced a real
nuance: Python backfills base-field defaults (tags, links, date_modified)
onto foreign records on load, where TS carries them verbatim. No data is
lost, so both conform to the preservation rule as now worded (no loss, no
reinterpretation; base-field normalization permitted) -- but the two produce
different canonical bytes for the same foreign record, which matters once
foreign kinds sync. Recorded in the spec's gap ledger to align before any
application ships them.

Verified: 4 of the 6 new TS tests fail against the previous schema; 230
core + 152 CLI tests and the cross-impl suite pass with the change.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Tkj4rzKBA3uhfgk7kqjsb8
2026-08-18 13:26:32 -04:00
thePR0M3TH3ANandClaude Opus 5 e3b1325a2b fix(vault): entry ids are never reused -- allocate from a persisted watermark
An entry id doubles as the BIP-85 derivation index for managed_account and
seed entries, which makes it a permanent cryptographic coordinate: "Alice is
account #184" is a recovery claim, and #184 must mean Alice forever. But both
implementations allocated ids as max(live ids)+1, and sync deletion removes
entries via tombstones -- so deleting #184 on one device let the next
creation reissue #184 to a NEW entry, which then re-derived the departed
identity's exact child seed and npub. Tombstone retention (capped at 2048)
made even the evidence expire.

Allocation now comes from a high-watermark persisted in _sync_meta.next_index:
never-decreasing, bumped on every insert/save (explicit-id inserts included),
merged as max(both sides' watermarks, one past any merged entry or tombstone
id), and identical in both implementations -- the merge computation must stay
byte-for-byte the same in sync_conflict.py and sync/merge.ts. _sync_meta was
chosen because both sides already round-trip it (it carries the tombstones),
so old builds preserve the field and no schema version bump is needed.
Legacy vaults heal on their next allocation: the floor folds in tombstone ids
still within retention, and the first save persists the watermark.

Python-side key parsing mirrors the TS regex exactly (ASCII digits, no
leading zeros, JS safe-integer range): str.isdigit() alone accepts Unicode
digits that Number() rejects, which would have made the two implementations
compute different watermarks from the same payload.

Fixtures regenerated from Python; the byte-for-byte entry-creation parity
test now passes with the watermark present, proving TS's insert-time bump
and Python's save-time bump converge on identical indexes. Verified: the
recycle regression tests fail on both pre-fix implementations; 1180 Python,
224 core, 149 CLI tests and all 36 cross-impl checks pass with the fix.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Tkj4rzKBA3uhfgk7kqjsb8
2026-08-18 13:11:50 -04:00
thePR0M3TH3ANandClaude Fable 5 b885573315 fix: make the test-profile generator hermetic and re-runnable
test_profile_init_integration failed on any machine with a generated test
profile already present. Two causes, both real bugs rather than test noise:

- scripts/generate_test_profile.py mutated constants.APP_DIR at import time
  to append "tests". The value was captured when constants was first
  imported -- long before a test can monkeypatch Path.home -- so the script
  read the developer's real ~/.seedpass/tests, and a reload appended another
  "tests" segment. The mutation also leaked into every other importer of
  constants in the same process. The tests directory is now derived per call
  from constants.APP_DIR instead.

- initialize_profile assumed add_fingerprint returns None for an existing
  profile, which is what its docstring still promises; it actually raises
  ValueError. Re-running the generator against an existing profile therefore
  crashed. It now treats "already exists" as the ordinary case.

test_profile_init_integration also repoints constants.APP_DIR so it no
longer depends on the developer's home directory, and
test_generate_test_profile asserts against the derived tests directory
rather than the mutated global.

Verified hermetic: the test passes repeatedly and touches nothing under the
real ~/.seedpass. Full Python suite: 1178 passed, 16 skipped, 0 failed.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-14 21:16:24 -04:00
thePR0M3TH3ANandClaude Fable 5 5dcd779a6f fix(python): TOTP codes must honor the entry's period and digits
TotpManager built pyotp.TOTP(secret) with library defaults (6 digits, 30s)
and ignored what the entry recorded, so any imported or derived entry using
other values produced codes the issuing service rejects. The service was
configured from the entry's values -- those govern the code.

- current_code_from_secret and current_code take period/digits
- EntryManager.get_totp_code passes the entry's values for both the
  derived and imported paths
- display_service, menu_handler (totp_list now carries digits) and the GUI
  updated at every call site
- regression tests for imported and deterministic entries with custom
  parameters, asserting against pyotp with the matching interval/digits,
  and confirming default entries are byte-identical to before

Found by scripts/cross_impl_check.py, which flagged it as a divergence
against the TypeScript port. That allowance is now removed: the suite
asserts TOTP equality and reports 30/30 with zero divergences.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-14 16:41:04 -04:00
thePR0M3TH3ANandClaude Opus 5 786106b15b Fix the 15 remaining test failures; suite is green
1176 passed, 0 failed. These were all reachable only after 9e0f045 stopped
the runner exiting early, and every one had a distinct cause.

One real bug, in index0.py. _build_children_view sorted entry ids with
int(str(id)), which raised ValueError on any id that is not a decimal
string. That happens inside view building during index load, so a single
malformed id made the whole vault fail to open -- for a password manager
that is indistinguishable from data loss. Non-numeric ids now sort after
numeric ones rather than crashing; a bad id should degrade the ordering, not
brick the index.

Eight stale assertions against a schema that moved on. The index now carries
a machine-managed _system.index0 block (canonical views, writer ids, scope
paths) added on load, so tests that saved a literal dict and compared it
back always differed by exactly that key -- the round trips themselves were
correct the whole time. Adds helpers.user_data() to drop it, and uses it in
the export/import, legacy-migration and nostr-fallback tests. Those tests
are about user data surviving; asserting on _system would pin an
implementation detail they are not about.

Three checksum tests patched seams that no longer exist. They stubbed
manager.verify_checksum (gone) and manager.calculate_checksum, which
handle_verify_checksum no longer reaches -- it now asks
_script_checksum_status, which returns early on a launch-target mismatch
before any checksum is computed. Rewritten against that seam, plus a new
case for the target-mismatch verdict, which nothing covered.

Two tests stubbed builtins.input but not sys.stdin.isatty. Both code paths
refuse a non-TTY before ever calling input, so they returned early under
pytest. Uses the isatty stub the rest of the suite already uses.

One test asserted the opposite of the intended contract.
test_setup_encryption_manager_rejects_wrong_argon2_params expected a seed to
be UNREADABLE when the profile's argon2_time_cost differs from the one it
was written with. But encrypt_parent_seed stores its KdfConfig and
setup_encryption_manager tries those stored params first, deliberately: the
profile config says how to derive NEW keys, and refusing an existing seed
because a preference changed would lock a user out of their own vault.
Renamed and inverted to assert the real behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01HtNMsKzzFTPURemX7tT19u
2026-07-31 22:48:38 -04:00
thePR0M3TH3ANandClaude Opus 5 9e0f045ccb Stop the test suite exiting 0 after 10% of its tests
`pytest src/tests` died at 123 of 1170 tests with exit code 0 -- no summary,
no coverage report, and a shell that saw success. Four tests reach code
paths that call os._exit(0), which skips interpreter finalization and takes
pytest with it.

The os._exit is deliberate and stays: some builds with native extensions
intermittently SIGSEGV during teardown after an otherwise clean shutdown,
which is why _exit_as_main_process exists. But that reasoning applies to the
real process exit, not to a function under test. Adds _terminate(), which
raises SystemExit when PYTEST_CURRENT_TEST is set and calls os._exit
otherwise, and routes the three in-process exit paths through it.

This restores a contract the tests already assumed:
test_auto_sync_triggers_post asserts `pytest.raises(SystemExit)`. It was
written when display_menu raised, and the later switch to os._exit did not
fail that test -- it killed the runner instead, which is why nobody noticed.

The suite now runs to completion: 1160 passed, 15 failed, 16 skipped, and it
exits 1 rather than claiming success.

Those 15 were always failing; they were simply never reached. Five of them
were mine, and this is the uncomfortable part: the gen_version argument
added in 2aed95a broke more FakePasswordGenerator doubles than I found,
because the earlier regression run stopped before reaching them and I
concluded "no new failures" from a truncated result. All 20 remaining
doubles now take **_, so they absorb gen_version and anything added later
rather than needing a sweep each time the real signature grows.

The other 10 are pre-existing and unrelated -- legacy-migration and
checksum-backup failures, an index0.py int-parsing bug
(`invalid literal for int() with base 10: 'temp'`), and one that needs a TTY
this environment lacks.

Determinism gate still green.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01HtNMsKzzFTPURemX7tT19u
2026-07-31 22:31:19 -04:00
thePR0M3TH3ANandClaude Opus 5 3192c9f306 Offer a 24-word master seed, and put the entropy draw beyond reach of any handler
L2: generate_bip85_seed and create_profile_from_generated_seed now take
words_num, accepting 12 or 24 and rejecting anything else with a clear
message. The default stays 12 -- every existing profile uses it, 128 bits is
entirely adequate, and moving the default would change nothing about
existing vaults while surprising anyone creating a new one. 24 is offered
because the master seed protects the whole vault and was the one secret
shorter than the seeds derived beneath it, which already default to 24.

L3: fixed structurally rather than by narrowing exception types.
os.urandom(32) now sits OUTSIDE the try block, so no handler can stand
between the entropy draw and the caller -- an entropy failure is unreachable
by anything that could substitute a value. Also removes a redundant
`except Exception` whose body was identical to the `Bip85Error` branch above
it.

The structural form is stronger than an exception list, and the test says so
precisely: it asserts the OSError reaching the caller is the SAME OBJECT
os.urandom threw, which holds only if nothing caught and repackaged it.
Listing exception types would still leave a handler in the path for someone
to widen later.

Adds five tests: the identity assertion above, 12- and 24-word generation,
the default staying 12, and rejection of 0/11/18/25/-12.

Determinism gate green (89 tests, up from 84). The seed/profile regression
subset is 143 passed with one failure --
test_profile_export_import_round_trip -- confirmed against the pre-change
baseline as pre-existing.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01HtNMsKzzFTPURemX7tT19u
2026-07-31 17:43:24 -04:00
thePR0M3TH3ANandClaude Opus 5 2aed95a687 Version password generation; freeze v1, add v2 without the class quota
Passwords are never stored -- they are re-derived on demand from
(seed, index, length, policy) -- and entries carried no algorithm version.
Any change to generate_password therefore changed every existing password in
every vault, unrecoverably. This adds the version field that makes such
changes possible at all, then makes three.

Safety net first (M4). test_entropy_integrity.py freezes 18 v1 vectors
spanning every policy and both sides of the 32-byte stream-wrap boundary,
and asserts the RNG fails closed: os.urandom raising must abort seed
generation, short reads must abort, AES-GCM nonces must be fresh. Each guard
was mutation-verified rather than assumed -- removing _balance_distribution,
perturbing the stream wrap, or altering the character mapping each turns all
18 vectors red, and adding a hash-of-timestamp fallback to
generate_bip85_seed turns the fail-closed test red.

generate_password now takes gen_version, defaulting to 1. Existing entries
carry no field and keep deriving v1 byte-identically; the argument is only
forwarded when non-legacy, so all 13 call sites across CLI, API and both
TUIs are untouched. New password entries are stamped gen_version 2. An
unreadable version raises rather than guessing which algorithm produced a
password.

v2 fixes three findings:
- M1: drops the forced equal-quarters class quota. 1 -> 82 distinct class
  compositions at length 16 over 200 samples; per-character entropy
  6.42 -> 6.53 bits against a 6.555 ceiling.
- M2: rejection sampling instead of byte % len(alphabet). Chi-square 96.1
  against df=93 (p<0.001 critical value ~147).
- M3: an unbounded HMAC-expanded stream instead of cycling a 32-byte key,
  which was a repeating pad for passwords over 32 characters. Lag-32 repeat
  rate now sits within one standard error of chance.

Dropping the quota removed an accidental backstop: v1's minimum enforcement
overwrites uniformly random positions, so raising one class minimum could
destroy another, and only the quota's near-total rewrite hid it. v2 chooses
donor positions solely from classes that currently have a surplus, and
rejects a policy whose minima cannot fit the requested length.

Also removes a dead HKDF derivation whose result was discarded while the
docstrings named it as the algorithm in use (L1); PBKDF2 was and remains the
real one. Verified as a negative control -- the vectors stayed green.

Six test doubles are updated to the real generate_password signature; they
began receiving gen_version once new entries carried it.

Existing entries stay on v1 by design. An opt-in per-entry upgrade, with a
confirmation making the consequence unmissable, is still to come.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01HtNMsKzzFTPURemX7tT19u
2026-07-31 16:27:23 -04:00
thePR0M3TH3AN f2cbb1b7ba Merge branch 'overstory/builder-atlas-handoff/SeedPass-63f4' into beta
# Conflicts:
#	src/seedpass/tui_v3/widgets/inspector.py
2026-03-11 07:39:49 -04:00
thePR0M3TH3ANandClaude Sonnet 4.6 b4e84a4327 feat(atlas-handoff): graph-pivot results through unified SearchService
- SearchService.graph_pivot_results(): wraps linked_neighbors() output
  in SearchResult model with match_reasons (graph_pivot, relation:X,
  direction:X, atlas:scope), scores (hop-distance based), and relation
  meta string for UI surfaces
- AtlasWayfinderScreen: propagates atlas_source_scope to LinkedItemsPanel
  when opening entries or recent items from the wayfinder
- LinkedItemsPanel: adds #linked-atlas-context label (◈ Atlas context:
  scope_path) that refreshes via property setter; atlas scope is
  preserved across linked-panel pivots; _update_atlas_context_label()
  helper keeps display in sync

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-11 07:34:48 -04:00
thePR0M3TH3AN 5eedea3762 feat: graph pivot UX — breadcrumb trail, back nav, relation grouping
- Add PivotBreadcrumbBar widget to inspector.py showing multi-hop path
- Add _pivot_history/pivot_to_entry/action_pivot_back to app.py
- LinkedItemsPanel: group linked items by relation type with section headers
- LinkedItemsPanel: b key for back navigation, breadcrumb rendered from app history
- External entry selection (grid) clears pivot history; pivot chain preserved on linked nav
2026-03-11 07:33:06 -04:00
thePR0M3TH3AN 2ab73c50a0 update 2026-03-10 21:39:50 -04:00
thePR0M3TH3AN 6cd33b7ea8 fix: update profile removal assertion to match new intro text 2026-03-10 16:09:30 -04:00
thePR0M3TH3AN 34ab129c7a fix: add missing Binding import in pubkey.py 2026-03-10 15:50:07 -04:00
thePR0M3TH3AN 842f0ca9c7 Merge branch 'overstory/builder-sortfilter/SeedPass-de11' into beta 2026-03-10 15:14:13 -04:00
thePR0M3TH3ANandClaude Sonnet 4.6 3b031eb365 feat(tui-v3): sort/filter keyboard shortcuts, active-state toolbar, clear filter
- Add keyboard bindings: n=sort label_asc, r=sort modified_desc, k=sort kind, f=clear filter
- Add action_sort_label/recent/kind/clear_filter actions to app.py
- GridToolbar: add 'Kind' sort button, '[X] Clear' button
- GridToolbar: visual active-state via .active CSS class (synced via watchers)
- Add 5 new smoke tests: keyboard sort, clear button, clear shortcut, state persistence, toolbar active class

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-10 08:23:35 -04:00
thePR0M3TH3AN 82d33925ad feat(graph): add multi_hop_neighbors, filtered_neighbors to SearchService; enhance LinkedItemsPanel with keyboard nav, kind filter, atlas context 2026-03-10 08:22:20 -04:00
thePR0M3TH3AN 4c03890266 Merge branch 'overstory/builder-sigsegv-v4/SeedPass-1d93' into beta 2026-03-09 20:55:19 -04:00
thePR0M3TH3ANandClaude Sonnet 4.6 5dc8e015ae fix: replace sys.exit(0) with os._exit(0) to prevent SIGSEGV on graceful exit
Python interpreter teardown with C extensions (Textual, crypto libs) can
trigger SIGSEGV (-11) when sys.exit() is called. os._exit() bypasses the
teardown entirely, producing a clean exit code 0.

Three call sites replaced:
- _cleanup_and_exit() in handle_remove_fingerprint (~line 254)
- graceful exit path in main loop (~line 1520)
- signal_handler shutdown path (~line 1862)

Fixes: SeedPass-1d93

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-09 12:49:23 -04:00
thePR0M3TH3AN f2b7feb738 Merge branch 'overstory/builder-profile-v4/SeedPass-9033' into beta 2026-03-09 12:48:43 -04:00
thePR0M3TH3AN 96f64d00bc tui3: guard against non-interactive stdin with TTY check
Add _stdin_is_tty() helper and check at start of _launch_tui3_or_exit.
When stdin is not a TTY, emit a clear error message and exit 1 instead
of propagating an EOF exception from seed setup prompts.
Add test for non-interactive stdin error path; patch _stdin_is_tty in
existing tests that expect tui3 launch to proceed.

Fixes SeedPass-6d26
2026-03-09 12:45:34 -04:00
thePR0M3TH3ANandClaude Sonnet 4.6 54316d33ed polish: profile/maintenance screen improvements
- maintenance.py: add status icons to format_status, add warning CSS class
- profile.py: improve intro text, use ► marker for active profile
- pubkey.py: use explicit Binding() for keybindings
- relays.py: remove stray `import traceback` in _perform_sync
- security.py: clarify backup passphrase label

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-09 12:45:07 -04:00
thePR0M3TH3ANandClaude Sonnet 4.6 44fe1c630a v3 polish: rename Profiles button, improve restore guidance
- action_bar.py: rename 'Remove Seed' label to 'Profiles' for clarity
- app.py: add warning prefix (⚠) to nostr/backup restore pre-action
  status messages; add success message to generate flow; improve
  import success message wording
- test_tui_v3_parity.py: update label assertions from 'Remove Seed'
  to 'Profiles' (two tests)

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-07 08:08:56 -05:00
thePR0M3TH3AN 41f000e802 Merge branch 'overstory/builder-action-strip/SeedPass-57f6' into beta
# Conflicts:
#	src/tests/test_tui_v3_parity.py
2026-03-06 13:47:40 -05:00
thePR0M3TH3AN a6c7391f78 Merge branch 'overstory/builder-app-cmds/SeedPass-865f' into beta 2026-03-06 13:47:35 -05:00
thePR0M3TH3AN 1a58c63658 Merge branch 'overstory/builder-settings-inspector/SeedPass-865f' into beta 2026-03-06 13:47:31 -05:00
thePR0M3TH3AN 00b1b7d3ec feat(tui-v3): migrate legacy utility/maintenance commands to v3 CommandProcessor 2026-03-06 13:44:07 -05:00
thePR0M3TH3ANandClaude Sonnet 4.6 ea5ad1f093 Fix managed_account QR exclusion and bare except in action_bar
- Remove 'qr' from managed_account context actions (accounts don't expose
  a QR-scannable key like seeds/nostr do)
- Change bare 'except:' to 'except Exception:' on vault manager check
- Add test_v3_action_bar_managed_account_excludes_qr in parity tests

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-06 13:41:24 -05:00
thePR0M3TH3ANandClaude Sonnet 4.6 446d1ef2b8 TUI v3: tighten action strip + restore/import guidance (SeedPass-57f6)
action_bar.py:
- Fix global row broken handlers: Remove Seed → open_profile_management,
  Backup Data → open_backup_parent_seed, Export/Import Data → open_palette
  (export/import need a path argument, palette is the right entry point)

add.py:
- seedplus-hint: mention BIP-85 explicitly, reference Seed+ (C) action
- AddEntryScreen success notify: include reveal/copy shortcut hint
- SeedPlusScreen: add #seedplus-warning label explaining deterministic
  derivation and index uniqueness requirement
- SeedPlusScreen success notify: mention index and parent seed

app.py:
- RecoverProfileScreen: set success status before bootstrap fires
- CreateProfileScreen: add intent messaging for nostr/backup restore modes
  with progress description and post-success status

grid.py:
- Add stored_password to secrets filter kind_map (was missing)

tests:
- test_tui_v3_parity: add test_v3_action_bar_global_row_uses_correct_handlers
- test_tui_v3_smoke: add seedplus-hint BIP-85 check, seedplus-warning label
  check, recover success-status check

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-06 13:36:34 -05:00
thePR0M3TH3ANandClaude Sonnet 4.6 02c4ef9e24 feat(tui-v3): add settings utility section and inspector kind-specific hints
Settings screen now renders a UTILITY & MAINTENANCE card listing stats,
db-export, db-import, checksum-verify/update, totp-export, and offline
mode commands. Renders gracefully when config service is offline (no
early return — all sections including utility always display).

Inspector adds UtilityHintsBar widget that persists across board swaps
via a #board-slot Vertical. Hints update to show kind-appropriate
palette commands (copy, reveal keybinds, QR, npub, managed-load, etc.).
BoardContainer now routes managed_account to its own SeedBoard hint set.

Two new smoke tests verify the utility section renders and that hints
update correctly for password, nostr, and idle states.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-06 13:35:49 -05:00
thePR0M3TH3ANandClaude Sonnet 4.6 50d7b1827d Polish TUI v3 maintenance and profile screens
- maintenance.py: refine CSS visual hierarchy (rounded intro card borders,
  red-tinted danger buttons, consistent panel/footer margins)
- profile.py: clearer label text, improved intro and confirmation messages
- security.py: updated intro text and footer hints for both screens
- pubkey.py: rename DEFAULT_CSS to CSS, update intro and footer text
- relays.py: rename DEFAULT_CSS to CSS, add intro card, update footer/status
- test_tui_v3_smoke.py: update assertions to match new text

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-06 13:30:08 -05:00
thePR0M3TH3AN 46ffff577b Improve v3 grid navigation and graph browsing 2026-03-06 07:10:36 -05:00
thePR0M3TH3AN 4011464c31 Advance v3 search, atlas, and comms planning 2026-03-05 23:26:05 -05:00
thePR0M3TH3AN e8a2b81e7b Make TUI v3 the default and add index0 foundations 2026-03-05 21:52:39 -05:00
thePR0M3TH3AN 033870f2df fix: harden legacy/tui3 non-tty flows and shutdown stability 2026-03-03 21:56:55 -05:00
thePR0M3TH3AN d3fc572c4e chore: track overstory workspace and session artifacts 2026-03-03 21:25:04 -05:00
thePR0M3TH3AN 538ce42338 chore: format codebase and harden v3 CLI/CI gates 2026-03-03 21:22:19 -05:00