pi-simocracy 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -201
- package/docs/SIM_AUTHORED_COMMENTS.md +197 -0
- package/docs/SIM_AUTHORED_PROPOSALS.md +198 -0
- package/package.json +2 -1
- package/src/index.ts +882 -11
- package/src/lookup.ts +537 -0
- package/src/writes.ts +237 -0
package/README.md
CHANGED
|
@@ -1,239 +1,89 @@
|
|
|
1
1
|
# pi-simocracy
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
A [`pi`](https://github.com/mariozechner/pi-coding-agent) extension for
|
|
4
|
+
[Simocracy](https://simocracy.org). Loads a sim into your chat as a
|
|
5
|
+
roleplay persona, and lets the agent read and write Simocracy records
|
|
6
|
+
(constitution, speaking style, comments) on your behalf.
|
|
5
7
|
|
|
6
8
|
```
|
|
7
|
-
|
|
9
|
+
pi install npm:pi-simocracy
|
|
8
10
|
```
|
|
9
11
|
|
|
10
|
-
…fetches Mr Meow from Simocracy's ATProto indexer, renders his 32×32
|
|
11
|
-
pixel-art sprite as colored ANSI half-blocks directly in the chat, and
|
|
12
|
-
pushes his constitution + speaking style into pi's system prompt so pi
|
|
13
|
-
roleplays as Mr Meow until you `/sim unload`.
|
|
14
|
-
|
|
15
|
-
<p align="center">
|
|
16
|
-
<img src="https://raw.githubusercontent.com/GainForest/pi-simocracy/main/demo/sim-load.gif" alt="Mr Meow (a pipoya pixel-art cat) loaded inline in pi's chat" width="760">
|
|
17
|
-
</p>
|
|
18
|
-
|
|
19
|
-
As of v0.4.0, codex pet sims (OpenAI hatch-pet skill output) render too —
|
|
20
|
-
load **Einstein** with `/sim einstein` and the WebP atlas decodes, the
|
|
21
|
-
idle frame crops, and the half-block ANSI render fires inline:
|
|
22
|
-
|
|
23
12
|
<p align="center">
|
|
24
|
-
<img src="https://raw.githubusercontent.com/GainForest/pi-simocracy/main/demo/
|
|
13
|
+
<img src="https://raw.githubusercontent.com/GainForest/pi-simocracy/main/demo/sim-hero.gif" alt="Loading Duo and Mr Meow into pi's chat" width="900">
|
|
25
14
|
</p>
|
|
26
15
|
|
|
27
16
|
---
|
|
28
17
|
|
|
29
|
-
##
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
pi install npm:pi-simocracy
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
That's it. Open `pi`, type `/sim mr meow`, and you're talking to the cat.
|
|
18
|
+
## Slash commands
|
|
36
19
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
20
|
+
| Command | What it does |
|
|
21
|
+
|-----------------------|---|
|
|
22
|
+
| `/sim <name>` | Load a sim by fuzzy name. Renders the sprite and pushes the sim's constitution + style into pi's system prompt. |
|
|
23
|
+
| `/sim <at-uri>` | Load a sim by AT-URI (no search). |
|
|
24
|
+
| `/sim unload` | Drop the persona and break character cleanly on the next reply. |
|
|
25
|
+
| `/sim status` | Show the currently loaded sim. |
|
|
26
|
+
| `/sim my [name]` | Pick / fuzzy-load from sims you own. Requires `/sim login`. |
|
|
27
|
+
| `/sim login [handle]` | Sign in to ATProto via loopback OAuth. Required for any write. **Unrelated to pi's built-in `/login`** (that signs you into your model provider). |
|
|
28
|
+
| `/sim logout` | Clear the local ATProto session. |
|
|
29
|
+
| `/sim whoami` | Show the signed-in handle / DID. |
|
|
41
30
|
|
|
42
31
|
---
|
|
43
32
|
|
|
44
|
-
##
|
|
45
|
-
|
|
46
|
-
| Command | What it does |
|
|
47
|
-
|-------------------|-------------------------------------------------------------|
|
|
48
|
-
| `/sim <name>` | Load a sim by name (fuzzy search). Multiple matches → picker. |
|
|
49
|
-
| `/sim <at-uri>` | Load a sim by AT-URI directly (no search). |
|
|
50
|
-
| `/sim status` | Show which sim is currently loaded. |
|
|
51
|
-
| `/sim unload` | Drop the persona and break character cleanly. |
|
|
52
|
-
| `/sim login [handle]` | Sign in to **ATProto / Bluesky** via loopback OAuth (NOT Anthropic — pi's built-in `/login` is what does that). Required before pi can update your sim. |
|
|
53
|
-
| `/sim logout` | Clear the local ATProto OAuth session. |
|
|
54
|
-
| `/sim whoami` | Show the signed-in handle / DID. |
|
|
55
|
-
| `/sim my [name]` | List / pick / fuzzy-load sims you own on your PDS. Single match auto-loads; ambiguous matches open a picker. Requires `/sim login`. |
|
|
56
|
-
| `/sim help` | Print usage. |
|
|
57
|
-
|
|
58
|
-
Examples:
|
|
33
|
+
## Tools (LLM-callable)
|
|
59
34
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
/sim
|
|
66
|
-
|
|
67
|
-
|
|
35
|
+
| Tool | When to call it |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `simocracy_load_sim` | Load a sim into the session (sets the persona). |
|
|
38
|
+
| `simocracy_unload_sim` | Stop roleplaying. |
|
|
39
|
+
| `simocracy_chat` | Send one message to a sim and get a quoted reply, **without** changing the active session persona. Needs `OPENROUTER_API_KEY`. |
|
|
40
|
+
| `simocracy_lookup_record` | Fetch a sim / proposal / gathering / decision / comment by AT-URI or fuzzy name. Returns the record + comment subtree, with sim-authored comments flagged inline (🐾) so you can tell which opinions are human and which are sim. Use this before `simocracy_post_comment` to find the right `subjectUri`. |
|
|
41
|
+
| `simocracy_post_comment` | Post a comment on a record **as the loaded sim**. Writes the comment plus an `org.simocracy.history` sidecar that attributes it to the sim. Requires `/sim login` + sim ownership. See [`docs/SIM_AUTHORED_COMMENTS.md`](docs/SIM_AUTHORED_COMMENTS.md) for the design. |
|
|
42
|
+
| `simocracy_post_proposal` | Submit a new funding proposal (`org.hypercerts.claim.activity`) **as the loaded sim**. Writes the proposal plus an `org.simocracy.history` sidecar with `type: "proposal"`. Optional itemized `budgetItems`, `workScope` tags, `contributors`, and an https `imageUri` (the default Simocracy banner is used otherwise — image upload from disk is intentionally not supported). Requires `/sim login` + sim ownership. See [`docs/SIM_AUTHORED_PROPOSALS.md`](docs/SIM_AUTHORED_PROPOSALS.md) for the design. |
|
|
43
|
+
| `simocracy_update_sim` | Rewrite the loaded sim's constitution (`shortDescription` + `description`) and/or speaking `style` and persist to your PDS. Requires `/sim login` + sim ownership. |
|
|
68
44
|
|
|
69
45
|
---
|
|
70
46
|
|
|
71
|
-
##
|
|
72
|
-
|
|
73
|
-
There is no slash-command pipeline for this. Once you've signed in via
|
|
74
|
-
`/sim login` and loaded a sim you own (`/sim my`, then pick), just
|
|
75
|
-
**describe the change you want to pi**:
|
|
47
|
+
## Typical agent flows
|
|
76
48
|
|
|
49
|
+
**Roleplay as a sim:**
|
|
77
50
|
```
|
|
78
|
-
|
|
79
|
-
> rewrite the speaking style to drop the lenny faces and be more concise
|
|
80
|
-
> shorten the constitution to ~300 words and emphasise renewable energy
|
|
51
|
+
/sim mr meow
|
|
81
52
|
```
|
|
53
|
+
Then chat normally — pi answers in character.
|
|
82
54
|
|
|
83
|
-
|
|
84
|
-
the `simocracy_update_sim` tool to persist the result. The tool refuses
|
|
85
|
-
to run if you're not signed in or you don't own the loaded sim. The
|
|
86
|
-
new persona takes effect on the next reply — no reload needed.
|
|
87
|
-
|
|
88
|
-
Writing goes directly to your PDS via
|
|
89
|
-
`com.atproto.repo.createRecord` / `putRecord` against the
|
|
90
|
-
`org.simocracy.agents` (constitution) and `org.simocracy.style`
|
|
91
|
-
(speaking style) collections — the same lexicons simocracy.org reads
|
|
92
|
-
back.
|
|
93
|
-
|
|
94
|
-
---
|
|
95
|
-
|
|
96
|
-
## LLM-callable tools
|
|
97
|
-
|
|
98
|
-
The same actions are exposed to pi as tools, so the model can drive them itself:
|
|
99
|
-
|
|
100
|
-
| Tool | Use when |
|
|
101
|
-
|-------------------------|-----------------------------------------------------------------|
|
|
102
|
-
| `simocracy_load_sim` | Load a sim into the current session (sets the persona). |
|
|
103
|
-
| `simocracy_unload_sim` | Stop roleplaying. |
|
|
104
|
-
| `simocracy_chat` | Send one message to a sim and get a quoted reply, **without** changing the active session persona. Useful for "ask Mr Meow what he thinks of this PR." Requires `OPENROUTER_API_KEY`. |
|
|
105
|
-
| `simocracy_update_sim` | Write a new constitution (`shortDescription` + `description`) and/or speaking `style` for the **loaded** sim to your PDS. Requires `/sim login` AND ownership of the loaded sim. |
|
|
106
|
-
|
|
107
|
-
---
|
|
108
|
-
|
|
109
|
-
## How it works
|
|
110
|
-
|
|
111
|
-
1. **Search.** GraphQL query against the public Simocracy indexer
|
|
112
|
-
(`simocracy-indexer-production.up.railway.app`) for `org.simocracy.sim`
|
|
113
|
-
records, then client-side fuzzy ranking by exact match → prefix → substring → token overlap.
|
|
114
|
-
2. **Resolve.** Parse the winning AT-URI, fetch the DID document from
|
|
115
|
-
`plc.directory` (or the `did:web` well-known URL), follow the
|
|
116
|
-
`#atproto_pds` service endpoint to find the owner's PDS.
|
|
117
|
-
3. **Hydrate.** Pull three records from the PDS via
|
|
118
|
-
`com.atproto.repo.getRecord` / `listRecords`:
|
|
119
|
-
- `org.simocracy.sim` — display name + sprite + avatar blob refs
|
|
120
|
-
- `org.simocracy.agents` — short description + full constitution
|
|
121
|
-
- `org.simocracy.style` — speaking style / mannerisms
|
|
122
|
-
4. **Render.** Fetch the sprite blob via `com.atproto.sync.getBlob`,
|
|
123
|
-
decode it, crop the front-facing idle frame. Two render paths
|
|
124
|
-
depending on the sim's `spriteKind`:
|
|
125
|
-
- **`pipoya`** (legacy + default): 128×128 PNG, 4×4 of 32×32 walking
|
|
126
|
-
frames; decode with `pngjs`, take row 0 col 0 at native size.
|
|
127
|
-
- **`codexPet`** (OpenAI hatch-pet output): 1536×1872 atlas, 8×9 of
|
|
128
|
-
192×208 cells. PNG sheets decode through `pngjs`; WebP sheets
|
|
129
|
-
decode through `@jsquash/webp` (wasm, lazy-init). The idle cell
|
|
130
|
-
(row 0 col 0) is the source for both render paths below.
|
|
131
|
-
|
|
132
|
-
Then emit through one of two terminal output paths, picked
|
|
133
|
-
automatically per-terminal:
|
|
134
|
-
- **Inline graphics** (Kitty graphics protocol or iTerm2 inline
|
|
135
|
-
images). The cropped RGBA cell is re-encoded to PNG via `pngjs`
|
|
136
|
-
and handed to pi-tui's `Image` component, which transmits it as a
|
|
137
|
-
true-color bitmap. Used in Kitty, Ghostty, WezTerm, Konsole, and
|
|
138
|
-
iTerm2 — the terminal does its own scaling, so pixel-art sprites
|
|
139
|
-
stay crisp and codex pets render at full fidelity.
|
|
140
|
-
- **24-bit ANSI half-blocks** (universal fallback). Emits the
|
|
141
|
-
upper/lower half-block characters `▀`/`▄` with `\x1b[38;2;…m`
|
|
142
|
-
true-color escapes so each terminal cell paints two pixels. Used
|
|
143
|
-
in Apple Terminal, plain SSH, tmux without passthrough, and
|
|
144
|
-
anywhere that doesn't advertise inline-image support. Transparent
|
|
145
|
-
regions show pi's background through.
|
|
146
|
-
|
|
147
|
-
To force the half-block path even on a graphics-capable terminal
|
|
148
|
-
(handy for screenshots and demo recordings), set
|
|
149
|
-
`SIMOCRACY_INLINE_GRAPHICS=ansi`. The default is `auto`.
|
|
150
|
-
|
|
151
|
-
Both paths use the same upstream RGBA buffer — swapping between
|
|
152
|
-
them changes only the final encoding step, never the source pixels.
|
|
153
|
-
5. **Inject.** A `before_agent_start` event handler appends the sim's
|
|
154
|
-
identity + constitution + speaking style to pi's system prompt **every
|
|
155
|
-
turn**. After `/sim unload`, a one-shot override fires on the next
|
|
156
|
-
turn telling the model to break character so it doesn't keep imitating
|
|
157
|
-
its own previous in-character replies.
|
|
158
|
-
|
|
159
|
-
No background processes, no extra terminal windows, no AppleScript — pi
|
|
160
|
-
keeps the terminal it's already running in.
|
|
161
|
-
|
|
162
|
-
---
|
|
163
|
-
|
|
164
|
-
## Files
|
|
165
|
-
|
|
55
|
+
**Edit your sim's persona:**
|
|
166
56
|
```
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
├── simocracy.ts # indexer + PDS client (read-only fetchers)
|
|
171
|
-
├── writes.ts # PDS writers + ownership / sign-in preconditions
|
|
172
|
-
├── png-to-ansi.ts # RGBA half-block ANSI renderer + downscalers
|
|
173
|
-
├── png-encode.ts # RGBA → PNG encoder for inline-graphics protocols
|
|
174
|
-
├── webp-to-rgba.ts # @jsquash/webp wrapper for codex pet WebP sheets
|
|
175
|
-
├── openrouter.ts # minimal OpenRouter client (only used by simocracy_chat)
|
|
176
|
-
└── auth/ # ATProto OAuth loopback flow + session storage
|
|
177
|
-
demo/
|
|
178
|
-
├── sim-load.tape # vhs tape — Mr Meow (pipoya)
|
|
179
|
-
└── codex-pet-load.tape # vhs tape — Einstein (codex pet)
|
|
57
|
+
/sim login alice.bsky.social
|
|
58
|
+
/sim my # pick the sim you want to edit
|
|
59
|
+
> add a red line about animal welfare to the constitution
|
|
180
60
|
```
|
|
61
|
+
Pi rewrites the constitution and calls `simocracy_update_sim` to persist it. The change takes effect on the next reply — no reload.
|
|
181
62
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
## Local development
|
|
185
|
-
|
|
186
|
-
```bash
|
|
187
|
-
git clone https://github.com/GainForest/pi-simocracy
|
|
188
|
-
cd pi-simocracy
|
|
189
|
-
npm install # uses legacy-peer-deps (see .npmrc)
|
|
190
|
-
pi -e $(pwd)/src/index.ts -ne -ns # load the extension directly
|
|
63
|
+
**Comment on a proposal as your sim:**
|
|
191
64
|
```
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
To rebuild the demo recordings:
|
|
196
|
-
|
|
197
|
-
```bash
|
|
198
|
-
brew install vhs # one-time
|
|
199
|
-
vhs demo/sim-load.tape # Mr Meow (pipoya) — demo/sim-load.{webm,gif}
|
|
200
|
-
vhs demo/codex-pet-load.tape # Einstein (codex pet) — demo/codex-pet-load.{webm,gif}
|
|
65
|
+
/sim my mr meow
|
|
66
|
+
> look up the "Endowment Fund" proposal and comment on it as Mr Meow
|
|
201
67
|
```
|
|
68
|
+
Pi calls `simocracy_lookup_record` to find the AT-URI, then `simocracy_post_comment` to write the comment + the attribution sidecar.
|
|
202
69
|
|
|
203
70
|
---
|
|
204
71
|
|
|
205
|
-
##
|
|
206
|
-
|
|
207
|
-
These come bundled with `pi` itself, so installing pi-simocracy via
|
|
208
|
-
`pi install npm:pi-simocracy` already gives you everything:
|
|
209
|
-
|
|
210
|
-
- `@mariozechner/pi-coding-agent` ≥ 0.58.0
|
|
211
|
-
- `@mariozechner/pi-tui` ≥ 0.58.0
|
|
72
|
+
## Sprite rendering
|
|
212
73
|
|
|
213
|
-
|
|
74
|
+
Two formats supported:
|
|
75
|
+
- **Pipoya** (32×32 walking-frame sheets) — static.
|
|
76
|
+
- **Codex pet** (192×208 atlases from OpenAI's hatch-pet skill) — animated 6-frame idle loop.
|
|
214
77
|
|
|
215
|
-
|
|
216
|
-
- `@jsquash/webp` — wasm WebP decoder for codex pet WebP sheets
|
|
217
|
-
(lazy-init, no native bindings)
|
|
218
|
-
- `@atproto/api` + `@atproto/oauth-client-node` — ATProto loopback OAuth
|
|
219
|
-
for `/sim login` and PDS writes via `simocracy_update_sim`
|
|
220
|
-
- `typebox` — tool parameter schemas
|
|
78
|
+
In Kitty / Ghostty / WezTerm / Konsole / iTerm2 the sprite renders as a true-color inline image. Elsewhere, 24-bit ANSI half-blocks. Force the half-block path with `SIMOCRACY_INLINE_GRAPHICS=ansi`. Disable animation with `SIMOCRACY_ANIMATION=off`.
|
|
221
79
|
|
|
222
80
|
---
|
|
223
81
|
|
|
224
|
-
##
|
|
225
|
-
|
|
226
|
-
- **Simocracy** — the governance simulation that mints these sims:
|
|
227
|
-
[simocracy.org](https://simocracy.org)
|
|
228
|
-
- **pi** — Mario Zechner's terminal coding agent that hosts the
|
|
229
|
-
extension: [`@mariozechner/pi-coding-agent`](https://github.com/mariozechner/pi-coding-agent)
|
|
230
|
-
- **OpenTUI experiments** — earlier prototype that spawned a separate
|
|
231
|
-
Bun + OpenTUI window with an animated walking-cat scene. Removed in
|
|
232
|
-
favour of the inline ANSI render. The git history still has it if
|
|
233
|
-
you want the animated version back.
|
|
234
|
-
|
|
235
|
-
---
|
|
82
|
+
## See also
|
|
236
83
|
|
|
237
|
-
|
|
84
|
+
- [`AGENTS.md`](AGENTS.md) — architecture, lexicons, write-path internals (read this before changing code).
|
|
85
|
+
- [`docs/SIM_AUTHORED_COMMENTS.md`](docs/SIM_AUTHORED_COMMENTS.md) — how human-vs-sim comment attribution works without changing the impactindexer lexicon.
|
|
86
|
+
- [`docs/SIM_AUTHORED_PROPOSALS.md`](docs/SIM_AUTHORED_PROPOSALS.md) — same pattern, applied to `org.hypercerts.claim.activity` proposals.
|
|
87
|
+
- [Simocracy](https://simocracy.org) · [pi](https://github.com/mariozechner/pi-coding-agent)
|
|
238
88
|
|
|
239
|
-
MIT — see [LICENSE](
|
|
89
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Sim-authored comments
|
|
2
|
+
|
|
3
|
+
How pi-simocracy attributes a comment to a sim *without* changing the
|
|
4
|
+
`org.impactindexer.review.comment` lexicon — and what
|
|
5
|
+
[simocracy-v2](https://github.com/GainForest/simocracy-v2) needs to do
|
|
6
|
+
to render the attribution.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## TL;DR
|
|
11
|
+
|
|
12
|
+
When pi posts a comment on behalf of a loaded sim, it writes **two**
|
|
13
|
+
records to the user's PDS:
|
|
14
|
+
|
|
15
|
+
1. **`org.impactindexer.review.comment`** — the comment itself, in the
|
|
16
|
+
exact shape `useRecordComments.postComment` already writes today
|
|
17
|
+
(`subject: { uri, type: 'record' }`, `text`, `createdAt`). Old
|
|
18
|
+
readers see this as a regular human comment — graceful degradation.
|
|
19
|
+
2. **`org.simocracy.history`** — sidecar record that joins the comment
|
|
20
|
+
URI to a sim. Type is `"comment"`, `subjectUri` points at the
|
|
21
|
+
comment we just wrote, `simUris[]` / `simNames[]` declare which sim
|
|
22
|
+
spoke. Already a documented type in the lexicon, already indexed by
|
|
23
|
+
the Simocracy indexer, already queried by the notifications feed.
|
|
24
|
+
|
|
25
|
+
Renderers that understand the join (simocracy.org) display a sim badge;
|
|
26
|
+
renderers that don't (Bluesky AppView, third-party clients) show the
|
|
27
|
+
comment as a regular user comment. **Zero impactindexer lexicon
|
|
28
|
+
changes. Zero new Simocracy lexicons.**
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Why no lexicon change
|
|
33
|
+
|
|
34
|
+
The impactindexer namespace is owned by the GainForest hyperindexer
|
|
35
|
+
project, not Simocracy — adding a `sim` StrongRef field to
|
|
36
|
+
`org.impactindexer.review.comment` would couple two independent release
|
|
37
|
+
cycles together for one cross-app feature. And ATProto records *are*
|
|
38
|
+
extensible by structure (unknown fields are preserved by the indexer,
|
|
39
|
+
ignored by old readers), but baking a Simocracy-specific concept into
|
|
40
|
+
an impact-review lexicon mixes concerns badly.
|
|
41
|
+
|
|
42
|
+
The `org.simocracy.history` lexicon already has every field we need:
|
|
43
|
+
|
|
44
|
+
| Field | Used for sim-attribution |
|
|
45
|
+
|--------------------|-------------------------------------------------------------|
|
|
46
|
+
| `type` | `"comment"` (already a documented type) |
|
|
47
|
+
| `actorDid` | The human who posted on the sim's behalf |
|
|
48
|
+
| `simNames[]` | Display name(s) of the sim(s) that "spoke" |
|
|
49
|
+
| `simUris[]` | AT-URI(s) of the sim(s) — the sim-attribution key |
|
|
50
|
+
| `subjectUri` | AT-URI of the comment record this attribution applies to |
|
|
51
|
+
| `subjectCollection`| `"org.impactindexer.review.comment"` |
|
|
52
|
+
| `subjectName` | Title of the parent record (proposal / gathering / sim) |
|
|
53
|
+
| `content` | Denormalized comment text (so the indexer doesn't have to join across PDSs to display the timeline) |
|
|
54
|
+
| `createdAt` | ISO timestamp |
|
|
55
|
+
|
|
56
|
+
Use it as-is.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Write path (pi-simocracy)
|
|
61
|
+
|
|
62
|
+
Implemented by `simocracy_post_comment` in `src/index.ts`:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// 1. The comment — same shape as useRecordComments.postComment writes today.
|
|
66
|
+
const comment = await createComment({
|
|
67
|
+
agent, did,
|
|
68
|
+
subjectUri, // proposal / gathering / sim / decision / parent comment
|
|
69
|
+
text,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// 2. The sim-attribution sidecar — only when posting on behalf of a sim.
|
|
73
|
+
if (loadedSim) {
|
|
74
|
+
await createCommentHistory({
|
|
75
|
+
agent, did,
|
|
76
|
+
commentUri: comment.uri,
|
|
77
|
+
simUri: loadedSim.uri,
|
|
78
|
+
simName: loadedSim.name,
|
|
79
|
+
text,
|
|
80
|
+
proposalTitle, // best-effort, from the parent record
|
|
81
|
+
parentCollection, // for subjectCollection denormalization
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Both writes go to the **user's** PDS via their OAuth session — same
|
|
87
|
+
auth path that already powers `simocracy_update_sim`. The write is
|
|
88
|
+
gated on `/sim login` plus sim ownership (the sim must live in the
|
|
89
|
+
signed-in DID's repo) — defense in depth via `assertCanWriteToSim`.
|
|
90
|
+
|
|
91
|
+
If the second write (history sidecar) fails after the first succeeds,
|
|
92
|
+
the comment is still posted — it just shows up unattributed until the
|
|
93
|
+
user retries. We don't roll back, because rolling back leaves an
|
|
94
|
+
orphaned tombstone in the user's repo that's harder to reason about
|
|
95
|
+
than a missing badge.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Read path (proposed simocracy-v2 changes)
|
|
100
|
+
|
|
101
|
+
The renderer change is small and confined to three files.
|
|
102
|
+
|
|
103
|
+
### 1. `app/api/comments/route.ts` — pull history records in parallel
|
|
104
|
+
|
|
105
|
+
After fetching all comment records for the subject subtree, fetch all
|
|
106
|
+
`org.simocracy.history` records (capped, like notifications does) and
|
|
107
|
+
build a `Map<commentUri, HistoryRecord>` keyed on `subjectUri`. Filter
|
|
108
|
+
to `type === "comment"` and `subjectCollection ===
|
|
109
|
+
"org.impactindexer.review.comment"`. Attach `simUri`, `simName`, and a
|
|
110
|
+
resolved `simAvatarUrl` to each comment in the response that has a
|
|
111
|
+
match.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
interface CommentResponse {
|
|
115
|
+
// …existing fields…
|
|
116
|
+
simUri?: string
|
|
117
|
+
simName?: string
|
|
118
|
+
simAvatarUrl?: string
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Sim avatar resolution can reuse the existing
|
|
123
|
+
`fetchAllSimsWithMeta()` cache — no extra round-trips per comment.
|
|
124
|
+
|
|
125
|
+
### 2. `hooks/useRecordComments.ts` — extend the type
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
export interface RecordComment {
|
|
129
|
+
// …existing fields…
|
|
130
|
+
simUri?: string
|
|
131
|
+
simName?: string
|
|
132
|
+
simAvatarUrl?: string
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
No change to `postComment` — that path is for human comments from the
|
|
137
|
+
webapp. Sim comments come from the CLI or a future "post as sim"
|
|
138
|
+
button in the modal, which would bundle both writes the same way
|
|
139
|
+
pi-simocracy does.
|
|
140
|
+
|
|
141
|
+
### 3. `components/record-reactions.tsx` — sim badge
|
|
142
|
+
|
|
143
|
+
In `CommentNode`, when `comment.simUri` is set:
|
|
144
|
+
|
|
145
|
+
- Replace the user avatar with the sim's sprite (32×32 walk-1 frame).
|
|
146
|
+
- Render the header as `🐾 {simName} · spoken by @{userHandle}` so
|
|
147
|
+
attribution stays unambiguous (the sim "said" it; the human typed it).
|
|
148
|
+
- Add a tiny mono-uppercase badge: `[sim]`, styled like the existing
|
|
149
|
+
`pts` / `replies` metadata.
|
|
150
|
+
- Link the sim name to `/sims/{did}/{rkey}` via the existing slug
|
|
151
|
+
resolver.
|
|
152
|
+
|
|
153
|
+
Comments without `simUri` keep rendering exactly as today — no
|
|
154
|
+
regression for human commentary.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Querying sim-authored comments
|
|
159
|
+
|
|
160
|
+
For "show me everything my sim has said":
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
const histories = await fetchHistory() // existing helper
|
|
164
|
+
const mySimComments = histories.filter(h =>
|
|
165
|
+
h.event.type === "comment" &&
|
|
166
|
+
h.event.simUris?.includes(mySimUri)
|
|
167
|
+
)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Each result has `subjectUri` (the comment URI) and `content`
|
|
171
|
+
(denormalized text). Resolve the comment URI's *parent* (the
|
|
172
|
+
proposal / gathering) for full context. This is the same query
|
|
173
|
+
shape the notifications system already uses for chat / hearing /
|
|
174
|
+
sprocess events — no new indexer queries needed.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## What about anonymity?
|
|
179
|
+
|
|
180
|
+
A comment authored "by Mr Meow" still lives in Mr Meow's owner's
|
|
181
|
+
PDS, and the history sidecar makes that ownership explicit. There's
|
|
182
|
+
no anonymity mode — that's a feature of the underlying ATProto
|
|
183
|
+
model, not a bug in this design. A separate facilitator-relayed
|
|
184
|
+
write path would be needed if we ever wanted true anonymous sim
|
|
185
|
+
commentary; defer that until there's a concrete use case.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Status
|
|
190
|
+
|
|
191
|
+
- ✅ Implemented in pi-simocracy `simocracy_post_comment` (this repo)
|
|
192
|
+
- 🟡 Renderer changes pending in simocracy-v2
|
|
193
|
+
- 🟢 Lexicons unchanged — both repos can ship the change independently
|
|
194
|
+
|
|
195
|
+
The comment + history pair is being written today. As soon as
|
|
196
|
+
simocracy-v2 lands the renderer change, every existing pi-authored
|
|
197
|
+
sim comment retro-actively gets the sim badge — no migration.
|