@zerwiz/ymir 0.1.7 → 0.1.9

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.
@@ -142,6 +142,75 @@ bin/ymir-migrate.sh apply # carry it
142
142
 
143
143
  Stop the runtime first if you want no stale pid files; nothing is lost either way.
144
144
 
145
+ ## Operating it afterwards — the doors, the cloth, and the shapes
146
+
147
+ ### The doors (the CLI), named for the figure who does the work
148
+
149
+ `npm install -g @zerwiz/ymir` puts **two** commands on PATH: `ymir` (the front
150
+ door) and `ymir-install` (the bare installer). Everything else the operator
151
+ needs is a door on `ymir` — never a script path inside `node_modules`.
152
+
153
+ ```
154
+ doors[14]{verb,door,what}:
155
+ "ymir","first setup (a bare call installs)","the plan, then consent, then the work"
156
+ "ymir install [...]","bin/ymir-install.sh","the same, said out loud"
157
+ "ymir plan","bin/ymir-plan.sh","what an install would do here — writes nothing"
158
+ "ymir raise / lower","scripts/start.sh · stop.sh","lift the hall, or lay it down"
159
+ "ymir eir","bin/eir-doctor.sh","what stands, and mend what does not (the healer)"
160
+ "ymir groa","bin/groa-update.sh","take the latest, then mend this home forward"
161
+ "ymir groa migrate","bin/ymir-migrate.sh","heal this home's structure"
162
+ "ymir heimdall","bin/ymir-setup-auth.sh","the way in — status · set · github (the guardian)"
163
+ "ymir invite","bin/ymir-invite.sh","let someone else in — mint · list · revoke"
164
+ "ymir smidja","bin/smidja-board.sh","the smithy's board on :8437 — build · start · stop · status"
165
+ "ymir hlidskjalf","scripts/electron.sh start --view hlidskjalf","the high seat's window"
166
+ "ymir sessrumnir","scripts/electron.sh start --view sessrumnir","the seat-hall's window"
167
+ "ymir mimir","bin/mimir.sh","the memory well"
168
+ "ymir sense","bin/host-sense.sh","what THIS machine is"
169
+ ```
170
+
171
+ A name the law has not given a home is still answered, once, with the name that
172
+ has it: `ymir doctor` → *the door is named `ymir eir` now*. Colour appears only
173
+ where a human watches.
174
+
175
+ ### The cloth — `bin/ymir-style.sh`
176
+
177
+ Ymir had correct output and no design. The cloth is cut from the same stone as
178
+ the halls (`midgard/design-system/tokens.css`): **bone** `#cfc3a9` for words,
179
+ **bronze** `#c9973f` for what acts, **steel** `#96a0a8` for what stands, **blood**
180
+ `#c2584a` for what is wrong, and a mark per state (`◆` do · `·` already · `—`
181
+ fact · `✕` blocked · `?` needs your word · `✓` proved).
182
+
183
+ ```
184
+ cloth_rules[4]{rule,why}:
185
+ "colour and marks only when stderr is a terminal and NO_COLOR is unset","a pipeline never parses a decoration"
186
+ "data on stdout, the human rendering on stderr","the TOON row is the data; the coloured line is for the eye"
187
+ "the words are never conditional — only the colour is","hiding information to save colour is the wrong trade"
188
+ "no banner over four lines, no rule longer than its text","density first (Monoline TUI · cli-guidelines)"
189
+ ```
190
+
191
+ `bin/ymir-plan.sh --colour` renders the plan in the cloth on stderr while the
192
+ TOON stays on stdout. The installer prints it at the consent, and ends with the
193
+ next steps — *a reaction for every action, a next step for every ending*.
194
+
195
+ ### Two libs the packaged tree needs
196
+
197
+ ```
198
+ libs[2]{path,resolves}:
199
+ "bin/smidja-lib.sh","the smithy: apps/smidja-factory in a clone, node_modules/@zerwiz/smidja-factory in a package — `smidja_factory_dir`, `smidja_visualizer_dir`"
200
+ "bin/electron-lib.sh","whether a shell's runtime VERIFIES: `electron_runtime_state` (ok · partial · absent) and the exact remedy"
201
+ ```
202
+
203
+ **Why they exist.** A packaged tree has no `apps/`: the smithy arrives as a
204
+ dependency and the skill symlink dangles. Four call sites assumed the clone's
205
+ layout, so the visualizer read as unbuildable in a package. They resolve through
206
+ `bin/smidja-lib.sh` now.
207
+
208
+ **And a shell is never declared ready on a directory's presence.** npm gates
209
+ install scripts; a skipped Electron postinstall leaves `dist/` partial and
210
+ `path.txt` unwritten, so the web app builds, every check passes, and the window
211
+ never opens. `electron_runtime_state` verifies, and the plan's `electron` row
212
+ says *the runtime is PARTIAL* with the command that mends it.
213
+
145
214
  ## The app packages — how the four surfaces arrive
146
215
 
147
216
  The distro depends on the surfaces as their own npm packages, so one
@@ -354,6 +423,11 @@ apply` **moves private data**, so the step chain runs it only on a real run;
354
423
  a preview leaves the home exactly as it found it. (It used to apply them even
355
424
  under `--check`, which moved a home during a "report only" pass.)
356
425
 
426
+ `--check` writes nothing — and that includes the migrations. `bin/ymir-migrate.sh
427
+ apply` **moves private data**, so the step chain runs it only on a real run;
428
+ a preview leaves the home exactly as it found it. (It used to apply them even
429
+ under `--check`, which moved a home during a "report only" pass.)
430
+
357
431
  ## The operator's way in (auth) — `bin/ymir-setup-auth.sh`
358
432
 
359
433
  A fresh checkout seeds **no credential**, so the gate has no way in until the
package/CHANGELOG.md CHANGED
@@ -1,4 +1,260 @@
1
1
 
2
+ ## 2026-09-17 — 0.1.9: both shapes reach the registry
3
+
4
+ - **`ymir raise` works on a packaged install.** 0.1.8 shipped before the resolver,
5
+ so a user who installed from npm hit `cd …/apps/hlidskjalf: No such file or
6
+ directory` — every package had arrived, and eighteen scripts were looking in the
7
+ clone's `apps/`. `bin/app-lib.sh` resolves a surface in either shape now.
8
+ - **A clone and a package are one tree to the scripts.** Proven against both: the
9
+ five surfaces resolve from `/home/zerwiz/Ymir` and from an npm-installed
10
+ package, and `ymir raise` against a packaged tree fetches the app's
11
+ dependencies, raises the gate API, starts Nornir cron, and brings up Bifrost and
12
+ the well.
13
+ - **The patience words** — `style_patience` — open the installer's long chain and
14
+ the raise path's build, so a long hour says what it is doing.
15
+
16
+ ## 2026-09-17 — both shapes: a clone and an npm install, and the patience words
17
+
18
+ - **`ymir raise` failed on a packaged install** — and the reason was ours, not
19
+ npm's. Every package had arrived; the scripts looked for the apps in the clone's
20
+ `apps/`, where a package keeps nothing. `scripts/start.sh` died on
21
+ `cd …/apps/hlidskjalf: No such file or directory`. **Eighteen files assumed the
22
+ clone's layout.**
23
+ - **One resolver, both shapes:** `bin/app-lib.sh` — `apps/<surface>` in a clone,
24
+ `node_modules/@zerwiz/<package>` in a package, with the surface→package map
25
+ (`smidja` → `@zerwiz/smidja-factory`). Wired into the raise path, the windows,
26
+ the invite door, the seat-hall, Eir, the icons, the placement, the hall snapshot
27
+ and the installer's own SPA and shell steps. `bin/smidja-lib.sh` delegates to it
28
+ now, so there is one truth about where things live.
29
+ - **Proven in both trees**: all five surfaces resolve from `/home/zerwiz/Ymir`
30
+ (a clone) and from an npm-installed package. And `ymir raise` run against the
31
+ packaged tree no longer dies — it fetches the app's dependencies, raises the
32
+ gate API, **starts Nornir cron**, and brings up Bifrost and the well.
33
+ - **A trap worth naming:** `printf -v <name>` writes to the *function's* scope, so
34
+ a helper whose scratch variable shares the caller's requested name swallows the
35
+ answer. Every scratch name in the resolvers is function-prefixed for that
36
+ reason.
37
+ - **The patience words.** A long install should say what it is doing and why it
38
+ takes a while. `style_patience` — the cloth's own line — now opens the
39
+ installer's long chain and the raise path's build:
40
+ *much moves · the halls are being set right · this hour is long, and nothing of
41
+ yours is lost in it — roots come home, shapes are re-cut, names are set true
42
+ again. Your patience is noted, and it is earned.*
43
+
44
+ ## 2026-09-17 — 0.1.8: the day's work reaches npm
45
+
46
+ - **The doors.** `ymir` now carries the operator's verbs, each named for the
47
+ figure who does the work: `raise` · `lower` · `eir` (heal) · `groa` (renew) ·
48
+ `heimdall` (the way in) · `invite` · `smidja` (the board) · `hlidskjalf` ·
49
+ `sessrumnir` · `mimir` · `sense` · `plan`. Before this, the package put two
50
+ commands on PATH and neither could raise the app or the board.
51
+ - **The cloth.** `bin/ymir-style.sh` — colour and marks cut from the halls' own
52
+ tokens (bone · bronze · steel · blood), shown only where a human watches, with
53
+ the data left as TOON on stdout. The plan, the installer, the validate report
54
+ and Eir all wear it.
55
+ - **The shapes.** `bin/smidja-lib.sh` tells a clone's smithy from a packaged one;
56
+ `bin/smidja-board.sh` is the board's own door (`ymir smidja`), built and served
57
+ from an npm install; `bin/electron-lib.sh` verifies a shell's runtime so a
58
+ skipped npm install script cannot pass as a launch.
59
+ - **The icons.** One truth where three maps disagreed, and no claim of a glyph
60
+ nobody drew: Óðrerir wears ansuz, Sessrúmnir othala, the smithy's icon is named
61
+ for the smithy.
62
+ - **The repair.** `4dd7273`'s hand-merge left `bin/ymir-install.sh` and
63
+ `scripts/start.sh` unparseable on main; both restored. Every runtime script
64
+ parses again.
65
+
66
+ ## 2026-09-17 — main is repaired, and the report wears the cloth
67
+
68
+ - **Main was broken and nobody had noticed.** A merge resolved by hand
69
+ (`4dd7273`, from a parallel branch) kept *both* sides of a conflict in
70
+ `bin/ymir-install.sh` and `scripts/start.sh`: a stray `<`, a duplicated step
71
+ line, orphaned comments, an `if` with no `fi`. The installer could not parse at
72
+ all — `bash -n` failed on main — and PR #53's merge carried the breakage
73
+ forward. Both files are restored from the last revision that parses (`5b7fc66`),
74
+ with the two fixes that revision predated re-applied: the `--phase` value is
75
+ consumed, and `--yes | --non-interactive | --accept-all-defaults` are one thing.
76
+ Every runtime script parses again; the installer's plan runs.
77
+ - **The lesson worth keeping:** a merge is not a place to guess. When both sides
78
+ of a conflict are real, the resolution is a decision — and `bash -n` on every
79
+ runtime script is cheap enough to be part of it.
80
+ - **The validate report wears the cloth** — the same rows rendered for the eye on
81
+ stderr (marks, colour, and a verdict with the next step) while the TOON stays
82
+ the data on stdout. It found real drift while it was being written: the SPA down
83
+ on `:3888`, Nornir cron stopped, `sessrumnir`, `mcp` and `hoard` broken.
84
+ - **Eir watches the shells.** A new surface reports the desktop shells' runtime
85
+ through `bin/electron-lib.sh` — a *partial* Electron runtime is exactly the
86
+ quiet failure Eir exists for — and her rows render in the cloth too.
87
+
88
+ ## 2026-09-17 — the house gains an unpublish door
89
+
90
+ - **`bin/npm-publish.sh --unpublish @scope/name@<version>`.** Publishing had a
91
+ door in this house and unpublishing had none, so the act was done by hand on a
92
+ machine whose `~/.npmrc` holds a stale token — the very reason the door exists.
93
+ The new mode resolves the token from the hoard exactly as publishing does,
94
+ demands a spec that names the version (a whole package is not a one-word act),
95
+ supports `--dry-run`, and says plainly that the registry's CDN may serve the
96
+ tarball for a while after the removal.
97
+ - **The window is 72 hours.** npm allows one version back within 72 hours of its
98
+ publication; past that only npm support can remove it. The help text says so,
99
+ because a door that does not name its own clock invites a late knock.
100
+ - **`@zerwiz/ymir@0.1.5` was unpublished** — the version that carried the memory
101
+ well. Publishing the clean 0.1.7 moves the `latest` tag off the tainted build at
102
+ once, which is the half of the repair that does not wait for propagation.
103
+
104
+ ## 2026-09-17 — the plan learns the smithy's package name, and the install forwards its flags
105
+
106
+ Two defects the first real end-to-end npm install surfaced — which is what an
107
+ end-to-end test is for.
108
+
109
+ - **A surface's name is not its package's name.** The plan looked for
110
+ `apps/smidja` or `@zerwiz/smidja`, but npm serves the smithy as
111
+ `@zerwiz/smidja-factory` — so the row read `BLOCKED` on a machine where the
112
+ smithy was installed and standing. `app_dir`/`app_row` now take the surface
113
+ name and the package name separately, and the row tells the truth: installed,
114
+ with the visualizer's UI still to build (the tarball ships its source, never a
115
+ stale build).
116
+ - **`ymir install --plan` forwarded only `--json`.** `--phase` and `--blocked`
117
+ were dropped on the way through the installer's door, so `--blocked` answered
118
+ *unknown flag*. Every plan flag is forwarded now.
119
+
120
+ **The install this came out of:** `npm install -g @zerwiz/ymir` from the registry
121
+ — `@zerwiz/ymir@0.1.7`, 331 packages, and all four surfaces
122
+ (`hlidskjalf` · `odrerir` · `sessrumnir` · `smidja-factory`) arrived inside the
123
+ distro. The tarball carries no memory store — the leak is closed in the artefact
124
+ as well as in the repo.
125
+
126
+ ## 2026-09-17 — the package shipped the memory well (one version, now excluded)
127
+
128
+ - **The exposure.** `@zerwiz/ymir@0.1.5` carries five files that are nobody's but
129
+ the operator's: `.agents/memory/kaia.engram`, its `-shm` and `-wal` sidecars, and
130
+ `.agents/memory/well/{episodes,workspace}.jsonl`. A public npm tarball contained
131
+ the live memory well — the store and the episodes. Verified by fetching the
132
+ published artefact and listing it; `0.1.0`–`0.1.4` are clean, and this branch's
133
+ build is clean.
134
+ - **The cause, and it is a trap worth naming.** `.gitignore` excludes
135
+ `.agents/memory/kaia.engram*` and `.agents/memory/well/*.jsonl`, and the repo is
136
+ clean — but **npm does not consult `.gitignore` when `files[]` names a whole
137
+ directory.** `files: [".agents/"]` packs that subtree *including* the ignored
138
+ files. Nothing warned: the leak travelled in the artefact, not the repo.
139
+ - **The fix.** The manifest excludes them explicitly, because a `files[]` list
140
+ cannot rely on the ignore file it overrides:
141
+ `!.agents/memory/kaia.engram*`, `!.agents/memory/well/*.jsonl`,
142
+ `!.agents/memory/*.db`, plus `!**/__pycache__/` and `!**/*.pyc` for the compiled
143
+ junk that was travelling the same way. Verified: `npm pack --dry-run` carries
144
+ 864 files and **no** memory store — the memory README and the ledger's scaffold
145
+ header are all that remain, as they should be.
146
+ - **The rule for every future package:** a `files[]` entry that names a directory
147
+ overrides `.gitignore` for everything beneath it. Name what ships, or exclude
148
+ what must not — and verify with a dry-run pack, never by assumption.
149
+ - **Remediation.** `@zerwiz/ymir@0.1.5` should be unpublished (it is inside npm's
150
+ window); the clean build publishes as 0.1.7 and supersedes it.
151
+
152
+ ## 2026-09-17 — the doors, the cloth, and the icons that were not there
153
+
154
+ Four things the operator met and could not use: doors that never opened, output
155
+ with no design, a board that could not be built from a package, and an icon map
156
+ that named a glyph nobody had drawn.
157
+
158
+ - **The doors are named for the figure who does the work.** `ymir` put two
159
+ commands on PATH and could neither start the app nor raise the board. It now
160
+ carries the doors: **raise · lower · eir** (heal) · **groa** (renew) ·
161
+ **heimdall** (the way in) · **invite** · **smidja** (the board) ·
162
+ **hlidskjalf** · **sessrumnir** · **mimir** · **sense** · **plan**. A name the
163
+ law has not given a home — `doctor`, `validate`, `auth` — still answers, once,
164
+ with the name that has it.
165
+ - **The cloth: `bin/ymir-style.sh`.** Ymir had correct output and no design. The
166
+ palette is cut from the halls' own tokens — bone for words, bronze for what
167
+ acts, steel for what stands, blood for what is wrong — with a mark per state
168
+ (`◆ · — ✕ ? ✓`). Colour and marks appear only where a human is watching
169
+ (stderr a TTY, `NO_COLOR` unset); the data on stdout stays TOON, and **the
170
+ words are never conditional, only the colour is**. The plan renders in the
171
+ cloth on stderr while the TOON stays pipeable; the installer opens with it and
172
+ ends with the next steps.
173
+ - **The packaged tree is told apart from a clone.** `bin/smidja-lib.sh` resolves
174
+ the smithy — `apps/smidja-factory` in a clone, `node_modules/@zerwiz/smidja-factory`
175
+ in a package — and four call sites that assumed the clone now resolve through
176
+ it. `bin/smidja-board.sh` is the board's own door (`build · start · stop ·
177
+ status`), which `ymir smidja` runs; built and served end to end from an npm
178
+ install: `{"ok":true,"sessions":1}`, `GET / → 200`.
179
+ - **A shell's runtime is verified, never assumed.** npm gates install scripts, so
180
+ a skipped Electron postinstall leaves a partial runtime that still builds the
181
+ web app and still reports success. `bin/electron-lib.sh` answers `ok · partial
182
+ · absent`, the plan's `electron` row says **PARTIAL** with the exact remedy, and
183
+ `step_desktop` refuses to claim a launch it cannot make.
184
+ - **The icons now name glyphs that exist.** Three maps disagreed: `runes.md` gave
185
+ Óðrerir the rune `wunjo` — **a glyph nobody had drawn** — and tinted Sessrúmnir
186
+ with a violet that is not in the tokens; `icons.md` gave `othala` to Óðrerir
187
+ *and* Sessrúmnir; `design-icon.sh` minted `valhalla` for Óðrerir. One truth now:
188
+ **Óðrerir → ansuz** (Odin's breath, the mead of poetry — what Óðrerir *is*),
189
+ Sessrúmnir → othala, Valhalla keeps `ᚹ` (drawn in `valhalla.svg`, named Wunjo),
190
+ Sowilo no longer claimed twice, every tint a house accent from the tokens, and
191
+ the smithy's icon named `ymir-smidja` rather than `ymir-visualizer`. All 22
192
+ glyphs parse; every claim resolves to a file. `docs/design.md` stopped calling
193
+ a living map *"to create"*.
194
+
195
+ ## 2026-09-17 — the house gains an unpublish door
196
+
197
+ - **`bin/npm-publish.sh --unpublish @scope/name@<version>`.** Publishing had a
198
+ door in this house and unpublishing had none, so the act was done by hand on a
199
+ machine whose `~/.npmrc` holds a stale token — the very reason the door exists.
200
+ The new mode resolves the token from the hoard exactly as publishing does,
201
+ demands a spec that names the version (a whole package is not a one-word act),
202
+ supports `--dry-run`, and says plainly that the registry's CDN may serve the
203
+ tarball for a while after the removal.
204
+ - **The window is 72 hours.** npm allows one version back within 72 hours of its
205
+ publication; past that only npm support can remove it. The help text says so,
206
+ because a door that does not name its own clock invites a late knock.
207
+ - **`@zerwiz/ymir@0.1.5` was unpublished** — the version that carried the memory
208
+ well. Publishing the clean 0.1.7 moves the `latest` tag off the tainted build at
209
+ once, which is the half of the repair that does not wait for propagation.
210
+
211
+ ## 2026-09-17 — the plan learns the smithy's package name, and the install forwards its flags
212
+
213
+ Two defects the first real end-to-end npm install surfaced — which is what an
214
+ end-to-end test is for.
215
+
216
+ - **A surface's name is not its package's name.** The plan looked for
217
+ `apps/smidja` or `@zerwiz/smidja`, but npm serves the smithy as
218
+ `@zerwiz/smidja-factory` — so the row read `BLOCKED` on a machine where the
219
+ smithy was installed and standing. `app_dir`/`app_row` now take the surface
220
+ name and the package name separately, and the row tells the truth: installed,
221
+ with the visualizer's UI still to build (the tarball ships its source, never a
222
+ stale build).
223
+ - **`ymir install --plan` forwarded only `--json`.** `--phase` and `--blocked`
224
+ were dropped on the way through the installer's door, so `--blocked` answered
225
+ *unknown flag*. Every plan flag is forwarded now.
226
+
227
+ **The install this came out of:** `npm install -g @zerwiz/ymir` from the registry
228
+ — `@zerwiz/ymir@0.1.7`, 331 packages, and all four surfaces
229
+ (`hlidskjalf` · `odrerir` · `sessrumnir` · `smidja-factory`) arrived inside the
230
+ distro. The tarball carries no memory store — the leak is closed in the artefact
231
+ as well as in the repo.
232
+
233
+ ## 2026-09-17 — the package shipped the memory well (one version, now excluded)
234
+
235
+ - **The exposure.** `@zerwiz/ymir@0.1.5` carries five files that are nobody's but
236
+ the operator's: `.agents/memory/kaia.engram`, its `-shm` and `-wal` sidecars, and
237
+ `.agents/memory/well/{episodes,workspace}.jsonl`. A public npm tarball contained
238
+ the live memory well — the store and the episodes. Verified by fetching the
239
+ published artefact and listing it; `0.1.0`–`0.1.4` are clean, and this branch's
240
+ build is clean.
241
+ - **The cause, and it is a trap worth naming.** `.gitignore` excludes
242
+ `.agents/memory/kaia.engram*` and `.agents/memory/well/*.jsonl`, and the repo is
243
+ clean — but **npm does not consult `.gitignore` when `files[]` names a whole
244
+ directory.** `files: [".agents/"]` packs that subtree *including* the ignored
245
+ files. Nothing warned: the leak travelled in the artefact, not the repo.
246
+ - **The fix.** The manifest excludes them explicitly, because a `files[]` list
247
+ cannot rely on the ignore file it overrides:
248
+ `!.agents/memory/kaia.engram*`, `!.agents/memory/well/*.jsonl`,
249
+ `!.agents/memory/*.db`, plus `!**/__pycache__/` and `!**/*.pyc` for the compiled
250
+ junk that was travelling the same way. Verified: `npm pack --dry-run` carries
251
+ 864 files and **no** memory store — the memory README and the ledger's scaffold
252
+ header are all that remain, as they should be.
253
+ - **The rule for every future package:** a `files[]` entry that names a directory
254
+ overrides `.gitignore` for everything beneath it. Name what ships, or exclude
255
+ what must not — and verify with a dry-run pack, never by assumption.
256
+ - **Remediation.** `@zerwiz/ymir@0.1.5` should be unpublished (it is inside npm's
257
+ window); the clean build publishes as 0.1.7 and supersedes it.
2
258
 
3
259
  ## 2026-09-17 — the house gains an unpublish door
4
260
 
@@ -59,7 +315,6 @@
59
315
  plan's phase-5 row is what says so out loud.
60
316
  - **`@zerwiz/ymir` 0.1.7** — the version published once this lands.
61
317
 
62
-
63
318
  ## 2026-09-17 — the four surfaces arrive by npm, and an old tree gives up what it holds
64
319
 
65
320
  - **The app packages arrive with the distro.** `@zerwiz/ymir` now depends on the
@@ -530,6 +785,122 @@ EmberBackground hearth section (shared module + ResizeObserver mend).
530
785
 
531
786
  # CHANGELOG
532
787
 
788
+ ## 2026-09-16 — a sandboxed worker cannot think: `auto` stops killing agents
789
+
790
+ Spawning an Eindri with the default `--isolation auto` produced an **empty pane
791
+ and no status line**. The cause: `auto` chose Utgard whenever the image existed,
792
+ and Utgard runs with `--network none` — so the worker could reach neither its
793
+ cloud model (OpenCode Go) nor a local one (llama.cpp) and died at launch, silently.
794
+
795
+ - **`auto` now keeps the worker in its worktree.** A spawned Eindri is a
796
+ model-driven worker: it must reach a model endpoint to think. Utgard is for
797
+ untrusted *code*, not for the agent's own brain, so the automatic choice is
798
+ `off`, and it says why.
799
+ - **`--isolation on` warns loudly.** It stays available for a sandbox that can
800
+ actually reach a model, but it now prints that Utgard has no network and the
801
+ agent will die silently without one.
802
+
803
+ ## 2026-09-16 — the roster's effect lands, and the backend pin stops being committable
804
+
805
+ Applying the private roster (`bin/agents-config.sh apply`) writes the resolved
806
+ model into each agent's canonical profile. Flipping the two local agents to Pi
807
+ made that visible — and surfaced a small ignore hole.
808
+
809
+ - **Profiles follow the roster.** `sindri-developer.md` and `kvasir-scout.md`
810
+ now carry their **Pi** ids (`llamacpp-coder/qwen3-coder-30b`,
811
+ `llamacpp/qwen3.5-9b`), and `huginn-researcher.md` its declared model — the
812
+ output of `apply` against the operator's roster. These profiles are what the
813
+ harnesses load, so the local agents now run on Pi rather than through OpenCode.
814
+ - **`.agents/config/backend` is ignored.** The pinned terminal backend is
815
+ machine-local, but `config` is a **symlink** to `.agents/config`, so the
816
+ `config/*` rule never matched it and the pin was committable. The file is named
817
+ in `.gitignore` instead.
818
+
819
+ ## 2026-09-16 — the roster can finally run an agent on Pi
820
+
821
+ `bin/agents-config.sh apply` wrote **every** agent's model into `opencode.json`'s
822
+ agent block, whatever harness that agent used. So an agent set to run on **Pi**
823
+ (native local models) would have had its Pi model id —
824
+ `llamacpp/qwen3.5-9b` — written into OpenCode's config, which cannot resolve it.
825
+ That is why the roster's two local agents were pinned to `harness: opencode` with
826
+ opencode-style ids: there was no working way to put an agent on Pi.
827
+
828
+ - **`apply` is now harness-aware.** Only `opencode`-harness agents are written
829
+ into `opencode.json`; a `pi` (or `hermes`) agent is left out, its model id going
830
+ to the resolve cache that `bin/agent-run.sh` reads. The providers block is
831
+ unchanged — a provider's endpoint is a real fact OpenCode may still want.
832
+ - The way this is meant to be used: declare a local agent's model as its **Pi**
833
+ id (`llamacpp/qwen3.5-9b`, `llamacpp-coder/qwen3-coder-30b` — the ids `pi
834
+ --list-models` reports) with `harness: pi`; the roster is then Pi-driven with no
835
+ per-machine hand-editing.
836
+ - `galdr-reread`: `assets/harness-integration/README.md` — the two writers of
837
+ `opencode.json`, and the rule that only OpenCode agents belong in it.
838
+
839
+ ## 2026-09-16 — the visualizer finds its DB, and the seat-hall actually builds
840
+
841
+ Two more readers left behind — both discovered by *starting the apps*, not by any
842
+ gate. Each failed silently in its own way.
843
+
844
+ - **`scripts/start.sh` pointed the smithy at a repo path that no longer holds the
845
+ DB.** `0003-private-data-separation` moved it to `$YMIR_HOME/smidja/smidja.db`,
846
+ but the starter still passed `CMD_DB=<repo>/apps/smidja/smidja_data/smidja.db`
847
+ — so the visualizer API died on boot with `smidja.db not found` and `:8437`
848
+ answered nothing. It now resolves the same pair `bin/smidja-bootstrap.sh` does
849
+ (home first, in-repo fallback, `SMIDJA_DB` override). The asset already
850
+ *described* this resolution; the code now implements it.
851
+ - **`bin/sessrumnir-ensure.sh` never built the app.** `[ "$built_present" ]` tests
852
+ a **literal, non-empty string** — always true — so `ensure --install` skipped
853
+ `build_app` and reported `built: no` with exit 0. Sessrúmnir only built when the
854
+ build was run by hand. Now `built_present` (the function) is called.
855
+ - `galdr-reread`: `.agents/skills/galdr-ymirsystem/assets/smidja.md` — the DB
856
+ path, the `scripts/start.sh` DB resolution, and the observer's read list.
857
+
858
+ ## 2026-09-16 — the credential reaches the gate, and the hall stops leaking an IP
859
+
860
+ Setting the operator password had **no effect**. This is the bug that made that
861
+ true, and the runtime state that leaked a private address.
862
+
863
+ - **`scripts/start.sh` never loaded the platform env for the gate.** The gate is
864
+ `bun run apps/hlidskjalf/server/index.ts`, and the server reads
865
+ `process.env.HLIDSKJALF_AUTH` — but the launcher passed only `PORT`, and nothing
866
+ sourced `.env.local`. So a credential written by `bin/ymir-setup-auth.sh`
867
+ (correctly: `0600`, gitignored) never arrived, and because the gate treats an
868
+ empty `GATE_AUTH` as authenticated (`authed: GATE_AUTH ? … : true`) the gate
869
+ stayed **open**. `start.sh` now loads `.env.local` once, before the ports, for
870
+ every service it raises — the gate, Bifrost and Mimir.
871
+ - **`apps/odrerir/.astro/dev.json` was tracked.** Astro rewrites it on every
872
+ start with the live `pid` and the host's own addresses (LAN + tailnet) — a
873
+ private-IP leak into a public tree. It is now untracked and gitignored; the
874
+ generated types beside it stay tracked.
875
+ - `galdr-reread`: `assets/hlidskjalf-ui.md` (the gate must receive the credential)
876
+ and `assets/odrerir-hall.md` (`.astro/dev.json` is runtime state).
877
+
878
+ ## 2026-09-16 — four readers that still pointed at the pre-split world
879
+
880
+ The app split and the hoard migration moved things; four readers never followed.
881
+ None failed loudly — each reported a clean PASS, a wrong SKIP, or a silent loss.
882
+
883
+ - **`bin/ymir-install.sh` wrote during `--check`.** The step chain ran
884
+ `bin/ymir-migrate.sh apply` unconditionally, so a preview that promises *"report
885
+ only, no writes"* actually **moved private data**. Migrations now apply only on
886
+ a real run: `if [ "$CHECK" = 0 ]; then ... apply; fi`.
887
+ - **`.agents/migrations/0003-private-data-separation.sh` skipped every directory.**
888
+ It pre-creates its target dirs, then its `copy` refused any target that already
889
+ existed — so `data/` (and every other directory source) was silently dropped.
890
+ The realm declaration never arrived and `0004` fell back to a neutral realm.
891
+ `copy` now **merges** a directory into its target (never overwriting a file) and
892
+ copies a single file only when it is absent.
893
+ - **`bin/smidja-bootstrap.sh` looked for the smithy at the old root.** `sys.path`
894
+ pointed at `smidja/`, but the split moved it to `apps/smidja/`, so the DB seed
895
+ died with `ModuleNotFoundError: No module named 'smidja_modules'`. It now
896
+ searches `apps/smidja` then `smidja`, so either layout works.
897
+ - **`bin/ymir-validate.sh` read the ledger from the pre-move path.** It looked at
898
+ `$YMIR_HOME/memory/`, but `0004-hoard-and-realms` put the ledger at
899
+ `$YMIR_HOME/hodd/memory/` — a false `runes FAIL` on a healthy home. It now tries
900
+ the hoard first, then the pre-move locations.
901
+ - `galdr-reread`: `.agents/skills/galdr-ymirsystem/assets/smidja.md` — the
902
+ protected-paths gotcha and the `smidja/` → `apps/smidja/` move.
903
+
533
904
  ## 2026-09-17 — Pi gets its agents, and a dead extension comes back
534
905
 
535
906
  - **Pi has no agent loader.** Agent loading in Pi is a *package* (`pi-agents`,
package/bin/app-lib.sh ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env bash
2
+ # app-lib.sh — where an app actually lives.
3
+ #
4
+ # Two installs, one name. In a clone the apps sit at `apps/<surface>` (the
5
+ # registry's `repo: apps/<path>` block clones them there). In a packaged install
6
+ # the tree has no `apps/` at all: each surface arrives as a dependency, at
7
+ # `node_modules/@zerwiz/<package>`.
8
+ #
9
+ # surface package
10
+ # ---------------- ---------------------
11
+ # hlidskjalf hlidskjalf
12
+ # hlidskjalf-mobile hlidskjalf-mobile
13
+ # odrerir odrerir
14
+ # sessrumnir sessrumnir
15
+ # smidja smidja-factory
16
+ #
17
+ # A surface's name is not always its package's name — and a path is not a name at
18
+ # all. Every script that needs an app resolves it HERE, so the two shapes can
19
+ # never disagree (Rule 07). Source-safe; functions only.
20
+ #
21
+ # app_dir <surface> <result-var> the app's directory (empty + exit 1 if absent)
22
+ # app_pkg <surface> the npm package name for that surface
23
+ set -u
24
+
25
+ app_pkg() { # <surface> → the package name npm serves
26
+ case "${1-}" in
27
+ smidja) printf '%s' 'smidja-factory' ;;
28
+ *) printf '%s' "${1-}" ;;
29
+ esac
30
+ }
31
+
32
+ app_dir() { # <surface> <result-var> — a clone's apps/<x>, else the package
33
+ # Scratch names are function-prefixed on purpose: `printf -v <name>` writes to THIS
34
+ # function's scope, so a scratch variable sharing the caller's requested name
35
+ # would swallow the answer (a caller asking for "c" or "root" would get nothing).
36
+ local _apd_surface="${1-}" _apd_rv="${2-}" _apd_root _apd_pkg _apd_c
37
+ [ -n "$_apd_surface" ] && [ -n "$_apd_rv" ] || return 2
38
+ _apd_root="${YMIR_ROOT_DIR:-}"
39
+ if [ -z "$_apd_root" ]; then _apd_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"; fi
40
+ _apd_pkg="$(app_pkg "$_apd_surface")"
41
+ for _apd_c in "$_apd_root/apps/$_apd_surface" "$_apd_root/node_modules/@zerwiz/$_apd_pkg"; do
42
+ [ -d "$_apd_c" ] && { printf -v "$_apd_rv" '%s' "$_apd_c"; return 0; }
43
+ done
44
+ printf -v "$_apd_rv" '%s' ""
45
+ return 1
46
+ }
47
+
48
+ # Some callers need the directory a mint writes INTO (an app's public/, electron/,
49
+ # dist/…). A packaged app is read-only in spirit, so the clone's apps/ is preferred
50
+ # when it exists; otherwise the package is the only truth there is.
51
+ app_root() { # <result-var> — where apps live in THIS tree
52
+ local _apr_rv="${1-}" _apr_root
53
+ [ -n "$_apr_rv" ] || return 2
54
+ _apr_root="${YMIR_ROOT_DIR:-}"
55
+ if [ -z "$_apr_root" ]; then _apr_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"; fi
56
+ if [ -d "$_apr_root/apps" ]; then printf -v "$_apr_rv" '%s' "$_apr_root/apps"; return 0; fi
57
+ printf -v "$_apr_rv" '%s' "$_apr_root/node_modules/@zerwiz"
58
+ }
@@ -19,8 +19,19 @@ set -u
19
19
  VERSION="1.0.0"
20
20
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
21
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
22
+ # Where an app lives: apps/<surface> in a clone, node_modules/@zerwiz/<pkg> in an
23
+ # npm install — both shapes, one resolver (bin/app-lib.sh).
24
+ if [ -z "${YMIR_APP_LIB_LOADED:-}" ]; then
25
+ _ya="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
26
+ for _yac in "$_ya/app-lib.sh" "$(dirname "$_ya")/bin/app-lib.sh"; do
27
+ [ -r "$_yac" ] && { . "$_yac"; YMIR_APP_LIB_LOADED=1; break; }
28
+ done
29
+ unset _ya _yac
30
+ fi
31
+ app_dir sessrumnir APP_SESSRUMNIR || APP_SESSRUMNIR=""
32
+
22
33
  TOKENS="$ROOT/midgard/design-system/tokens.css"
23
- SEEDS="$ROOT/apps/sessrumnir/src/renderer/src/themes/fensalir.json"
34
+ SEEDS="$APP_SESSRUMNIR/src/renderer/src/themes/fensalir.json"
24
35
 
25
36
  case "${1-}" in
26
37
  -v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;;
@@ -16,6 +16,21 @@ set -u
16
16
  VERSION="1.0.0"
17
17
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
18
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
19
+ # Where an app lives: apps/<surface> in a clone, node_modules/@zerwiz/<pkg> in an
20
+ # npm install — both shapes, one resolver (bin/app-lib.sh).
21
+ if [ -z "${YMIR_APP_LIB_LOADED:-}" ]; then
22
+ _ya="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23
+ for _yac in "$_ya/app-lib.sh" "$(dirname "$_ya")/bin/app-lib.sh"; do
24
+ [ -r "$_yac" ] && { . "$_yac"; YMIR_APP_LIB_LOADED=1; break; }
25
+ done
26
+ unset _ya _yac
27
+ fi
28
+ app_dir hlidskjalf APP_HLIDSKJALF || APP_HLIDSKJALF=""
29
+ app_dir hlidskjalf-mobile APP_HLIDSKJALF_MOBILE || APP_HLIDSKJALF_MOBILE=""
30
+ app_dir odrerir APP_ODRERIR || APP_ODRERIR=""
31
+ app_dir sessrumnir APP_SESSRUMNIR || APP_SESSRUMNIR=""
32
+ app_dir smidja-factory APP_SMIDJA_FACTORY || APP_SMIDJA_FACTORY=""
33
+
19
34
  ICONS="$ROOT/midgard/design-system/icons"
20
35
  STONE="#0e0c09"
21
36
 
@@ -30,16 +45,19 @@ ACTION="${1:-list}"; shift || true
30
45
  APPS=(
31
46
  "apps/hlidskjalf|ehwaz|#c9973f|the seat — Hlidskjalf, the high seat of the control plane|ymir-hlidskjalf|scripts/electron.sh start --view hlidskjalf|ymir-hlidskjalf|Ymir · Hlidskjalf"
32
47
  "apps/hlidskjalf-mobile|raidho|#c9973f|the road — the seat carried, Hlidskjalf on a phone|ymir-hlidskjalf-mobile|scripts/electron.sh start --view hlidskjalf|ymir-hlidskjalf-mobile|Ymir · Hlidskjalf Mobile"
33
- "apps/odrerir|valhalla|#c9973f|the hall — Óðrerir, the Live Hall|ymir-odrerir|scripts/electron.sh start --view odrerir|ymir-odrerir|Ymir · Óðrerir"
48
+ "apps/odrerir|ansuz|#c9973f|the mead — Óðrerir, the Live Hall (Odin's breath, inspiration)|ymir-odrerir|scripts/electron.sh start --view odrerir|ymir-odrerir|Ymir · Óðrerir"
34
49
  "apps/sessrumnir|othala|#c9973f|the hall — Sessrúmnir, the seat that holds the cloth|ymir-sessrumnir|bin/sessrumnir.sh start|ymir-sessrumnir|Ymir · Sessrúmnir"
35
- "apps/smidja-factory/apps/visualizer|kaunan|#f59e0b|the torch — the forge's eye, Smíðja's trace|ymir-visualizer|scripts/electron.sh start --view smidja|ymir-smidja|Ymir · Smíðja"
50
+ "apps/smidja-factory/apps/visualizer|kaunan|#f59e0b|the torch — the forge's eye, Smíðja's trace|ymir-smidja|scripts/electron.sh start --view smidja|ymir-smidja|Ymir · Smíðja"
36
51
  )
37
52
 
38
53
  list() {
39
54
  printf 'app_icons[%d]{app,rune,tint,says}:\n' "${#APPS[@]}"
40
55
  for row in "${APPS[@]}"; do
41
- IFS='|' read -r dir glyph tint says _icon _exec _klass _name <<<"$row"
42
- printf ' "%s","%s","%s","%s"\n' "$(basename "$dir")" "$glyph" "$tint" "$says"
56
+ IFS='|' read -r dir glyph tint says icon _exec _klass _name <<<"$row"
57
+ # The surface's name is the one the desktop knows it by (ymir-smidja →
58
+ # smidja), never the directory's leaf — a path is not a name (the naming law).
59
+ surface="${icon#ymir-}"
60
+ printf ' "%s","%s","%s","%s"\n' "$surface" "$glyph" "$tint" "$says"
43
61
  done
44
62
  printf 'runes[6]{glyph,name,meaning}:\n'
45
63
  printf ' "algiz","ᛉ","the Ymir emblem — the platform itself"\n'
@@ -29,6 +29,19 @@ fi
29
29
  VERSION="1.0.0"
30
30
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
31
31
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
32
+ # Where an app lives: apps/<surface> in a clone, node_modules/@zerwiz/<pkg> in an
33
+ # npm install — both shapes, one resolver (bin/app-lib.sh).
34
+ if [ -z "${YMIR_APP_LIB_LOADED:-}" ]; then
35
+ _ya="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
36
+ for _yac in "$_ya/app-lib.sh" "$(dirname "$_ya")/bin/app-lib.sh"; do
37
+ [ -r "$_yac" ] && { . "$_yac"; YMIR_APP_LIB_LOADED=1; break; }
38
+ done
39
+ unset _ya _yac
40
+ fi
41
+ app_dir hlidskjalf APP_HLIDSKJALF || APP_HLIDSKJALF=""
42
+ app_dir sessrumnir APP_SESSRUMNIR || APP_SESSRUMNIR=""
43
+ app_dir odrerir APP_ODRERIR || APP_ODRERIR=""
44
+
32
45
  HYPR_DIR="$HOME/.config/hypr"
33
46
  RULE_FILE="$HYPR_DIR/ymir-desktops.lua"
34
47
  # The three Ymir surfaces, in the order they should claim desktops.
@@ -46,7 +59,7 @@ CLASS_odrerir="ymir-odrerir"
46
59
  # not a branch of this one.
47
60
  install_entries() {
48
61
  local dst="$HOME/.local/share/applications" n=0
49
- local dirs=("$ROOT/apps/hlidskjalf/electron" "$ROOT/apps/sessrumnir/resources" "$ROOT/apps/odrerir/electron")
62
+ local dirs=("$APP_HLIDSKJALF/electron" "$APP_SESSRUMNIR/resources" "$APP_ODRERIR/electron")
50
63
  if [ "$(ymir_os)" != linux ] && [ "$(ymir_os)" != wsl ]; then
51
64
  printf 'skip: desktop entries are Omarchy/Linux-shaped; this host is %s\n' "$(ymir_os)" >&2
52
65
  return 0