@zerwiz/ymir 0.1.7 → 0.1.8

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,218 @@
1
1
 
2
+ ## 2026-09-17 — 0.1.8: the day's work reaches npm
3
+
4
+ - **The doors.** `ymir` now carries the operator's verbs, each named for the
5
+ figure who does the work: `raise` · `lower` · `eir` (heal) · `groa` (renew) ·
6
+ `heimdall` (the way in) · `invite` · `smidja` (the board) · `hlidskjalf` ·
7
+ `sessrumnir` · `mimir` · `sense` · `plan`. Before this, the package put two
8
+ commands on PATH and neither could raise the app or the board.
9
+ - **The cloth.** `bin/ymir-style.sh` — colour and marks cut from the halls' own
10
+ tokens (bone · bronze · steel · blood), shown only where a human watches, with
11
+ the data left as TOON on stdout. The plan, the installer, the validate report
12
+ and Eir all wear it.
13
+ - **The shapes.** `bin/smidja-lib.sh` tells a clone's smithy from a packaged one;
14
+ `bin/smidja-board.sh` is the board's own door (`ymir smidja`), built and served
15
+ from an npm install; `bin/electron-lib.sh` verifies a shell's runtime so a
16
+ skipped npm install script cannot pass as a launch.
17
+ - **The icons.** One truth where three maps disagreed, and no claim of a glyph
18
+ nobody drew: Óðrerir wears ansuz, Sessrúmnir othala, the smithy's icon is named
19
+ for the smithy.
20
+ - **The repair.** `4dd7273`'s hand-merge left `bin/ymir-install.sh` and
21
+ `scripts/start.sh` unparseable on main; both restored. Every runtime script
22
+ parses again.
23
+
24
+ ## 2026-09-17 — main is repaired, and the report wears the cloth
25
+
26
+ - **Main was broken and nobody had noticed.** A merge resolved by hand
27
+ (`4dd7273`, from a parallel branch) kept *both* sides of a conflict in
28
+ `bin/ymir-install.sh` and `scripts/start.sh`: a stray `<`, a duplicated step
29
+ line, orphaned comments, an `if` with no `fi`. The installer could not parse at
30
+ all — `bash -n` failed on main — and PR #53's merge carried the breakage
31
+ forward. Both files are restored from the last revision that parses (`5b7fc66`),
32
+ with the two fixes that revision predated re-applied: the `--phase` value is
33
+ consumed, and `--yes | --non-interactive | --accept-all-defaults` are one thing.
34
+ Every runtime script parses again; the installer's plan runs.
35
+ - **The lesson worth keeping:** a merge is not a place to guess. When both sides
36
+ of a conflict are real, the resolution is a decision — and `bash -n` on every
37
+ runtime script is cheap enough to be part of it.
38
+ - **The validate report wears the cloth** — the same rows rendered for the eye on
39
+ stderr (marks, colour, and a verdict with the next step) while the TOON stays
40
+ the data on stdout. It found real drift while it was being written: the SPA down
41
+ on `:3888`, Nornir cron stopped, `sessrumnir`, `mcp` and `hoard` broken.
42
+ - **Eir watches the shells.** A new surface reports the desktop shells' runtime
43
+ through `bin/electron-lib.sh` — a *partial* Electron runtime is exactly the
44
+ quiet failure Eir exists for — and her rows render in the cloth too.
45
+
46
+ ## 2026-09-17 — the house gains an unpublish door
47
+
48
+ - **`bin/npm-publish.sh --unpublish @scope/name@<version>`.** Publishing had a
49
+ door in this house and unpublishing had none, so the act was done by hand on a
50
+ machine whose `~/.npmrc` holds a stale token — the very reason the door exists.
51
+ The new mode resolves the token from the hoard exactly as publishing does,
52
+ demands a spec that names the version (a whole package is not a one-word act),
53
+ supports `--dry-run`, and says plainly that the registry's CDN may serve the
54
+ tarball for a while after the removal.
55
+ - **The window is 72 hours.** npm allows one version back within 72 hours of its
56
+ publication; past that only npm support can remove it. The help text says so,
57
+ because a door that does not name its own clock invites a late knock.
58
+ - **`@zerwiz/ymir@0.1.5` was unpublished** — the version that carried the memory
59
+ well. Publishing the clean 0.1.7 moves the `latest` tag off the tainted build at
60
+ once, which is the half of the repair that does not wait for propagation.
61
+
62
+ ## 2026-09-17 — the plan learns the smithy's package name, and the install forwards its flags
63
+
64
+ Two defects the first real end-to-end npm install surfaced — which is what an
65
+ end-to-end test is for.
66
+
67
+ - **A surface's name is not its package's name.** The plan looked for
68
+ `apps/smidja` or `@zerwiz/smidja`, but npm serves the smithy as
69
+ `@zerwiz/smidja-factory` — so the row read `BLOCKED` on a machine where the
70
+ smithy was installed and standing. `app_dir`/`app_row` now take the surface
71
+ name and the package name separately, and the row tells the truth: installed,
72
+ with the visualizer's UI still to build (the tarball ships its source, never a
73
+ stale build).
74
+ - **`ymir install --plan` forwarded only `--json`.** `--phase` and `--blocked`
75
+ were dropped on the way through the installer's door, so `--blocked` answered
76
+ *unknown flag*. Every plan flag is forwarded now.
77
+
78
+ **The install this came out of:** `npm install -g @zerwiz/ymir` from the registry
79
+ — `@zerwiz/ymir@0.1.7`, 331 packages, and all four surfaces
80
+ (`hlidskjalf` · `odrerir` · `sessrumnir` · `smidja-factory`) arrived inside the
81
+ distro. The tarball carries no memory store — the leak is closed in the artefact
82
+ as well as in the repo.
83
+
84
+ ## 2026-09-17 — the package shipped the memory well (one version, now excluded)
85
+
86
+ - **The exposure.** `@zerwiz/ymir@0.1.5` carries five files that are nobody's but
87
+ the operator's: `.agents/memory/kaia.engram`, its `-shm` and `-wal` sidecars, and
88
+ `.agents/memory/well/{episodes,workspace}.jsonl`. A public npm tarball contained
89
+ the live memory well — the store and the episodes. Verified by fetching the
90
+ published artefact and listing it; `0.1.0`–`0.1.4` are clean, and this branch's
91
+ build is clean.
92
+ - **The cause, and it is a trap worth naming.** `.gitignore` excludes
93
+ `.agents/memory/kaia.engram*` and `.agents/memory/well/*.jsonl`, and the repo is
94
+ clean — but **npm does not consult `.gitignore` when `files[]` names a whole
95
+ directory.** `files: [".agents/"]` packs that subtree *including* the ignored
96
+ files. Nothing warned: the leak travelled in the artefact, not the repo.
97
+ - **The fix.** The manifest excludes them explicitly, because a `files[]` list
98
+ cannot rely on the ignore file it overrides:
99
+ `!.agents/memory/kaia.engram*`, `!.agents/memory/well/*.jsonl`,
100
+ `!.agents/memory/*.db`, plus `!**/__pycache__/` and `!**/*.pyc` for the compiled
101
+ junk that was travelling the same way. Verified: `npm pack --dry-run` carries
102
+ 864 files and **no** memory store — the memory README and the ledger's scaffold
103
+ header are all that remain, as they should be.
104
+ - **The rule for every future package:** a `files[]` entry that names a directory
105
+ overrides `.gitignore` for everything beneath it. Name what ships, or exclude
106
+ what must not — and verify with a dry-run pack, never by assumption.
107
+ - **Remediation.** `@zerwiz/ymir@0.1.5` should be unpublished (it is inside npm's
108
+ window); the clean build publishes as 0.1.7 and supersedes it.
109
+
110
+ ## 2026-09-17 — the doors, the cloth, and the icons that were not there
111
+
112
+ Four things the operator met and could not use: doors that never opened, output
113
+ with no design, a board that could not be built from a package, and an icon map
114
+ that named a glyph nobody had drawn.
115
+
116
+ - **The doors are named for the figure who does the work.** `ymir` put two
117
+ commands on PATH and could neither start the app nor raise the board. It now
118
+ carries the doors: **raise · lower · eir** (heal) · **groa** (renew) ·
119
+ **heimdall** (the way in) · **invite** · **smidja** (the board) ·
120
+ **hlidskjalf** · **sessrumnir** · **mimir** · **sense** · **plan**. A name the
121
+ law has not given a home — `doctor`, `validate`, `auth` — still answers, once,
122
+ with the name that has it.
123
+ - **The cloth: `bin/ymir-style.sh`.** Ymir had correct output and no design. The
124
+ palette is cut from the halls' own tokens — bone for words, bronze for what
125
+ acts, steel for what stands, blood for what is wrong — with a mark per state
126
+ (`◆ · — ✕ ? ✓`). Colour and marks appear only where a human is watching
127
+ (stderr a TTY, `NO_COLOR` unset); the data on stdout stays TOON, and **the
128
+ words are never conditional, only the colour is**. The plan renders in the
129
+ cloth on stderr while the TOON stays pipeable; the installer opens with it and
130
+ ends with the next steps.
131
+ - **The packaged tree is told apart from a clone.** `bin/smidja-lib.sh` resolves
132
+ the smithy — `apps/smidja-factory` in a clone, `node_modules/@zerwiz/smidja-factory`
133
+ in a package — and four call sites that assumed the clone now resolve through
134
+ it. `bin/smidja-board.sh` is the board's own door (`build · start · stop ·
135
+ status`), which `ymir smidja` runs; built and served end to end from an npm
136
+ install: `{"ok":true,"sessions":1}`, `GET / → 200`.
137
+ - **A shell's runtime is verified, never assumed.** npm gates install scripts, so
138
+ a skipped Electron postinstall leaves a partial runtime that still builds the
139
+ web app and still reports success. `bin/electron-lib.sh` answers `ok · partial
140
+ · absent`, the plan's `electron` row says **PARTIAL** with the exact remedy, and
141
+ `step_desktop` refuses to claim a launch it cannot make.
142
+ - **The icons now name glyphs that exist.** Three maps disagreed: `runes.md` gave
143
+ Óðrerir the rune `wunjo` — **a glyph nobody had drawn** — and tinted Sessrúmnir
144
+ with a violet that is not in the tokens; `icons.md` gave `othala` to Óðrerir
145
+ *and* Sessrúmnir; `design-icon.sh` minted `valhalla` for Óðrerir. One truth now:
146
+ **Óðrerir → ansuz** (Odin's breath, the mead of poetry — what Óðrerir *is*),
147
+ Sessrúmnir → othala, Valhalla keeps `ᚹ` (drawn in `valhalla.svg`, named Wunjo),
148
+ Sowilo no longer claimed twice, every tint a house accent from the tokens, and
149
+ the smithy's icon named `ymir-smidja` rather than `ymir-visualizer`. All 22
150
+ glyphs parse; every claim resolves to a file. `docs/design.md` stopped calling
151
+ a living map *"to create"*.
152
+
153
+ ## 2026-09-17 — the house gains an unpublish door
154
+
155
+ - **`bin/npm-publish.sh --unpublish @scope/name@<version>`.** Publishing had a
156
+ door in this house and unpublishing had none, so the act was done by hand on a
157
+ machine whose `~/.npmrc` holds a stale token — the very reason the door exists.
158
+ The new mode resolves the token from the hoard exactly as publishing does,
159
+ demands a spec that names the version (a whole package is not a one-word act),
160
+ supports `--dry-run`, and says plainly that the registry's CDN may serve the
161
+ tarball for a while after the removal.
162
+ - **The window is 72 hours.** npm allows one version back within 72 hours of its
163
+ publication; past that only npm support can remove it. The help text says so,
164
+ because a door that does not name its own clock invites a late knock.
165
+ - **`@zerwiz/ymir@0.1.5` was unpublished** — the version that carried the memory
166
+ well. Publishing the clean 0.1.7 moves the `latest` tag off the tainted build at
167
+ once, which is the half of the repair that does not wait for propagation.
168
+
169
+ ## 2026-09-17 — the plan learns the smithy's package name, and the install forwards its flags
170
+
171
+ Two defects the first real end-to-end npm install surfaced — which is what an
172
+ end-to-end test is for.
173
+
174
+ - **A surface's name is not its package's name.** The plan looked for
175
+ `apps/smidja` or `@zerwiz/smidja`, but npm serves the smithy as
176
+ `@zerwiz/smidja-factory` — so the row read `BLOCKED` on a machine where the
177
+ smithy was installed and standing. `app_dir`/`app_row` now take the surface
178
+ name and the package name separately, and the row tells the truth: installed,
179
+ with the visualizer's UI still to build (the tarball ships its source, never a
180
+ stale build).
181
+ - **`ymir install --plan` forwarded only `--json`.** `--phase` and `--blocked`
182
+ were dropped on the way through the installer's door, so `--blocked` answered
183
+ *unknown flag*. Every plan flag is forwarded now.
184
+
185
+ **The install this came out of:** `npm install -g @zerwiz/ymir` from the registry
186
+ — `@zerwiz/ymir@0.1.7`, 331 packages, and all four surfaces
187
+ (`hlidskjalf` · `odrerir` · `sessrumnir` · `smidja-factory`) arrived inside the
188
+ distro. The tarball carries no memory store — the leak is closed in the artefact
189
+ as well as in the repo.
190
+
191
+ ## 2026-09-17 — the package shipped the memory well (one version, now excluded)
192
+
193
+ - **The exposure.** `@zerwiz/ymir@0.1.5` carries five files that are nobody's but
194
+ the operator's: `.agents/memory/kaia.engram`, its `-shm` and `-wal` sidecars, and
195
+ `.agents/memory/well/{episodes,workspace}.jsonl`. A public npm tarball contained
196
+ the live memory well — the store and the episodes. Verified by fetching the
197
+ published artefact and listing it; `0.1.0`–`0.1.4` are clean, and this branch's
198
+ build is clean.
199
+ - **The cause, and it is a trap worth naming.** `.gitignore` excludes
200
+ `.agents/memory/kaia.engram*` and `.agents/memory/well/*.jsonl`, and the repo is
201
+ clean — but **npm does not consult `.gitignore` when `files[]` names a whole
202
+ directory.** `files: [".agents/"]` packs that subtree *including* the ignored
203
+ files. Nothing warned: the leak travelled in the artefact, not the repo.
204
+ - **The fix.** The manifest excludes them explicitly, because a `files[]` list
205
+ cannot rely on the ignore file it overrides:
206
+ `!.agents/memory/kaia.engram*`, `!.agents/memory/well/*.jsonl`,
207
+ `!.agents/memory/*.db`, plus `!**/__pycache__/` and `!**/*.pyc` for the compiled
208
+ junk that was travelling the same way. Verified: `npm pack --dry-run` carries
209
+ 864 files and **no** memory store — the memory README and the ledger's scaffold
210
+ header are all that remain, as they should be.
211
+ - **The rule for every future package:** a `files[]` entry that names a directory
212
+ overrides `.gitignore` for everything beneath it. Name what ships, or exclude
213
+ what must not — and verify with a dry-run pack, never by assumption.
214
+ - **Remediation.** `@zerwiz/ymir@0.1.5` should be unpublished (it is inside npm's
215
+ window); the clean build publishes as 0.1.7 and supersedes it.
2
216
 
3
217
  ## 2026-09-17 — the house gains an unpublish door
4
218
 
@@ -59,7 +273,6 @@
59
273
  plan's phase-5 row is what says so out loud.
60
274
  - **`@zerwiz/ymir` 0.1.7** — the version published once this lands.
61
275
 
62
-
63
276
  ## 2026-09-17 — the four surfaces arrive by npm, and an old tree gives up what it holds
64
277
 
65
278
  - **The app packages arrive with the distro.** `@zerwiz/ymir` now depends on the
@@ -530,6 +743,122 @@ EmberBackground hearth section (shared module + ResizeObserver mend).
530
743
 
531
744
  # CHANGELOG
532
745
 
746
+ ## 2026-09-16 — a sandboxed worker cannot think: `auto` stops killing agents
747
+
748
+ Spawning an Eindri with the default `--isolation auto` produced an **empty pane
749
+ and no status line**. The cause: `auto` chose Utgard whenever the image existed,
750
+ and Utgard runs with `--network none` — so the worker could reach neither its
751
+ cloud model (OpenCode Go) nor a local one (llama.cpp) and died at launch, silently.
752
+
753
+ - **`auto` now keeps the worker in its worktree.** A spawned Eindri is a
754
+ model-driven worker: it must reach a model endpoint to think. Utgard is for
755
+ untrusted *code*, not for the agent's own brain, so the automatic choice is
756
+ `off`, and it says why.
757
+ - **`--isolation on` warns loudly.** It stays available for a sandbox that can
758
+ actually reach a model, but it now prints that Utgard has no network and the
759
+ agent will die silently without one.
760
+
761
+ ## 2026-09-16 — the roster's effect lands, and the backend pin stops being committable
762
+
763
+ Applying the private roster (`bin/agents-config.sh apply`) writes the resolved
764
+ model into each agent's canonical profile. Flipping the two local agents to Pi
765
+ made that visible — and surfaced a small ignore hole.
766
+
767
+ - **Profiles follow the roster.** `sindri-developer.md` and `kvasir-scout.md`
768
+ now carry their **Pi** ids (`llamacpp-coder/qwen3-coder-30b`,
769
+ `llamacpp/qwen3.5-9b`), and `huginn-researcher.md` its declared model — the
770
+ output of `apply` against the operator's roster. These profiles are what the
771
+ harnesses load, so the local agents now run on Pi rather than through OpenCode.
772
+ - **`.agents/config/backend` is ignored.** The pinned terminal backend is
773
+ machine-local, but `config` is a **symlink** to `.agents/config`, so the
774
+ `config/*` rule never matched it and the pin was committable. The file is named
775
+ in `.gitignore` instead.
776
+
777
+ ## 2026-09-16 — the roster can finally run an agent on Pi
778
+
779
+ `bin/agents-config.sh apply` wrote **every** agent's model into `opencode.json`'s
780
+ agent block, whatever harness that agent used. So an agent set to run on **Pi**
781
+ (native local models) would have had its Pi model id —
782
+ `llamacpp/qwen3.5-9b` — written into OpenCode's config, which cannot resolve it.
783
+ That is why the roster's two local agents were pinned to `harness: opencode` with
784
+ opencode-style ids: there was no working way to put an agent on Pi.
785
+
786
+ - **`apply` is now harness-aware.** Only `opencode`-harness agents are written
787
+ into `opencode.json`; a `pi` (or `hermes`) agent is left out, its model id going
788
+ to the resolve cache that `bin/agent-run.sh` reads. The providers block is
789
+ unchanged — a provider's endpoint is a real fact OpenCode may still want.
790
+ - The way this is meant to be used: declare a local agent's model as its **Pi**
791
+ id (`llamacpp/qwen3.5-9b`, `llamacpp-coder/qwen3-coder-30b` — the ids `pi
792
+ --list-models` reports) with `harness: pi`; the roster is then Pi-driven with no
793
+ per-machine hand-editing.
794
+ - `galdr-reread`: `assets/harness-integration/README.md` — the two writers of
795
+ `opencode.json`, and the rule that only OpenCode agents belong in it.
796
+
797
+ ## 2026-09-16 — the visualizer finds its DB, and the seat-hall actually builds
798
+
799
+ Two more readers left behind — both discovered by *starting the apps*, not by any
800
+ gate. Each failed silently in its own way.
801
+
802
+ - **`scripts/start.sh` pointed the smithy at a repo path that no longer holds the
803
+ DB.** `0003-private-data-separation` moved it to `$YMIR_HOME/smidja/smidja.db`,
804
+ but the starter still passed `CMD_DB=<repo>/apps/smidja/smidja_data/smidja.db`
805
+ — so the visualizer API died on boot with `smidja.db not found` and `:8437`
806
+ answered nothing. It now resolves the same pair `bin/smidja-bootstrap.sh` does
807
+ (home first, in-repo fallback, `SMIDJA_DB` override). The asset already
808
+ *described* this resolution; the code now implements it.
809
+ - **`bin/sessrumnir-ensure.sh` never built the app.** `[ "$built_present" ]` tests
810
+ a **literal, non-empty string** — always true — so `ensure --install` skipped
811
+ `build_app` and reported `built: no` with exit 0. Sessrúmnir only built when the
812
+ build was run by hand. Now `built_present` (the function) is called.
813
+ - `galdr-reread`: `.agents/skills/galdr-ymirsystem/assets/smidja.md` — the DB
814
+ path, the `scripts/start.sh` DB resolution, and the observer's read list.
815
+
816
+ ## 2026-09-16 — the credential reaches the gate, and the hall stops leaking an IP
817
+
818
+ Setting the operator password had **no effect**. This is the bug that made that
819
+ true, and the runtime state that leaked a private address.
820
+
821
+ - **`scripts/start.sh` never loaded the platform env for the gate.** The gate is
822
+ `bun run apps/hlidskjalf/server/index.ts`, and the server reads
823
+ `process.env.HLIDSKJALF_AUTH` — but the launcher passed only `PORT`, and nothing
824
+ sourced `.env.local`. So a credential written by `bin/ymir-setup-auth.sh`
825
+ (correctly: `0600`, gitignored) never arrived, and because the gate treats an
826
+ empty `GATE_AUTH` as authenticated (`authed: GATE_AUTH ? … : true`) the gate
827
+ stayed **open**. `start.sh` now loads `.env.local` once, before the ports, for
828
+ every service it raises — the gate, Bifrost and Mimir.
829
+ - **`apps/odrerir/.astro/dev.json` was tracked.** Astro rewrites it on every
830
+ start with the live `pid` and the host's own addresses (LAN + tailnet) — a
831
+ private-IP leak into a public tree. It is now untracked and gitignored; the
832
+ generated types beside it stay tracked.
833
+ - `galdr-reread`: `assets/hlidskjalf-ui.md` (the gate must receive the credential)
834
+ and `assets/odrerir-hall.md` (`.astro/dev.json` is runtime state).
835
+
836
+ ## 2026-09-16 — four readers that still pointed at the pre-split world
837
+
838
+ The app split and the hoard migration moved things; four readers never followed.
839
+ None failed loudly — each reported a clean PASS, a wrong SKIP, or a silent loss.
840
+
841
+ - **`bin/ymir-install.sh` wrote during `--check`.** The step chain ran
842
+ `bin/ymir-migrate.sh apply` unconditionally, so a preview that promises *"report
843
+ only, no writes"* actually **moved private data**. Migrations now apply only on
844
+ a real run: `if [ "$CHECK" = 0 ]; then ... apply; fi`.
845
+ - **`.agents/migrations/0003-private-data-separation.sh` skipped every directory.**
846
+ It pre-creates its target dirs, then its `copy` refused any target that already
847
+ existed — so `data/` (and every other directory source) was silently dropped.
848
+ The realm declaration never arrived and `0004` fell back to a neutral realm.
849
+ `copy` now **merges** a directory into its target (never overwriting a file) and
850
+ copies a single file only when it is absent.
851
+ - **`bin/smidja-bootstrap.sh` looked for the smithy at the old root.** `sys.path`
852
+ pointed at `smidja/`, but the split moved it to `apps/smidja/`, so the DB seed
853
+ died with `ModuleNotFoundError: No module named 'smidja_modules'`. It now
854
+ searches `apps/smidja` then `smidja`, so either layout works.
855
+ - **`bin/ymir-validate.sh` read the ledger from the pre-move path.** It looked at
856
+ `$YMIR_HOME/memory/`, but `0004-hoard-and-realms` put the ledger at
857
+ `$YMIR_HOME/hodd/memory/` — a false `runes FAIL` on a healthy home. It now tries
858
+ the hoard first, then the pre-move locations.
859
+ - `galdr-reread`: `.agents/skills/galdr-ymirsystem/assets/smidja.md` — the
860
+ protected-paths gotcha and the `smidja/` → `apps/smidja/` move.
861
+
533
862
  ## 2026-09-17 — Pi gets its agents, and a dead extension comes back
534
863
 
535
864
  - **Pi has no agent loader.** Agent loading in Pi is a *package* (`pi-agents`,
@@ -30,16 +30,19 @@ ACTION="${1:-list}"; shift || true
30
30
  APPS=(
31
31
  "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
32
  "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"
33
+ "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
34
  "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"
35
+ "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
36
  )
37
37
 
38
38
  list() {
39
39
  printf 'app_icons[%d]{app,rune,tint,says}:\n' "${#APPS[@]}"
40
40
  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"
41
+ IFS='|' read -r dir glyph tint says icon _exec _klass _name <<<"$row"
42
+ # The surface's name is the one the desktop knows it by (ymir-smidja →
43
+ # smidja), never the directory's leaf — a path is not a name (the naming law).
44
+ surface="${icon#ymir-}"
45
+ printf ' "%s","%s","%s","%s"\n' "$surface" "$glyph" "$tint" "$says"
43
46
  done
44
47
  printf 'runes[6]{glyph,name,meaning}:\n'
45
48
  printf ' "algiz","ᛉ","the Ymir emblem — the platform itself"\n'
@@ -342,6 +342,12 @@ else
342
342
  fi
343
343
 
344
344
  # --- isolation resolution ----------------------------------------------------
345
+ # A spawned Eindri is a MODEL-DRIVEN worker: it must reach a model endpoint to
346
+ # think at all. Utgard runs with `--network none`, so a sandboxed agent cannot
347
+ # reach either a cloud model (OpenCode Go) or a local one (llama.cpp) — it dies
348
+ # silently at launch, leaving an empty pane and no status line. Utgard is for
349
+ # untrusted CODE, not for the agent's own brain. So `auto` keeps the worker in
350
+ # its worktree, and `on` is an explicit choice that warns.
345
351
  ISOLATION_EFFECTIVE=off
346
352
  case "$ISOLATION" in
347
353
  on)
@@ -351,14 +357,14 @@ case "$ISOLATION" in
351
357
  exit 1
352
358
  }
353
359
  ISOLATION_EFFECTIVE=on
360
+ echo "warning: Utgard runs with --network none — the worker's model endpoint must be reachable INSIDE the sandbox, or the agent dies silently. Pass --isolation off to run in the worktree." >&2
354
361
  ;;
355
362
  off) ISOLATION_EFFECTIVE=off ;;
356
363
  auto)
364
+ ISOLATION_EFFECTIVE=off
357
365
  if [ -n "$ENGINE" ] && "$ENGINE" image inspect utgard-runner:latest >/dev/null 2>&1; then
358
- ISOLATION_EFFECTIVE=on
359
- echo "isolation: on (Utgard image present; pass --isolation off to run in the worktree)" >&2
366
+ echo "isolation: off (Utgard image present, but a model-driven worker needs its endpoint and Utgard has no network — pass --isolation on only for a sandbox that can reach a model)" >&2
360
367
  else
361
- ISOLATION_EFFECTIVE=off
362
368
  echo "isolation: off (no Utgard image; pass --isolation on after building it)" >&2
363
369
  fi
364
370
  ;;
package/bin/eir-doctor.sh CHANGED
@@ -18,6 +18,11 @@ ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
18
18
  YMIR_HOME="${YMIR_HOME:-$HOME/Documents/Ymir}"
19
19
  STATE="${BROKK_STATE_OVERRIDE:-$YMIR_HOME/state}"
20
20
 
21
+ # The cloth: colour and marks for the human reading this report; the TOON rows on
22
+ # stdout stay the data (bin/ymir-style.sh).
23
+ if [ -z "${YMIR_STYLE_LOADED:-}" ]; then . "$SCRIPT_DIR/ymir-style.sh"; YMIR_STYLE_LOADED=1; fi
24
+ style_init
25
+
21
26
  case "${1-}" in -v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;; -h|--help|"") sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; esac
22
27
  ACTION="${1:-check}"; shift || true
23
28
 
@@ -25,7 +30,7 @@ have() { command -v "$1" >/dev/null 2>&1; }
25
30
  say_ok() { return 0; }
26
31
 
27
32
  # Each surface: `s_<name>` = healthy? (exit 0), `f_<name>` = the repair.
28
- SURFACES=(floors herdr a2abridge hermes sessrumnir well mcp lock migrations hoard)
33
+ SURFACES=(floors herdr a2abridge hermes sessrumnir shells well mcp lock migrations hoard)
29
34
 
30
35
  s_floors() { [ -x "$SCRIPT_DIR/prereq-ensure.sh" ] && "$SCRIPT_DIR/prereq-ensure.sh" status >/dev/null 2>&1; }
31
36
  f_floors() { "$SCRIPT_DIR/prereq-ensure.sh" ensure --install >/dev/null 2>&1; }
@@ -37,6 +42,34 @@ s_hermes() { [ -x "$SCRIPT_DIR/hermes-ensure.sh" ] && "$SCRIPT_DIR/hermes-ens
37
42
  f_hermes() { "$SCRIPT_DIR/hermes-ensure.sh" ensure --install >/dev/null 2>&1; }
38
43
  s_sessrumnir(){ [ -x "$SCRIPT_DIR/sessrumnir-ensure.sh" ] && "$SCRIPT_DIR/sessrumnir-ensure.sh" status >/dev/null 2>&1; }
39
44
  f_sessrumnir(){ "$SCRIPT_DIR/sessrumnir-ensure.sh" ensure --install >/dev/null 2>&1; }
45
+ # The desktop shells: absent is healthy (the web surfaces stand alone), PARTIAL is
46
+ # not — npm gated the Electron postinstall, so the window cannot open while every
47
+ # build still passes (bin/electron-lib.sh).
48
+ shell_dirs() {
49
+ local a d
50
+ for a in hlidskjalf odrerir sessrumnir; do
51
+ for d in "$ROOT/apps/$a" "$ROOT/node_modules/@zerwiz/$a"; do
52
+ [ -d "$d" ] && { printf '%s\n' "$d"; break; }
53
+ done
54
+ done
55
+ }
56
+ s_shells() {
57
+ local d state
58
+ while IFS= read -r d; do
59
+ state="$(electron_runtime_state "$d" 2>/dev/null || true)"
60
+ [ "$state" = partial ] && return 1
61
+ done < <(shell_dirs)
62
+ return 0
63
+ }
64
+ f_shells() {
65
+ printf 'eir: the shells need YOUR hand — npm gated the runtime download:\n' >&2
66
+ electron_remedy >&2
67
+ return 1
68
+ }
69
+ if [ -z "${YMIR_ELECTRON_LIB_LOADED:-}" ] && [ -r "$SCRIPT_DIR/electron-lib.sh" ]; then
70
+ . "$SCRIPT_DIR/electron-lib.sh"; YMIR_ELECTRON_LIB_LOADED=1
71
+ fi
72
+
40
73
  s_well() { [ -x "$SCRIPT_DIR/mimir.sh" ] && "$SCRIPT_DIR/mimir.sh" health >/dev/null 2>&1; }
41
74
  f_well() { "$SCRIPT_DIR/mimir.sh" start >/dev/null 2>&1; }
42
75
  # MCP: both A2A servers wired into opencode + pi.
@@ -126,6 +159,7 @@ detail() { # <name> -> one short fact
126
159
  mcp) "echo 'a2abridge + engram'" ;;
127
160
  lock) "cat $STATE/.lock 2>/dev/null | tr -d '[:space:]' | sed 's/^/pid /' || echo none" ;;
128
161
  migrations)"echo 'structure'" ;;
162
+ shells) 'shell_dirs | while IFS= read -r d; do printf "%s %s; " "$(basename "$d")" "$(electron_runtime_state "$d" 2>/dev/null || true)"; done' ;;
129
163
  hoard) "printf 'hoard %s' \"$(_hoard_root)\" ; [ -d \"$YMIR_HOME/identity\" ] && printf ' +flat-duplicate' ; printf '\\n'" ;;
130
164
  esac
131
165
  }
@@ -151,6 +185,14 @@ done
151
185
  printf 'eir[1]{action,root,broken}:\n "%s","%s",%s\n' "$ACTION" "$ROOT" "$broken"
152
186
  printf 'health[%d]{surface,state,detail}:\n' "$count"
153
187
  printf '%b' "$rows"
188
+ # For the eye, on stderr: marks and colour; for the pipe, the TOON above.
189
+ if [ -t 2 ]; then
190
+ printf '\n' >&2
191
+ while IFS='|' read -r surface state detail; do
192
+ [ -n "$surface" ] || continue
193
+ style_line "$state" "$surface" "$detail"
194
+ done <<<"$(printf '%b' "$rows" | sed 's/^ //; s/"//g; s/,/|/; s/,/|/')"
195
+ fi
154
196
  if [ "$broken" != 0 ]; then
155
197
  printf 'eir: %s surface(s) need mending — run: bin/eir-doctor.sh fix\n' "$broken"
156
198
  exit 1
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+ # electron-lib.sh — is a desktop shell's runtime actually there?
3
+ #
4
+ # The trap this exists for: npm blocks a package's install scripts by default
5
+ # (npm 11.16+ warns, npm 12 refuses), and Electron's postinstall is what downloads
6
+ # its ~100 MB runtime. Without it the install still reports SUCCESS and the web app
7
+ # still builds — `node_modules/electron/dist/` is simply partial and `path.txt` is
8
+ # never written. So the shell fails to launch while every check says healthy, and
9
+ # nobody is told.
10
+ #
11
+ # Ymir refuses that silence: a shell is only "ready" when the runtime verifies.
12
+ # Source-safe, functions only.
13
+ #
14
+ # electron_runtime_state <app-dir> ok | partial | absent (exit 0 only for ok)
15
+ # electron_remedy the exact command that mends a partial one
16
+ set -u
17
+
18
+ electron_runtime_state() { # <app-dir>
19
+ local dir="${1-}"
20
+ [ -n "$dir" ] && [ -d "$dir" ] || { printf 'absent'; return 1; }
21
+ [ -d "$dir/node_modules/electron" ] || { printf 'absent'; return 1; }
22
+ # path.txt is written by the postinstall; dist/electron is what it points at.
23
+ if [ -f "$dir/node_modules/electron/path.txt" ] \
24
+ && [ -x "$dir/node_modules/electron/dist/electron" ]; then
25
+ printf 'ok'; return 0
26
+ fi
27
+ printf 'partial'; return 1
28
+ }
29
+
30
+ electron_remedy() {
31
+ printf 'the runtime download was skipped by npm — approve and rebuild:\n'
32
+ printf ' npm install-scripts approve electron && npm rebuild electron\n'
33
+ printf 'yonder: bin/ymir-install.sh --no-desktop if the web surfaces are enough'
34
+ }