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.
- package/README.md +4 -0
- package/package.json +2 -1
- package/skills/coding-harness-reflect/SKILL.md +230 -0
- package/skills/ei-curate/SKILL.md +174 -0
- package/skills/ei-curate/references/cli.md +160 -0
- package/skills/ei-curate/references/provenance.md +88 -0
- package/skills/ei-curate/references/recipes.md +144 -0
- package/skills/ei-curate/references/talking-to-the-user.md +71 -0
- package/skills/ei-persona/SKILL.md +238 -0
- package/skills/ei-persona/references/cli.md +265 -0
- package/skills/ei-persona/references/recipes.md +247 -0
- package/skills/ei-persona/references/talking-to-the-user.md +107 -0
- package/src/cli/README.md +72 -2
- package/src/cli/commands/personas.ts +46 -1
- package/src/cli/corrections-endpoints.ts +297 -0
- package/src/cli/corrections-writer.ts +138 -0
- package/src/cli/install.ts +252 -157
- package/src/cli/mcp.ts +80 -1
- package/src/cli/persona-corrections.ts +442 -0
- package/src/cli/retrieval.ts +46 -2
- package/src/cli.ts +148 -1
- package/src/core/corrections.ts +233 -0
- package/src/core/handlers/human-extraction.ts +8 -2
- package/src/core/handlers/human-matching.ts +2 -2
- package/src/core/llm-client.ts +7 -1
- package/src/core/orchestrators/human-extraction.ts +1 -0
- package/src/core/persona-tools.ts +92 -0
- package/src/core/personas/opencode-agent.ts +1 -3
- package/src/core/processor.ts +113 -1
- package/src/core/state/human.ts +10 -0
- package/src/core/state/personas.ts +8 -0
- package/src/core/state-manager.ts +11 -0
- package/src/core/types/entities.ts +1 -0
- package/src/core/utils/identifier-utils.ts +3 -2
- package/src/integrations/pi/importer.ts +142 -50
- package/src/integrations/pi/reader.ts +1 -0
- package/src/integrations/pi/types.ts +4 -0
- package/src/storage/file-lock.ts +120 -0
- package/tui/README.md +2 -0
- package/tui/src/commands/provider.tsx +1 -1
- package/tui/src/components/WelcomeOverlay.tsx +3 -3
- package/tui/src/context/ei.tsx +14 -0
- package/tui/src/index.tsx +13 -1
- package/tui/src/storage/file.ts +15 -83
- package/tui/src/util/instance-lock.ts +3 -2
- package/tui/src/util/provider-detection.ts +4 -2
- 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.
|