aegis-desktop 0.8.5 → 0.8.7

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 (54) hide show
  1. package/README.md +15 -25
  2. package/docs/demo.gif +0 -0
  3. package/docs/marketing-assets/01-model-class-picker.png +0 -0
  4. package/docs/marketing-assets/02-answer-complete.png +0 -0
  5. package/docs/marketing-assets/03-tool-approval-diff.png +0 -0
  6. package/docs/marketing-assets/README.md +89 -0
  7. package/docs/screenshot.png +0 -0
  8. package/lib/avatar/companion.js +2 -2
  9. package/lib/avatar/cosmetics.js +2 -2
  10. package/lib/avatar/events.js +2 -2
  11. package/lib/avatar/holder-turn.js +6 -7
  12. package/lib/avatar/holders.js +6 -7
  13. package/lib/avatar/identity.js +7 -7
  14. package/lib/avatar/level.js +1 -1
  15. package/lib/avatar/mirror.js +6 -7
  16. package/lib/avatar/packs/README.md +1 -1
  17. package/lib/avatar/persona.js +5 -5
  18. package/lib/avatar/profile.js +13 -14
  19. package/lib/avatar/register.js +2 -2
  20. package/lib/avatar/store.js +6 -6
  21. package/lib/avatar/transfer.js +1 -2
  22. package/lib/avatar/turn.js +4 -4
  23. package/lib/local/agents.js +1 -1
  24. package/lib/local/autonomous.js +70 -3
  25. package/lib/local/engine.js +19 -9
  26. package/lib/local/entitlement.js +487 -0
  27. package/lib/local/git-scope.js +1 -1
  28. package/lib/local/prompt.js +3 -3
  29. package/lib/local/queue.js +2 -2
  30. package/lib/local/shell.js +1 -1
  31. package/lib/local/telemetry.js +467 -0
  32. package/lib/local/tools.js +2 -2
  33. package/lib/local/turn-guard.js +1 -1
  34. package/lib/local/worktree-lock.js +1 -1
  35. package/lib/settings.js +36 -3
  36. package/main.js +189 -9
  37. package/package.json +5 -8
  38. package/preload.js +34 -0
  39. package/renderer/app.js +319 -40
  40. package/renderer/avatar/assemble.js +1 -1
  41. package/renderer/avatar/avatar.css +2 -2
  42. package/renderer/avatar/avatar.js +5 -5
  43. package/renderer/avatar/cards.js +1 -2
  44. package/renderer/avatar/hud.js +1 -2
  45. package/renderer/avatar/machine.js +1 -3
  46. package/renderer/avatar/pane.js +3 -3
  47. package/renderer/avatar/parts.js +4 -4
  48. package/renderer/budget.js +2 -2
  49. package/renderer/index.html +123 -55
  50. package/renderer/style.css +91 -7
  51. package/renderer/transcript-view.js +1 -1
  52. package/renderer/usage.js +6 -6
  53. package/vendor/aegis.js +146 -2
  54. package/vendor/credentials.js +4 -0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # AEGIS Desktop
2
2
 
3
- ![AEGIS Desktop running: the three-class model picker, an agent turn, a gated tool-approval diff, and the unattended work queue](https://raw.githubusercontent.com/aegisinfo/aegiscode-plugin/main/desktop/docs/demo.gif)
3
+ ![AEGIS Desktop running: the three-class model picker, an agent turn, a gated tool-approval diff, and the unattended work queue](docs/demo.gif)
4
4
 
5
5
  A standalone Electron chat app over the [AEGIS](https://aegiscloud.org) API,
6
6
  with an **agentic tool loop**: the model can read, write, and edit files,
@@ -9,9 +9,8 @@ delegate whole sub-tasks to subagents. It does **not** require Claude Code.
9
9
 
10
10
  ## Install
11
11
 
12
- Download a build from the
13
- [releases page](https://github.com/aegisinfo/aegiscode-plugin/releases) — the
14
- repo the release workflow publishes binaries to — or install from npm:
12
+ Prebuilt binaries (AppImage / MSI+NSIS / dmg) ship with each release, and the
13
+ package is on npm:
15
14
 
16
15
  ```bash
17
16
  npm install -g aegis-desktop # requires Node 18+
@@ -114,16 +113,16 @@ stub and refuse to publish an asset that fails their assertions.
114
113
 
115
114
  **The model-class picker** — all three routes, switchable mid-conversation:
116
115
 
117
- ![The model-class picker listing Aegis Cloud, Bring your own key, and Local](https://raw.githubusercontent.com/aegisinfo/aegiscode-plugin/main/docs/marketing-assets/01-model-class-picker.png)
116
+ ![The model-class picker listing Aegis Cloud, Bring your own key, and Local](docs/marketing-assets/01-model-class-picker.png)
118
117
 
119
118
  **A completed answer** in the transcript:
120
119
 
121
- ![A completed answer in the transcript](https://raw.githubusercontent.com/aegisinfo/aegiscode-plugin/main/docs/marketing-assets/02-answer-complete.png)
120
+ ![A completed answer in the transcript](docs/marketing-assets/02-answer-complete.png)
122
121
 
123
122
  **The tool-call approval card** with a proposed edit — what blocks `exec`,
124
123
  `writeFile` and `editFile` until you allow, allow for the session, or deny:
125
124
 
126
- ![The tool-call approval card with a proposed edit](https://raw.githubusercontent.com/aegisinfo/aegiscode-plugin/main/docs/marketing-assets/03-tool-approval-diff.png)
125
+ ![The tool-call approval card with a proposed edit](docs/marketing-assets/03-tool-approval-diff.png)
127
126
 
128
127
  ## Tools available to the model
129
128
 
@@ -156,7 +155,7 @@ aegiscode autonomous add "fix the flaky retry test" --cwd ~/repo --commit
156
155
  aegiscode autonomous list
157
156
  aegiscode autonomous run # drain one task, then stop
158
157
  aegiscode autonomous proceed --max 3 # drain up to three
159
- aegiscode autonomous reconcile --auto # queue the next unfinished PLAN.md phase, then drain it
158
+ aegiscode autonomous reconcile --auto # queue the next unfinished phase of your plan file, then drain it
160
159
  aegiscode autonomous retry <id> # put a finished task back
161
160
  aegiscode autonomous clear --all # empty the queue
162
161
  ```
@@ -278,17 +277,12 @@ The app registers an `aegis://` protocol handler:
278
277
 
279
278
  ## Run from source
280
279
 
281
- ```bash
282
- git clone https://github.com/aegiscloud/aegiscode-desktop.git
283
- cd aegiscode-desktop
284
- npm install
285
- npm start
286
- ```
287
-
288
- Or, from the monorepo checkout, run this directory directly:
280
+ A source checkout is available to contributors with repository access only — the
281
+ desktop host is not a public repository. From the checkout, run this directory
282
+ directly:
289
283
 
290
284
  ```bash
291
- cd aegiscode-plugin/desktop
285
+ cd desktop
292
286
  npm install
293
287
  npm start
294
288
  ```
@@ -328,11 +322,7 @@ transport over the same AEGIS backend — see the repo root for that fuller
328
322
  architecture picture, and [cli/README.md](../cli/README.md) for the terminal
329
323
  host over the same engine.
330
324
 
331
- **Two repos, one product.** This directory is the source of truth. The
332
- standalone repo at
333
- [aegiscloud/aegiscode-desktop](https://github.com/aegiscloud/aegiscode-desktop)
334
- is a `git subtree split` of it — same code, published separately so it can be
335
- cloned and built on its own. Edits land here and are synced out; nothing is
336
- authored there. The npm package
337
- [`aegis-desktop`](https://www.npmjs.com/package/aegis-desktop) is built from
338
- that standalone repo.
325
+ **One source of truth.** This directory is it. The npm package
326
+ [`aegis-desktop`](https://www.npmjs.com/package/aegis-desktop) is built from a
327
+ `git subtree split` of it — same code, published without the rest of the
328
+ monorepo. Edits land here and are synced out; nothing is authored elsewhere.
package/docs/demo.gif ADDED
Binary file
@@ -0,0 +1,89 @@
1
+ # Marketing assets — captions, claim, and tagged links
2
+
3
+ The three PNGs in this directory are the **existing** asset kit (1440×900, shot
4
+ against the real app — see `docs/marketing-log.md`, 2026-09-22 entry). Images
5
+ carry no link of their own, so the outbound link for each placement lives here,
6
+ in the same directory as the image it belongs to. Nothing in this directory is a
7
+ new asset: these are the captions for the three files below, rewritten for the
8
+ Phase 5 promotion pass (`docs/upgrade-conversion-plan.md` §5).
9
+
10
+ **The one claim (Phase 3, verbatim, same sentence as the welcome panel):**
11
+
12
+ > **The autonomous queue keeps running after you close the laptop.**
13
+
14
+ Plus the honesty line that sits next to it:
15
+
16
+ > Everything on your machine is free, permanently. The cloud drain is what you
17
+ > pay for.
18
+
19
+ (`desktop/renderer/app.js` → `PAID_PROMISE` / `FREE_HONESTY`, rendered by
20
+ `#welcome-template` in `desktop/renderer/index.html`.)
21
+
22
+ **What the caption must not be:** a tour of the three model classes, or the free
23
+ local row written up as if it were the product. The screenshots show the picker
24
+ and the diff card — that is *what the image is of*, not the *offer*. One claim
25
+ per asset; the class list stays out of the caption.
26
+
27
+ **Channel rule.** Facebook and Reddit are image-friendly; X and YouTube captions
28
+ follow `docs/launch-copy-x-youtube.md` §2/§3. Every link is
29
+ `https://aegiscloud.org/go/upgrade?s=<channel>&c=<campaign>` with `s` in
30
+ `(x, youtube, reddit, facebook)` — the only channels `SIGNUP_SOURCES` in
31
+ `aegis1/app.py` accepts — and one campaign id per channel, stable across every
32
+ asset on that channel.
33
+
34
+ | Image | Placement | Channel `s` | Campaign `c` | Tagged link (paste this exact URL) |
35
+ |---|---|---|---|---|
36
+ | `01-model-class-picker.png` | X post (picker + claim in the reply) | `x` | `x_launch` | `https://aegiscloud.org/go/upgrade?s=x&c=x_launch` |
37
+ | `01-model-class-picker.png` | Reddit image post (r/LocalLLaMA, r/ollama — link in the closing block, after the limitations) | `reddit` | `reddit_launch` | `https://aegiscloud.org/go/upgrade?s=reddit&c=reddit_launch` |
38
+ | `01-model-class-picker.png` | Facebook Page + groups (image + claim + link) | `facebook` | `facebook_launch` | `https://aegiscloud.org/go/upgrade?s=facebook&c=facebook_launch` |
39
+ | `02-answer-complete.png` | YouTube Short / long-form B-roll; `Upgrade:` line in the description | `youtube` | `yt_launch` | `https://aegiscloud.org/go/upgrade?s=youtube&c=yt_launch` |
40
+ | `02-answer-complete.png` | X post (answer in flight) | `x` | `x_launch` | `https://aegiscloud.org/go/upgrade?s=x&c=x_launch` |
41
+ | `03-tool-approval-diff.png` | X pinned post / thread 3- (the diff card) | `x` | `x_launch` | `https://aegiscloud.org/go/upgrade?s=x&c=x_launch` |
42
+ | `03-tool-approval-diff.png` | Reddit GIF+text post (r/ChatGPTCoding) | `reddit` | `reddit_launch` | `https://aegiscloud.org/go/upgrade?s=reddit&c=reddit_launch` |
43
+ | `03-tool-approval-diff.png` | YouTube thumbnail / description | `youtube` | `yt_launch` | `https://aegiscloud.org/go/upgrade?s=youtube&c=yt_launch` |
44
+
45
+ Every row above is registered in `docs/marketing-log.md` §7 so post-hoc revenue
46
+ can be joined to the channel and the asset face that produced it.
47
+
48
+ ## Captions
49
+
50
+ ### `01-model-class-picker.png` — the class picker
51
+
52
+ > The autonomous queue keeps running after you close the laptop. Everything on
53
+ > your machine is free, permanently — the cloud drain is what you pay for.
54
+ > npm i -g aegis-desktop
55
+ > https://aegiscloud.org/go/upgrade?s=x&c=x_launch
56
+
57
+ Facebook variant (same claim, same tagged shape, different channel):
58
+
59
+ > The autonomous queue keeps running after you close the laptop. Cloud-backed
60
+ > drain, survives a reboot, fans a task out across workers, memory carried
61
+ > between tasks. Everything on your machine is free, permanently.
62
+ > npm i -g aegis-desktop
63
+ > https://aegiscloud.org/go/upgrade?s=facebook&c=facebook_launch
64
+
65
+ Do **not** caption this image with the picker's three rows as the pitch. The
66
+ picker is a fact about the product; the claim above is the offer.
67
+
68
+ ### `02-answer-complete.png` — an answer, complete
69
+
70
+ > The autonomous queue keeps running after you close the laptop — the work does
71
+ > not stop when the laptop does. Everything on your machine is free, permanently.
72
+ > Install: `npm i -g aegis-desktop && aegis`
73
+ > https://aegiscloud.org/go/upgrade?s=youtube&c=yt_launch
74
+
75
+ ### `03-tool-approval-diff.png` — the diff/approval card
76
+
77
+ > The diff card is the whole product locally — and it is free, permanently. The
78
+ > pocket-book item is the drain that keeps running after you close the laptop.
79
+ > Install: `npm i -g aegis-desktop && aegis`
80
+ > https://aegiscloud.org/go/upgrade?s=x&c=x_launch
81
+
82
+ ## Before posting (§0 of `docs/marketing-plan-social.md`)
83
+
84
+ 1. Re-verify the version (`desktop/package.json`) and re-shoot the image if the
85
+ UI moved; log that check in `docs/marketing-log.md` §6.
86
+ 2. Confirm the link still resolves and still carries `s=`/`c=`. A bare
87
+ `aegiscloud.org/subscribe` (bare, untagged) link files as `direct` and the channel
88
+ gets no row.
89
+ 3. Never invent a number in a caption (marketing-log §2 rule 1).
Binary file
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * companion.js — the four companion behaviours (PLAN Phase 23, spec §4).
4
+ * companion.js — the four companion behaviours.
5
5
  *
6
6
  * The behaviours are the morning brief, the idea log, the session reflection
7
7
  * and in-session recall. What they have in common is not their content, it is
@@ -42,7 +42,7 @@
42
42
  * checkable: nothing here may be a shell, a network call or anything that sends
43
43
  * data off the device, and `carveOuts()` is the assertion that says so.
44
44
  *
45
- * Pure: no `fs`, no Electron, no globals. See `docs/avatar-plan.md` §2 and §4.
45
+ * Pure: no `fs`, no Electron, no globals.
46
46
  */
47
47
 
48
48
  const level = require('./level.js');
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * cosmetics.js — the cosmetics pack format and its validator (Phase 24).
4
+ * cosmetics.js — the cosmetics pack format and its validator.
5
5
  *
6
6
  * WHY A FORMAT AT ALL. Spec §3 says the avatar is *data, not art*: a persona is
7
7
  * one JSON record and a look is a layered SVG assembled from a part manifest, so
@@ -196,7 +196,7 @@ function validatePack(raw) {
196
196
  for (const where of forbidden) {
197
197
  errors.push(
198
198
  `cosmetics packs may not carry \`${where}\` — a pack sells LOOKS only, never XP, ` +
199
- 'levels, recall breadth, approval friction or personalization (docs/avatar-plan.md §3)',
199
+ 'levels, recall breadth, approval friction or personalization',
200
200
  );
201
201
  }
202
202
 
@@ -17,7 +17,7 @@
17
17
  * that throws on an unrecognised signal turns every future feature into a
18
18
  * crash in the avatar, so the default transition is "stay put".
19
19
  *
20
- * The states are the ones `docs/avatar-plan.md` §4 lists, wired to signals that
20
+ * The states are wired to signals that
21
21
  * already exist: turn start/stream/end, `tool.*` from the tool host, the
22
22
  * approval card, queue progress, and sync status. No new event bus.
23
23
  *
@@ -175,7 +175,7 @@ const TABLE = Object.freeze({
175
175
 
176
176
  /**
177
177
  * Expression id per state. The renderer maps these to the persona's expression
178
- * pack; keeping the mapping here means Phase 22 needs no state knowledge.
178
+ * pack; keeping the mapping here means the renderer needs no state knowledge.
179
179
  */
180
180
  const EXPRESSIONS = Object.freeze({
181
181
  idle: 'neutral',
@@ -1,13 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * holder-turn.js — the seam where a holder reaches turn assembly (PLAN Phase 28;
5
- * docs/avatar-identity-plan.md §3, §4, §7).
4
+ * holder-turn.js — the seam where a holder reaches turn assembly.
6
5
  *
7
6
  * `register.js` turns a persona into a bounded tone fragment, `profile.js` folds
8
7
  * a holder's memory into evidence-gated facets, and `holders.js` owns the
9
8
  * directory and the isolation filter. This file is the only place they meet, and
10
- * it is where the one claim Phase 28 is really about gets decided:
9
+ * it is where the one claim this file is really about gets decided:
11
10
  *
12
11
  * **Holder A's assembled prompt cannot contain holder B's text.**
13
12
  *
@@ -22,10 +21,10 @@
22
21
  * - *The recall block.* The same filter runs on the recall rows, so an entry
23
22
  * that arrives after the wrong cache still cannot be quoted into A's turn.
24
23
  *
25
- * The Phase 28 test drives both through `assemble()` — the function main.js
24
+ * The test drives both through `assemble()` — the function main.js
26
25
  * actually calls — and then through `engine.js` with this file's prompt builder
27
26
  * injected, grepping the system prompt a scripted transport received. That is
28
- * the Phase 18 lesson (never assert on a harness leg that was re-pointed) turned
27
+ * the lesson (never assert on a harness leg that was re-pointed) turned
29
28
  * on privacy: the interesting failure is a filter applied after the wrong cache,
30
29
  * and only the real path can show it.
31
30
  *
@@ -157,7 +156,7 @@ function profileFragment(fold, opts = {}) {
157
156
  * rows. Exported because main.js renders it into the memory pane as well, and
158
157
  * one implementation is one thing to get right.
159
158
  *
160
- * `opts.header` exists for the one caller that has no holder: Phase 20's turn
159
+ * `opts.header` exists for the one caller that has no holder: the account-level turn
161
160
  * assembly recalls the account's memory before any holder exists, and captioning
162
161
  * that block "this holder's memory" would be a lie told in the prompt. The
163
162
  * default stays the holder header, so every holder-scoped caller is unchanged.
@@ -253,7 +252,7 @@ function assemble(input = {}) {
253
252
  entryIds: scoped.map((row) => String(row && row.id)).filter(Boolean),
254
253
  recallIds: recall.entryIds,
255
254
  // Rows the recall block could not fit. Facet drops have their own field, so
256
- // neither the usage accounting nor the Phase 28 assertions have to guess
255
+ // neither the usage accounting nor the assertions have to guess
257
256
  // which kind of "dropped" this is.
258
257
  dropped: recall.dropped,
259
258
  neutralizations: profile.neutralizations + recall.neutralizations + registerFragment.neutralizations,
@@ -1,11 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * holders.js — who holds the memory (PLAN Phase 28; docs/avatar-identity-plan.md
5
- * §3, §4, §5).
4
+ * holders.js — who holds the memory.
6
5
  *
7
- * `identity.js` (Phase 25) decides *which* holder a set of signals points at and
8
- * refuses to merge two people on a fingerprint match. `profile.js` (Phase 25)
6
+ * `identity.js` decides *which* holder a set of signals points at and
7
+ * refuses to merge two people on a fingerprint match. `profile.js`
9
8
  * folds one holder's entries into bounded, evidence-gated facets. Neither of
10
9
  * them owns a directory. This file is that owner — the layout, the registry, the
11
10
  * switch, the conflict decision and the "why does it know this" surface — and it
@@ -32,7 +31,7 @@
32
31
  * is the client-side filter §3 calls "the assertion that the fix happened":
33
32
  * the server-side scope is the fix, and this is what proves it ran. It is
34
33
  * used by the fold *and* by the recall block, so a leak needs to defeat one
35
- * function rather than two call sites — and the Phase 28 test drives two
34
+ * function rather than two call sites — and the test drives two
36
35
  * holders through the real turn assembly and greps the prompt.
37
36
  *
38
37
  * 4. **Every claim is traceable and deletable.** `provenance()` renders a
@@ -63,7 +62,7 @@ const PROFILE_FILE = 'profile.json';
63
62
  const FORGOTTEN_FILE = 'forgotten.json';
64
63
 
65
64
  /**
66
- * The one thing a switch may leave behind. A list, not a sentence: the Phase 28
65
+ * The one thing a switch may leave behind. A list, not a sentence: the
67
66
  * test iterates it, so adding a survivor to the code without adding it here
68
67
  * makes the test fail rather than the promise quietly rot.
69
68
  */
@@ -445,7 +444,7 @@ function applyConflict(registry, resolution, choice = {}) {
445
444
  * particular and therefore to the holder asking, which is what makes a legacy
446
445
  * unstamped ledger readable instead of invisible.
447
446
  *
448
- * `clientFilter:false` exists only so the Phase 28 isolation test can prove the
447
+ * `clientFilter:false` exists only so the isolation test can prove the
449
448
  * filter is load-bearing by removing it and watching the leg go red. It is not
450
449
  * reachable from any IPC handler (holder-ipc.js never passes it).
451
450
  */
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * identity.js — whose memory it is.
5
5
  *
6
- * `docs/avatar-plan.md` made memory the value; value is meaningless
6
+ * Memory is the value; value is meaningless
7
7
  * unattributed. This module answers exactly one question — *which holder is at
8
8
  * the keyboard* — from signals the caller already has, and it answers with a
9
9
  * **proposal plus provenance**, never with a merge.
@@ -38,7 +38,7 @@
38
38
  * 4. **Resolution is pure and never writes.** `holders.json` is validated,
39
39
  * repaired and copied; the input registry is never mutated, and the fields
40
40
  * `proposal`/`conflict`/`needsMint` exist precisely so main writes the file
41
- * with the user's confirmation (Phase 26) instead of this module guessing.
41
+ * with the user's confirmation instead of this module guessing.
42
42
  *
43
43
  * Pure: no fs, no Electron, no crypto import — the caller supplies `hash` so
44
44
  * this file is testable in plain Node exactly like `lib/sync/*`.
@@ -149,10 +149,10 @@ function looksLikeRawKey(value) {
149
149
  *
150
150
  * Deterministic on purpose — the same machine seed produces the same id on a
151
151
  * reinstall, which is what keeps "the ledger survives a reinstall"
152
- * (`avatar-plan.md` §1.5) true for an install that never had a cloud key.
152
+ * true for an install that never had a cloud key.
153
153
  *
154
154
  * @returns {string|null} `null` when there is nothing trustworthy to mint from;
155
- * the caller then falls back to random bytes (main, Phase 26).
155
+ * the caller then falls back to random bytes.
156
156
  */
157
157
  function mintHolderId(hash, seed) {
158
158
  if (typeof hash !== 'function') return null;
@@ -424,7 +424,7 @@ function setActive(current, id) {
424
424
  return validate(base);
425
425
  }
426
426
 
427
- /** Phase 29 «forget»: drop one row, and re-point `active` if it was that row. */
427
+ /** «forget»: drop one row, and re-point `active` if it was that row. */
428
428
  function removeHolder(current, id) {
429
429
  const base = validate(current).registry;
430
430
  const wanted = holderIdShape(id);
@@ -475,7 +475,7 @@ const CONFIDENCE = Object.freeze({
475
475
  * `conflict.a` is what you would be switching away from (the registry's
476
476
  * active holder, or the losing signal's holder) and `conflict.b` is the
477
477
  * proposal. Returned so main can show a "who is at the keyboard?" card
478
- * (Phase 28) instead of switching silently.
478
+ * instead of switching silently.
479
479
  */
480
480
  function resolve(signals, registry) {
481
481
  const raw = signals && typeof signals === 'object' ? signals : {};
@@ -519,7 +519,7 @@ function resolve(signals, registry) {
519
519
  // A fingerprint that matches no registry row proves nothing about who this
520
520
  // is, so it can never *select* a holder. It can only be proposed for
521
521
  // binding onto the holder that WAS resolved, which main writes only after
522
- // the user confirms (Phase 26) — this module never writes anything. Built
522
+ // the user confirms — this module never writes anything. Built
523
523
  // here rather than in the last rung so every rung reports it identically:
524
524
  // an explicit persona import with a freshly rotated key is the same
525
525
  // question as a fresh install with one.
@@ -31,7 +31,7 @@
31
31
  * they too cannot widen write/shell/network approval.
32
32
  *
33
33
  * Pure: no fs, no Electron, no process access except the `env` the caller
34
- * passes. See `docs/avatar-plan.md` §2.
34
+ * passes.
35
35
  */
36
36
 
37
37
  const { TIERS, tierFor, tierIndex } = require('./tiers.js');
@@ -1,11 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * mirror.js — the per-holder cloud mirror (PLAN Phase 29;
5
- * docs/avatar-identity-plan.md §6, extending docs/avatar-plan.md §1.5).
4
+ * mirror.js — the per-holder cloud mirror.
6
5
  *
7
- * Phase 20 put the ledger on disk, Phase 25 made a profile foldable from it and
8
- * Phase 28 gave every row a holder. All three are local. This file is the half
6
+ * Earlier work put the ledger on disk, made a profile foldable from it and
7
+ * gave every row a holder. All three are local. This file is the half
9
8
  * that leaves the machine, and it exists to make four claims true — each of
10
9
  * which is a test in `desktop/test/avatar-mirror.test.mjs`, not a paragraph:
11
10
  *
@@ -22,8 +21,8 @@
22
21
  * `reconcile()` unions two sides by a content-addressed row id, deduplicates
23
22
  * a row both machines already have, and sorts by `(t, id)`. Two machines
24
23
  * that appended in opposite orders converge on the *same bytes*, and
25
- * `derive()`'s order independence (docs/avatar-plan.md §1) makes that the
26
- * same level. The Phase 29 ordering test feeds both interleavings and
24
+ * `derive()`'s order independence makes that the
25
+ * same level. The ordering test feeds both interleavings and
27
26
  * asserts one level; the idempotence leg asserts feeding the union back in
28
27
  * cannot double a level, which is what would actually hurt.
29
28
  *
@@ -337,7 +336,7 @@ function readMirror(memoryRows, holderId) {
337
336
  * trusted as a conclusion — `payload.profile`/`payload.facets` are dropped
338
337
  * explicitly and recorded in `refused`. The fold itself is `profile.fold()`
339
338
  * with `holder` supplied, so a facet can only cite an entry id that survived
340
- * the holder filter (Phase 25's pure-seam isolation) *and* exists in this
339
+ * the holder filter *and* exists in this
341
340
  * input (the `citable` set inside the fold).
342
341
  */
343
342
  function refold(input = {}) {
@@ -23,7 +23,7 @@ paid.
23
23
  ## The rule this directory exists to enforce
24
24
 
25
25
  **Cosmetics only.** Never XP, levels, recall breadth, approval friction or
26
- personalization (`docs/avatar-plan.md` §3, invariant 8). `validatePack` refuses a
26
+ personalization. `validatePack` refuses a
27
27
  pack carrying `xp`, `level`, `recallEntries`, `approvals`, `toolGrants`,
28
28
  `modelFloor`, `proactivity`, `price`, `entitlement`, … at any depth, and refuses
29
29
  a `tier` that contradicts the unlock table — so a "paid pack that also widens a
@@ -7,7 +7,7 @@
7
7
  * that is a settings pane with hand-written branches for every option, which
8
8
  * makes a new outfit a code change and a commissioned art pack a fork. The way
9
9
  * this module does it: a persona is one JSON record, validated and coerced
10
- * here, and Phase 22's renderer assembles layered SVG from a part manifest. A
10
+ * here, and the renderer assembles layered SVG from a part manifest. A
11
11
  * theme is then a file. Zero code changes.
12
12
  *
13
13
  * Validation is REPAIR-ORIENTED, not reject-oriented, with exactly two hard
@@ -23,20 +23,20 @@
23
23
  * it is the user's own file.
24
24
  *
25
25
  * The one place that IS adversarial is the `register` block and the identity
26
- * strings, because they end up in the system prompt (Phase 21). Those are
26
+ * strings, because they end up in the system prompt. Those are
27
27
  * sanitized here at the storage boundary — control characters and newlines are
28
28
  * stripped so a name cannot break out of its line — and the *prompt* fragment
29
29
  * is additionally bounded and neutralized in `register.js`. Two layers, because
30
30
  * a name is user-authored text going into a prompt.
31
31
  *
32
- * Pure: no fs, no Electron. Phase 21's main process reads/writes the file.
32
+ * Pure: no fs, no Electron. The main process reads/writes the file.
33
33
  */
34
34
 
35
35
  /** Bump when the record shape changes; `migrate` walks old records forward. */
36
36
  const SCHEMA_VERSION = 1;
37
37
 
38
38
  /**
39
- * The part manifest. Ids only — geometry lives in `renderer/avatar/` (Phase 22),
39
+ * The part manifest. Ids only — geometry lives in `renderer/avatar/`,
40
40
  * which reads these same ids. Validation is against THIS list so a typo in a
41
41
  * hand-edited persona is caught at load rather than rendering an empty head.
42
42
  */
@@ -82,7 +82,7 @@ function has(list, value) {
82
82
 
83
83
  /**
84
84
  * Collapse whitespace and strip control characters. A name is a single line of
85
- * text in a UI, and (Phase 21) a single line in a prompt — so newlines, tabs and
85
+ * text in a UI, and a single line in a prompt — so newlines, tabs and
86
86
  * the unicode line separators are removed rather than escaped.
87
87
  */
88
88
  function cleanText(value, max) {
@@ -1,8 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * profile.js — what *this holder's* memory is allowed to personalise (Phase 25,
5
- * identity plan §4).
4
+ * profile.js — what *this holder's* memory is allowed to personalise.
6
5
  *
7
6
  * `identity.js` answers "whose memory is it"; this module answers the question
8
7
  * that follows immediately: given that holder's own rows, what may the avatar
@@ -36,21 +35,21 @@
36
35
  * Absence of evidence produces no facet: never "probably prefers terse
37
36
  * answers".
38
37
  * 4. COLD START IS NEUTRAL. No evidence ⇒ `coldStart: true`, `facets: []`,
39
- * `budget.used: 0`. Phase 27 renders that as an empty fragment, i.e. an
38
+ * `budget.used: 0`. The renderer shows that as an empty fragment, i.e. an
40
39
  * avatar that behaves exactly like an unconfigured install. It does not
41
- * guess a personality, the same honesty rule as `avatar-plan.md` §1.4.
40
+ * guess a personality.
42
41
  * 5. DETERMINISTIC. No clock (`now` is an argument), no randomness, no map
43
42
  * iteration order leaking into output: facets are ordered by
44
43
  * `(confidence desc, lastSeen desc, id asc)` and, for two facets of the
45
44
  * same class on the same day, `key asc` — one extra term so the order is
46
45
  * total and a permutation of the input cannot change the output bytes.
47
- * 6. BOUNDED. The assembled facets are capped at `BUDGET_TOKENS` (900, per
48
- * §Phase 27), dropped whole, lowest priority first — never sliced
46
+ * 6. BOUNDED. The assembled facets are capped at `BUDGET_TOKENS` (900),
47
+ * dropped whole, lowest priority first — never sliced
49
48
  * mid-sentence, which is the failure mode a naive `slice()` produces.
50
49
  *
51
50
  * Pure: no fs, no Electron, no clock, no crypto. It reuses `persona.cleanText`
52
51
  * (the storage boundary's sanitizer) and `register.estimateTokens` (the same
53
- * 4-chars-per-token ceiling Phase 21 uses) so the two prompt fragments Phase 27
52
+ * 4-chars-per-token ceiling used elsewhere) so the two prompt fragments the renderer
54
53
  * concatenates are measured with one ruler.
55
54
  */
56
55
 
@@ -73,7 +72,7 @@ const FACET_IDS = Object.freeze([
73
72
  'toneCalibration',
74
73
  ]);
75
74
 
76
- /** §Phase 27: the profile fragment's ceiling. */
75
+ /** The profile fragment's ceiling. */
77
76
  const BUDGET_TOKENS = 900;
78
77
 
79
78
  /** Longest a single facet `value` may be (it is UI text and prompt text). */
@@ -360,7 +359,7 @@ function orderFacets(facets) {
360
359
  /**
361
360
  * Take facets in priority order while they fit. Whole facets only: a fragment
362
361
  * that ends mid-sentence reads as a truncated thought, which is worse than a
363
- * shorter one, and §Phase 27 asserts exactly this.
362
+ * shorter one, and the test asserts exactly this.
364
363
  */
365
364
  function applyBudget(ordered, limit = BUDGET_TOKENS) {
366
365
  const kept = [];
@@ -384,12 +383,12 @@ function applyBudget(ordered, limit = BUDGET_TOKENS) {
384
383
  /**
385
384
  * Does this row belong to the holder being folded?
386
385
  *
387
- * Unstamped rows belong to whoever is asking: they predate Phase 26's stamping
386
+ * Unstamped rows belong to whoever is asking: they predate the stamping
388
387
  * and the alternative — dropping them — would silently empty the profile of
389
388
  * every existing install. A row stamped for *someone else* is never folded, and
390
389
  * that is the pure-seam half of §3's "isolation is the load-bearing property":
391
390
  * `sources` built downstream can only cite ids that survived this filter. The
392
- * real-seam half (Phase 28) asserts it again through actual turn assembly,
391
+ * real-seam half asserts it again through actual turn assembly,
393
392
  * because a filter applied after the wrong cache is exactly the bug a
394
393
  * pure-function test misses.
395
394
  */
@@ -844,9 +843,9 @@ if (Object.keys(FOLDERS).length !== FACET_IDS.length || FACET_IDS.some((id) => t
844
843
  * @param {object} input
845
844
  * @param {string} [input.holder] the holder these rows belong to. Supplying
846
845
  * it is how a caller states *whose* memory this is: rows stamped for another
847
- * holder are dropped here and can therefore never be cited by a facet. Phase
848
- * 26 always passes it (the registry's `active`); omitting it means "the caller
849
- * already scoped this input", which is why Phase 28 asserts the same property
846
+ * holder are dropped here and can therefore never be cited by a facet. The
847
+ * caller always passes it (the registry's `active`); omitting it means "the caller
848
+ * already scoped this input", which is why the test asserts the same property
850
849
  * again at the real turn-assembly seam.
851
850
  * @param {Array} [input.entries] memory entries for this holder, aegis1's
852
851
  * row shape: `{ id, content, role, tags: [], session, createdAt, … }`.
@@ -2,7 +2,7 @@
2
2
 
3
3
  /**
4
4
  * register.js — the persona's register turned into ONE bounded system-prompt
5
- * fragment (Phase 21; shared by Phase 27's profile fragment).
5
+ * fragment.
6
6
  *
7
7
  * `persona.js` is the storage boundary: it sanitizes identity/register text on
8
8
  * the way to disk. This is the SECOND layer, and the one that matters at the
@@ -38,7 +38,7 @@
38
38
 
39
39
  const personaModule = require('./persona.js');
40
40
 
41
- /** ~300 tokens, per docs/avatar-plan.md §3. */
41
+ /** ~300 tokens. */
42
42
  const MAX_FRAGMENT_TOKENS = 300;
43
43
 
44
44
  /**
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * store.js — the main process's half of level authority (Phase 20).
4
+ * store.js — the main process's half of level authority.
5
5
  *
6
- * Phase 19 shipped the avatar core as five pure modules with no IO at all:
6
+ * The avatar core shipped as five pure modules with no IO at all:
7
7
  * `xp.js` knows what an event is worth and how a ledger replays into a level,
8
8
  * `level.js` knows what a level buys, but neither can read a file or a clock.
9
9
  * This file is the missing half and it deliberately owns *only* the two things
@@ -51,11 +51,11 @@ const personaModule = require('./persona.js');
51
51
  const LEDGER_DIR = 'avatar';
52
52
  const LEDGER_FILE = 'ledger.jsonl';
53
53
  /**
54
- * `<userData>/avatar/persona.json` — the persona DOCUMENT (Phase 21).
54
+ * `<userData>/avatar/persona.json` — the persona DOCUMENT.
55
55
  *
56
56
  * It lives here, next to the ledger and not in settings.json, because a persona
57
57
  * is a document rather than a preference: it is imported, exported, hand-edited
58
- * and eventually synced per holder (Phase 26), and every one of those paths
58
+ * and eventually synced per holder, and every one of those paths
59
59
  * wants validation on the way in. The `__avatar` settings namespace holds only
60
60
  * the three switches (lib/settings.js), so a renderer can never write a persona
61
61
  * through the settings surface.
@@ -206,7 +206,7 @@ function createAvatarStore({ dir, now = () => Date.now(), env = process.env, log
206
206
  *
207
207
  * `award()` above is the internal path: its callers are hooks that already
208
208
  * know which kind they are recording. This is the externally reachable one
209
- * (Phase 21's `aegis:avatarAward`), so it is the one that must not be a
209
+ * (`aegis:avatarAward`), so it is the one that must not be a
210
210
  * generic "write a line into the ledger" bridge: an unknown kind is refused
211
211
  * with a reason rather than written as a zero-XP line, which keeps the ledger
212
212
  * a record of priced events only. `xp` is never accepted from the caller —
@@ -226,7 +226,7 @@ function createAvatarStore({ dir, now = () => Date.now(), env = process.env, log
226
226
  return { ok: true, kind: name, xp: xp.xpFor(name), entry };
227
227
  }
228
228
 
229
- // --- persona document (Phase 21) ----------------------------------------
229
+ // --- persona document ----------------------------------------
230
230
  // The persona is read and written ONLY here, by main. Both directions run
231
231
  // through persona.js validation, so a hand-edited or imported file is
232
232
  // repaired with warnings rather than trusted, and the identity/register text
@@ -1,8 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * transfer.js — persona export, persona import and per-holder forget
5
- * (PLAN Phase 29; docs/avatar-identity-plan.md §5 and §7.3).
4
+ * transfer.js — persona export, persona import and per-holder forget.
6
5
  *
7
6
  * A persona is taste. Memory is not. This file exists to keep those two apart
8
7
  * across the one boundary where they would otherwise merge — moving a persona