@zerwiz/ymir 0.1.5 → 0.1.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 (118) hide show
  1. package/.agents/agents/huginn-researcher.md +1 -1
  2. package/.agents/agents/kvasir-scout.md +1 -1
  3. package/.agents/agents/sindri-developer.md +1 -1
  4. package/.agents/backend/README.md +74 -0
  5. package/.agents/backend/model-bridge.py +74 -4
  6. package/.agents/config/cron.yaml +2 -0
  7. package/.agents/config/eindri-dispatch.json +6 -6
  8. package/.agents/migrations/0003-private-data-separation.sh +11 -1
  9. package/.agents/migrations/0005-roots-out-of-tree.sh +158 -0
  10. package/.agents/skills/README.md +1 -1
  11. package/.agents/skills/galdr-ymirsystem/SKILL.md +2 -1
  12. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +73 -10
  13. package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +44 -1
  14. package/.agents/skills/galdr-ymirsystem/assets/installation.md +215 -23
  15. package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +3 -1
  16. package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +39 -1
  17. package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +4 -0
  18. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +7 -2
  19. package/.agents/skills/galdr-ymirsystem/assets/registry.md +1 -1
  20. package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +2 -2
  21. package/.agents/skills/galdr-ymirsystem/assets/smidja.md +16 -11
  22. package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +147 -4
  23. package/.agents/skills/groa-update/SKILL.md +20 -0
  24. package/.agents/skills/hamr-adapters/SKILL.md +2 -2
  25. package/.agents/skills/lifecycle/smoke_test.sh +7 -2
  26. package/.agents/skills/ymir-host/SKILL.md +2 -1
  27. package/.agents/skills/ymir-host/assets/install.md +69 -0
  28. package/AGENTS.md +78 -34
  29. package/CHANGELOG.md +1033 -1
  30. package/README.md +1 -1
  31. package/RULES/02-agents.md +2 -2
  32. package/RULES/04-hoard.md +32 -0
  33. package/RULES/06-append-only.md +50 -0
  34. package/RULES/07-config.md +56 -0
  35. package/RULES/09-electron.md +58 -0
  36. package/RULES/README.md +2 -0
  37. package/bin/a2a-mcp.sh +42 -7
  38. package/bin/agents-config.sh +30 -7
  39. package/bin/apodex-smoke-test.sh +14 -1
  40. package/bin/bifrost-bridge.sh +25 -6
  41. package/bin/bootstrap-macos.sh +6 -1
  42. package/bin/brokk-send.sh +14 -1
  43. package/bin/changelog-assemble.sh +154 -0
  44. package/bin/changelog-guard.sh +39 -7
  45. package/bin/crash-sense.sh +14 -1
  46. package/bin/design-check.sh +80 -0
  47. package/bin/design-icon.sh +153 -0
  48. package/bin/docs-guard.sh +2 -2
  49. package/bin/editor-place.sh +33 -15
  50. package/bin/eindri-acclaim.sh +14 -1
  51. package/bin/eindri-seat.sh +83 -0
  52. package/bin/eindri-seen.sh +14 -1
  53. package/bin/eindri-start.sh +14 -1
  54. package/bin/eir-doctor.sh +47 -1
  55. package/bin/feature-inventory.sh +73 -0
  56. package/bin/fleet-apply.sh +15 -2
  57. package/bin/github-deploy.sh +14 -1
  58. package/bin/gjallarhorn-expose.sh +43 -9
  59. package/bin/gjallarhorn-notify.sh +14 -1
  60. package/bin/gjallarhorn-purge.sh +14 -1
  61. package/bin/gjallarhorn-tunnel.sh +16 -3
  62. package/bin/groa-update.sh +9 -1
  63. package/bin/hall-snapshot.sh +21 -2
  64. package/bin/herdr-run.sh +23 -2
  65. package/bin/hlidskjalf-agents.sh +172 -0
  66. package/bin/hlidskjalf-usage.sh +196 -0
  67. package/bin/hoard-guard.sh +135 -0
  68. package/bin/hoard-lib.sh +91 -11
  69. package/bin/hodd.sh +28 -4
  70. package/bin/huginn-research-worker.sh +14 -1
  71. package/bin/local-model-lock.sh +22 -3
  72. package/bin/mimir-bridge.py +33 -0
  73. package/bin/mimir-bridge.sh +14 -1
  74. package/bin/mimir-reflect.sh +27 -0
  75. package/bin/mjollnir-webhook.sh +14 -1
  76. package/bin/mjollnir.sh +22 -3
  77. package/bin/model-hardware.sh +17 -3
  78. package/bin/model-resolve.sh +14 -1
  79. package/bin/nornir-job-hall-snapshot.sh +28 -0
  80. package/bin/nornir-job-nsr-compliance.sh +54 -0
  81. package/bin/npm-publish.sh +136 -0
  82. package/bin/omarchy-install.sh +18 -5
  83. package/bin/omarchy-plugins.sh +15 -2
  84. package/bin/omarchy-sense.sh +14 -1
  85. package/bin/pi-model-agnostic.sh +71 -0
  86. package/bin/private-guard.sh +20 -0
  87. package/bin/project-git.sh +18 -4
  88. package/bin/public-guard.sh +3 -2
  89. package/bin/sessrumnir-ensure.sh +1 -1
  90. package/bin/sessrumnir.sh +16 -3
  91. package/bin/smidja-bootstrap.sh +5 -1
  92. package/bin/syn-asset-pretool-check.sh +14 -1
  93. package/bin/syn-spawn-pretool-check.sh +58 -0
  94. package/bin/tailscale-sync.sh +15 -2
  95. package/bin/telegram-bot.sh +14 -1
  96. package/bin/ui-truth-check.sh +75 -0
  97. package/bin/valknut-load.sh +42 -4
  98. package/bin/wedge-notify.sh +14 -1
  99. package/bin/workspace-rag.sh +14 -1
  100. package/bin/ymir-install.sh +219 -54
  101. package/bin/ymir-plan.sh +356 -0
  102. package/bin/ymir-say.sh +14 -1
  103. package/bin/ymir-setup-auth.sh +12 -1
  104. package/bin/ymir-validate.sh +28 -8
  105. package/docs/lore.md +5 -1
  106. package/docs/runbooks/agents.md +1 -1
  107. package/package.json +13 -3
  108. package/scripts/electron.sh +74 -15
  109. package/scripts/lower.sh +34 -0
  110. package/scripts/raise.sh +42 -0
  111. package/scripts/start.sh +52 -2
  112. package/.agents/config/agents.yaml +0 -50
  113. package/.agents/config/tailscale-sync.yaml +0 -27
  114. package/.agents/memory/kaia.engram +0 -0
  115. package/.agents/memory/kaia.engram-shm +0 -0
  116. package/.agents/memory/kaia.engram-wal +0 -0
  117. package/.agents/memory/well/episodes.jsonl +0 -367
  118. package/.agents/memory/well/workspace.jsonl +0 -27
package/CHANGELOG.md CHANGED
@@ -1,5 +1,656 @@
1
+
2
+
3
+ ## 2026-09-17 — the house gains an unpublish door
4
+
5
+ - **`bin/npm-publish.sh --unpublish @scope/name@<version>`.** Publishing had a
6
+ door in this house and unpublishing had none, so the act was done by hand on a
7
+ machine whose `~/.npmrc` holds a stale token — the very reason the door exists.
8
+ The new mode resolves the token from the hoard exactly as publishing does,
9
+ demands a spec that names the version (a whole package is not a one-word act),
10
+ supports `--dry-run`, and says plainly that the registry's CDN may serve the
11
+ tarball for a while after the removal.
12
+ - **The window is 72 hours.** npm allows one version back within 72 hours of its
13
+ publication; past that only npm support can remove it. The help text says so,
14
+ because a door that does not name its own clock invites a late knock.
15
+ - **`@zerwiz/ymir@0.1.5` was unpublished** — the version that carried the memory
16
+ well. Publishing the clean 0.1.7 moves the `latest` tag off the tainted build at
17
+ once, which is the half of the repair that does not wait for propagation.
18
+
19
+ ## 2026-09-17 — the package shipped the memory well (one version, now excluded)
20
+
21
+ - **The exposure.** `@zerwiz/ymir@0.1.5` carries five files that are nobody's but
22
+ the operator's: `.agents/memory/kaia.engram`, its `-shm` and `-wal` sidecars, and
23
+ `.agents/memory/well/{episodes,workspace}.jsonl`. A public npm tarball contained
24
+ the live memory well — the store and the episodes. Verified by fetching the
25
+ published artefact and listing it; `0.1.0`–`0.1.4` are clean, and this branch's
26
+ build is clean.
27
+ - **The cause, and it is a trap worth naming.** `.gitignore` excludes
28
+ `.agents/memory/kaia.engram*` and `.agents/memory/well/*.jsonl`, and the repo is
29
+ clean — but **npm does not consult `.gitignore` when `files[]` names a whole
30
+ directory.** `files: [".agents/"]` packs that subtree *including* the ignored
31
+ files. Nothing warned: the leak travelled in the artefact, not the repo.
32
+ - **The fix.** The manifest excludes them explicitly, because a `files[]` list
33
+ cannot rely on the ignore file it overrides:
34
+ `!.agents/memory/kaia.engram*`, `!.agents/memory/well/*.jsonl`,
35
+ `!.agents/memory/*.db`, plus `!**/__pycache__/` and `!**/*.pyc` for the compiled
36
+ junk that was travelling the same way. Verified: `npm pack --dry-run` carries
37
+ 864 files and **no** memory store — the memory README and the ledger's scaffold
38
+ header are all that remain, as they should be.
39
+ - **The rule for every future package:** a `files[]` entry that names a directory
40
+ overrides `.gitignore` for everything beneath it. Name what ships, or exclude
41
+ what must not — and verify with a dry-run pack, never by assumption.
42
+ - **Remediation.** `@zerwiz/ymir@0.1.5` should be unpublished (it is inside npm's
43
+ window); the clean build publishes as 0.1.7 and supersedes it.
44
+
45
+ ## 2026-09-17 — the smithy's dependency names the package npm will actually serve
46
+
47
+ - **`@zerwiz/smidja` is published and not served.** npm accepted it twice (0.1.0,
48
+ then 0.1.1): the tarball and the version document existed, the package document
49
+ never did, and a republish of 0.1.0 was refused as *"previously published"*. The
50
+ name answers `404`, the website `403`, while `@zerwiz/hlidskjalf`,
51
+ `@zerwiz/odrerir` and `@zerwiz/sessrumnir` resolve normally and always have.
52
+ - **A second free name failed identically.** `@zerwiz/smidja-factory` was published
53
+ and shows the same shape — version served, package document withheld. Two names,
54
+ one behaviour, while packages published yesterday serve fine: the withholding is
55
+ npm's, not the name's, and the CLI does not report it.
56
+ - **The distro depends on the name that can resolve.** `optionalDependencies` now
57
+ names `@zerwiz/smidja-factory`, so the moment npm serves that package the smithy
58
+ arrives with the rest. Being optional, it is skipped silently until then — the
59
+ plan's phase-5 row is what says so out loud.
60
+ - **`@zerwiz/ymir` 0.1.7** — the version published once this lands.
61
+
62
+
63
+ ## 2026-09-17 — the four surfaces arrive by npm, and an old tree gives up what it holds
64
+
65
+ - **The app packages arrive with the distro.** `@zerwiz/ymir` now depends on the
66
+ surfaces as their own packages — `@zerwiz/hlidskjalf`, `@zerwiz/odrerir`,
67
+ `@zerwiz/sessrumnir` (live on the registry) and `@zerwiz/smiddja` (published at
68
+ 0.1.0 today). They are **optionalDependencies**, deliberately: a broken app
69
+ package must never stop the core from installing, and a package not yet on the
70
+ registry is skipped by npm and starts arriving the moment it is published.
71
+ **Proven**: a global install from the packed tarball lands the core plus all
72
+ three live apps, 330 packages, and the bin answers `0.1.6`. A local install
73
+ hoists them to `node_modules/@zerwiz/<app>`; a global one nests them under the
74
+ distro's own `node_modules` — the plan checks both shapes.
75
+ - **The plan tells the shapes apart.** "Declared as a dependency but not fetched"
76
+ is a `DO`; "no package and no dependency" is a `BLOCKED`. All four surfaces read
77
+ `DO` on a machine that has the distro but not yet the apps.
78
+ - **Migration 0005 — the operator's things leave the code tree.** It carries this
79
+ machine's records, the runtime state (pids · logs · locks · the applied-marker),
80
+ the operator's credentials, and the settings git does not track. Run on the
81
+ Allfather's machine: **44 carried, 3 stale twins kept, 1 identical duplicate
82
+ removed, 0 refused** — the tree left holding `.gitkeep` and the distro's own
83
+ tracked defaults.
84
+ - **A collision is decided by evidence, never assumption.** Two files, one name:
85
+ identical content means the tree's copy goes (the content is provably at home);
86
+ different content means both are real, so the home's keeps the name and the
87
+ tree's is carried beside it as `<name>.stale-<UTC>`. Never merged, never
88
+ discarded. A template is not only `*.example` — `agents.machine.example.yaml`
89
+ matched none of the old guards and was carried off as if it were the operator's;
90
+ it is back, and the pattern now covers `*.example.*`.
91
+ - **The plan's `purity` row applies the same law.** A settings file git *tracks* is
92
+ the distro's shipped default, not the operator's — and the check asks git about
93
+ the real path, because the tree's `config` is a symlink into `.agents/config`
94
+ and the index holds the real name. Asking about the link reported "untracked" and
95
+ flagged the distro's own `cron.yaml`. It now reads: *nothing of the operator's is
96
+ written into the code tree*.
97
+ - **`bin/npm-publish.sh --all` learns the smithy** — the registry maps `smidja` to
98
+ `apps/smidja-factory`, so it is a publish target like any other app.
99
+ - **The smithy gets a front page.** `zerwiz/smidja`'s README was a three-line stub;
100
+ it now says what Smíðja is, how to install it, how to run the visualizer on
101
+ `:8437`, what the tree holds, and the shape of a run. The tarball also carries
102
+ `LICENSE` and `NOTICE` — npm attaches the licence and the readme on its own, but
103
+ **not the notice**.
104
+
105
+ ## 2026-09-17 — the install asks with a plan, and the operator's things leave the code tree
106
+
107
+ - **The consent a real install asks for is now computed, not recited.** The old
108
+ prompt was a hardcoded paragraph, and a paragraph cannot know the host: it named
109
+ an Omarchy version on a Mac, promised a workspace tree that already stood, and
110
+ never mentioned that no application had been installed at all. `bin/ymir-plan.sh`
111
+ probes this machine and prints one row per step with its state and the reason —
112
+ `DO · SKIP · INFO · BLOCKED · CONSENT` — across nine phases (resolve · code ·
113
+ home · runtimes · engines · apps · wire · raise · verify). `ymir-install.sh`
114
+ prints it at the consent prompt; `--plan` / `--dry-run` prints it and writes
115
+ nothing; `--json`, `--phase N` and `--blocked` are there for automation and for
116
+ reading what is holding an install back.
117
+ - **The four app surfaces are named in the plan.** hlidskjalf, odrerir, sessrumnir
118
+ and smidja each get a row: whether the source is present, whether the web build
119
+ exists, and — when it is absent — the honest reason (`no apps/<name> and no
120
+ @zerwiz/<name> package`). The Electron shells are a `CONSENT` row, not a silent
121
+ download: three shells each pull a ~100 MB runtime the web surfaces do not need.
122
+ - **The home is the operator's to choose.** `step_home` asks once, records the
123
+ answer as machine state under `~/.config/ymir/home`, and every later script
124
+ resolves it through `bin/hoard-lib.sh` (`$YMIR_HOME` → the recorded choice → one
125
+ documented default). `--check` never writes; `--yes` takes what is recorded.
126
+ - **The roots law, applied across the tree.** The package is the **code that runs
127
+ the programs**; everything the operator owns lives in the home. Two resolvers
128
+ were added (`hoard_data_dir`, `hoard_state_dir`, `hoard_settings_dir`,
129
+ `hoard_local_env`) and **42 call sites** converted: `$ROOT/data` and
130
+ `$ROOT/state` (25 scripts), then `$ROOT/.env.local` and `$ROOT/config/*`
131
+ (17 scripts). A credential no longer sits in a tree that ships, and machine
132
+ records no longer sit where npm will erase them on upgrade.
133
+ - **The plan is the ward for that law.** Its phase-1 `purity` row names anything of
134
+ the operator's found in the code tree — `data/`, `state/`, `config/*.yaml`,
135
+ `.env.local` — so drift is reported on every run instead of discovered after an
136
+ upgrade.
137
+ - **The install no longer lectures about local models.** The `step_models` console
138
+ line told the operator to "load the modeltesting skill" — a skill that does not
139
+ exist anywhere in the tree — mid-install. It is gone; the real method is named in
140
+ the one place it belongs, the Galdr asset `assets/local-models.md`. `step_models`
141
+ still ensures the Pi harness and seeds `~/.pi/agent/models.json`.
142
+ - **The package no longer claims to ship `config/`.** `config` is a symlink to
143
+ `.agents/config`, which npm cannot carry; the tarball now declares what it
144
+ actually contains (147 bin scripts, the agent-set template under `.agents/`).
145
+ Settings are seeded into `$YMIR_HOME/config/agents.yaml` from the tracked
146
+ template, falling back to `.agents/config/` when the symlink is absent.
147
+ - **A corrupted TOON block in `AGENTS.md`** — `outputs[7]` carrying eight rows and
148
+ duplicate `midgard/` lines — is mended; the compliance gate reports 84 valid
149
+ blocks again.
150
+ ## 2026-09-17 — the registry says where a repo lives and what it is for
151
+
152
+ - **A project entry can now name its machine.** `machine:` records the host
153
+ where a checkout lives; `data/machines.md` holds the fleet (omarchy-1,
154
+ zerwiz, zerwizserver live on the tailnet). A repo is no longer assumed to be
155
+ on the box you happen to be standing on.
156
+ - **A project entry can now say what it is for.** `about:` is one plain
157
+ sentence per repo. `bin/project-git.sh list --field about` prints the whole
158
+ inventory without opening the YAML.
159
+ - **`project-git.sh` resolves more of the block.** `--field` now accepts
160
+ `machine`, `company`, `workspace` and `about` alongside the git fields, and
161
+ non-git keys read from the project block rather than the `git:` line.
162
+ - **One registry, not two.** The live registry is `hodd/identity/projects.yaml`
163
+ (what the runtime reads). A stale duplicate at `Documents/Ymir/identity/` had
164
+ been drifting apart from it — different GitHub owners, different project sets
165
+ — and is retired, with a backup kept in `hodd/state/`.
166
+
167
+ ## 2026-09-17 — the private home gets the ward it never had
168
+
169
+ - **Six guards watched the public repo; the home had none — yet the home is where
170
+ every private byte lives.** The one real leak of this day (`platform.env.prev-fill`,
171
+ 44 credentials) happened *there*, in a scratch backup swept up by `git add -A`,
172
+ with nothing watching. `bin/hoard-guard.sh` closes that.
173
+ - **It is a hook, not a script.** Seated as `$YMIR_HOME/.git/hooks/pre-commit`,
174
+ **git runs it on every commit** — including one made in a hurry, by a loop, or by
175
+ an agent that has never heard of it. Verified by attempting a real `git commit`
176
+ with no guard invoked: git refused, exit 1.
177
+ - **It catches what actually leaked**, verified by planting each: a scratch-backup
178
+ name (`*.prev-*`, `*.bak`, `*.orig`), a plaintext secret filename, a secret shape,
179
+ and — the evasion that would have hidden a key — **a base64 blob whose decode
180
+ contains a PEM header**, under an innocent filename.
181
+ - **`bin/ymir-install.sh` seats it at both repos** (the `hoard-gate` step reports
182
+ the home separately, so a dormant vault is visible), on every install layer.
183
+ - **A bypass is logged, not silent.** `hoard-guard.sh --log-bypass` appends to the
184
+ Runes ledger when a `--no-verify` commit is detected.
185
+ - **Two stale flat paths fixed in the guards themselves.** `bin/docs-guard.sh` told
186
+ an operator to move private docs to `$YMIR_HOME/docs/` — a path that no longer
187
+ exists — and `bin/groa-update.sh` read `$YMIR_HOME/data/eindri-homes.md` instead of
188
+ the hoard's. A guard pointing at the wrong door teaches the drift it exists to stop;
189
+ `groa-update.sh` now resolves through `hoard_root`.
190
+
191
+ ## 2026-09-17 — the NorthStar gate runs nightly; the platform door it caught is mended
192
+
193
+ - **`bin/nornir-job-nsr-compliance.sh` (02:30)** runs every
194
+ `.compliance/gates/check_*.sh` (danger · env · paths · platform · wiring)
195
+ and carves the verdict — `nsr.compliance` clean, or `nsr.compliance.failed`
196
+ (exit 1) naming the broken gate, so the 07:00 briefing reads it at sunrise.
197
+ - **The gate it caught, mended:** `scripts/electron.sh` used `kill -9`, which
198
+ the platform gate rightly refuses as non-portable. It now uses `kill -KILL`
199
+ (the house idiom already in `scripts/stop.sh`) — the NSR round is 5/5 green.
200
+
201
+ galdr-reread: `.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md` — new
202
+ §3.5 (hall snapshot) and §3.6 (NSR round) rows.
203
+
204
+ ## 2026-09-17 — the Live Hall's board becomes true: the snapshot lands on the loom
205
+
206
+ - **The Hall was a glass with nothing behind it.** The Óðrerir page reads
207
+ `apps/odrerir/public/livehall.json` same-origin, but nothing wrote it on a
208
+ schedule — the board showed the saga's own static count and said so.
209
+ `bin/nornir-job-hall-snapshot.sh` (08:00, after the 06:00 observer and the
210
+ 07:00 briefing) now drives `bin/hall-snapshot.sh` from real state — runes,
211
+ projects, the cron gauge, the wake queue, standing smiths, armed when-
212
+ sources, landed errands — and carves Rune `odrerir / hall.snapshot`.
213
+ - The snapshot is runtime, never repo: `apps/odrerir/public/livehall.json` is
214
+ gitignored, so the job never dirties a branch.
215
+
216
+ galdr-reread: `.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md` — new
217
+ §3.5 row (reads/writes table, idempotence note).
218
+
219
+ ## 2026-09-17 — the hoard gets a guard, and Rule 04 stops contradicting the disk
220
+
221
+ - **Eir gains a `hoard` surface.** Private data drifting outside the hoard is
222
+ now caught rather than discovered by asking "where did that go?". The check
223
+ fails on two things: a `.ymir-layout.yaml` entry naming a directory that does
224
+ not exist (a stale map is what lets private work land outside the hoard), and
225
+ a flat `$YMIR_HOME/{identity,data,docs,secrets,tenants}` sitting beside
226
+ `hodd/`. `fix` merges a flat duplicate into the hoard without clobbering, then
227
+ repoints any stale layout entry at the hoard.
228
+ - **Rule 04 corrected, append-only.** The rule's mapping table read as if
229
+ `hodd/x/` and `$YMIR_HOME/x/` were two locations; on a real home that produced
230
+ two parallel stores which drifted. The appended correction states the truth:
231
+ `$YMIR_HOME/hodd/` **is** the private data path, `bin/hoard-lib.sh` is the one
232
+ source of truth for it, and `.ymir-layout.yaml` must name only paths that
233
+ exist. Nothing above the correction was rewritten.
234
+
235
+ ## 2026-09-17 — the hoard encrypts its secrets at rest
236
+
237
+ - **`bin/hodd.sh` decrypts transparently.** `load`, `emit`, and `tenant` now
238
+ accept a plaintext file *or* an `.age` ciphertext with the sibling plaintext
239
+ absent. A shared `resolve_secret` finds the readable form; when it decrypts,
240
+ it uses a mode-600 temp and removes it afterward. An agent asks for the
241
+ resolved path and never handles ciphertext directly.
242
+ - **Secrets stay encrypted at rest.** `hodd/secrets/platform.env.age` is the
243
+ committed form; the plaintext is scratch and gitignored. Round-trip verified
244
+ byte-identical before the plaintext was untracked.
245
+ - **The vault invariant, written down.** The age key and the ciphertext travel
246
+ together in the private `ymirhome` repo, so a dead disk loses nothing — and
247
+ `ymirhome` **must never be made public**. Stated in `hodd/docs/secrets-vault.md`.
248
+ - **The honest limit.** This is encryption-at-rest, not encryption-against-yourself:
249
+ on a single-user machine, a process running as the operator can read what the
250
+ operator can read. What it buys is the *file* and the *repo*, not the *account*.
251
+
252
+ ## 2026-09-17 — the Forge speaks: each skill's own text in the index
253
+
254
+ - **The Forge listed skills as bare names.** `Forge.tsx` rendered only
255
+ `{s.name}` + the aett rune, while the live `/api/skills` index carries every
256
+ skill's `description` (240-char truncation) — the data was there, the
257
+ surface never showed it. The skill list items are now two-line cards: name +
258
+ aett on the head row, the description beneath (`forge-item-desc`, dimmed
259
+ until hover), scoped to the skill variant so the Eindri rows keep their
260
+ side-by-side name+status shape.
261
+
262
+ galdr-reread: `.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md` — the
263
+ Forge gate bullet (skill list shows name + aett + description).
264
+
265
+ ## 2026-09-17 — the feed follows Brokk's thinking past the composer
266
+
267
+ - **Sessrúmnir's auto-scroll never counted streamed THINKING as growth.**
268
+ `useChatScroll` watched `messages` and `streamingContent` only, so a
269
+ reasoning-heavy turn (thinking before any visible content) grew the feed
270
+ without moving it — the thinking block ended up below the floating composer,
271
+ behind it, and the Allfather had to scroll down to read it.
272
+ - **The growth signal now also includes `streamingThinking` and streaming tool
273
+ calls.** While Brokk reasons, the feed keeps the thinking tail in view above
274
+ the composer (still only when Auto Scroll is on and the reader is at the
275
+ bottom — scrolling up to re-read never gets yanked).
276
+ - Live hall updated: rebuilt renderer delivered to the running Sessrúmnir and
277
+ the app restarted via `bin/sessrumnir.sh` (window back on workspace 8).
278
+
279
+ ## 2026-09-17 — the editor stops being banished to its own desktop
280
+
281
+ - **`bin/editor-place.sh` pinned the editor to its own desktop with
282
+ `no_focus = true`** — VS Code mapped onto a different workspace than the one
283
+ the Allfather worked on and never took focus, so nothing inside it was
284
+ clickable. The rule was written by Ymir itself into
285
+ `~/.config/hypr/ymir-desktops.lua`.
286
+ - **The editor is now UNMANAGED by default**: it opens on the active desktop,
287
+ focused and clickable, like any other app. The old pin remains reachable
288
+ explicitly with `YMIR_EDITOR_PIN=1 bin/editor-place.sh apply`.
289
+ - Live desk healed: the `code` windowrule was removed from
290
+ `~/.config/hypr/ymir-desktops.lua` (backup kept alongside), Hyprland reloaded
291
+ with no config errors, and the stranded VS Code windows were moved to the
292
+ active desktop and focused.
293
+
294
+ ## 2026-09-17 — models, providers, and paths are the user's — and the gate says so
295
+
296
+ - **The law, amended (Rule 07).** Two clauses appended, the rule itself untouched:
297
+ - **A. Models and providers live in the hoard, per user, read from YAML.**
298
+ Every operator's differ, so there is **no global model or provider** — a
299
+ shipped default naming a model is a claim about someone else's machine. The
300
+ user's own live in `$YMIR_HOME/hodd/`, read from a YAML file there; the repo
301
+ ships a template.
302
+ - **B. No hardcoded absolute file paths.** No `/home/<user>/…`, no
303
+ `/Users/<user>/…`, no `C:\Users\…`. A path is relative to a resolved root or
304
+ comes from env/config with one documented default.
305
+ - **The gate, `config`.** `compliance-check.sh` gains a check that fails on a
306
+ model id or provider name used as a **value** in shipped code or a tracked
307
+ config, and on an absolute path naming a user. Comments, `*.example` templates,
308
+ `CHANGELOG*`, and `assets/reference/` are exempt by intent — they document,
309
+ they do not configure. **192 files clean.**
310
+ - **Three real violations found and fixed:**
311
+ - `.agents/config/eindri-dispatch.json` — three dispatch rules named
312
+ `opencode-go/deepseek-v4.1-flash`. Now a template with `<your-model-id>`.
313
+ - `.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml` — the PI boot
314
+ profile named `lmstudio/qwen3.6-35b-a3b`. Now a placeholder.
315
+ - `bin/bootstrap-macos.sh` — `/home/ubuntu/Documents/Ymir`. Now resolved from
316
+ `YMIR_VM_USER` with one documented default.
317
+ - **Two false positives found and fixed in the gates themselves.** The `mocks`
318
+ gate flagged a prose comment explaining why a guard exists; the new `config`
319
+ gate first flagged a sentence mentioning `llama.cpp/llama-swap`. Both now
320
+ exclude comments and require a value position — a gate that cries wolf is a
321
+ gate that gets ignored.
322
+ - **Four pre-existing failures on main mended** so the gate could be added to a
323
+ green tree: a duplicated `midgard/` row in `AGENTS.md` (a bad merge left three),
324
+ the `mocks` false positive, `smidja-factory` indexed but app-provided, and the
325
+ seat's cloth read from an app repo that is absent until `step_apps` runs
326
+ (`design-check` now skips cleanly rather than failing every worktree).
327
+
328
+ ## 2026-09-17 — midgard holds public assets only; the company wiki is gone
329
+
330
+ - **`midgard/company_wiki/` did not belong in this repo.** It was an empty
331
+ placeholder (one `.gitkeep`), but a company wiki is *tenant data* — policies,
332
+ vision, specs, client names — and this repo is public. Removed, with every
333
+ reference to it corrected in `midgard/README.md`, `STRUCTURE.md`, `docs/lore.md`
334
+ and `assets/Ymir.md`.
335
+ - **`midgard/` is now described precisely.** It was called "shared company
336
+ assets" in one place and "cross-tenant" in another; a wiki is neither. The
337
+ contract says **public assets**, and `midgard/README.md` names what does *not*
338
+ belong: company knowledge, which lives at `$YMIR_HOME/hodd/identity/companies/`.
339
+ - **`bin/private-guard.sh` gains a tenant-document rule** so the shape cannot
340
+ return: a `company_wiki/`, `wiki/`, `policies/`, `handbook/` or `vision`/
341
+ `strategy`/`roadmap` document staged in the public tree is refused, with a
342
+ message naming where it belongs. Verified by planting `midgard/company_wiki/policies.md`.
343
+ - **Nothing had leaked.** A full sweep of every tracked midgard file for personal
344
+ handles, home paths, key shapes, tunnel UUIDs and LAN/tailnet addresses found
345
+ no hits — the ingress configs use `${VARS}` and `example.org` throughout, and
346
+ the DB schema is generic with row-level security on every tenant table.
347
+
348
+ ## 2026-09-17 — AGENTS.md states the one law: no private data in the public repo
349
+
350
+ - **The law is now first.** A `first_law` block sits directly under the mandate
351
+ in `AGENTS.md`: never store personal or private data in this repo — not a
352
+ secret, a key, a name, a plan, a schedule, a client, a credential, or a note.
353
+ The repo is public; private data lives at `$YMIR_HOME`, under `hodd/`.
354
+ - **The private-data section was rebuilt against the disk.** It had duplicated
355
+ `secrets/` and `identity/` lines, no `hodd/` level, and a layout that no longer
356
+ matched the home. It now shows the real tree, names `$YMIR_HOME/hodd/` as *the*
357
+ private data path, and records that `bin/hoard-lib.sh` is the one source of
358
+ truth for it.
359
+ - **Every `$YMIR_HOME/...` path in the file was corrected** to `$YMIR_HOME/hodd/...`
360
+ — the directory rules table, the registry reference, the secrets reference, and
361
+ the append-only ledger path all pointed at the old flat layout.
362
+ - **Two wards are now documented**: `bin/secret-guard.sh` (a secret entering the
363
+ repo) and Eir's `hoard` surface (private data drifting outside the hoard, or a
364
+ `.ymir-layout.yaml` naming a path that does not exist).
365
+ - **Staging discipline is written down.** Stage named files in the home, never
366
+ `git add -A` — a scratch file written seconds earlier gets swept into a commit
367
+ and pushed. This happened this day and cost a history rewrite.
368
+ - **`hodd/AGENTS.example.md`** carried the same stale flat paths and a wrong
369
+ command (`secret-guard.sh emit`, which does not exist); both corrected.
370
+
371
+ ## 2026-09-16 — the validator stops calling a dead visualizer healthy
372
+
373
+ `bin/ymir-validate.sh` reported `visualizer PASS "UI built and served on :8437"`
374
+ by looking at `./dist` alone. When the API had crashed on a bad `CMD_DB` and
375
+ nothing was listening on `:8437`, the validator still said PASS — the false-pass
376
+ class this night kept surfacing, in the very tool meant to catch it.
377
+
378
+ - **The check now probes the port as well as the build.** `dist` missing → FAIL;
379
+ built but nothing on `:8437` → FAIL with the fix (`run scripts/start.sh`);
380
+ built and listening → PASS. A PASS now means the thing is actually up.
381
+ - **The smidja-db check resolves the same DB pair** `scripts/start.sh` and
382
+ `bin/smidja-bootstrap.sh` do (an existing `$YMIR_HOME/smidja/smidja.db` first,
383
+ then an in-repo copy, `SMIDJA_DB` override) — previously it named the home path
384
+ only, so an in-repo DB read as missing.
385
+ - `galdr-reread`: `assets/installation.md` — a `visualizer` PASS means built *and*
386
+ listening.
387
+
388
+ ## 2026-09-16 — the roster's effect lands, and the backend pin stops being committable
389
+
390
+ Applying the private roster (`bin/agents-config.sh apply`) writes the resolved
391
+ model into each agent's canonical profile. Flipping the two local agents to Pi
392
+ made that visible — and surfaced a small ignore hole.
393
+
394
+ - **Profiles follow the roster.** `sindri-developer.md` and `kvasir-scout.md`
395
+ now carry their **Pi** ids (`llamacpp-coder/qwen3-coder-30b`,
396
+ `llamacpp/qwen3.5-9b`), and `huginn-researcher.md` its declared model — the
397
+ output of `apply` against the operator's roster. These profiles are what the
398
+ harnesses load, so the local agents now run on Pi rather than through OpenCode.
399
+ - **`.agents/config/backend` is ignored.** The pinned terminal backend is
400
+ machine-local, but `config` is a **symlink** to `.agents/config`, so the
401
+ `config/*` rule never matched it and the pin was committable. The file is named
402
+ in `.gitignore` instead.
403
+
404
+ ## 2026-09-16 — the roster can finally run an agent on Pi
405
+
406
+ `bin/agents-config.sh apply` wrote **every** agent's model into `opencode.json`'s
407
+ agent block, whatever harness that agent used. So an agent set to run on **Pi**
408
+ (native local models) would have had its Pi model id —
409
+ `llamacpp/qwen3.5-9b` — written into OpenCode's config, which cannot resolve it.
410
+ That is why the roster's two local agents were pinned to `harness: opencode` with
411
+ opencode-style ids: there was no working way to put an agent on Pi.
412
+
413
+ - **`apply` is now harness-aware.** Only `opencode`-harness agents are written
414
+ into `opencode.json`; a `pi` (or `hermes`) agent is left out, its model id going
415
+ to the resolve cache that `bin/agent-run.sh` reads. The providers block is
416
+ unchanged — a provider's endpoint is a real fact OpenCode may still want.
417
+ - The way this is meant to be used: declare a local agent's model as its **Pi**
418
+ id (`llamacpp/qwen3.5-9b`, `llamacpp-coder/qwen3-coder-30b` — the ids `pi
419
+ --list-models` reports) with `harness: pi`; the roster is then Pi-driven with no
420
+ per-machine hand-editing.
421
+ - `galdr-reread`: `assets/harness-integration/README.md` — the two writers of
422
+ `opencode.json`, and the rule that only OpenCode agents belong in it.
423
+
424
+ ## 2026-09-16 — the mesh reaches Teams and Anchor, and `show` stops guessing
425
+
426
+ `bin/a2a-mcp.sh` wired only the well (`engram`) and the mesh (`a2abridge`). The
427
+ **Teams** plane and **Anchor** memory existed only in the operator's personal
428
+ harness config, so no agent *inside* Ymir could see tickets or anchored memory —
429
+ and `a2a-mcp.sh show` insisted on a **fixed** server list, reporting
430
+ `wayofteams-mcp not on PATH` even when the plane was wired and working.
431
+
432
+ - **Remote MCP servers, URL-driven.** `bin/a2a-mcp.sh` now wires `wayofteams`
433
+ (from `WAYOFTEAMS_MCP_URL`) and `way-of-anchor-sse` (from `ANCHOR_MCP_URL`).
434
+ OpenCode gets them natively (`type: remote`); Pi, which has no remote
435
+ transport, gets them through the `mcp-remote` stdio bridge. A URL wins over a
436
+ local `wayofteams-mcp` binary when both exist. The URLs are credential-ish and
437
+ live in the private platform env — never the tracked tree.
438
+ - **`show` reports what is actually wired.** It enumerates every key present in
439
+ Pi's `mcpServers` and OpenCode's `mcp` (union with what this run would add), so
440
+ a hand-added or previously-installed server is visible, and the hint only fires
441
+ when Teams/Anchor is genuinely absent.
442
+ - `galdr-reread`: `assets/harness-integration/README.md` — the MCP scope is now
443
+ well · mesh · Teams · Anchor, with the two env keys named.
444
+
445
+ ## 2026-09-16 — the credential reaches the gate, and the hall stops leaking an IP
446
+
447
+ Setting the operator password had **no effect**. This is the bug that made that
448
+ true, and the runtime state that leaked a private address.
449
+
450
+ - **`scripts/start.sh` never loaded the platform env for the gate.** The gate is
451
+ `bun run apps/hlidskjalf/server/index.ts`, and the server reads
452
+ `process.env.HLIDSKJALF_AUTH` — but the launcher passed only `PORT`, and nothing
453
+ sourced `.env.local`. So a credential written by `bin/ymir-setup-auth.sh`
454
+ (correctly: `0600`, gitignored) never arrived, and because the gate treats an
455
+ empty `GATE_AUTH` as authenticated (`authed: GATE_AUTH ? … : true`) the gate
456
+ stayed **open**. `start.sh` now loads `.env.local` once, before the ports, for
457
+ every service it raises — the gate, Bifrost and Mimir.
458
+ - **`apps/odrerir/.astro/dev.json` was tracked.** Astro rewrites it on every
459
+ start with the live `pid` and the host's own addresses (LAN + tailnet) — a
460
+ private-IP leak into a public tree. It is now untracked and gitignored; the
461
+ generated types beside it stay tracked.
462
+ - `galdr-reread`: `assets/hlidskjalf-ui.md` (the gate must receive the credential)
463
+ and `assets/odrerir-hall.md` (`.astro/dev.json` is runtime state).
464
+
465
+ ## 2026-09-17 — the fragment convention is enforced where GitHub runs it
466
+
467
+ - **The gap, found by doing it.** The pre-push hook folds fragments before a
468
+ push — but a **GitHub merge bypasses the local hook entirely**. PR #43 merged
469
+ and left its own fragment unfolded on `main`, so the ledger silently fell
470
+ behind what the fragments already told. `bin/changelog-assemble.sh --check`
471
+ catches exactly that state; nothing was calling it.
472
+ - **The gate, at the layer that runs.** `.github/workflows/ymir-changelog-fragments.yml`:
473
+ - **on `push` to main** — `bin/changelog-assemble.sh --check` fails when any
474
+ fragment is unfolded, so the ledger can never fall behind a merge.
475
+ - **on a PR** — a change touching code or docs must add a fragment, edit
476
+ `CHANGELOG.md`, or carry the `no-changelog` label. The label records the
477
+ deliberate decision that no entry belongs, rather than letting it be
478
+ forgotten.
479
+ - **The pattern is the standard one.** This is the well-trodden "news fragments"
480
+ design (towncrier — Twisted, pytest, pip, attrs; changesets; CPython). The
481
+ zeroc-ice proposal states the root cause exactly: *"PRs edit one shared file.
482
+ Better merge discipline doesn't fix it; not editing the shared file does."*
483
+ - **`merge=union` was considered and rejected on evidence.** The Rigor ADR-105
484
+ tried it and measured it: *"GitHub's PR-mergeability and merge computation
485
+ ignore `.gitattributes` merge drivers, union included."* It works against local
486
+ git and does nothing on GitHub — a trap already falsified.
487
+ - **Main's own pending fragment is folded in this change**, so the ledger and the
488
+ fragments agree from here on.
489
+
490
+ ## 2026-09-17 — the changelog stops conflicting: fragments, folded by a script
491
+
492
+ - **The problem, measured.** `CHANGELOG.md` is one file and every branch appends
493
+ at the same position — the top. Git sees two branches inserting different lines
494
+ at the same spot and calls it a conflict, so **every merge re-conflicts every
495
+ open branch**. Sixteen open PRs were each resolved by hand for this reason
496
+ alone, and each merge re-conflicted the rest: O(N²) meaningless conflicts.
497
+ - **The fix — `CHANGELOG.d/`.** A change adds **one file with a unique name**
498
+ (`<YYYY-MM-DD>-<slug>.md`) instead of editing the ledger. Two branches never
499
+ touch the same fragment, so the collision cannot occur.
500
+ - **`bin/changelog-assemble.sh`** folds fragments into `CHANGELOG.md` — newest
501
+ first, above everything already recorded. Folding is an **append**: existing
502
+ entries are copied verbatim, never reordered, never rewritten. `--dry-run`
503
+ shows what would fold; `--check` exits 1 when fragments are unfolded.
504
+ - **The pre-push hook folds first.** `bin/changelog-guard.sh --install` now
505
+ writes a hook that runs the assembler before the guards, so a push never leaves
506
+ fragments unfolded; if folding changes the ledger the push is refused until the
507
+ fold is committed.
508
+ - **The guard accepts either form.** A push satisfies the duty by appending to
509
+ `CHANGELOG.md` **or** by adding a fragment. The refusal message now names both.
510
+ - **Rule 06 is amended, append-only** — the clause stands; the amendment records
511
+ fragments, and `CHANGELOG.d/` joins the append-only set so a move must carry it.
512
+
513
+ ## 2026-09-17 — the hearth re-seeds on its container: Sessrúmnir's chat fire returns
514
+
515
+ - **The ember glow was gone from the chat because the canvas sized itself once
516
+ at mount and only re-seeded on `window.resize`.** Sessrúmnir's split panes
517
+ (file tree · editor · image) resize the chat column without any window resize,
518
+ so the hearth kept a stale or zero-sized canvas — invisible. Both the shared
519
+ hearth (`midgard/design-system/ember.js`) and Sessrúmnir's React port
520
+ (`ember-background.tsx`) now watch their container with a ResizeObserver and
521
+ re-seed only when the measured size actually changes (never per-frame).
522
+ Hlidskjalf inherits the same mend through the shared module.
523
+ - **Verified live**: Sessrúmnir restarted on the fresh build; a screen capture
524
+ of the running window shows the fire drawing (ember + warm-glow pixels across
525
+ the hearth area). The running app had been serving a stale bundle (missing
526
+ `react-i18next` — deps installed, rebuilt).
527
+
528
+ galdr-reread: `.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md` — the
529
+ EmberBackground hearth section (shared module + ResizeObserver mend).
530
+
1
531
  # CHANGELOG
2
532
 
533
+ ## 2026-09-17 — Pi gets its agents, and a dead extension comes back
534
+
535
+ - **Pi has no agent loader.** Agent loading in Pi is a *package* (`pi-agents`,
536
+ `pi-agent-mode`, `pi-simple-agents`), never a core feature — and Ymir installs
537
+ none. So `.pi/agents/` held twenty correct profile links that **nothing in Pi
538
+ ever read**. The same failure as OpenCode's singular `.opencode/agent/`,
539
+ arrived at from the other side.
540
+ - **The fix ships in this repo, not a root-pi package.**
541
+ `.pi/shared/extensions/ymir-subagents.ts` discovers the canonical
542
+ `.agents/agents/*.md` tree itself and registers a `subagent` tool. A call runs
543
+ the chosen figure as a nested model call in the current session: the figure's
544
+ markdown body is its system prompt, its frontmatter `model:` picks the model
545
+ where the machine serves it. `subagent({})` and `/subagents` list the roster.
546
+ - **It imports nothing.** `@earendil-works/pi-coding-agent` is not installed as a
547
+ package, so an extension importing its types cannot load at all. This one takes
548
+ `pi` as `any` and declares `parameters` as a plain JSON schema — the pattern
549
+ every working extension in the tree already uses.
550
+ - **A rename that left a reader behind.** `skuld-branch-supervision.ts` imported
551
+ `calmTranscriptClassIsVisible` / `CalmPresentationState` from
552
+ `./lib/ro-visibility.ts`, but that module exports `roTranscriptClassIsVisible` /
553
+ `RoPresentationState`. Pi refuses the **whole extension** at load, so Skuld's
554
+ supervision branch was dead in every session and nothing reported it. Fixed.
555
+ - **The check that catches it** is now recorded in the owning asset: import every
556
+ deployed extension with `node --input-type=module` and confirm it loads — a
557
+ module that cannot resolve is invisible from the file listing.
558
+ ## 2026-09-17 — every figure loads: the agent directory was never read
559
+
560
+ - **The bug, stated plainly.** Twenty agent profiles existed, twenty symlinks
561
+ pointed at them, and every gate was green — yet OpenCode loaded **three**
562
+ agents. The symlinks lived in `.opencode/agent/` (SINGULAR). OpenCode reads
563
+ `.opencode/agents/` (PLURAL). Twenty correct links sat in a directory no loader
564
+ opened, so only `brokk` and `hnoss` — the two declared by hand in
565
+ `opencode.json` — ever appeared.
566
+ - **The directory is fixed and migrated.** `.opencode/agent/` → `.opencode/agents/`
567
+ (20 links, all resolving). `bin/valknut-load.sh` now writes the plural path and
568
+ **migrates a legacy singular dir forward**, so an old home heals instead of
569
+ silently keeping its agents invisible.
570
+ - **The roster now declares what it names.** `bin/agents-config.sh apply` only
571
+ touched an agent already present in `opencode.json` (`if a in ablock`), so a
572
+ figure the roster knew but the config had never seen stayed undeclared. The
573
+ roster is the source of truth; a name in it now reaches the harness config.
574
+ - **A new gate catches the class.** `compliance-check.sh` gains **`roster`**: the
575
+ roster (`config/agents.yaml.example`) and the canonical tree
576
+ (`.agents/agents/*.md`) must name the same figures. A figure in the tree but not
577
+ the roster falls to `default_model` and is never declared; a figure in the
578
+ roster with no profile is a phantom. This is the check that would have caught
579
+ the real failure while every other gate stayed green.
580
+ - **Verified, not assumed.** `opencode agent list` in the worktree reports all
581
+ twenty figures (bragi … vor) plus Ymir's own subagents — 33 agents, up from 3.
582
+ Compliance: 14/14 PASS.
583
+ - **References swept.** Every `.opencode/agent/` reference in code and docs moved
584
+ to the plural path: `bin/valknut-load.sh`, `bin/agents-config.sh`,
585
+ `apps/hlidskjalf/server/index.ts`, `RULES/02-agents.md`, `README.md`,
586
+ `docs/runbooks/agents.md`, `.pi/extensions/README.md`, and the Galdr assets
587
+ (`harness-integration`, `runtime-compliance`, `registry`, `hlidskjalf-ui`).
588
+ `CHANGELOG.md`'s own historical entry is left as written — append-only.
589
+
590
+ ## 2026-09-16 — the core senses the real host, not Omarchy's shadow
591
+
592
+ - **`bin/ymir-install.sh` `step_host` ran the wrong sensor.** The core host step
593
+ called `bin/omarchy-sense.sh` — the **Omarchy-only** sensor — so on any
594
+ non-Omarchy host it learnt nothing and printed the sensor's own SKIP row as a
595
+ host snapshot: `host snapshot: sensor,SKIP,not an Omarchy host…`. On a Fedora
596
+ workstation that is the whole discovery step, reporting a skip as fact.
597
+ - **Now it senses THIS machine with `bin/host-sense.sh`** — the portable sensor
598
+ (the same one behind `ymir sense`) — on **every** host (Rule 05). Recording
599
+ stays the Omarchy layer's job: `omarchy-sense observe`, the post-update hook,
600
+ desktop placement, the plugin offer, and the alarm channel remain gated behind
601
+ `step_omarchy`. Nothing here assumes Omarchy; nothing here skips the core
602
+ machine either.
603
+ - `step_host` no longer branches on `--check`; it senses once and reports once:
604
+ `host sensed: <os> / <id> / <family> / <session> / <desktop>`.
605
+ - The owning asset reflects it in the same change:
606
+ `.agents/skills/galdr-ymirsystem/assets/installation.md` — the `host` step row,
607
+ the Verify list, the two-layer table (recording, not learning), and the
608
+ "Omarchy branches" note.
609
+ ## 2026-09-16 — the visualizer finds its DB, and the seat-hall actually builds
610
+
611
+ Two more readers left behind — both discovered by *starting the apps*, not by any
612
+ gate. Each failed silently in its own way.
613
+
614
+ - **`scripts/start.sh` pointed the smithy at a repo path that no longer holds the
615
+ DB.** `0003-private-data-separation` moved it to `$YMIR_HOME/smidja/smidja.db`,
616
+ but the starter still passed `CMD_DB=<repo>/apps/smidja/smidja_data/smidja.db`
617
+ — so the visualizer API died on boot with `smidja.db not found` and `:8437`
618
+ answered nothing. It now resolves the same pair `bin/smidja-bootstrap.sh` does
619
+ (home first, in-repo fallback, `SMIDJA_DB` override). The asset already
620
+ *described* this resolution; the code now implements it.
621
+ - **`bin/sessrumnir-ensure.sh` never built the app.** `[ "$built_present" ]` tests
622
+ a **literal, non-empty string** — always true — so `ensure --install` skipped
623
+ `build_app` and reported `built: no` with exit 0. Sessrúmnir only built when the
624
+ build was run by hand. Now `built_present` (the function) is called.
625
+ - `galdr-reread`: `.agents/skills/galdr-ymirsystem/assets/smidja.md` — the DB
626
+ path, the `scripts/start.sh` DB resolution, and the observer's read list.
627
+
628
+ ## 2026-09-16 — four readers that still pointed at the pre-split world
629
+
630
+ The app split and the hoard migration moved things; four readers never followed.
631
+ None failed loudly — each reported a clean PASS, a wrong SKIP, or a silent loss.
632
+
633
+ - **`bin/ymir-install.sh` wrote during `--check`.** The step chain ran
634
+ `bin/ymir-migrate.sh apply` unconditionally, so a preview that promises *"report
635
+ only, no writes"* actually **moved private data**. Migrations now apply only on
636
+ a real run: `if [ "$CHECK" = 0 ]; then ... apply; fi`.
637
+ - **`.agents/migrations/0003-private-data-separation.sh` skipped every directory.**
638
+ It pre-creates its target dirs, then its `copy` refused any target that already
639
+ existed — so `data/` (and every other directory source) was silently dropped.
640
+ The realm declaration never arrived and `0004` fell back to a neutral realm.
641
+ `copy` now **merges** a directory into its target (never overwriting a file) and
642
+ copies a single file only when it is absent.
643
+ - **`bin/smidja-bootstrap.sh` looked for the smithy at the old root.** `sys.path`
644
+ pointed at `smidja/`, but the split moved it to `apps/smidja/`, so the DB seed
645
+ died with `ModuleNotFoundError: No module named 'smidja_modules'`. It now
646
+ searches `apps/smidja` then `smidja`, so either layout works.
647
+ - **`bin/ymir-validate.sh` read the ledger from the pre-move path.** It looked at
648
+ `$YMIR_HOME/memory/`, but `0004-hoard-and-realms` put the ledger at
649
+ `$YMIR_HOME/hodd/memory/` — a false `runes FAIL` on a healthy home. It now tries
650
+ the hoard first, then the pre-move locations.
651
+ - `galdr-reread`: `.agents/skills/galdr-ymirsystem/assets/smidja.md` — the
652
+ protected-paths gotcha and the `smidja/` → `apps/smidja/` move.
653
+
3
654
  ## 2026-09-16 — Omarchy-first, host-aware: Windows and macOS get a door
4
655
 
5
656
  - **`bin/host-sense.sh`** — the one place that looks before anything acts. It
@@ -633,7 +1284,6 @@ subsystem was never loaded before the code changed. Five layers now prevent it:
633
1284
  - **GitHub:** `bin/project-git.sh` reads `workspace/projects.yaml` `git{}`.
634
1285
  - **Verified:** build green, compliance 8/8, smoke 8/8, lint 4/4.
635
1286
 
636
-
637
1287
  All significant runtime, policy, and architectural changes for the Ymir platform.
638
1288
  Entries are appended chronologically; never rewritten.
639
1289
 
@@ -879,3 +1529,385 @@ Entries are appended chronologically; never rewritten.
879
1529
  `.agents/state/.wake-queue`) are empty, a repeat carry is the same drained
880
1530
  news and is skipped. Genuine new content (different message, or a door with
881
1531
  a line) always delivers. Harness asset updated in the same change.
1532
+
1533
+ ## 2026-09-16 — one login for every app on the web; the gate fronts them all
1534
+
1535
+ - **Every app host is gated**, not just Smíðja. The gate now routes by host from
1536
+ an `APP_HOSTS` table and fronts each one: unauthenticated visitors get that
1537
+ app's own sign-in page, an API path gets 401, and the desktop seat (loopback +
1538
+ marker) walks straight in. Verified: `smidjadell…` → *Smíðja — Sign in*,
1539
+ `odrerirdell…` → *Óðrerir — Sign in*, API 401, desktop seat 200 (the hall).
1540
+ - **Every public hostname points at the gate (:3889)** — never at an app's own
1541
+ port. That was a hole straight past the login: `gjallarhorn-expose.sh` had been
1542
+ exposing each app on its own port, so the app answered the world directly.
1543
+ - **The gate learns the host map at start** (`state/gjallarhorn-hosts.env`,
1544
+ written by the expose script and sourced by `scripts/start.sh`), so routing is
1545
+ not an accident of whoever launched it last.
1546
+ - **No guessed credentials.** The generated tunnel config wrote
1547
+ `credentials-file: …/ymir.json`; cloudflared names that file for the tunnel's
1548
+ UUID, so it refused to start and the world got 530. The line is gone —
1549
+ cloudflared resolves a named tunnel's own credentials.
1550
+ - Verified through the tunnel: `https://ymirdell.zerwiz.org/` → 200. The app
1551
+ hostnames still answer 530: their DNS routes were made against an earlier
1552
+ tunnel and need re-creating (`cloudflared tunnel route dns ymir <host>`).
1553
+
1554
+ ## 2026-09-16 — NO BLUE: the forge language replaces the old blue
1555
+
1556
+ The blue had a name. It was the platform's own house tint, `--ymir-house-ymirlabs:
1557
+ #38bdf8`, carried into every surface that wears Ymir's colours — the login, the
1558
+ emblem, the realm data, the Smíðja chrome. The design doctrine (homepage repo,
1559
+ `DESIGN-UNIFICATION-PLAN.md`) says the landing page **is** the language, and that
1560
+ language is **forge**: stone ground (`#0e0c09`), bronze accent (`#c9973f`), bone
1561
+ text (`#cfc3a9`).
1562
+
1563
+ - `midgard/design-system/tokens.css` — the house tint is bronze now, with the
1564
+ reason recorded in the token itself.
1565
+ - The blue's footprints removed: `Emblem.tsx`, `state/store.ts`, `data/realms.ts`
1566
+ (Hlidskjalf), `style.css` (Smíðja visualizer), and Sessrúmnir's terminal accent
1567
+ fallback.
1568
+ - Both built stylesheets rebuilt and verified: **zero blue** in the CSS the browser
1569
+ actually receives (`apps/hlidskjalf/dist`, the visualizer's `dist`).
1570
+ - The visualizer's build was failing on my own earlier edit (unused imports left
1571
+ when I moved `repoRootOf` into `db.ts`) — fixed, so `bun run build` is green.
1572
+
1573
+ The canonical token home is answered by what already exists: `midgard/design-system/`
1574
+ (tokens, icons, `ymir-mark.svg`, `icons.md`) — one source the apps import, so a
1575
+ colour changes once.
1576
+
1577
+ ## 2026-09-16 — every app wears its own rune
1578
+
1579
+ The icon set is runecoded (`midgard/design-system/icons.md`): an Elder Futhark
1580
+ rune, stroked at the chisel bevel, tinted by the app's house colour. The apps were
1581
+ wearing the generic Ymir mark — or, worse, the old **blue** logo.
1582
+
1583
+ - **`bin/design-icon.sh`** mints an app's icon from a glyph plus a house tint: a
1584
+ stone tile with the rune stroked in bronze. `list` shows the mapping.
1585
+ - Minted and wired: **Hlidskjalf** `ehwaz` ᛖ (the seat), **Óðrerir** `valhalla` ᚹ
1586
+ (the hall), **Sessrúmnir** `sowilo` ᛊ (the sun), **Smíðja** `ansuz` ᚨ (Odin's
1587
+ breath — the forge). Óðrerir already carried a forge-coloured favicon set.
1588
+ - **The blue logo is retired**: `public/logo.svg` removed, and Smíðja's *inline*
1589
+ copy in `App.vue` — the mark in its own header, still `#0f172a`/`#1e293b` —
1590
+ replaced with the ansuz rune in bronze. Rebuilt; no blue in the bundle.
1591
+
1592
+ ### The hearth stays, and spreads
1593
+
1594
+ Sessrúmnir's background fire is **`EmberBackground`** — 26 embers rising with a
1595
+ gentle sway on a canvas, *"the hearth of the landing page, carried into the
1596
+ chat"* — used by its home screen and chat panel. It stays. Next: one shared
1597
+ ember (a framework-neutral `midgard/design-system/ember.js` with a
1598
+ `prefers-reduced-motion` guard) so Hlidskjalf's shell, the login screen,
1599
+ Óðrerir's hall and the Smíðja chrome can warm the same fire.
1600
+
1601
+ ## 2026-09-16 — the hearth spreads: one fire, three more surfaces
1602
+
1603
+ Sessrúmnir's fire is now shared rather than copied. `midgard/design-system/ember.js`
1604
+ is the framework-neutral original (the land page's Ginnungagap embers: 30 rising
1605
+ with a gentle sway over six drifting haze pools), with `ember.d.ts` for TypeScript
1606
+ consumers and a reader who asked for less motion gets a **still frame** instead of
1607
+ an animation.
1608
+
1609
+ Wired, and built:
1610
+
1611
+ - **Hlidskjalf** — `src/components/EmberBackground.tsx` imports the shared module;
1612
+ mounted behind the *login* and behind the *shell*, so the door and the hall burn
1613
+ over one hearth.
1614
+ - **Smíðja visualizer** — a `<canvas class="ember-bg">` behind every view.
1615
+ - **Óðrerir** — a `data-ember` canvas in its shell, so the hall warms too.
1616
+ - Sessrúmnir keeps its own React port untouched (two ports of one fire, the same
1617
+ physics; consolidating them on the shared module is the next tidy).
1618
+
1619
+ And the reference for the missing furniture: **Óðrerir already carries it all** —
1620
+ four favicons, `mask-icon` in bronze, `site.webmanifest`, canonical, description,
1621
+ theme-colour and Open Graph. The other apps should be brought up to *it*.
1622
+
1623
+ ## 2026-09-16 — icons the operator can pin, and one pair of hands for the shells
1624
+
1625
+ **"Fail, no icons" — and the cause was a trap I had already been bitten by.**
1626
+ `bin/design-icon.sh install` writes an app's rune into the icon theme and a
1627
+ `.desktop` entry into the applications dir. It used `XDG_DATA_HOME`, and this
1628
+ agent session exports that as a **sandbox** (`…/opencode-rd`), so the icons went
1629
+ somewhere no desktop can see. It now targets the operator's real data dir
1630
+ (`$HOME/.local/share`, with `YMIR_DESKTOP_DATA_HOME` for a throwaway test) — the
1631
+ same class of bug that once hid `gh` from this session.
1632
+
1633
+ Installed now, for **every** app: hlidskjalf, **odrerir**, **sessrumnir**, the
1634
+ Smíðja visualizer and hlidskjalf-mobile — each with its rune icon and a dockable
1635
+ entry. Óðrerir and Sessrúmnir had **none**, which is exactly why they could not
1636
+ be pinned. The stale `ymir-smidja.desktop` (pointing at a repo PNG) is folded
1637
+ into the rune's own entry.
1638
+
1639
+ **One pair of hands for two lifecycles.** `scripts/start.sh` and
1640
+ `scripts/stop.sh` manage the web; `scripts/electron.sh` manages the shells — and
1641
+ they never met, which is why "the electrons are not running" was true while 28
1642
+ processes stood, and why a restart could leave a shell watching a dead port.
1643
+ `scripts/raise.sh` / `scripts/lower.sh` own both: lower takes the windows down
1644
+ **first**, so none is left watching a port that just vanished.
1645
+
1646
+ **A bug the restart surfaced:** `scripts/electron.sh stop` used `local` outside a
1647
+ function — it worked only because the assignment happened to be harmless, and it
1648
+ printed a shell error on every stop. Fixed.
1649
+
1650
+ **And the inventory is a tool:** `bin/feature-inventory.sh` — 132 tools, 5 jobs,
1651
+ 26 skills, 20 agents, 8 workflows, 6 apps, 4 migrations, 6 registered projects,
1652
+ each with the way it must be proven written beside it.
1653
+
1654
+ ## 2026-09-16 — the icons are real runes now (eight of them were drawings)
1655
+
1656
+ The Allfather looked at the icons and said what no check had: *"the icons must be
1657
+ based on real runes."* He was right, and the fault was in the **glyph set**, not
1658
+ just in the two icons I had minted.
1659
+
1660
+ `midgard/design-system/icons.md` claims every glyph is an Elder Futhark rune and
1661
+ gives each its unicode. **Eight of the twenty-one files were drawings** — a hall
1662
+ (`valhalla`, which Óðrerir was wearing), a horn, a shield, a spear, a well, a
1663
+ squirrel, a gate, a world-tree. Replaced with the real runes their own rows
1664
+ declare:
1665
+
1666
+ ```
1667
+ gjallarhorn ᚷ Gebo · gungnir ᚦ Thurisaz · heimdall ᚺ Hagall · mimirsbrunn ᛜ Ingwaz
1668
+ ratatoskr ᛒ Berkanan · utgard ᚢ Uruz · valhalla ᚹ Wunjo · yggdrasil ᛃ Jera
1669
+ ```
1670
+
1671
+ Gungnir was claiming Gebo, which Gjallarhorn already holds, so it takes
1672
+ **Thurisaz** (the thorn — the spear) and `icons.md` is corrected to match.
1673
+
1674
+ **Five apps, five different runes:** Hlidskjalf `ehwaz`, mobile `raidho` (the
1675
+ road), Óðrerir `valhalla`→ now genuinely `ᚹ Wunjo`, Sessrúmnir `sowilo`, Smíðja
1676
+ `kaunan` (the torch). The install table drives both mint and install now — it had
1677
+ been guessing paths, which is why the Smíðja visualizer had been given
1678
+ Hlidskjalf's icon.
1679
+
1680
+ Also this pass: the visualizer gained its furniture (description, canonical,
1681
+ manifest, OG + Twitter), Óðrerir's Electron shell asks for a window icon, and the
1682
+ apple-touch PNGs are rasterised (ImageMagick is present).
1683
+
1684
+ ## 2026-09-16 — the desktop never dials out: Cloudflare stays outside Electron
1685
+
1686
+ The Allfather's rule: **"electron should never have anything with cloudflare to
1687
+ do."** It was not quite true — three doors stood open:
1688
+
1689
+ - **Sessrúmnir's "To the Hall"** preferred the local hall and, when it did not
1690
+ answer, fell back to `https://hall.ymir.zerwiz.org`. A desktop seat that
1691
+ reaches for a public hostname when its own machine is quiet is a seat that
1692
+ leaves the building to do its errands. It is now **local only** — when the hall
1693
+ is not running, the seat says so.
1694
+ - **Hlidskjalf's shell** took `HLIDSKJALF_URL` and `SMIDJA_URL` from the
1695
+ environment, and **Óðrerir's** took `HALL_URL` — each able to point a desktop
1696
+ window at a tunnel with one exported variable.
1697
+ - Both now pass their URL through a **loopback guard**: anything that is not
1698
+ `127.0.0.1`, `localhost` or `::1` is refused with a warning and replaced by the
1699
+ local default. The shell loads the machine, never the internet.
1700
+
1701
+ **Why this is the same fight as the bridge:** a desktop app that dials out is a
1702
+ desktop app whose failures belong to somebody else's edge. The bridge disguised a
1703
+ cloud endpoint as `127.0.0.1` and cost a night; these three would have disguised a
1704
+ tunnel as a local seat.
1705
+
1706
+ ## 2026-09-16 — the panels that crashed, the gate that lied, and the well that would not open
1707
+
1708
+ - **Statistics now reports the HARNESSES** (pi + opencode), not the smithy's runs.
1709
+ `bin/hlidskjalf-usage.sh` aggregates opencode's SQLite token store and both pi
1710
+ stores, and emits the numbers under the names the gate renders (`gate{}`):
1711
+ totals, usage, providers local/online with per_model, by_chain, by_model.
1712
+ Verified: 21,497 runs (opencode 21,422 · pi 75) · 331.5M tokens ·
1713
+ local 239,623 / online 331,289,420 · cache-hit 91.5%.
1714
+ - **A failed fetch no longer takes the hall down.** Endpoints answering `{error}`
1715
+ were handed to panels as data — Rail read `.filter` on an object, Stats read
1716
+ `.totals` on a string, and both crashed. The store now runs every bootstrap
1717
+ answer through `ok()`: an error keeps the last good value.
1718
+ - **`/api/usage` was answering with a traceback** while the script passed by hand:
1719
+ the gate process had been started before the fix and caches a failure for 60s.
1720
+ Restarted; the endpoint serves the harness numbers.
1721
+ - **The engram MCP (`-32000: Connection closed`) is mended.** Not config, not the
1722
+ engram package: `python3.12 -c "import mcp"` failed because the system
1723
+ `python3-rpds-py` ships without its compiled `rpds.rpds` module, which
1724
+ `jsonschema` (inside `mcp` 1.x) imports. Mended with
1725
+ `python3.12 -m pip install --user --break-system-packages --force-reinstall rpds-py`
1726
+ (plus `mcp<2` under python3.12). The server now starts and waits on stdio.
1727
+ Eir should check this pair: `mcp` present for the interpreter that runs the
1728
+ binary, and `rpds` importable.
1729
+
1730
+ ## 2026-09-17 — Sessrúmnir stops wearing Pi's clothes
1731
+
1732
+ - **The naming law, broken at the root.** Clicking the Sessrúmnir icon opened a
1733
+ window titled **Pi Desktop**: `apps/sessrumnir` is the Pi Desktop shell adopted
1734
+ whole, and its product name was still Pi's —
1735
+ `export const PI_DESKTOP_PRODUCT_NAME = 'Pi Desktop'` — rendered as the heading on
1736
+ the home screen, the sidebar and the chat panel. That is the title the Allfather
1737
+ saw, twice, on every launch.
1738
+ - **Mended at the name:** `SESSRUMNIR_PRODUCT_NAME = 'Sessrúmnir'` (Odin's hall of
1739
+ many seats), the three rendering components walked, and the old constant kept as an
1740
+ alias so no unwalked import breaks. `tsc --noEmit` exit 0.
1741
+ - **Still owed:** 1,255 English strings, **86 of them naming Pi** ("Pi is working",
1742
+ "Quit Pi Desktop", "Pi Desktop v{{latestVersion}} is available", "Start Pi/OMP
1743
+ before planning with Council"), and Pi's logo on the home screen. The locale is the
1744
+ one place they live — that pass is the order, not a patch.
1745
+ - Also in the launcher this session: an icon click now raises THE SYSTEM (converge
1746
+ the service, reborn a windowless app, focus a live window), and `stop --view X`
1747
+ stops only X — the loop that killed every view is gone. The Hall's own outage had
1748
+ a cause: an Astro dev server on :4323 while the app waited on :4322.
1749
+
1750
+ ## 2026-09-17 — the smithy can see the well
1751
+
1752
+ - **Why the memory looked absent in the smithy:** the visualizer proxies the bridge's
1753
+ `/inspect`, and `/inspect` reported three fields — store, episodes, agents — while
1754
+ the well holds six layers. A panel told only the episode count cannot show that
1755
+ facts, entities and reflections exist. `/inspect` now reports them all:
1756
+ episodes, facts (active/superseded), entities, edges, reflections (with the last
1757
+ run's time) and the vector index.
1758
+ - **Through the smithy right now:**
1759
+ `store $YMIR_HOME/memory/kaia.engram · episodes 8 · facts 36 (35 active) ·
1760
+ entities 60 · edges 480 · reflections 1 (2026-09-16T21:01) · vec index 8`.
1761
+ - **A trap found while doing it:** restarting the bridge RAW (`python3 bin/mimir-bridge.py`)
1762
+ loses the env that carries the well's path, and the bridge silently re-points at the
1763
+ old store in the repo — 364 stale episodes and no facts. The well is the **blessed
1764
+ starter's** to raise: `bin/mimir-bridge.sh --start`. A raw restart is a different
1765
+ well wearing the same port.
1766
+
1767
+ ## 2026-09-17 — Skrymir opens the hoard, not the checkout
1768
+
1769
+ - **The file browser was rooted in the wrong tree.** `workspaceRoot()` built every
1770
+ realm path under `ROOT` — the checkout — so `work` resolved to a directory that does
1771
+ not exist and the walk fell back to the repo's `docs/`. That is why Skrymir showed
1772
+ `lore.md`, `Architecture.md`, `research/`, `runbooks/`, and why a file it had just
1773
+ listed answered **"not found"**: the listing came from one tree and the read from
1774
+ another.
1775
+ - **The hoard first now (Rule 04):** `$YMIR_HOME/svartalfaheim/<realm>` → company
1776
+ container → `$YMIR_HOME/workspace/<realm>` → the checkout only as legacy. An empty
1777
+ realm shows the hoard's home, never the repo's docs. Verified: `work` lists
1778
+ `memory/daily/2026-09-13.md` and `workspace/`, and a read returns its body.
1779
+ - Note for the next hand: the gate runs **without** `--watch` — a server edit is on
1780
+ disk and not in the process until `scripts/start.sh` raises it again.
1781
+
1782
+ ## 2026-09-17 — the Fleet was a rack of terminals; now it is the roster
1783
+
1784
+ - **We were tracking the tools.** `.agents/agents/` holds **21 agent definitions** —
1785
+ Brokk, Sindri, Bragi, Forseti, Mímir, Kvasir, Snotra, Huginn, Hnoss, Sága, Muninn,
1786
+ Frigg, Gróa, Jörð, Sýn, Týr, Galdr… — and the Fleet served **13 panes all named
1787
+ "OpenCode"**, role `opencode`. The board showed seats and never the smiths who might
1788
+ be standing in them.
1789
+ - **`bin/hlidskjalf-agents.sh` now reads the roster** (`.agents/agents/*.md`, canonical
1790
+ per RULES/02) and joins each agent to its standing pane: figure, craft, model from
1791
+ the definition's frontmatter, and `live{}` only when a pane answers to it. An
1792
+ **unseated agent is still an agent** — hiding it is what made the board a rack.
1793
+ - Harness seats that answer to no one on the roster are kept visible and named
1794
+ honestly, never dressed as agents.
1795
+ - Served: **34 entries — 21 agents (roster first) + 13 harness seats, 14 joined**.
1796
+
1797
+ ## 2026-09-17 — the extensions deployed without their modules
1798
+
1799
+ - **pi would not start a seated worker, and the reason was a half-deploy.**
1800
+ The four shared pi extensions all reported
1801
+ `Failed to load extension: Cannot find module ./lib/<module>.ts` — and every one
1802
+ of those modules was present in the repo, one level away under the extensions'
1803
+ own `lib/`. The loader copied the top-level extension files and never their
1804
+ supporting modules.
1805
+ - **Mended in two places.** (1) The missing modules are now beside the deployed
1806
+ extensions, so a running pi loads all four. (2) `bin/valknut-load.sh` — the
1807
+ loader — now deploys that lib alongside the extensions, idempotently (identical
1808
+ files untouched), so a fresh machine cannot hit it. A deploy that copies a file
1809
+ but not the module it imports is not a deploy.
1810
+ - **What this cost:** the Forseti audit was seated on a **local** model, and the
1811
+ pane fell back to a bare shell when pi could not start — which is why the brief
1812
+ was typed at a prompt and answered `bash: syntax error near unexpected token '('`.
1813
+ With the extensions mended, pi's remaining blocker is the model: the llama-router
1814
+ has no raisable seat tonight (`:8080` unbound, and the installed llama.cpp cannot
1815
+ load the Apodex weights), so a local-first dispatch has nothing to reach.
1816
+
1817
+ ## 2026-09-17 — the well as tools: Ymir's first custom pi extension
1818
+
1819
+ - **Why the extension tree was failing, from the upstream law:** pi auto-discovers
1820
+ extensions from BOTH the global home and the project-local tree. The same extension
1821
+ in both loads twice and pi refuses the duplicate tool —
1822
+ `Tool "gna_watch_arm" conflicts with …`. Ymir's own loader already says the shared
1823
+ extensions have **one** home; the tree contradicted it by carrying the extension
1824
+ files project-locally as well.
1825
+ - **Built: `ymir-well`** — the well, as tools inside every pi session.
1826
+ `well_recall(query, k)` reads Kaia's memory before work begins;
1827
+ `well_observe(content, tags, actors, salience)` writes the lesson after. Written in
1828
+ the house voice, honest about failure (a failed write says the lesson was NOT
1829
+ written), and it sends tags as LISTS — a comma-string is stored as an array of
1830
+ characters, which is exactly how a recall once returned `['r','u','n']`.
1831
+ - Authored in the SHARED source and deployed to the global home — one home, never a
1832
+ project copy. That is the pattern every future Ymir extension follows.
1833
+
1834
+ ## 2026-09-17 — the duplicate extensions removed from the project tree
1835
+
1836
+ - **The fault, now measured exactly.** The project-local extension tree carried four
1837
+ files that the shared source also carries — `gna-pi-watch.ts`, `ro.ts`,
1838
+ `skuld-branch-supervision.ts`, `syn-turnend-guard.ts`. pi auto-discovers both the
1839
+ global home and the project tree, so each loaded twice and pi refused the second
1840
+ copy of every tool: `Tool "gna_watch_arm" conflicts with …`. Every `pi` start in
1841
+ every worktree died on it, which is why a seated worker fell back to a shell.
1842
+ - **The fix:** those four files are gone from the project tree. What remains there is
1843
+ `lib/` — the modules the *global* extensions import — and its README. The
1844
+ extensions themselves live in the shared source and in the one global home.
1845
+ - The seatbelt refused the plain shapes (`rm`, `mv`, `>`-bearing commands) on this
1846
+ path and named the remedy: *a reviewed commit*. This is that commit.
1847
+
1848
+ ## 2026-09-17 — smoke test all UI: two checks that lied about their own subjects
1849
+
1850
+ - **The truth gate failed on my own change.** It demanded the connector's total equal
1851
+ the herdr pane count; the connector is roster-first now, so it legitimately reports
1852
+ agents AND seats (33 vs 13 panes). Panes are a subset of what the board must show.
1853
+ Corrected: a FAIL now means FEWER entries than panes — a standing seat hidden from
1854
+ the board — which is the fault worth catching. **Verdict: PASS.**
1855
+ - **The smoke test reported a present database absent.** It looked for the smithy's db
1856
+ at the repo path; the runtime keeps it under `$YMIR_HOME/smidja/`, which is where
1857
+ the bootstrap and the gate both resolve it. Same wound as Skrymir and the well: the
1858
+ check and the owner disagreeing about where the record lives. **Now: OK, 8 tables.**
1859
+ - **Smoke test all UI: 5/5 OK** — spa, api, well, smidja-db, loaders. Truth gate: PASS.
1860
+
1861
+ Also mended in the same pass: `StatusChip` read `STATUS_META[status].tone`, and the
1862
+ fleet now reports a status it was never taught (`seated`), so an unknown status threw
1863
+ and killed the gate. An unknown status can no longer take a panel down.
1864
+
1865
+ ## 2026-09-17 — Glitnir stops hanging, and one process is one row
1866
+
1867
+ - **`/api/reviews` ran two 60-second shell checks on every poll**, so the review board
1868
+ hung for a minute and read as a dead surface. It is memoised for a minute now, the
1869
+ same discipline the usage endpoint already follows: a board a human reads does not
1870
+ need an answer fresher than its own usefulness.
1871
+ - **`/api/processes` returns the same id twice** — `systemd:dbus` arrives from two
1872
+ sources, 45 entries for 44 processes. The panel dedupes by id, so a repeated process
1873
+ renders as the one process it is, and React is no longer handed a duplicate key.
1874
+ The duplicate at the source is recorded, not hidden.
1875
+ - `/api/stream` answers 200; the live stream is not broken, it was the window.
1876
+
1877
+ ## 2026-09-17 — the fleet stops colliding: a grid fan, and each smith's own house
1878
+
1879
+ - **The Fleet graph collided above ~8 agents.** `Fleet.tsx` fanned every
1880
+ non-hub agent into a single two-row line at ~4.4% pitch on 46px rings — with
1881
+ 20 roster cards the rings and labels overlapped into an unreadable pile.
1882
+ `layout()` now fans a square-ish grid (`cols = ceil(sqrt(n))`, hub at top,
1883
+ rows pitched past ring+label), so 20 agents render separated.
1884
+ - **Every ring showed the same rune.** `bin/hlidskjalf-agents.sh` hardcoded
1885
+ `domain: ymirlabs` for every card and its roster parser never read the
1886
+ `domain:` frontmatter each figure carries — so all twenty cards wore the
1887
+ anonymous ᛦ. The roster now parses `domain:` and passes it through;
1888
+ `galdr.md` names its house (`brokkforge`); the graph falls back to
1889
+ `DOMAINS.ymirlabs` only when a domain is genuinely unknown (was a crash)
1890
+ — and the `AgentCard` already fell back safely.
1891
+
1892
+ galdr-reread: `.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md` — the
1893
+ Fleet graph paragraph (grid fan, roster domains, fallback).
1894
+
1895
+ ## 2026-09-17 — the apps leave the monorepo; installation pulls them from their own repos
1896
+
1897
+ - **The app split lands in the installer.** The five apps (hlidskjalf,
1898
+ hlidskjalf-mobile, odrerir, sessrumnir, smidja) now live in their own repos
1899
+ registered in the home registry (`$HOARD/identity/projects.yaml`) — the
1900
+ monorepo never tracks them, and **`step_apps`** clones or fast-forwards each
1901
+ `apps/<path>` from its registered `git{}` block (never guessing a remote,
1902
+ always reading the registry). The smithy engine (`apps/smidja`) is stamped
1903
+ from the cloned factory's `templates/smidja`, exactly as `install.py` does
1904
+ for a target repo — so a fresh clone still gets a working smithy.
1905
+ - **The vendored app trees leave the index.** `apps/{hlidskjalf,
1906
+ hlidskjalf-mobile,odrerir,sessrumnir,smidja,smidja-factory}` are gitignored
1907
+ and `git rm --cached`-ed (751 files, 135k lines); `apps/README.md` stays.
1908
+ A dir present that is not a git clone is reported (`move it aside and
1909
+ re-run`) rather than silently replaced.
1910
+
1911
+ galdr-reread: `.agents/skills/galdr-ymirsystem/assets/installation.md` — the
1912
+ step table (`22 steps`, `24` `step_*` functions), the new `apps` row, and the
1913
+ Sessrúmnir rows now say "its own repo", not "vendored".