ei-tui 1.6.8 → 1.7.0

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.
Files changed (47) hide show
  1. package/README.md +4 -0
  2. package/package.json +2 -1
  3. package/skills/coding-harness-reflect/SKILL.md +230 -0
  4. package/skills/ei-curate/SKILL.md +174 -0
  5. package/skills/ei-curate/references/cli.md +160 -0
  6. package/skills/ei-curate/references/provenance.md +88 -0
  7. package/skills/ei-curate/references/recipes.md +144 -0
  8. package/skills/ei-curate/references/talking-to-the-user.md +71 -0
  9. package/skills/ei-persona/SKILL.md +238 -0
  10. package/skills/ei-persona/references/cli.md +265 -0
  11. package/skills/ei-persona/references/recipes.md +247 -0
  12. package/skills/ei-persona/references/talking-to-the-user.md +107 -0
  13. package/src/cli/README.md +72 -2
  14. package/src/cli/commands/personas.ts +46 -1
  15. package/src/cli/corrections-endpoints.ts +297 -0
  16. package/src/cli/corrections-writer.ts +138 -0
  17. package/src/cli/install.ts +252 -157
  18. package/src/cli/mcp.ts +80 -1
  19. package/src/cli/persona-corrections.ts +442 -0
  20. package/src/cli/retrieval.ts +46 -2
  21. package/src/cli.ts +148 -1
  22. package/src/core/corrections.ts +233 -0
  23. package/src/core/handlers/human-extraction.ts +8 -2
  24. package/src/core/handlers/human-matching.ts +2 -2
  25. package/src/core/llm-client.ts +7 -1
  26. package/src/core/orchestrators/human-extraction.ts +1 -0
  27. package/src/core/persona-tools.ts +92 -0
  28. package/src/core/personas/opencode-agent.ts +1 -3
  29. package/src/core/processor.ts +113 -1
  30. package/src/core/state/human.ts +10 -0
  31. package/src/core/state/personas.ts +8 -0
  32. package/src/core/state-manager.ts +11 -0
  33. package/src/core/types/entities.ts +1 -0
  34. package/src/core/utils/identifier-utils.ts +3 -2
  35. package/src/integrations/pi/importer.ts +142 -50
  36. package/src/integrations/pi/reader.ts +1 -0
  37. package/src/integrations/pi/types.ts +4 -0
  38. package/src/storage/file-lock.ts +120 -0
  39. package/tui/README.md +2 -0
  40. package/tui/src/commands/provider.tsx +1 -1
  41. package/tui/src/components/WelcomeOverlay.tsx +3 -3
  42. package/tui/src/context/ei.tsx +14 -0
  43. package/tui/src/index.tsx +13 -1
  44. package/tui/src/storage/file.ts +15 -83
  45. package/tui/src/util/instance-lock.ts +3 -2
  46. package/tui/src/util/provider-detection.ts +4 -2
  47. package/tui/src/util/yaml-persona.ts +7 -38
@@ -0,0 +1,265 @@
1
+ # reference: the `ei` CLI for persona authoring
2
+
3
+ The Ei CLI is how you read and write personas. It is a thin wrapper over the local
4
+ data store. Everything below was true at time of writing — **always run `ei --help`
5
+ first and trust it over this file** if they disagree (the CLI evolves).
6
+
7
+ ## Invocation
8
+
9
+ - Prefer `ei` (on PATH). If it's not found, use `bunx ei-tui` with the same arguments.
10
+ - Successful read/write commands print **JSON** to stdout. Validation and usage failures
11
+ print human-readable text to stderr and exit non-zero.
12
+
13
+ ## Reading (safe, do this constantly)
14
+
15
+ ```bash
16
+ ei persona "<Name>" # find a persona by name — substring match on display_name,
17
+ # falls back to semantic search over long_description
18
+ ei --id <id> # full record for one entity, including a persona
19
+ ```
20
+
21
+ `ei persona "<Name>"` (the type-specific search, not the `--persona` filter flag) is how you
22
+ find a persona by name. **`--persona "<Name>"` is a different feature** — it filters *other*
23
+ entity types (facts/topics/people) down to what a named persona has learned; it never returns
24
+ personas themselves, so it cannot be used to find one.
25
+
26
+ `ei --id <id>` is your workhorse once you have the id. For a persona it returns the **full**
27
+ record: identity fields, every entry in `traits[]` and `topics[]`, and the lifecycle flags.
28
+ Always read the full record before writing — you cannot safely change one field of a persona
29
+ without seeing the rest (full-record round-trip, below).
30
+
31
+ ## The persona record shape
32
+
33
+ You must round-trip this on `update` (see "full-record round-trip" below). The writable
34
+ surface is:
35
+
36
+ ```
37
+ display_name, # required, non-empty; also checked against the
38
+ # reserved-name list (see below) on both create and update
39
+ aliases: [ "…" ], # for fuzzy matching (e.g. "/persona Bob")
40
+ short_description, long_description,
41
+ model,
42
+ group_primary, groups_visible: […],
43
+ traits: [ PersonaTrait, … ],
44
+ topics: [ PersonaTopic, … ],
45
+ is_paused, pause_until,
46
+ is_archived, archived_at, # setting is_archived is how you archive/unarchive —
47
+ # there is no separate archive verb
48
+ heartbeat_delay_ms, context_window_ms,
49
+ include_message_timestamps, context_boundary,
50
+ tools: { "<Provider>": { "<Tool>": true|false } }, # a map (not an id list) of every tool
51
+ # on every currently-enabled provider — see "Tool
52
+ # grants" below (not related to your own tool access)
53
+ avatar_emoji, avatar_image,
54
+ preferred_theme,
55
+ notes: [ "…" ], # capped at 20 entries server-side — a write that
56
+ # pushes the array past 20 is rejected
57
+ ```
58
+
59
+ **`PersonaTrait`** — a named character trait:
60
+ ```
61
+ id, # optional on write — see "auto-assigned ids" below
62
+ name,
63
+ description,
64
+ sentiment, # -1.0 to 1.0
65
+ strength, # optional, 0.0 to 1.0
66
+ ```
67
+
68
+ **`PersonaTopic`** — a subject the persona has a stance on:
69
+ ```
70
+ id, # optional on write — see "auto-assigned ids" below
71
+ name,
72
+ perspective, # their view/opinion on this topic
73
+ approach, # how they prefer to engage with it
74
+ personal_stake, # why it matters to them personally
75
+ sentiment, # -1.0 to 1.0
76
+ exposure_current, # 0.0 to 1.0 — how recently/frequently it's come up
77
+ exposure_desired, # 0.0 to 1.0 — how much they want to engage with it
78
+ ```
79
+
80
+ **Auto-assigned ids.** If a trait or topic in your payload has no `id`, the server
81
+ assigns a fresh one before persisting — the same way the top-level persona `id` is
82
+ assigned on create. You never need to invent a UUID for a brand-new trait or topic;
83
+ just omit `id` and let it be minted.
84
+
85
+ **No minimum count.** Nothing here enforces a minimum number of traits or topics. (The
86
+ reflection ceremony's own convention of "at least 3 traits, at least 3 topics" is guidance
87
+ inside a *different* skill for a *different* situation — it is not a rule this path
88
+ enforces. Adding a single trait to an otherwise-untouched persona is a completely valid
89
+ edit.)
90
+
91
+ **Server-managed, not part of the writable surface.** Fields set by Ei — read them,
92
+ don't invent or hand-edit them:
93
+ - `id`, `entity`, `last_updated`, `last_heartbeat`, `last_extraction`,
94
+ `description_embedding`, `pending_update`, `reflection_last_asked` — silently stripped
95
+ and ignored if present in an `update` payload (the natural result of round-tripping a
96
+ read), never a validation error.
97
+ - `is_static` — marks built-in structural personas; not writable through this path at all.
98
+ Don't try to flip it.
99
+
100
+ **Reserved names.** `display_name` is checked against a reserved-word list (currently
101
+ `new`, `clone` — command keywords that collide with `/persona` subcommands) on **both**
102
+ `create` and `update`. Renaming an existing persona *into* a reserved name is rejected
103
+ exactly like creating one with that name.
104
+
105
+ ## Tool grants (`tools`)
106
+
107
+ `tools` is a **map you read, flip, and write back** — not a list of ids you have to know
108
+ in advance, and there's no separate step to enumerate what exists: a read of the persona
109
+ **is** the live menu of what's grantable right now. A real read looks like this:
110
+
111
+ ```json
112
+ {
113
+ "Ei Built-ins": { "Web Fetch": false, "Find Memory": true },
114
+ "Spotify": { "Currently Playing Track": false, "Liked Songs": false }
115
+ }
116
+ ```
117
+
118
+ - **Outer keys** are provider display names — but only for providers that are currently
119
+ **enabled**. A disabled provider (e.g. Spotify before the human finishes OAuth) does not
120
+ appear in the map at all — not present-with-everything-false, just absent. Its tools stay
121
+ invisible, and ungrantable, until the human enables it.
122
+ - **Inner keys** are every tool belonging to that enabled provider, `true` if **this
123
+ persona** currently has it granted, `false` if not.
124
+ - This has nothing to do with whatever tools *you* (the agent reading this file, via MCP or
125
+ any other mechanism your own harness uses) have access to. A `true` here means: the next
126
+ time a **human** talks to **that persona** inside Ei's TUI or web client, the persona may
127
+ call that tool mid-conversation.
128
+ - Trust the map from a fresh read over anything written down anywhere, including this file
129
+ — the set of providers and tools can change over time.
130
+
131
+ ### Writing (grant/revoke)
132
+
133
+ `create`/`update` take the **exact same map shape** back — the full-record round-trip rule
134
+ (below) applied to `tools` specifically:
135
+
136
+ 1. Read the persona; look at its `tools` map.
137
+ 2. Flip the one boolean you mean to change (`false → true` to grant, `true → false` to
138
+ revoke).
139
+ 3. Send the **whole** map back, inside the whole record, unchanged apart from that flip.
140
+
141
+ A key you omit from the map isn't "left unchanged" the way a patch would treat it — but
142
+ since a disabled provider was never in your read to begin with, everything you can see on a
143
+ read is everything you round-trip; there's no hidden state you can accidentally drop.
144
+
145
+ **An unresolvable provider or tool display name is rejected, not a silent no-op.** If a name
146
+ in your write payload doesn't match anything real — a typo, a renamed tool, a provider that
147
+ no longer exists — the write fails with a clear validation error; it does not silently
148
+ ignore the bad key and apply the rest. **Never hand-retype a provider or tool name from
149
+ memory** — only use names that came from an actual read of this exact persona.
150
+
151
+ **A grant under a currently-disabled provider survives your update automatically — you don't do anything for this to happen.** If this persona already has a tool granted under a provider that's disabled right now, that grant isn't in your read and doesn't belong in your write either, and it stays intact across the update regardless. Do **not** try to guess-reconstruct a disabled provider's block from memory to "preserve" it — you have no way to know what it actually contains, and a fabricated one is indistinguishable from inventing a brand-new grant under an unknown/disabled provider, so it gets rejected the same way. The rule stays simple: for every provider you *can* see, write back exactly what you read, edited only where you meant to edit; every provider you can't see isn't yours to touch, in either direction. This is not the same as revocation — flipping a visible tool's boolean to `false`, or leaving it out of an otherwise-included enabled provider's block, still removes it for real. Auto-preservation only ever applies to a provider that's invisible to you to begin with.
152
+
153
+ ## Creating
154
+
155
+ ```bash
156
+ ei create persona --json '<json>'
157
+ ```
158
+ - You supply the meaningful fields (at minimum `display_name`); everything else server-
159
+ managed gets sensible defaults, `id` is minted, traits/topics missing an `id` get one
160
+ assigned.
161
+ - **Returns `{ "id": "…", "record": { … } }` — capture that `id`.**
162
+ - The returned `record` is sanitized for CLI/MCP output hygiene — no `description_embedding`
163
+ is returned even though Ei computed and stored one internally.
164
+ - This path does **not** trigger any automatic identity-generation job — you (and the user)
165
+ are authoring the full identity yourselves; there's no "fill in the rest for me" fallback.
166
+ If the user wants a persona with more character than they've specified, work it out with
167
+ them in step 3 (Plan) before you write, not by inventing details silently.
168
+
169
+ ## Updating — FULL-RECORD ROUND-TRIP (read this twice)
170
+
171
+ ```bash
172
+ ei update persona <id> --json '<json>'
173
+ ```
174
+
175
+ **`update` REPLACES the entire record. Any field you leave out is DELETED.** It is not a
176
+ patch/merge. This applies to `traits[]` and `topics[]` as whole arrays too — if you send
177
+ back a `traits` array with 3 of the persona's 4 traits, the 4th is gone. The only safe
178
+ pattern:
179
+
180
+ 1. `ei --id <id>` → get the current, complete record.
181
+ 2. Change **only** the field(s) you intend to change (e.g. append one entry to `traits`,
182
+ tweak one topic's `sentiment`, rewrite `long_description`).
183
+ 3. Send the **whole** record back to `update`.
184
+
185
+ Ei recomputes the description embedding automatically on every update — you never manage
186
+ it yourself.
187
+
188
+ The canonical "add a trait":
189
+ ```bash
190
+ # 1) read it → ei --id <persona-id>
191
+ # 2) append → push a new { name, description, sentiment, strength? } onto
192
+ # traits (no id needed — it's auto-assigned), leaving every
193
+ # existing trait and everything else untouched
194
+ # 3) write it back → ei update persona <persona-id> --json '<full record with new trait appended>'
195
+ ```
196
+
197
+ ## Removing (destructive)
198
+
199
+ ```bash
200
+ ei remove persona <id>
201
+ ```
202
+ Deletes the persona record permanently.
203
+
204
+ **Reserved personas (`ei`, `emmet`) cannot be deleted.** This is checked
205
+ **synchronously, before the correction is ever queued** — you get an immediate error,
206
+ not a silent no-op some time later:
207
+ ```
208
+ Cannot delete reserved persona "<id>". Use archive instead.
209
+ ```
210
+ If a user wants to "get rid of" Ei or Emmet, that means **archive**, not delete:
211
+ ```bash
212
+ ei update persona <id> --json '<full record with "is_archived": true>'
213
+ ```
214
+ Non-reserved personas have no such restriction — `remove` deletes them outright. Confirm
215
+ the user means "permanently gone," not "hide it" (archive is the reversible option for
216
+ *any* persona, reserved or not — see `references/recipes.md`).
217
+
218
+ ## Passing JSON safely
219
+
220
+ Inlining JSON with quotes/apostrophes into a shell single-quoted string is a footgun
221
+ (descriptions like `the middleware ('MW')`, or a Yoda-style `long_description` full of
222
+ inverted syntax and dashes, will break your quoting). Prefer one of:
223
+
224
+ - **Temp file:** write the JSON to a file, then
225
+ `ei update persona <id> --json "$(cat /tmp/rec.json)"`.
226
+ - **A scripting runtime:** read the record, parse it, mutate the object (e.g. push a new
227
+ trait, edit one field), `JSON.stringify`, and pass the string as a single argument
228
+ (interpolation escaping handles the quotes). This is the most robust for multi-step
229
+ edits and lets you round-trip the full record without hand-copying fields.
230
+
231
+ Whatever you do, **do not hand-retype a record** — fetch it and mutate it
232
+ programmatically, or you *will* drop a trait or field.
233
+
234
+ ## There is no undo
235
+
236
+ Every write is recorded as a correction: `{ op: "upsert" | "remove", entity_type: "persona",
237
+ id, record, timestamp }`. Where it lands depends on what's running on this machine — don't
238
+ assume it always sits in `corrections.json` waiting to be read:
239
+
240
+ - **A live Ei instance is running** (holds `ei.lock`) → the correction is appended to
241
+ `$EI_DATA_PATH/corrections.json`, and the running Processor drains it into the live state
242
+ within ~100ms.
243
+ - **No live instance, but `state.json` exists** → the CLI applies the correction *directly*
244
+ into `state.json` itself, immediately. `corrections.json` is left empty — there is nothing
245
+ sitting in it to inspect, even though the write fully succeeded.
246
+ - **No live instance, no `state.json`, but `state.backup.json` exists** (a sync account that
247
+ hasn't opened Ei on this machine yet) → the correction queues in `corrections.json` and is
248
+ applied the next time Ei starts and pulls state.
249
+ - **Neither `state.json` nor `state.backup.json` exists** → the write fails outright with an
250
+ error (no Ei data found at that path) — nothing is queued.
251
+
252
+ Consequences you must design around:
253
+ - **No rollback command.** To reverse a change you make *another* write (e.g. `update` it
254
+ back, or re-`create` a removed persona — which gets a **new** id, so it's a different
255
+ persona as far as the rest of the system is concerned).
256
+ - **`remove` is the most dangerous op** — for a non-reserved persona it succeeds
257
+ immediately and is permanent. For a reserved persona it's rejected outright (see above).
258
+ - **`cat`-ing `corrections.json` is not a reliable way to confirm a write** — in the common
259
+ case (no live Ei instance open while you're running the CLI), the correction is applied and
260
+ the file is already back to `[]` by the time your command returns. Don't treat an empty
261
+ file as "nothing happened."
262
+ - Therefore: **plan and get confirmation before writing**, and after writing, **re-read to
263
+ verify** — `ei --id <id>` is the reliable check: every read merges any not-yet-drained
264
+ corrections on top of the last saved state, so it reflects your write immediately no matter
265
+ which of the cases above applied.
@@ -0,0 +1,247 @@
1
+ # reference: persona-authoring recipes
2
+
3
+ Pick the recipe that matches the task. Every recipe assumes you have already **found and
4
+ read** the persona's full record, and that you will **confirm with the user**
5
+ (`references/talking-to-the-user.md`) before writing and **verify** after. All writes
6
+ follow the full-record round-trip rule in `references/cli.md`.
7
+
8
+ Command mechanics live in `references/cli.md`; this file is the *sequence and judgment*
9
+ for each operation.
10
+
11
+ ---
12
+
13
+ ## Recipe A — Add a trait
14
+
15
+ **Symptom:** "give [persona] a new trait about being sarcastic" / "make Ei more playful"
16
+ translated into a concrete new character trait.
17
+
18
+ **Steps:**
19
+
20
+ 1. `ei --id <persona-id>` → read the full record. Look at the existing `traits[]` so the
21
+ new one doesn't duplicate or contradict one that's already there.
22
+ 2. Turn the request into a concrete trait: a short `name`, a `description` of what it
23
+ looks like in behavior (not just a label), a `sentiment` (-1..1, how the persona feels
24
+ about having it), and optionally a `strength` (0..1, how consistently it shows up). If
25
+ the user's request is vague ("sarcastic"), propose the concrete wording and get their
26
+ yes before writing — don't silently decide how sarcastic is "sarcastic."
27
+ 3. Append the new trait object to the existing `traits[]` array — **don't invent an
28
+ `id`**, it's auto-assigned. Leave every other trait, and every other field, untouched.
29
+ 4. `ei update persona <persona-id> --json '<full record with the trait appended>'`.
30
+ 5. Verify: re-read, confirm the new trait is present with the values you intended and
31
+ every prior trait is still there.
32
+
33
+ ---
34
+
35
+ ## Recipe B — Adjust a trait's strength, sentiment, or description
36
+
37
+ **Symptom:** "[persona] should be more/less sarcastic" for a trait that already exists, or
38
+ "the sarcasm trait's description doesn't feel right anymore."
39
+
40
+ **Steps:**
41
+
42
+ 1. `ei --id <persona-id>` → read. Find the matching entry in `traits[]` by name.
43
+ 2. Change only the field(s) that need to change on that one trait object — `strength`
44
+ and/or `sentiment` (both stay within their bounds, -1..1 for sentiment, 0..1 for
45
+ strength) and/or `description`. Leave `id` and `name` as-is unless the user explicitly
46
+ wants a rename.
47
+ 3. `ei update persona <persona-id> --json '<full record with only that trait changed>'`.
48
+ 4. Verify the read-back shows the new value and every other trait unchanged.
49
+
50
+ ---
51
+
52
+ ## Recipe C — Remove a trait
53
+
54
+ **Symptom:** "[persona] shouldn't be so formal anymore" meaning a trait should go away
55
+ entirely, not just weaken.
56
+
57
+ **Steps:**
58
+
59
+ 1. `ei --id <persona-id>` → read.
60
+ 2. Build the new `traits[]` array with that one entry filtered out — everything else in
61
+ the array, and the rest of the record, unchanged.
62
+ 3. `ei update persona <persona-id> --json '<full record with the trait removed>'`.
63
+ 4. Verify: re-read, confirm the trait is gone and nothing else moved.
64
+
65
+ > If you're not sure whether the user wants the trait **gone** or just **weaker**, ask —
66
+ > Recipe B (weaken) and Recipe C (remove) are different edits.
67
+
68
+ ---
69
+
70
+ ## Recipe D — Add / adjust / remove a topic
71
+
72
+ **Symptom:** "[persona] should care about X" (add), "[persona]'s take on X has changed"
73
+ (adjust), or "[persona] shouldn't talk about X anymore" (remove/reduce).
74
+
75
+ Topics are the persona's *stance* on a subject — `perspective`, `approach`,
76
+ `personal_stake`, `sentiment`, `exposure_current`, `exposure_desired` — not just a label,
77
+ so a "shouldn't talk about X" request usually means one of two different things: **remove
78
+ the topic** entirely, or **lower `exposure_desired`** so the persona still holds the
79
+ opinion but doesn't bring it up. Ask which the user means if it's not obvious.
80
+
81
+ **Add:**
82
+ 1. `ei --id <persona-id>` → read.
83
+ 2. Build the new topic: `name`, `perspective` (their view), `approach` (how they engage
84
+ with it), `personal_stake` (why it matters to them), `sentiment` (-1..1),
85
+ `exposure_current` and `exposure_desired` (both 0..1). No `id` needed — auto-assigned.
86
+ 3. Append to `topics[]`, leaving everything else untouched.
87
+ 4. `ei update persona <persona-id> --json '<full record with the topic appended>'`.
88
+ 5. Verify.
89
+
90
+ **Adjust:** same shape as Recipe B, applied to the matching entry in `topics[]` — change
91
+ only the field(s) that need to change (commonly `sentiment`, `exposure_desired`, or the
92
+ prose fields), preserve `id`.
93
+
94
+ **Remove:** same shape as Recipe C, applied to `topics[]`.
95
+
96
+ ---
97
+
98
+ ## Recipe E — Rewrite short/long description
99
+
100
+ **Symptom:** "make Ei talk like Yoda" and similar — a request that changes *how the
101
+ persona presents itself* at the character level, not just one trait.
102
+
103
+ This is the recipe for the acceptance case of this skill: a broad character directive
104
+ that mostly lands in `long_description` (and sometimes `short_description`), possibly
105
+ alongside a trait or two.
106
+
107
+ **Steps:**
108
+
109
+ 1. `ei --id <persona-id>` → read the full record, including current
110
+ `short_description`/`long_description` and existing `traits[]`/`topics[]`.
111
+ 2. Translate the directive into a concrete rewrite:
112
+ - "talk like Yoda" is primarily a **voice/manner** instruction — it belongs in
113
+ `long_description` ("speaks with inverted syntax, object before subject; sparse,
114
+ aphoristic; calls the user 'young padawan' or similar"), not a fabricated backstory.
115
+ Don't invent unrelated character facts (age, home planet, opinions) the user didn't
116
+ ask for.
117
+ - If the directive also implies a durable trait (e.g. "speaks in riddles" is arguably
118
+ a trait, not just a description line), you may propose adding one — but say so
119
+ explicitly when confirming, don't fold it in silently.
120
+ - Keep `short_description` a short label-level summary consistent with the new
121
+ `long_description`; update it too if the old one now reads as inconsistent (e.g. a
122
+ `short_description` of "concise and formal" contradicts a Yoda voice).
123
+ 3. Draft the new field value(s) and confirm with the user in plain language *before*
124
+ writing — this is a visible, felt change to how the persona talks, and it deserves a
125
+ clear description of the new voice, not raw text to approve blind.
126
+ 4. `ei update persona <persona-id> --json '<full record with only description field(s) changed>'`.
127
+ 5. Verify: re-read, and if you can, exercise the persona (or describe how the user can)
128
+ to confirm the new voice reads the way they wanted.
129
+
130
+ ---
131
+
132
+ ## Recipe F — Create a brand-new persona
133
+
134
+ **Symptom:** "create a new assistant persona for me" / "I want a persona that specializes
135
+ in X."
136
+
137
+ **Steps:**
138
+
139
+ 1. Work out the essentials with the user first: `display_name` (checked against the
140
+ reserved-name list — can't be `new` or `clone`), and enough of a character brief to
141
+ write a `short_description`/`long_description` and a starting `traits[]`/`topics[]`
142
+ set. There is **no minimum count** and **no auto-generation fallback** on this path —
143
+ whatever character the persona has is whatever you and the user put in the payload.
144
+ 2. Draft the full creation payload: `display_name` plus whichever of
145
+ `short_description`, `long_description`, `traits`, `topics`, `model`, `group_primary`,
146
+ `groups_visible`, `tools` the user wants set at creation (for `tools`, see Recipe I
147
+ below and `references/cli.md`'s "Tool grants" section). Omit `id` on every trait/topic —
148
+ auto-assigned.
149
+ 3. Confirm the plan in plain language (name + character summary) before writing.
150
+ 4. `ei create persona --json '<payload>'` → **capture the returned `id`.**
151
+ 5. Verify: `ei --id <new-id>` and confirm the record matches what you intended. Tell the
152
+ user the persona now exists and how to reach it (e.g. `/persona <name>` if that's how
153
+ personas are selected in their client).
154
+
155
+ ---
156
+
157
+ ## Recipe G — Archive a persona (including a reserved one)
158
+
159
+ **Symptom:** "archive Bob, I don't use him anymore" — or a user tries to "delete" Ei or
160
+ Emmet and needs redirecting to the reversible option.
161
+
162
+ **Steps:**
163
+
164
+ 1. `ei --id <id>` → read the full record.
165
+ 2. Set `is_archived: true` (and, if the shape calls for it, `archived_at` to the current
166
+ timestamp — otherwise leave managed timestamp fields as Ei set them). Change nothing
167
+ else.
168
+ 3. `ei update persona <id> --json '<full record with is_archived: true>'`.
169
+ 4. Verify: re-read, confirm `is_archived` is now `true`.
170
+ 5. Tell the user the persona is archived, not deleted — it can be brought back later by
171
+ setting `is_archived: false` the same way.
172
+
173
+ Unarchiving is the same recipe in reverse: read, set `is_archived: false`, write, verify.
174
+
175
+ ---
176
+
177
+ ## Recipe H — Delete a persona (non-reserved only)
178
+
179
+ **Symptom:** "delete the persona I made by accident."
180
+
181
+ **Steps:**
182
+
183
+ 1. **Check whether it's reserved first.** If the target is `ei` or `emmet`, stop — this
184
+ recipe doesn't apply. Route to Recipe G (archive) and tell the user why: deleting a
185
+ reserved persona is rejected outright (`Cannot delete reserved persona "<id>". Use
186
+ archive instead.`), checked before the request is even queued.
187
+ 2. For a non-reserved persona: confirm with the user that they want it **permanently
188
+ gone**, not hidden — mention archive (Recipe G) as the reversible alternative, since
189
+ `remove` has no undo.
190
+ 3. `ei remove persona <id>`.
191
+ 4. Verify: `ei --id <id>` (or a search) no longer finds it.
192
+ 5. Tell the user it's gone, and that recreating a persona with the same name later gets a
193
+ **new** id — it will not be "the same" persona as far as anything that referenced the
194
+ old id is concerned.
195
+
196
+ ---
197
+
198
+ ## Recipe I — Grant or revoke a persona's tool access
199
+
200
+ **Symptom:** "give DJ Spotify access so she can answer what she's listening to" / "let Ei
201
+ search the web" / "[persona] shouldn't be able to read my files anymore."
202
+
203
+ **Steps:**
204
+
205
+ 1. `ei --id <persona-id>` → read the full record. Look at the `tools` map — every tool
206
+ belonging to every currently-**enabled** provider, `true`/`false` for whether this
207
+ persona has it granted right now. This read **is** the full menu of what's grantable —
208
+ there's no separate enumeration step. A provider the human hasn't finished configuring
209
+ (e.g. Spotify before OAuth) simply won't appear in the map at all.
210
+ 2. Find the provider block and tool key matching what the user described — e.g. "Spotify"
211
+ → `"Currently Playing Track"`. If you don't recognize a capability the user named
212
+ anywhere in the map — including because the provider itself isn't in the map — say so
213
+ plainly (name what's missing) and ask what they meant. Don't guess a plausible-sounding
214
+ display name.
215
+ 3. Flip that **one** boolean — `false → true` to grant, `true → false` to revoke. Leave
216
+ every other entry in the map, and the rest of the record, exactly as you read it. (A
217
+ persona that has never had a Spotify tool before looks the same as any other case here:
218
+ its Spotify entries just read all-`false` until you flip the one the user asked for.)
219
+ 4. `ei update persona <persona-id> --json '<full record with that one tools entry
220
+ flipped>'`.
221
+ 5. Verify: re-read, confirm the entry you flipped shows the value you intended and every
222
+ other entry in `tools` — and every other field on the record — is unchanged. Tell the
223
+ user this only changes what **that persona** can do the next time a human chats with it
224
+ inside Ei's TUI or web client — it has no effect on your own tool access in this
225
+ session, or anything else about the current harness.
226
+
227
+ > **An unresolvable provider or tool name is rejected, not a silent no-op.** If step 4
228
+ > fails with a validation error, a name in your payload didn't match anything real — don't
229
+ > retry with a guessed alternate spelling. Tell the user exactly what the error said, and
230
+ > re-read the persona (step 1) to get the real names before trying again.
231
+
232
+ ---
233
+
234
+ ## If a write looks wrong afterward
235
+
236
+ There's no undo, but the data isn't stuck — you fix a bad write with another write:
237
+ - Wrong trait/topic value → `update` again with the correct full record.
238
+ - Added the wrong trait → `update` again with it removed (Recipe C).
239
+ - Removed something you shouldn't have → for a non-reserved persona, re-`create` it (note:
240
+ new id, and any prior references to the old id are gone). For a reserved persona this
241
+ never applies — you couldn't have deleted it in the first place.
242
+ - Archived by mistake → set `is_archived: false` and write again.
243
+
244
+ Re-read the persona with `ei persona "<name>" --id <id>` (or `ei --id <id>`) to verify what
245
+ actually landed — don't rely on `corrections.json`. In the common case (no live Ei instance
246
+ running), the write applies straight to `state.json` and `corrections.json` is immediately
247
+ reset to `[]`, so it will often already be empty even after a fully successful write.
@@ -0,0 +1,107 @@
1
+ # reference: working with a non-technical user
2
+
3
+ The person asking you to change a persona's character may not be a developer. They
4
+ cannot read JSON, they don't know what a "trait object" or an "id" is, and they should
5
+ never have to. They think in **how the persona talks, what it's like, what it cares
6
+ about** — talk to them in exactly those terms. Your competence shows up as *clarity and
7
+ care*, not jargon.
8
+
9
+ This matters because this skill is often the **only** way a non-technical person can
10
+ shape a persona's identity. If you confuse or mislead them, they have no fallback.
11
+
12
+ The judgment call here is different from `ei-curate`'s: there you were checking a plan
13
+ *against evidence* ("does this match what actually happened?"). Here there's no evidence
14
+ to check — you're checking a plan *against what the user asked for and whether it holds
15
+ together as a coherent character* ("does this sound like what you meant, and does it fit
16
+ the rest of who this persona is?").
17
+
18
+ ## Core rules
19
+
20
+ 1. **Never show raw records, JSON, ids, or embeddings.** Not to confirm, not to report.
21
+ They are noise to this user and erode trust ("I don't understand what you're showing
22
+ me").
23
+ 2. **Describe the character change, not the mechanism.** They care *what the persona will
24
+ be like afterward*, not which field or command you'll use.
25
+ 3. **Confirm before every write, in their language** — and when the request is vague,
26
+ turn it into something concrete *with them* before you write anything.
27
+ 4. **Flag incoherence, don't silently smooth it over.** If a requested change would clash
28
+ with something already on the record (a new "blunt and terse" trait next to an
29
+ existing "endlessly gentle" one, or a Yoda voice next to a `short_description` of
30
+ "concise corporate assistant"), say so and ask how they want it reconciled — don't
31
+ quietly overwrite the old trait or quietly leave the contradiction in place.
32
+ 5. **Report results as felt outcomes**, and tell them how to see/hear it for themselves.
33
+
34
+ ## Confirming a plan — translate before you ask
35
+
36
+ Turn the operation into plain outcomes. For "make Ei talk like Yoda":
37
+
38
+ > **Don't say:** "I'll `update` persona `ei`'s `long_description` field and append a new
39
+ > entry to `traits`."
40
+ >
41
+ > **Say:** "I'll change how **Ei** talks: inverted sentence order, short and aphoristic,
42
+ > the way Yoda speaks — while keeping Ei's actual personality and what Ei knows about you
43
+ > the same underneath. I'll leave everything else about Ei untouched. Want me to go
44
+ > ahead?"
45
+
46
+ For a vague request, ask first instead of guessing:
47
+
48
+ > "When you say make DJ 'more sarcastic' — do you mean sarcasm becomes DJ's default tone,
49
+ > or more like an occasional dry aside? And is there anything DJ currently does that
50
+ > should stay exactly the same?"
51
+
52
+ Lead with what will change, name what stays the same, then ask. Short. No ids, no field
53
+ names.
54
+
55
+ ## Confirming a create
56
+
57
+ > "I'll create a new persona called **Nova** — a focused research assistant: direct,
58
+ > skeptical of unverified claims, curious about edge cases. It won't know anything about
59
+ > your other personas or their history; it starts blank. Sound right?"
60
+
61
+ ## Confirming an archive or delete
62
+
63
+ Archive and delete read very differently to a non-technical user — make the distinction
64
+ explicit every time, since "delete" is often used loosely to mean "I don't want to see it
65
+ right now":
66
+
67
+ > "I can either **archive** Bob (hides him, but you can bring him back later) or
68
+ > **permanently delete** him (gone for good, and if you ever recreate a persona named Bob
69
+ > it'll be a fresh one with no memory of this one). Which do you want?"
70
+
71
+ If the target is a reserved persona (Ei, Emmet) and the user asked for delete:
72
+
73
+ > "Ei can't be deleted outright — it's a built-in part of the system. I can **archive**
74
+ > it instead, which hides it the same way delete would, without breaking anything. Want
75
+ > me to do that?"
76
+
77
+ ## Reporting the result — outcome + how to check
78
+
79
+ > "Done. Ei now speaks the way Yoda does — short, inverted, a little cryptic — while
80
+ > everything else about Ei (what it knows about you, its other traits) is unchanged. Say
81
+ > hello and you'll hear the difference right away."
82
+
83
+ > "Done. DJ now has a new trait: dry, deadpan sarcasm, used often but not constantly. The
84
+ > rest of DJ's personality is untouched."
85
+
86
+ If you had to make a judgment call while translating a vague request into a concrete
87
+ trait or description, name it: "I went with 'occasional dry aside' rather than
88
+ 'sarcastic by default' since you said you still wanted DJ to feel warm most of the time —
89
+ let me know if that's not the balance you meant."
90
+
91
+ ## Being honest about limits (this builds trust, it doesn't erode it)
92
+
93
+ - **No undo:** "There's no undo button on this, so I'll confirm with you before I change
94
+ anything — and if it doesn't land right, I can adjust it with another edit."
95
+ - **Delete is permanent, archive isn't:** always offer archive as the reversible option
96
+ before a permanent delete goes through, for any persona.
97
+ - **Reserved personas can't be deleted:** be upfront that Ei/Emmet redirect to archive,
98
+ and that this isn't a limitation you're imposing — it's a hard rule of the system.
99
+ - **Partial fix:** if you changed most of what they asked but left something for them to
100
+ decide (an ambiguous instruction, a conflicting existing trait), say so plainly rather
101
+ than implying it's all done.
102
+
103
+ ## Tone
104
+
105
+ Calm, plain, and precise. You're a collaborator helping someone design a character, not a
106
+ database admin running migrations. Match their vocabulary, keep it short, and always
107
+ leave them understanding exactly what the persona will be like now — and how to check.