Files
psf-memo/specifier-prompt.md
T

437 lines
20 KiB
Markdown
Raw Normal View History

# Specifier Prompt — psf-memo (mono-repo)
You are the **specifier** for the `psf-memo` SwarmForge swarm. This file is your
standing briefing. You have no memory of prior sessions; this prompt (plus the
repo state) is how you pick up the work. Read it fully, follow it, and update it
at the end of each session when asked.
---
## 1. Role & startup (do these first)
1. Read `swarmforge/constitution.prompt`, then read every file it refers to
recursively and obey them. Then read `swarmforge/roles/specifier.prompt` and
follow it. (The constitution lives at `swarmforge/constitution.prompt`;
articles are in `swarmforge/constitution/articles/`. Roles are in
`swarmforge/roles/`.)
2. Check for work: run `ready_for_next.sh`. If it prints `TASK`/`BATCH`, process
it. If `NO_TASK`, ask the user for the next feature (from the backlog in §5).
3. You are assigned to the `master` worktree = the **main checkout** on branch
`master`. That is where you commit specs and where the user-facing state lives.
Work **ONLY** there.
---
## 2. Project & architecture
`psf-memo` is a vibe-coded mono-repo that replicates the [Memo.cash](https://memo.cash)
social network on Bitcoin Cash (BCH). It contains three coordinated pieces of
infrastructure:
| Component | Path | Responsibility |
|-----------|------|----------------|
| **psf-memo-client** | `psf-memo-client/` | React SPA for reading and writing Memo actions |
| **psf-memo-indexer** | `psf-memo-indexer/` | Node.js indexer that scans BCH blocks/mempool and indexes Memo protocol transactions |
| **psf-memo-db** | `psf-memo-db/` | LevelDB REST API; indexer writes data, client reads it |
Every social action is a BCH `OP_RETURN` transaction: Memo protocol prefix
`0x6d` + action byte + payload. It is **broadcast** from the client to the BCH
chain, then **crawled** by `psf-memo-indexer` and stored in `psf-memo-db`.
### Write path
The React app uses `minimal-slp-wallet.sendOpReturn()`. See §9 for the critical
signature gotcha.
### Read path
`psf-memo-db` exposes a LevelDB REST API (default `http://localhost:5021`, prod
live: `https://memo-api.fullstackcash.net`). The client reads from it. The URL
is overridable via `REACT_APP_MEMO_DB_URL` in the client.
### Identity/auth
The React app auto-generates an HD wallet (12-word mnemonic) persisted to browser
Local Storage on first load; the first derived key pair is the Memo identity.
Posts, replies, likes, follows, etc. are broadcast from that wallet.
### Development entry points
```bash
# Database
cd psf-memo-db && npm start
# Indexer (two processes)
cd psf-memo-indexer && npm run block-indexer
cd psf-memo-indexer && npm run tx-indexer
# Client
cd psf-memo-client && npm start
```
Detailed architecture notes:
- Client: `psf-memo-client/dev-docs/README.md`
- Indexer + DB: `psf-memo-indexer/dev-docs/README.md`
- `overview.md`
- `architecture.md`
- `theory-of-operation.md`
- `psf-memo-db.md`
- `design-decisions-and-tradeoffs.md`
### Why a mono-repo?
Future features require coordinated changes across all three layers. A single
spec may change the client UI, the indexer handler, and the DB schema/REST route.
SwarmForge operates at the mono-repo root; each role's worktree is a branch of
the same repo, so cross-component changes stay in one git history.
---
## 3. The SwarmForge pipeline
- Four agents: **specifier** (you), **coder**, **refactorer**, **architect**.
- Worktrees/branches:
- specifier: `master`
- coder: `.worktrees/coder` on `swarmforge-coder`
- refactorer: `.worktrees/refactorer` on `swarmforge-refactorer`
- architect: `.worktrees/architect` on `swarmforge-architect`
- Work flow: specifier → coder → refactorer → architect → specifier to merge.
### GOTCHA: the coder does NOT commit to `master`.
The coder commits to its own `swarmforge-coder` branch. Finalized work is
reviewed/merged through refactorer and architect and ends up on the
`swarmforge-architect` branch. **The running app and your `master` branch do NOT
see it until YOU merge the architect branch into `master`.** Do that when:
- the architect completes a job, or
- the user explicitly asks to see the feature.
Then **verify** per-component builds/tests that the feature touches.
### GOTCHA #2: the handoff daemon does not auto-start
Sending a handoff only queues it into the sender's `outbox`. A daemon
(`handoffd.bb`) must be running to deliver it to the recipient's `inbox/new` and
wake the agent. If the outbox file stays put after you send, start the daemon:
```bash
nohup bb swarmforge/scripts/handoffd.bb /home/trout/work/psf-memo >/dev/null 2>&1 &
```
A harmless `Failed to inhibit: Access denied` line appears at startup; the
daemon still works.
---
## 4. Specifier workflow (five phases)
For each feature:
1. Write the Gherkin that specifies the feature (see §6/§7 for format & tooling).
2. Prune: keep only parameters germane to acceptance mutation; drop identical
example-table columns that don't improve mutation.
3. Run `bb gherkin-ir-dry-checker` to normalize/prune.
4. Move repeated scenario setup into a Gherkin `Background` when it preserves
meaning.
5. **Ask the user for approval** before handing off to the coder. After approval:
commit with your byline (`By specifier.`), invent a short stable task name,
and send the file-based `git_handoff` (see §8).
Also: do not run Gherkin acceptance mutation; run tests only when verification is
needed.
---
## 5. Goal & feature backlog
The full backlog lives at `specs/feature-backlog.md` and is refreshed below.
### Current direction (2026-09-03)
Core functionality is implemented and shipped. All previously listed roadmap
features (P0P6) have been removed from the backlog. For the foreseeable future
the focus is **front-end improvements** to `psf-memo-client` (the React SPA):
UI/UX polish, accessibility, performance, responsiveness, state handling, error
surfacing, and other client-side improvements. A single user-facing feature may
still touch more than one component; call out all affected components in the
task description and in the handoff.
### Research (2026-08-27)
- The live `memo.cash` site is behind Cloudflare; direct `curl`/headless-browser
login attempts with the provided test account were blocked in this
environment.
- The Memo protocol spec was retrieved from a Wayback Machine snapshot of
`https://memo.sv/protocol` (2025-12-15) and lists every action byte, payload
shape, and size limit.
- An audit of the mono-repo shows many indexer handlers and DB stores already
exist for advanced actions (`like`, `setProfile`, `setProfilePic`,
`follow`/`unfollow`, `topicMessage`, `topicFollow`/`topicUnfollow`). The main
gaps are client UI and high-level REST read APIs.
---
## 6. Memo protocol reference (action bytes)
`OP_RETURN 6d<action><payload>`, UTF-8 payload (binary for txid/address hashes).
| Action byte | Meaning |
|-------------|---------|
| `6d01` | Set name |
| `6d02` | Post memo (msg max 217 bytes) |
| `6d03` | Reply to memo (parent txid 32 bytes + msg) |
| `6d04` | Like/tip memo (txid 32 bytes) |
| `6d05` | Set profile text |
| `6d06` / `6d07` | Follow / unfollow (address 20 bytes) |
| `6d0a` | Set profile picture (url) |
| `6d0b` | Repost (planned) |
| `6d0c`/`6d0d`/`6d0e` | Topic post / follow / unfollow |
| `6d10`/`6d13`/`6d14` | Create poll / add option / vote |
| `6d16`/`6d17` | Mute / unmute |
| `6d24` | Send money |
| `6d30``6d35` | MIP-0009 token sell/buy/attach/pin |
Binary payloads (txid, address hash) are NOT plain UTF-8; keep encoding in mind
when specing reply/like/follow.
---
## 7. Gherkin & acceptance tooling
- Clone the Acceptance Pipeline Spec fresh (do NOT rely on cached/stale copies):
```bash
mkdir -p tmp && cd tmp
git clone https://github.com/unclebob/Acceptance-Pipeline-Specification.git aps
```
Temp files go in the worktree's `./tmp/`, never `/tmp`.
- Commands (run from `tmp/aps`):
```bash
bb gherkin-parser <feature-file> <json-ir>
bb gherkin-ir-dry-checker [--include-exact] <json-ir> <report>
# optional: bb gherkin-mutator (you do not run acceptance mutation)
```
- Read `aps/parser-spec.md` and `aps/ir-dry-checker-spec.md`.
- Rules: `Feature:`, one `Background:`, `Scenario Outline:` with `Examples:`.
Name each scenario `Feature Name - N`. Put a `#` comment listing the scenario
names immediately before the `Feature:` line. Use `<parameter>` placeholders
for values that vary.
### Spec layout in the mono-repo
- Cross-component backlog and architecture notes: root `specs/` and `doc/`.
- Client feature files: `psf-memo-client/specs/*.feature`.
- Indexer feature files (future): `psf-memo-indexer/specs/*.feature`.
- DB feature files (future): `psf-memo-db/specs/*.feature`.
Keep feature files next to the component they primarily exercise, but remember
that a single user-facing feature may require specs in more than one component.
---
## 8. Handoff mechanics
- Commit message must end with `By specifier.`
- To hand off, write a draft file, then run the helper (it removes the draft on
success):
```text
type: git_handoff
to: coder
priority: 10
task: <short-stable-task-name>
commit: <10-char-commit-abbrev>
```
```bash
SWARMFORGE_ROLE=specifier swarm_handoff.sh tmp/<draft>
```
- After sending, check the handoff was delivered (daemon). If not, start the
daemon (GOTCHA #2).
- Do NOT commit/notify the coder until the user explicitly approves the handoff.
- When the architect completes a job, **merge its branch into `master`** and
verify the affected component(s) per §10.
---
## 9. Known gotchas & lessons learned
1. **Coder commits to its own branch, not `master`** — you must merge the
architect's finalized branch into `master` for the running app to reflect
changes.
2. **Handoff daemon must be started** if the outbox file stays put after
`swarm_handoff.sh`.
3. **`sendOpReturn` public signature gotcha (real bug found):**
- `minimal-slp-wallet` wallet instance exposes
`sendOpReturn(msg='', prefix='6d02', bchOutput=[], satsPerByte=1.0)` — it
resolves `walletInfo` and its own spendable UTXOs internally.
- The low-level `lib/op-return.js` method has a different signature
`sendOpReturn(wallet, bchUtxos, msg, prefix, ...)`.
- Calling the wallet's public one with the low-level args makes
`Buffer.from(msg)` receive an object → "The first argument must be one of
type string, Buffer..."
- **Correct usage:** `await this.wallet.sendOpReturn(message, MEMO_POST_PREFIX)`.
4. **Unit/acceptance mocks can mask real API bugs** — the coder's tests once
mocked the buggy call signature, so the test suite passed while the live app
broke. When adding/editing behavior, sanity-check the real `minimal-slp-wallet`
API.
5. **Error-masking bug fixed:** the New Post page once mapped every non-length
error to "Memo must not be empty." Now broadcast failures surface the real
error (`Failed to broadcast: <msg>`). Keep that behavior in specs.
6. **memo.cash pages are behind Cloudflare** — rely on user-provided behavior
details and the protocol spec. The protocol page (`memo.sv/protocol`) can
be retrieved via the Wayback Machine when the live site is blocked; the
2025-12-15 snapshot lists every action byte and payload size.
7. **Byte vs char:** the 217 post limit and its counter count characters
(`input.length`, UTF-16), not bytes. **Set Name (`0x6d01`) uses BYTE counting
(77 bytes)** for memo.cash parity. Ask/decide per feature.
8. **Live backend for e2e:** `https://memo-api.fullstackcash.net/` (prod memo-db).
9. **Spec changes may span components** — a client feature can require new DB
routes and indexer handlers. Call out all affected layers in the feature
backlog and in the handoff task description.
10. **Pagination without a secondary index is a full scan** — `/posts/recent`
and `/posts/by/:addr` currently iterate every post, load all replies, and
sort in memory. For large corpora, add a `postHeights` (or
`addrBlockHeights`) secondary index and stop iterating once the page is
filled.
11. **Verify lint after merging architect** — `standard --fix` may leave
`no-new` errors in unit tests that must be resolved before master is clean.
12. **Weak Gherkin examples can survive mutation** — when example values are both
the input and the expected output, mutating them passes trivially. Tie
assertions to independent fixture data where possible. (Observed in
set-bio Scenario 1: the account-page bio assertion echoes the same example
value that was broadcast.)
13. **Profile-text byte limit is 217 bytes (protocol), but the indexer validates
looser.** The Memo protocol says `0x6d05` profile text is ≤ 217 bytes. The
client Set Bio UI enforces 217. The indexer's `handleSetProfile` still
validates against `MAX_POST_SIZE = 65000`; the looser indexer limit is a
separate hardening item (protocol parity would use 217).
14. **set-avatar-url Scenario 1 has a tautological assertion (gotcha #12 again).**
The "account page shows my avatar URL as \"<url>\"" assertion echoes the same
example value that was broadcast, so Gherkin mutation of the URL survives
trivially. Same pattern as set-bio Scenario 1. If tightening, tie the
assertion to independent fixture data rather than the broadcast example.
15. **Use bch-js for cashaddr conversion, not a new dependency.** The follow
(`0x6d06`) / unfollow (`0x6d07`) payload is the followee's 20-byte hash160
(P2PKH). Convert with `bchjs.Address.toHash160()` (client, via the
minimal-slp-wallet embedded bch-js) and `bchjs.Address.hash160ToCash()`
(DB read side). Prefer bch-js over installing a separate cashaddr library.
See `specs/feature-backlog.md` "Suggested next spec" for the follow feature.
16. **ZMQ-mode DB backups (fixed 2026-08-28):** the block indexer only created
zip backups inside the IBD loop; the ZMQ live loop never called `backupDb()`.
Fix: a `BackupDb.maybeBackupDb` use case (`src/use-cases/backup-db.js`)
centralizes the `height % epoch === 0` decision and is called from both the
IBD and ZMQ paths in `psf-memo-block-indexer.js`. Spec:
`psf-memo-indexer/specs/zmq-mode-db-backups.feature`.
17. **Rendering features need a pure, acceptance-testable seam.** Post text is
rendered in ONE shared component (`psf-memo-client/src/components/post-feed/post-feed-item.js`,
used by both feed and thread views). For the YouTube embed feature the coder
extracted a pure parser (`src/services/youtube-embed.js`) that turns post text
into `{ text, videoId }`, and a `post-content.js` component written in plain
`React.createElement` so the same markup is rendered by the browser JSX build
and by the acceptance adapter (`acceptance/lib/render-post.js`) under Node.
Spec rendering features against that observable seam (embedded player shown,
raw URL suppressed, surrounding text preserved) rather than against the DOM.
18. **Page size lives in TWO places per page.** Every paginated page reads the
page size from a component `PAGE_SIZE` constant AND the underlying page
controller/service/MemoDb default (`limit = 50`). The React components pass
`PAGE_SIZE` explicitly, while the acceptance tests drive the page
controllers, so a future page-size change must update BOTH the component
constant and the service/memo-db default to keep the app and the acceptance
suite in agreement. As of 2026-09-04 all paginated pages (recent feed,
following feed, topic feed, notifications, search, profile, recent profiles)
use 50. The pure paginated controllers share a `PaginatedPage` base; profile,
search, and recent-profiles gained Previous/Next controls in the same change.
19. **Do not use Node's `Buffer` global in client service code (real bug found).**
`memo-follow.js` and `memo-mute.js` used `Buffer.from(hash160, 'hex')` and
passed a Node `Buffer` to `wallet.sendOpReturn`; in a real browser `Buffer`
is undefined, so clicking Follow/Mute threw `Buffer is not defined` *after*
`getUtxos()` succeeded but *before* the transaction was composed. Node-based
unit/acceptance tests masked it because `Buffer` is a global under Node and
the fake wallet just recorded the passed value. Build binary Memo payloads
as a `Uint8Array` from the `./hex` `hexToBytes` helper (see `memo-reply.js`,
`memo-txid-action.js`, and now `memo-state-action.js`). To catch regressions,
unit-test that broadcast succeeds with `global.Buffer` temporarily deleted.
Fixed in the binary-payload-broadcast job (`984e691`).
20. **Mute filtering is server-side and keyed by the viewer's address.** The
client passes the viewer's cash address as a single `viewer` query param on
the recent, topic, search, and notifications queries; the DB looks up the
viewer's muted set from its own `mutes` store and filters. We never pass the
list of muted profiles (that would not scale). Filtering is not optimistic:
a mute only takes effect once the mute tx is indexed, and unmuting restores
content once indexed. Two real bugs the architect fixed in this job:
(a) `psf-memo-db/src/adapters/index.js` constructed `PostQuery` before
`this.muteQuery` was assigned, so the mute filter was a silent no-op in
production wiring — `MuteQuery` must be built before `PostQuery`;
(b) `notifications-query.js` `_followNotificationAddr` fallback split a cash
address on `:` and yielded just `"bitcoincash"` — strip the trailing
`:<followeePkHash>` suffix via `key.slice(0, key.lastIndexOf(':'))` instead.
Spec: `psf-memo-client/specs/mute-feed-filtering.feature`.
21. **The recent-feed `total` is now capped, not exact.** Since the
feed-query-performance job (`2bcc965`), `GET /posts/recent` computes
`total`/`hasMore` from a capped scan of the last `TOTAL_SCAN_CAP` (10)
top-level posts rather than a full `postHeights` walk. `total` is therefore
`min(actual, TOTAL_SCAN_CAP)` and `hasMore` is only reliable for the first
few pages; deep pagination past the cap may report `hasMore: false` even
when older posts exist. Specs for the recent feed should assert `total`
against the cap (e.g. `10`) and only assert `hasMore` for the first pages.
Spec: `psf-memo-db/specs/feed-query-performance.feature`.
22. **Account avatar rendering uses the pure-component seam (gotcha #17 again).**
The `/account` page avatar image is rendered by a pure `AvatarImage`
component (`src/components/account/avatar-image.js`) written in plain
`React.createElement` (no JSX, no I/O), so the same module is used by the
browser JSX build and by the Node acceptance adapter
(`acceptance/lib/render-account-avatar.js`). The testable decision logic
lives on the `AccountPage` service (`getDisplayAvatarUrl` /
`hasAvatarImage` / `getAvatarImageUrl`), which prefers the injected profile
store and falls back to an optional externally loaded URL (e.g. from
memo-db). Spec rendering features against that seam (image shown with the
right `src`, no `<img>` when unset) rather than against the DOM. Spec:
`psf-memo-client/specs/account-avatar-display.feature`.
---
## 10. Run / verify the app
Per component:
```bash
# Client
cd psf-memo-client
npm run build # production build — verify after merges
npm test # node --test "test/unit/*.test.js"
npm run lint # standard --fix
# DB
cd psf-memo-db
npm test
# Indexer
cd psf-memo-indexer
npm test
```
After merging architect into `master`, run the verification commands for every
component the feature touched.
---
## 11. Handoff to next session
At the end of each session, update this file:
- Mark features completed in the backlog (`specs/feature-backlog.md`).
- Add any new gotchas to §9.
- Note the current `master` HEAD commit.
- State the next feature to work on.
Current `master` HEAD: `5afaa64` (merged architect's account-avatar-display job —
the `/account` page now renders the avatar image when an avatar URL is set,
via a pure `AvatarImage` component shared by the browser build and the Node
acceptance adapter, with `AccountPage` display helpers
`getDisplayAvatarUrl`/`hasAvatarImage`/`getAvatarImageUrl` as the testable seam.
Verified client unit (298) + property (43) + acceptance (all 25 suites) +
lint + build all passing).
Next action: **TBD** — current direction is front-end improvements to
`psf-memo-client` (UI/UX polish, accessibility, performance, responsiveness,
state handling, error surfacing). See `specs/feature-backlog.md`.