@zerwiz/ymir 0.1.5 → 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.
Files changed (125) 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 +290 -24
  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 +1362 -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 +156 -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/einherjar-spawn.sh +9 -3
  55. package/bin/eir-doctor.sh +89 -1
  56. package/bin/electron-lib.sh +34 -0
  57. package/bin/feature-inventory.sh +73 -0
  58. package/bin/fleet-apply.sh +15 -2
  59. package/bin/github-deploy.sh +14 -1
  60. package/bin/gjallarhorn-expose.sh +43 -9
  61. package/bin/gjallarhorn-notify.sh +14 -1
  62. package/bin/gjallarhorn-purge.sh +14 -1
  63. package/bin/gjallarhorn-tunnel.sh +16 -3
  64. package/bin/groa-update.sh +9 -1
  65. package/bin/hall-snapshot.sh +21 -2
  66. package/bin/herdr-run.sh +23 -2
  67. package/bin/hlidskjalf-agents.sh +172 -0
  68. package/bin/hlidskjalf-usage.sh +196 -0
  69. package/bin/hoard-guard.sh +135 -0
  70. package/bin/hoard-lib.sh +91 -11
  71. package/bin/hodd.sh +28 -4
  72. package/bin/huginn-research-worker.sh +14 -1
  73. package/bin/local-model-lock.sh +22 -3
  74. package/bin/mimir-bridge.py +33 -0
  75. package/bin/mimir-bridge.sh +14 -1
  76. package/bin/mimir-reflect.sh +27 -0
  77. package/bin/mjollnir-webhook.sh +14 -1
  78. package/bin/mjollnir.sh +22 -3
  79. package/bin/model-hardware.sh +17 -3
  80. package/bin/model-resolve.sh +14 -1
  81. package/bin/nornir-job-hall-snapshot.sh +28 -0
  82. package/bin/nornir-job-nsr-compliance.sh +54 -0
  83. package/bin/npm-publish.sh +136 -0
  84. package/bin/omarchy-install.sh +18 -5
  85. package/bin/omarchy-plugins.sh +15 -2
  86. package/bin/omarchy-sense.sh +14 -1
  87. package/bin/pi-model-agnostic.sh +71 -0
  88. package/bin/private-guard.sh +20 -0
  89. package/bin/project-git.sh +18 -4
  90. package/bin/public-guard.sh +3 -2
  91. package/bin/sessrumnir-ensure.sh +1 -1
  92. package/bin/sessrumnir.sh +16 -3
  93. package/bin/smidja-board.sh +136 -0
  94. package/bin/smidja-bootstrap.sh +5 -1
  95. package/bin/smidja-lib.sh +49 -0
  96. package/bin/syn-asset-pretool-check.sh +14 -1
  97. package/bin/syn-spawn-pretool-check.sh +58 -0
  98. package/bin/tailscale-sync.sh +15 -2
  99. package/bin/telegram-bot.sh +14 -1
  100. package/bin/ui-truth-check.sh +75 -0
  101. package/bin/valknut-load.sh +42 -4
  102. package/bin/wedge-notify.sh +14 -1
  103. package/bin/workspace-rag.sh +14 -1
  104. package/bin/ymir-install.sh +257 -57
  105. package/bin/ymir-plan.sh +390 -0
  106. package/bin/ymir-say.sh +14 -1
  107. package/bin/ymir-setup-auth.sh +12 -1
  108. package/bin/ymir-style.sh +108 -0
  109. package/bin/ymir-validate.sh +64 -11
  110. package/bin/ymir.js +82 -34
  111. package/docs/design.md +2 -2
  112. package/docs/lore.md +5 -1
  113. package/docs/runbooks/agents.md +1 -1
  114. package/package.json +13 -3
  115. package/scripts/electron.sh +74 -15
  116. package/scripts/lower.sh +34 -0
  117. package/scripts/raise.sh +42 -0
  118. package/scripts/start.sh +65 -3
  119. package/.agents/config/agents.yaml +0 -50
  120. package/.agents/config/tailscale-sync.yaml +0 -27
  121. package/.agents/memory/kaia.engram +0 -0
  122. package/.agents/memory/kaia.engram-shm +0 -0
  123. package/.agents/memory/kaia.engram-wal +0 -0
  124. package/.agents/memory/well/episodes.jsonl +0 -367
  125. package/.agents/memory/well/workspace.jsonl +0 -27
package/CHANGELOG.md CHANGED
@@ -1,5 +1,985 @@
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.
216
+
217
+ ## 2026-09-17 — the house gains an unpublish door
218
+
219
+ - **`bin/npm-publish.sh --unpublish @scope/name@<version>`.** Publishing had a
220
+ door in this house and unpublishing had none, so the act was done by hand on a
221
+ machine whose `~/.npmrc` holds a stale token — the very reason the door exists.
222
+ The new mode resolves the token from the hoard exactly as publishing does,
223
+ demands a spec that names the version (a whole package is not a one-word act),
224
+ supports `--dry-run`, and says plainly that the registry's CDN may serve the
225
+ tarball for a while after the removal.
226
+ - **The window is 72 hours.** npm allows one version back within 72 hours of its
227
+ publication; past that only npm support can remove it. The help text says so,
228
+ because a door that does not name its own clock invites a late knock.
229
+ - **`@zerwiz/ymir@0.1.5` was unpublished** — the version that carried the memory
230
+ well. Publishing the clean 0.1.7 moves the `latest` tag off the tainted build at
231
+ once, which is the half of the repair that does not wait for propagation.
232
+
233
+ ## 2026-09-17 — the package shipped the memory well (one version, now excluded)
234
+
235
+ - **The exposure.** `@zerwiz/ymir@0.1.5` carries five files that are nobody's but
236
+ the operator's: `.agents/memory/kaia.engram`, its `-shm` and `-wal` sidecars, and
237
+ `.agents/memory/well/{episodes,workspace}.jsonl`. A public npm tarball contained
238
+ the live memory well — the store and the episodes. Verified by fetching the
239
+ published artefact and listing it; `0.1.0`–`0.1.4` are clean, and this branch's
240
+ build is clean.
241
+ - **The cause, and it is a trap worth naming.** `.gitignore` excludes
242
+ `.agents/memory/kaia.engram*` and `.agents/memory/well/*.jsonl`, and the repo is
243
+ clean — but **npm does not consult `.gitignore` when `files[]` names a whole
244
+ directory.** `files: [".agents/"]` packs that subtree *including* the ignored
245
+ files. Nothing warned: the leak travelled in the artefact, not the repo.
246
+ - **The fix.** The manifest excludes them explicitly, because a `files[]` list
247
+ cannot rely on the ignore file it overrides:
248
+ `!.agents/memory/kaia.engram*`, `!.agents/memory/well/*.jsonl`,
249
+ `!.agents/memory/*.db`, plus `!**/__pycache__/` and `!**/*.pyc` for the compiled
250
+ junk that was travelling the same way. Verified: `npm pack --dry-run` carries
251
+ 864 files and **no** memory store — the memory README and the ledger's scaffold
252
+ header are all that remain, as they should be.
253
+ - **The rule for every future package:** a `files[]` entry that names a directory
254
+ overrides `.gitignore` for everything beneath it. Name what ships, or exclude
255
+ what must not — and verify with a dry-run pack, never by assumption.
256
+ - **Remediation.** `@zerwiz/ymir@0.1.5` should be unpublished (it is inside npm's
257
+ window); the clean build publishes as 0.1.7 and supersedes it.
258
+
259
+ ## 2026-09-17 — the smithy's dependency names the package npm will actually serve
260
+
261
+ - **`@zerwiz/smidja` is published and not served.** npm accepted it twice (0.1.0,
262
+ then 0.1.1): the tarball and the version document existed, the package document
263
+ never did, and a republish of 0.1.0 was refused as *"previously published"*. The
264
+ name answers `404`, the website `403`, while `@zerwiz/hlidskjalf`,
265
+ `@zerwiz/odrerir` and `@zerwiz/sessrumnir` resolve normally and always have.
266
+ - **A second free name failed identically.** `@zerwiz/smidja-factory` was published
267
+ and shows the same shape — version served, package document withheld. Two names,
268
+ one behaviour, while packages published yesterday serve fine: the withholding is
269
+ npm's, not the name's, and the CLI does not report it.
270
+ - **The distro depends on the name that can resolve.** `optionalDependencies` now
271
+ names `@zerwiz/smidja-factory`, so the moment npm serves that package the smithy
272
+ arrives with the rest. Being optional, it is skipped silently until then — the
273
+ plan's phase-5 row is what says so out loud.
274
+ - **`@zerwiz/ymir` 0.1.7** — the version published once this lands.
275
+
276
+ ## 2026-09-17 — the four surfaces arrive by npm, and an old tree gives up what it holds
277
+
278
+ - **The app packages arrive with the distro.** `@zerwiz/ymir` now depends on the
279
+ surfaces as their own packages — `@zerwiz/hlidskjalf`, `@zerwiz/odrerir`,
280
+ `@zerwiz/sessrumnir` (live on the registry) and `@zerwiz/smiddja` (published at
281
+ 0.1.0 today). They are **optionalDependencies**, deliberately: a broken app
282
+ package must never stop the core from installing, and a package not yet on the
283
+ registry is skipped by npm and starts arriving the moment it is published.
284
+ **Proven**: a global install from the packed tarball lands the core plus all
285
+ three live apps, 330 packages, and the bin answers `0.1.6`. A local install
286
+ hoists them to `node_modules/@zerwiz/<app>`; a global one nests them under the
287
+ distro's own `node_modules` — the plan checks both shapes.
288
+ - **The plan tells the shapes apart.** "Declared as a dependency but not fetched"
289
+ is a `DO`; "no package and no dependency" is a `BLOCKED`. All four surfaces read
290
+ `DO` on a machine that has the distro but not yet the apps.
291
+ - **Migration 0005 — the operator's things leave the code tree.** It carries this
292
+ machine's records, the runtime state (pids · logs · locks · the applied-marker),
293
+ the operator's credentials, and the settings git does not track. Run on the
294
+ Allfather's machine: **44 carried, 3 stale twins kept, 1 identical duplicate
295
+ removed, 0 refused** — the tree left holding `.gitkeep` and the distro's own
296
+ tracked defaults.
297
+ - **A collision is decided by evidence, never assumption.** Two files, one name:
298
+ identical content means the tree's copy goes (the content is provably at home);
299
+ different content means both are real, so the home's keeps the name and the
300
+ tree's is carried beside it as `<name>.stale-<UTC>`. Never merged, never
301
+ discarded. A template is not only `*.example` — `agents.machine.example.yaml`
302
+ matched none of the old guards and was carried off as if it were the operator's;
303
+ it is back, and the pattern now covers `*.example.*`.
304
+ - **The plan's `purity` row applies the same law.** A settings file git *tracks* is
305
+ the distro's shipped default, not the operator's — and the check asks git about
306
+ the real path, because the tree's `config` is a symlink into `.agents/config`
307
+ and the index holds the real name. Asking about the link reported "untracked" and
308
+ flagged the distro's own `cron.yaml`. It now reads: *nothing of the operator's is
309
+ written into the code tree*.
310
+ - **`bin/npm-publish.sh --all` learns the smithy** — the registry maps `smidja` to
311
+ `apps/smidja-factory`, so it is a publish target like any other app.
312
+ - **The smithy gets a front page.** `zerwiz/smidja`'s README was a three-line stub;
313
+ it now says what Smíðja is, how to install it, how to run the visualizer on
314
+ `:8437`, what the tree holds, and the shape of a run. The tarball also carries
315
+ `LICENSE` and `NOTICE` — npm attaches the licence and the readme on its own, but
316
+ **not the notice**.
317
+
318
+ ## 2026-09-17 — the install asks with a plan, and the operator's things leave the code tree
319
+
320
+ - **The consent a real install asks for is now computed, not recited.** The old
321
+ prompt was a hardcoded paragraph, and a paragraph cannot know the host: it named
322
+ an Omarchy version on a Mac, promised a workspace tree that already stood, and
323
+ never mentioned that no application had been installed at all. `bin/ymir-plan.sh`
324
+ probes this machine and prints one row per step with its state and the reason —
325
+ `DO · SKIP · INFO · BLOCKED · CONSENT` — across nine phases (resolve · code ·
326
+ home · runtimes · engines · apps · wire · raise · verify). `ymir-install.sh`
327
+ prints it at the consent prompt; `--plan` / `--dry-run` prints it and writes
328
+ nothing; `--json`, `--phase N` and `--blocked` are there for automation and for
329
+ reading what is holding an install back.
330
+ - **The four app surfaces are named in the plan.** hlidskjalf, odrerir, sessrumnir
331
+ and smidja each get a row: whether the source is present, whether the web build
332
+ exists, and — when it is absent — the honest reason (`no apps/<name> and no
333
+ @zerwiz/<name> package`). The Electron shells are a `CONSENT` row, not a silent
334
+ download: three shells each pull a ~100 MB runtime the web surfaces do not need.
335
+ - **The home is the operator's to choose.** `step_home` asks once, records the
336
+ answer as machine state under `~/.config/ymir/home`, and every later script
337
+ resolves it through `bin/hoard-lib.sh` (`$YMIR_HOME` → the recorded choice → one
338
+ documented default). `--check` never writes; `--yes` takes what is recorded.
339
+ - **The roots law, applied across the tree.** The package is the **code that runs
340
+ the programs**; everything the operator owns lives in the home. Two resolvers
341
+ were added (`hoard_data_dir`, `hoard_state_dir`, `hoard_settings_dir`,
342
+ `hoard_local_env`) and **42 call sites** converted: `$ROOT/data` and
343
+ `$ROOT/state` (25 scripts), then `$ROOT/.env.local` and `$ROOT/config/*`
344
+ (17 scripts). A credential no longer sits in a tree that ships, and machine
345
+ records no longer sit where npm will erase them on upgrade.
346
+ - **The plan is the ward for that law.** Its phase-1 `purity` row names anything of
347
+ the operator's found in the code tree — `data/`, `state/`, `config/*.yaml`,
348
+ `.env.local` — so drift is reported on every run instead of discovered after an
349
+ upgrade.
350
+ - **The install no longer lectures about local models.** The `step_models` console
351
+ line told the operator to "load the modeltesting skill" — a skill that does not
352
+ exist anywhere in the tree — mid-install. It is gone; the real method is named in
353
+ the one place it belongs, the Galdr asset `assets/local-models.md`. `step_models`
354
+ still ensures the Pi harness and seeds `~/.pi/agent/models.json`.
355
+ - **The package no longer claims to ship `config/`.** `config` is a symlink to
356
+ `.agents/config`, which npm cannot carry; the tarball now declares what it
357
+ actually contains (147 bin scripts, the agent-set template under `.agents/`).
358
+ Settings are seeded into `$YMIR_HOME/config/agents.yaml` from the tracked
359
+ template, falling back to `.agents/config/` when the symlink is absent.
360
+ - **A corrupted TOON block in `AGENTS.md`** — `outputs[7]` carrying eight rows and
361
+ duplicate `midgard/` lines — is mended; the compliance gate reports 84 valid
362
+ blocks again.
363
+ ## 2026-09-17 — the registry says where a repo lives and what it is for
364
+
365
+ - **A project entry can now name its machine.** `machine:` records the host
366
+ where a checkout lives; `data/machines.md` holds the fleet (omarchy-1,
367
+ zerwiz, zerwizserver live on the tailnet). A repo is no longer assumed to be
368
+ on the box you happen to be standing on.
369
+ - **A project entry can now say what it is for.** `about:` is one plain
370
+ sentence per repo. `bin/project-git.sh list --field about` prints the whole
371
+ inventory without opening the YAML.
372
+ - **`project-git.sh` resolves more of the block.** `--field` now accepts
373
+ `machine`, `company`, `workspace` and `about` alongside the git fields, and
374
+ non-git keys read from the project block rather than the `git:` line.
375
+ - **One registry, not two.** The live registry is `hodd/identity/projects.yaml`
376
+ (what the runtime reads). A stale duplicate at `Documents/Ymir/identity/` had
377
+ been drifting apart from it — different GitHub owners, different project sets
378
+ — and is retired, with a backup kept in `hodd/state/`.
379
+
380
+ ## 2026-09-17 — the private home gets the ward it never had
381
+
382
+ - **Six guards watched the public repo; the home had none — yet the home is where
383
+ every private byte lives.** The one real leak of this day (`platform.env.prev-fill`,
384
+ 44 credentials) happened *there*, in a scratch backup swept up by `git add -A`,
385
+ with nothing watching. `bin/hoard-guard.sh` closes that.
386
+ - **It is a hook, not a script.** Seated as `$YMIR_HOME/.git/hooks/pre-commit`,
387
+ **git runs it on every commit** — including one made in a hurry, by a loop, or by
388
+ an agent that has never heard of it. Verified by attempting a real `git commit`
389
+ with no guard invoked: git refused, exit 1.
390
+ - **It catches what actually leaked**, verified by planting each: a scratch-backup
391
+ name (`*.prev-*`, `*.bak`, `*.orig`), a plaintext secret filename, a secret shape,
392
+ and — the evasion that would have hidden a key — **a base64 blob whose decode
393
+ contains a PEM header**, under an innocent filename.
394
+ - **`bin/ymir-install.sh` seats it at both repos** (the `hoard-gate` step reports
395
+ the home separately, so a dormant vault is visible), on every install layer.
396
+ - **A bypass is logged, not silent.** `hoard-guard.sh --log-bypass` appends to the
397
+ Runes ledger when a `--no-verify` commit is detected.
398
+ - **Two stale flat paths fixed in the guards themselves.** `bin/docs-guard.sh` told
399
+ an operator to move private docs to `$YMIR_HOME/docs/` — a path that no longer
400
+ exists — and `bin/groa-update.sh` read `$YMIR_HOME/data/eindri-homes.md` instead of
401
+ the hoard's. A guard pointing at the wrong door teaches the drift it exists to stop;
402
+ `groa-update.sh` now resolves through `hoard_root`.
403
+
404
+ ## 2026-09-17 — the NorthStar gate runs nightly; the platform door it caught is mended
405
+
406
+ - **`bin/nornir-job-nsr-compliance.sh` (02:30)** runs every
407
+ `.compliance/gates/check_*.sh` (danger · env · paths · platform · wiring)
408
+ and carves the verdict — `nsr.compliance` clean, or `nsr.compliance.failed`
409
+ (exit 1) naming the broken gate, so the 07:00 briefing reads it at sunrise.
410
+ - **The gate it caught, mended:** `scripts/electron.sh` used `kill -9`, which
411
+ the platform gate rightly refuses as non-portable. It now uses `kill -KILL`
412
+ (the house idiom already in `scripts/stop.sh`) — the NSR round is 5/5 green.
413
+
414
+ galdr-reread: `.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md` — new
415
+ §3.5 (hall snapshot) and §3.6 (NSR round) rows.
416
+
417
+ ## 2026-09-17 — the Live Hall's board becomes true: the snapshot lands on the loom
418
+
419
+ - **The Hall was a glass with nothing behind it.** The Óðrerir page reads
420
+ `apps/odrerir/public/livehall.json` same-origin, but nothing wrote it on a
421
+ schedule — the board showed the saga's own static count and said so.
422
+ `bin/nornir-job-hall-snapshot.sh` (08:00, after the 06:00 observer and the
423
+ 07:00 briefing) now drives `bin/hall-snapshot.sh` from real state — runes,
424
+ projects, the cron gauge, the wake queue, standing smiths, armed when-
425
+ sources, landed errands — and carves Rune `odrerir / hall.snapshot`.
426
+ - The snapshot is runtime, never repo: `apps/odrerir/public/livehall.json` is
427
+ gitignored, so the job never dirties a branch.
428
+
429
+ galdr-reread: `.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md` — new
430
+ §3.5 row (reads/writes table, idempotence note).
431
+
432
+ ## 2026-09-17 — the hoard gets a guard, and Rule 04 stops contradicting the disk
433
+
434
+ - **Eir gains a `hoard` surface.** Private data drifting outside the hoard is
435
+ now caught rather than discovered by asking "where did that go?". The check
436
+ fails on two things: a `.ymir-layout.yaml` entry naming a directory that does
437
+ not exist (a stale map is what lets private work land outside the hoard), and
438
+ a flat `$YMIR_HOME/{identity,data,docs,secrets,tenants}` sitting beside
439
+ `hodd/`. `fix` merges a flat duplicate into the hoard without clobbering, then
440
+ repoints any stale layout entry at the hoard.
441
+ - **Rule 04 corrected, append-only.** The rule's mapping table read as if
442
+ `hodd/x/` and `$YMIR_HOME/x/` were two locations; on a real home that produced
443
+ two parallel stores which drifted. The appended correction states the truth:
444
+ `$YMIR_HOME/hodd/` **is** the private data path, `bin/hoard-lib.sh` is the one
445
+ source of truth for it, and `.ymir-layout.yaml` must name only paths that
446
+ exist. Nothing above the correction was rewritten.
447
+
448
+ ## 2026-09-17 — the hoard encrypts its secrets at rest
449
+
450
+ - **`bin/hodd.sh` decrypts transparently.** `load`, `emit`, and `tenant` now
451
+ accept a plaintext file *or* an `.age` ciphertext with the sibling plaintext
452
+ absent. A shared `resolve_secret` finds the readable form; when it decrypts,
453
+ it uses a mode-600 temp and removes it afterward. An agent asks for the
454
+ resolved path and never handles ciphertext directly.
455
+ - **Secrets stay encrypted at rest.** `hodd/secrets/platform.env.age` is the
456
+ committed form; the plaintext is scratch and gitignored. Round-trip verified
457
+ byte-identical before the plaintext was untracked.
458
+ - **The vault invariant, written down.** The age key and the ciphertext travel
459
+ together in the private `ymirhome` repo, so a dead disk loses nothing — and
460
+ `ymirhome` **must never be made public**. Stated in `hodd/docs/secrets-vault.md`.
461
+ - **The honest limit.** This is encryption-at-rest, not encryption-against-yourself:
462
+ on a single-user machine, a process running as the operator can read what the
463
+ operator can read. What it buys is the *file* and the *repo*, not the *account*.
464
+
465
+ ## 2026-09-17 — the Forge speaks: each skill's own text in the index
466
+
467
+ - **The Forge listed skills as bare names.** `Forge.tsx` rendered only
468
+ `{s.name}` + the aett rune, while the live `/api/skills` index carries every
469
+ skill's `description` (240-char truncation) — the data was there, the
470
+ surface never showed it. The skill list items are now two-line cards: name +
471
+ aett on the head row, the description beneath (`forge-item-desc`, dimmed
472
+ until hover), scoped to the skill variant so the Eindri rows keep their
473
+ side-by-side name+status shape.
474
+
475
+ galdr-reread: `.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md` — the
476
+ Forge gate bullet (skill list shows name + aett + description).
477
+
478
+ ## 2026-09-17 — the feed follows Brokk's thinking past the composer
479
+
480
+ - **Sessrúmnir's auto-scroll never counted streamed THINKING as growth.**
481
+ `useChatScroll` watched `messages` and `streamingContent` only, so a
482
+ reasoning-heavy turn (thinking before any visible content) grew the feed
483
+ without moving it — the thinking block ended up below the floating composer,
484
+ behind it, and the Allfather had to scroll down to read it.
485
+ - **The growth signal now also includes `streamingThinking` and streaming tool
486
+ calls.** While Brokk reasons, the feed keeps the thinking tail in view above
487
+ the composer (still only when Auto Scroll is on and the reader is at the
488
+ bottom — scrolling up to re-read never gets yanked).
489
+ - Live hall updated: rebuilt renderer delivered to the running Sessrúmnir and
490
+ the app restarted via `bin/sessrumnir.sh` (window back on workspace 8).
491
+
492
+ ## 2026-09-17 — the editor stops being banished to its own desktop
493
+
494
+ - **`bin/editor-place.sh` pinned the editor to its own desktop with
495
+ `no_focus = true`** — VS Code mapped onto a different workspace than the one
496
+ the Allfather worked on and never took focus, so nothing inside it was
497
+ clickable. The rule was written by Ymir itself into
498
+ `~/.config/hypr/ymir-desktops.lua`.
499
+ - **The editor is now UNMANAGED by default**: it opens on the active desktop,
500
+ focused and clickable, like any other app. The old pin remains reachable
501
+ explicitly with `YMIR_EDITOR_PIN=1 bin/editor-place.sh apply`.
502
+ - Live desk healed: the `code` windowrule was removed from
503
+ `~/.config/hypr/ymir-desktops.lua` (backup kept alongside), Hyprland reloaded
504
+ with no config errors, and the stranded VS Code windows were moved to the
505
+ active desktop and focused.
506
+
507
+ ## 2026-09-17 — models, providers, and paths are the user's — and the gate says so
508
+
509
+ - **The law, amended (Rule 07).** Two clauses appended, the rule itself untouched:
510
+ - **A. Models and providers live in the hoard, per user, read from YAML.**
511
+ Every operator's differ, so there is **no global model or provider** — a
512
+ shipped default naming a model is a claim about someone else's machine. The
513
+ user's own live in `$YMIR_HOME/hodd/`, read from a YAML file there; the repo
514
+ ships a template.
515
+ - **B. No hardcoded absolute file paths.** No `/home/<user>/…`, no
516
+ `/Users/<user>/…`, no `C:\Users\…`. A path is relative to a resolved root or
517
+ comes from env/config with one documented default.
518
+ - **The gate, `config`.** `compliance-check.sh` gains a check that fails on a
519
+ model id or provider name used as a **value** in shipped code or a tracked
520
+ config, and on an absolute path naming a user. Comments, `*.example` templates,
521
+ `CHANGELOG*`, and `assets/reference/` are exempt by intent — they document,
522
+ they do not configure. **192 files clean.**
523
+ - **Three real violations found and fixed:**
524
+ - `.agents/config/eindri-dispatch.json` — three dispatch rules named
525
+ `opencode-go/deepseek-v4.1-flash`. Now a template with `<your-model-id>`.
526
+ - `.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml` — the PI boot
527
+ profile named `lmstudio/qwen3.6-35b-a3b`. Now a placeholder.
528
+ - `bin/bootstrap-macos.sh` — `/home/ubuntu/Documents/Ymir`. Now resolved from
529
+ `YMIR_VM_USER` with one documented default.
530
+ - **Two false positives found and fixed in the gates themselves.** The `mocks`
531
+ gate flagged a prose comment explaining why a guard exists; the new `config`
532
+ gate first flagged a sentence mentioning `llama.cpp/llama-swap`. Both now
533
+ exclude comments and require a value position — a gate that cries wolf is a
534
+ gate that gets ignored.
535
+ - **Four pre-existing failures on main mended** so the gate could be added to a
536
+ green tree: a duplicated `midgard/` row in `AGENTS.md` (a bad merge left three),
537
+ the `mocks` false positive, `smidja-factory` indexed but app-provided, and the
538
+ seat's cloth read from an app repo that is absent until `step_apps` runs
539
+ (`design-check` now skips cleanly rather than failing every worktree).
540
+
541
+ ## 2026-09-17 — midgard holds public assets only; the company wiki is gone
542
+
543
+ - **`midgard/company_wiki/` did not belong in this repo.** It was an empty
544
+ placeholder (one `.gitkeep`), but a company wiki is *tenant data* — policies,
545
+ vision, specs, client names — and this repo is public. Removed, with every
546
+ reference to it corrected in `midgard/README.md`, `STRUCTURE.md`, `docs/lore.md`
547
+ and `assets/Ymir.md`.
548
+ - **`midgard/` is now described precisely.** It was called "shared company
549
+ assets" in one place and "cross-tenant" in another; a wiki is neither. The
550
+ contract says **public assets**, and `midgard/README.md` names what does *not*
551
+ belong: company knowledge, which lives at `$YMIR_HOME/hodd/identity/companies/`.
552
+ - **`bin/private-guard.sh` gains a tenant-document rule** so the shape cannot
553
+ return: a `company_wiki/`, `wiki/`, `policies/`, `handbook/` or `vision`/
554
+ `strategy`/`roadmap` document staged in the public tree is refused, with a
555
+ message naming where it belongs. Verified by planting `midgard/company_wiki/policies.md`.
556
+ - **Nothing had leaked.** A full sweep of every tracked midgard file for personal
557
+ handles, home paths, key shapes, tunnel UUIDs and LAN/tailnet addresses found
558
+ no hits — the ingress configs use `${VARS}` and `example.org` throughout, and
559
+ the DB schema is generic with row-level security on every tenant table.
560
+
561
+ ## 2026-09-17 — AGENTS.md states the one law: no private data in the public repo
562
+
563
+ - **The law is now first.** A `first_law` block sits directly under the mandate
564
+ in `AGENTS.md`: never store personal or private data in this repo — not a
565
+ secret, a key, a name, a plan, a schedule, a client, a credential, or a note.
566
+ The repo is public; private data lives at `$YMIR_HOME`, under `hodd/`.
567
+ - **The private-data section was rebuilt against the disk.** It had duplicated
568
+ `secrets/` and `identity/` lines, no `hodd/` level, and a layout that no longer
569
+ matched the home. It now shows the real tree, names `$YMIR_HOME/hodd/` as *the*
570
+ private data path, and records that `bin/hoard-lib.sh` is the one source of
571
+ truth for it.
572
+ - **Every `$YMIR_HOME/...` path in the file was corrected** to `$YMIR_HOME/hodd/...`
573
+ — the directory rules table, the registry reference, the secrets reference, and
574
+ the append-only ledger path all pointed at the old flat layout.
575
+ - **Two wards are now documented**: `bin/secret-guard.sh` (a secret entering the
576
+ repo) and Eir's `hoard` surface (private data drifting outside the hoard, or a
577
+ `.ymir-layout.yaml` naming a path that does not exist).
578
+ - **Staging discipline is written down.** Stage named files in the home, never
579
+ `git add -A` — a scratch file written seconds earlier gets swept into a commit
580
+ and pushed. This happened this day and cost a history rewrite.
581
+ - **`hodd/AGENTS.example.md`** carried the same stale flat paths and a wrong
582
+ command (`secret-guard.sh emit`, which does not exist); both corrected.
583
+
584
+ ## 2026-09-16 — the validator stops calling a dead visualizer healthy
585
+
586
+ `bin/ymir-validate.sh` reported `visualizer PASS "UI built and served on :8437"`
587
+ by looking at `./dist` alone. When the API had crashed on a bad `CMD_DB` and
588
+ nothing was listening on `:8437`, the validator still said PASS — the false-pass
589
+ class this night kept surfacing, in the very tool meant to catch it.
590
+
591
+ - **The check now probes the port as well as the build.** `dist` missing → FAIL;
592
+ built but nothing on `:8437` → FAIL with the fix (`run scripts/start.sh`);
593
+ built and listening → PASS. A PASS now means the thing is actually up.
594
+ - **The smidja-db check resolves the same DB pair** `scripts/start.sh` and
595
+ `bin/smidja-bootstrap.sh` do (an existing `$YMIR_HOME/smidja/smidja.db` first,
596
+ then an in-repo copy, `SMIDJA_DB` override) — previously it named the home path
597
+ only, so an in-repo DB read as missing.
598
+ - `galdr-reread`: `assets/installation.md` — a `visualizer` PASS means built *and*
599
+ listening.
600
+
601
+ ## 2026-09-16 — the roster's effect lands, and the backend pin stops being committable
602
+
603
+ Applying the private roster (`bin/agents-config.sh apply`) writes the resolved
604
+ model into each agent's canonical profile. Flipping the two local agents to Pi
605
+ made that visible — and surfaced a small ignore hole.
606
+
607
+ - **Profiles follow the roster.** `sindri-developer.md` and `kvasir-scout.md`
608
+ now carry their **Pi** ids (`llamacpp-coder/qwen3-coder-30b`,
609
+ `llamacpp/qwen3.5-9b`), and `huginn-researcher.md` its declared model — the
610
+ output of `apply` against the operator's roster. These profiles are what the
611
+ harnesses load, so the local agents now run on Pi rather than through OpenCode.
612
+ - **`.agents/config/backend` is ignored.** The pinned terminal backend is
613
+ machine-local, but `config` is a **symlink** to `.agents/config`, so the
614
+ `config/*` rule never matched it and the pin was committable. The file is named
615
+ in `.gitignore` instead.
616
+
617
+ ## 2026-09-16 — the roster can finally run an agent on Pi
618
+
619
+ `bin/agents-config.sh apply` wrote **every** agent's model into `opencode.json`'s
620
+ agent block, whatever harness that agent used. So an agent set to run on **Pi**
621
+ (native local models) would have had its Pi model id —
622
+ `llamacpp/qwen3.5-9b` — written into OpenCode's config, which cannot resolve it.
623
+ That is why the roster's two local agents were pinned to `harness: opencode` with
624
+ opencode-style ids: there was no working way to put an agent on Pi.
625
+
626
+ - **`apply` is now harness-aware.** Only `opencode`-harness agents are written
627
+ into `opencode.json`; a `pi` (or `hermes`) agent is left out, its model id going
628
+ to the resolve cache that `bin/agent-run.sh` reads. The providers block is
629
+ unchanged — a provider's endpoint is a real fact OpenCode may still want.
630
+ - The way this is meant to be used: declare a local agent's model as its **Pi**
631
+ id (`llamacpp/qwen3.5-9b`, `llamacpp-coder/qwen3-coder-30b` — the ids `pi
632
+ --list-models` reports) with `harness: pi`; the roster is then Pi-driven with no
633
+ per-machine hand-editing.
634
+ - `galdr-reread`: `assets/harness-integration/README.md` — the two writers of
635
+ `opencode.json`, and the rule that only OpenCode agents belong in it.
636
+
637
+ ## 2026-09-16 — the mesh reaches Teams and Anchor, and `show` stops guessing
638
+
639
+ `bin/a2a-mcp.sh` wired only the well (`engram`) and the mesh (`a2abridge`). The
640
+ **Teams** plane and **Anchor** memory existed only in the operator's personal
641
+ harness config, so no agent *inside* Ymir could see tickets or anchored memory —
642
+ and `a2a-mcp.sh show` insisted on a **fixed** server list, reporting
643
+ `wayofteams-mcp not on PATH` even when the plane was wired and working.
644
+
645
+ - **Remote MCP servers, URL-driven.** `bin/a2a-mcp.sh` now wires `wayofteams`
646
+ (from `WAYOFTEAMS_MCP_URL`) and `way-of-anchor-sse` (from `ANCHOR_MCP_URL`).
647
+ OpenCode gets them natively (`type: remote`); Pi, which has no remote
648
+ transport, gets them through the `mcp-remote` stdio bridge. A URL wins over a
649
+ local `wayofteams-mcp` binary when both exist. The URLs are credential-ish and
650
+ live in the private platform env — never the tracked tree.
651
+ - **`show` reports what is actually wired.** It enumerates every key present in
652
+ Pi's `mcpServers` and OpenCode's `mcp` (union with what this run would add), so
653
+ a hand-added or previously-installed server is visible, and the hint only fires
654
+ when Teams/Anchor is genuinely absent.
655
+ - `galdr-reread`: `assets/harness-integration/README.md` — the MCP scope is now
656
+ well · mesh · Teams · Anchor, with the two env keys named.
657
+
658
+ ## 2026-09-16 — the credential reaches the gate, and the hall stops leaking an IP
659
+
660
+ Setting the operator password had **no effect**. This is the bug that made that
661
+ true, and the runtime state that leaked a private address.
662
+
663
+ - **`scripts/start.sh` never loaded the platform env for the gate.** The gate is
664
+ `bun run apps/hlidskjalf/server/index.ts`, and the server reads
665
+ `process.env.HLIDSKJALF_AUTH` — but the launcher passed only `PORT`, and nothing
666
+ sourced `.env.local`. So a credential written by `bin/ymir-setup-auth.sh`
667
+ (correctly: `0600`, gitignored) never arrived, and because the gate treats an
668
+ empty `GATE_AUTH` as authenticated (`authed: GATE_AUTH ? … : true`) the gate
669
+ stayed **open**. `start.sh` now loads `.env.local` once, before the ports, for
670
+ every service it raises — the gate, Bifrost and Mimir.
671
+ - **`apps/odrerir/.astro/dev.json` was tracked.** Astro rewrites it on every
672
+ start with the live `pid` and the host's own addresses (LAN + tailnet) — a
673
+ private-IP leak into a public tree. It is now untracked and gitignored; the
674
+ generated types beside it stay tracked.
675
+ - `galdr-reread`: `assets/hlidskjalf-ui.md` (the gate must receive the credential)
676
+ and `assets/odrerir-hall.md` (`.astro/dev.json` is runtime state).
677
+
678
+ ## 2026-09-17 — the fragment convention is enforced where GitHub runs it
679
+
680
+ - **The gap, found by doing it.** The pre-push hook folds fragments before a
681
+ push — but a **GitHub merge bypasses the local hook entirely**. PR #43 merged
682
+ and left its own fragment unfolded on `main`, so the ledger silently fell
683
+ behind what the fragments already told. `bin/changelog-assemble.sh --check`
684
+ catches exactly that state; nothing was calling it.
685
+ - **The gate, at the layer that runs.** `.github/workflows/ymir-changelog-fragments.yml`:
686
+ - **on `push` to main** — `bin/changelog-assemble.sh --check` fails when any
687
+ fragment is unfolded, so the ledger can never fall behind a merge.
688
+ - **on a PR** — a change touching code or docs must add a fragment, edit
689
+ `CHANGELOG.md`, or carry the `no-changelog` label. The label records the
690
+ deliberate decision that no entry belongs, rather than letting it be
691
+ forgotten.
692
+ - **The pattern is the standard one.** This is the well-trodden "news fragments"
693
+ design (towncrier — Twisted, pytest, pip, attrs; changesets; CPython). The
694
+ zeroc-ice proposal states the root cause exactly: *"PRs edit one shared file.
695
+ Better merge discipline doesn't fix it; not editing the shared file does."*
696
+ - **`merge=union` was considered and rejected on evidence.** The Rigor ADR-105
697
+ tried it and measured it: *"GitHub's PR-mergeability and merge computation
698
+ ignore `.gitattributes` merge drivers, union included."* It works against local
699
+ git and does nothing on GitHub — a trap already falsified.
700
+ - **Main's own pending fragment is folded in this change**, so the ledger and the
701
+ fragments agree from here on.
702
+
703
+ ## 2026-09-17 — the changelog stops conflicting: fragments, folded by a script
704
+
705
+ - **The problem, measured.** `CHANGELOG.md` is one file and every branch appends
706
+ at the same position — the top. Git sees two branches inserting different lines
707
+ at the same spot and calls it a conflict, so **every merge re-conflicts every
708
+ open branch**. Sixteen open PRs were each resolved by hand for this reason
709
+ alone, and each merge re-conflicted the rest: O(N²) meaningless conflicts.
710
+ - **The fix — `CHANGELOG.d/`.** A change adds **one file with a unique name**
711
+ (`<YYYY-MM-DD>-<slug>.md`) instead of editing the ledger. Two branches never
712
+ touch the same fragment, so the collision cannot occur.
713
+ - **`bin/changelog-assemble.sh`** folds fragments into `CHANGELOG.md` — newest
714
+ first, above everything already recorded. Folding is an **append**: existing
715
+ entries are copied verbatim, never reordered, never rewritten. `--dry-run`
716
+ shows what would fold; `--check` exits 1 when fragments are unfolded.
717
+ - **The pre-push hook folds first.** `bin/changelog-guard.sh --install` now
718
+ writes a hook that runs the assembler before the guards, so a push never leaves
719
+ fragments unfolded; if folding changes the ledger the push is refused until the
720
+ fold is committed.
721
+ - **The guard accepts either form.** A push satisfies the duty by appending to
722
+ `CHANGELOG.md` **or** by adding a fragment. The refusal message now names both.
723
+ - **Rule 06 is amended, append-only** — the clause stands; the amendment records
724
+ fragments, and `CHANGELOG.d/` joins the append-only set so a move must carry it.
725
+
726
+ ## 2026-09-17 — the hearth re-seeds on its container: Sessrúmnir's chat fire returns
727
+
728
+ - **The ember glow was gone from the chat because the canvas sized itself once
729
+ at mount and only re-seeded on `window.resize`.** Sessrúmnir's split panes
730
+ (file tree · editor · image) resize the chat column without any window resize,
731
+ so the hearth kept a stale or zero-sized canvas — invisible. Both the shared
732
+ hearth (`midgard/design-system/ember.js`) and Sessrúmnir's React port
733
+ (`ember-background.tsx`) now watch their container with a ResizeObserver and
734
+ re-seed only when the measured size actually changes (never per-frame).
735
+ Hlidskjalf inherits the same mend through the shared module.
736
+ - **Verified live**: Sessrúmnir restarted on the fresh build; a screen capture
737
+ of the running window shows the fire drawing (ember + warm-glow pixels across
738
+ the hearth area). The running app had been serving a stale bundle (missing
739
+ `react-i18next` — deps installed, rebuilt).
740
+
741
+ galdr-reread: `.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md` — the
742
+ EmberBackground hearth section (shared module + ResizeObserver mend).
743
+
1
744
  # CHANGELOG
2
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
+
862
+ ## 2026-09-17 — Pi gets its agents, and a dead extension comes back
863
+
864
+ - **Pi has no agent loader.** Agent loading in Pi is a *package* (`pi-agents`,
865
+ `pi-agent-mode`, `pi-simple-agents`), never a core feature — and Ymir installs
866
+ none. So `.pi/agents/` held twenty correct profile links that **nothing in Pi
867
+ ever read**. The same failure as OpenCode's singular `.opencode/agent/`,
868
+ arrived at from the other side.
869
+ - **The fix ships in this repo, not a root-pi package.**
870
+ `.pi/shared/extensions/ymir-subagents.ts` discovers the canonical
871
+ `.agents/agents/*.md` tree itself and registers a `subagent` tool. A call runs
872
+ the chosen figure as a nested model call in the current session: the figure's
873
+ markdown body is its system prompt, its frontmatter `model:` picks the model
874
+ where the machine serves it. `subagent({})` and `/subagents` list the roster.
875
+ - **It imports nothing.** `@earendil-works/pi-coding-agent` is not installed as a
876
+ package, so an extension importing its types cannot load at all. This one takes
877
+ `pi` as `any` and declares `parameters` as a plain JSON schema — the pattern
878
+ every working extension in the tree already uses.
879
+ - **A rename that left a reader behind.** `skuld-branch-supervision.ts` imported
880
+ `calmTranscriptClassIsVisible` / `CalmPresentationState` from
881
+ `./lib/ro-visibility.ts`, but that module exports `roTranscriptClassIsVisible` /
882
+ `RoPresentationState`. Pi refuses the **whole extension** at load, so Skuld's
883
+ supervision branch was dead in every session and nothing reported it. Fixed.
884
+ - **The check that catches it** is now recorded in the owning asset: import every
885
+ deployed extension with `node --input-type=module` and confirm it loads — a
886
+ module that cannot resolve is invisible from the file listing.
887
+ ## 2026-09-17 — every figure loads: the agent directory was never read
888
+
889
+ - **The bug, stated plainly.** Twenty agent profiles existed, twenty symlinks
890
+ pointed at them, and every gate was green — yet OpenCode loaded **three**
891
+ agents. The symlinks lived in `.opencode/agent/` (SINGULAR). OpenCode reads
892
+ `.opencode/agents/` (PLURAL). Twenty correct links sat in a directory no loader
893
+ opened, so only `brokk` and `hnoss` — the two declared by hand in
894
+ `opencode.json` — ever appeared.
895
+ - **The directory is fixed and migrated.** `.opencode/agent/` → `.opencode/agents/`
896
+ (20 links, all resolving). `bin/valknut-load.sh` now writes the plural path and
897
+ **migrates a legacy singular dir forward**, so an old home heals instead of
898
+ silently keeping its agents invisible.
899
+ - **The roster now declares what it names.** `bin/agents-config.sh apply` only
900
+ touched an agent already present in `opencode.json` (`if a in ablock`), so a
901
+ figure the roster knew but the config had never seen stayed undeclared. The
902
+ roster is the source of truth; a name in it now reaches the harness config.
903
+ - **A new gate catches the class.** `compliance-check.sh` gains **`roster`**: the
904
+ roster (`config/agents.yaml.example`) and the canonical tree
905
+ (`.agents/agents/*.md`) must name the same figures. A figure in the tree but not
906
+ the roster falls to `default_model` and is never declared; a figure in the
907
+ roster with no profile is a phantom. This is the check that would have caught
908
+ the real failure while every other gate stayed green.
909
+ - **Verified, not assumed.** `opencode agent list` in the worktree reports all
910
+ twenty figures (bragi … vor) plus Ymir's own subagents — 33 agents, up from 3.
911
+ Compliance: 14/14 PASS.
912
+ - **References swept.** Every `.opencode/agent/` reference in code and docs moved
913
+ to the plural path: `bin/valknut-load.sh`, `bin/agents-config.sh`,
914
+ `apps/hlidskjalf/server/index.ts`, `RULES/02-agents.md`, `README.md`,
915
+ `docs/runbooks/agents.md`, `.pi/extensions/README.md`, and the Galdr assets
916
+ (`harness-integration`, `runtime-compliance`, `registry`, `hlidskjalf-ui`).
917
+ `CHANGELOG.md`'s own historical entry is left as written — append-only.
918
+
919
+ ## 2026-09-16 — the core senses the real host, not Omarchy's shadow
920
+
921
+ - **`bin/ymir-install.sh` `step_host` ran the wrong sensor.** The core host step
922
+ called `bin/omarchy-sense.sh` — the **Omarchy-only** sensor — so on any
923
+ non-Omarchy host it learnt nothing and printed the sensor's own SKIP row as a
924
+ host snapshot: `host snapshot: sensor,SKIP,not an Omarchy host…`. On a Fedora
925
+ workstation that is the whole discovery step, reporting a skip as fact.
926
+ - **Now it senses THIS machine with `bin/host-sense.sh`** — the portable sensor
927
+ (the same one behind `ymir sense`) — on **every** host (Rule 05). Recording
928
+ stays the Omarchy layer's job: `omarchy-sense observe`, the post-update hook,
929
+ desktop placement, the plugin offer, and the alarm channel remain gated behind
930
+ `step_omarchy`. Nothing here assumes Omarchy; nothing here skips the core
931
+ machine either.
932
+ - `step_host` no longer branches on `--check`; it senses once and reports once:
933
+ `host sensed: <os> / <id> / <family> / <session> / <desktop>`.
934
+ - The owning asset reflects it in the same change:
935
+ `.agents/skills/galdr-ymirsystem/assets/installation.md` — the `host` step row,
936
+ the Verify list, the two-layer table (recording, not learning), and the
937
+ "Omarchy branches" note.
938
+ ## 2026-09-16 — the visualizer finds its DB, and the seat-hall actually builds
939
+
940
+ Two more readers left behind — both discovered by *starting the apps*, not by any
941
+ gate. Each failed silently in its own way.
942
+
943
+ - **`scripts/start.sh` pointed the smithy at a repo path that no longer holds the
944
+ DB.** `0003-private-data-separation` moved it to `$YMIR_HOME/smidja/smidja.db`,
945
+ but the starter still passed `CMD_DB=<repo>/apps/smidja/smidja_data/smidja.db`
946
+ — so the visualizer API died on boot with `smidja.db not found` and `:8437`
947
+ answered nothing. It now resolves the same pair `bin/smidja-bootstrap.sh` does
948
+ (home first, in-repo fallback, `SMIDJA_DB` override). The asset already
949
+ *described* this resolution; the code now implements it.
950
+ - **`bin/sessrumnir-ensure.sh` never built the app.** `[ "$built_present" ]` tests
951
+ a **literal, non-empty string** — always true — so `ensure --install` skipped
952
+ `build_app` and reported `built: no` with exit 0. Sessrúmnir only built when the
953
+ build was run by hand. Now `built_present` (the function) is called.
954
+ - `galdr-reread`: `.agents/skills/galdr-ymirsystem/assets/smidja.md` — the DB
955
+ path, the `scripts/start.sh` DB resolution, and the observer's read list.
956
+
957
+ ## 2026-09-16 — four readers that still pointed at the pre-split world
958
+
959
+ The app split and the hoard migration moved things; four readers never followed.
960
+ None failed loudly — each reported a clean PASS, a wrong SKIP, or a silent loss.
961
+
962
+ - **`bin/ymir-install.sh` wrote during `--check`.** The step chain ran
963
+ `bin/ymir-migrate.sh apply` unconditionally, so a preview that promises *"report
964
+ only, no writes"* actually **moved private data**. Migrations now apply only on
965
+ a real run: `if [ "$CHECK" = 0 ]; then ... apply; fi`.
966
+ - **`.agents/migrations/0003-private-data-separation.sh` skipped every directory.**
967
+ It pre-creates its target dirs, then its `copy` refused any target that already
968
+ existed — so `data/` (and every other directory source) was silently dropped.
969
+ The realm declaration never arrived and `0004` fell back to a neutral realm.
970
+ `copy` now **merges** a directory into its target (never overwriting a file) and
971
+ copies a single file only when it is absent.
972
+ - **`bin/smidja-bootstrap.sh` looked for the smithy at the old root.** `sys.path`
973
+ pointed at `smidja/`, but the split moved it to `apps/smidja/`, so the DB seed
974
+ died with `ModuleNotFoundError: No module named 'smidja_modules'`. It now
975
+ searches `apps/smidja` then `smidja`, so either layout works.
976
+ - **`bin/ymir-validate.sh` read the ledger from the pre-move path.** It looked at
977
+ `$YMIR_HOME/memory/`, but `0004-hoard-and-realms` put the ledger at
978
+ `$YMIR_HOME/hodd/memory/` — a false `runes FAIL` on a healthy home. It now tries
979
+ the hoard first, then the pre-move locations.
980
+ - `galdr-reread`: `.agents/skills/galdr-ymirsystem/assets/smidja.md` — the
981
+ protected-paths gotcha and the `smidja/` → `apps/smidja/` move.
982
+
3
983
  ## 2026-09-16 — Omarchy-first, host-aware: Windows and macOS get a door
4
984
 
5
985
  - **`bin/host-sense.sh`** — the one place that looks before anything acts. It
@@ -633,7 +1613,6 @@ subsystem was never loaded before the code changed. Five layers now prevent it:
633
1613
  - **GitHub:** `bin/project-git.sh` reads `workspace/projects.yaml` `git{}`.
634
1614
  - **Verified:** build green, compliance 8/8, smoke 8/8, lint 4/4.
635
1615
 
636
-
637
1616
  All significant runtime, policy, and architectural changes for the Ymir platform.
638
1617
  Entries are appended chronologically; never rewritten.
639
1618
 
@@ -879,3 +1858,385 @@ Entries are appended chronologically; never rewritten.
879
1858
  `.agents/state/.wake-queue`) are empty, a repeat carry is the same drained
880
1859
  news and is skipped. Genuine new content (different message, or a door with
881
1860
  a line) always delivers. Harness asset updated in the same change.
1861
+
1862
+ ## 2026-09-16 — one login for every app on the web; the gate fronts them all
1863
+
1864
+ - **Every app host is gated**, not just Smíðja. The gate now routes by host from
1865
+ an `APP_HOSTS` table and fronts each one: unauthenticated visitors get that
1866
+ app's own sign-in page, an API path gets 401, and the desktop seat (loopback +
1867
+ marker) walks straight in. Verified: `smidjadell…` → *Smíðja — Sign in*,
1868
+ `odrerirdell…` → *Óðrerir — Sign in*, API 401, desktop seat 200 (the hall).
1869
+ - **Every public hostname points at the gate (:3889)** — never at an app's own
1870
+ port. That was a hole straight past the login: `gjallarhorn-expose.sh` had been
1871
+ exposing each app on its own port, so the app answered the world directly.
1872
+ - **The gate learns the host map at start** (`state/gjallarhorn-hosts.env`,
1873
+ written by the expose script and sourced by `scripts/start.sh`), so routing is
1874
+ not an accident of whoever launched it last.
1875
+ - **No guessed credentials.** The generated tunnel config wrote
1876
+ `credentials-file: …/ymir.json`; cloudflared names that file for the tunnel's
1877
+ UUID, so it refused to start and the world got 530. The line is gone —
1878
+ cloudflared resolves a named tunnel's own credentials.
1879
+ - Verified through the tunnel: `https://ymirdell.zerwiz.org/` → 200. The app
1880
+ hostnames still answer 530: their DNS routes were made against an earlier
1881
+ tunnel and need re-creating (`cloudflared tunnel route dns ymir <host>`).
1882
+
1883
+ ## 2026-09-16 — NO BLUE: the forge language replaces the old blue
1884
+
1885
+ The blue had a name. It was the platform's own house tint, `--ymir-house-ymirlabs:
1886
+ #38bdf8`, carried into every surface that wears Ymir's colours — the login, the
1887
+ emblem, the realm data, the Smíðja chrome. The design doctrine (homepage repo,
1888
+ `DESIGN-UNIFICATION-PLAN.md`) says the landing page **is** the language, and that
1889
+ language is **forge**: stone ground (`#0e0c09`), bronze accent (`#c9973f`), bone
1890
+ text (`#cfc3a9`).
1891
+
1892
+ - `midgard/design-system/tokens.css` — the house tint is bronze now, with the
1893
+ reason recorded in the token itself.
1894
+ - The blue's footprints removed: `Emblem.tsx`, `state/store.ts`, `data/realms.ts`
1895
+ (Hlidskjalf), `style.css` (Smíðja visualizer), and Sessrúmnir's terminal accent
1896
+ fallback.
1897
+ - Both built stylesheets rebuilt and verified: **zero blue** in the CSS the browser
1898
+ actually receives (`apps/hlidskjalf/dist`, the visualizer's `dist`).
1899
+ - The visualizer's build was failing on my own earlier edit (unused imports left
1900
+ when I moved `repoRootOf` into `db.ts`) — fixed, so `bun run build` is green.
1901
+
1902
+ The canonical token home is answered by what already exists: `midgard/design-system/`
1903
+ (tokens, icons, `ymir-mark.svg`, `icons.md`) — one source the apps import, so a
1904
+ colour changes once.
1905
+
1906
+ ## 2026-09-16 — every app wears its own rune
1907
+
1908
+ The icon set is runecoded (`midgard/design-system/icons.md`): an Elder Futhark
1909
+ rune, stroked at the chisel bevel, tinted by the app's house colour. The apps were
1910
+ wearing the generic Ymir mark — or, worse, the old **blue** logo.
1911
+
1912
+ - **`bin/design-icon.sh`** mints an app's icon from a glyph plus a house tint: a
1913
+ stone tile with the rune stroked in bronze. `list` shows the mapping.
1914
+ - Minted and wired: **Hlidskjalf** `ehwaz` ᛖ (the seat), **Óðrerir** `valhalla` ᚹ
1915
+ (the hall), **Sessrúmnir** `sowilo` ᛊ (the sun), **Smíðja** `ansuz` ᚨ (Odin's
1916
+ breath — the forge). Óðrerir already carried a forge-coloured favicon set.
1917
+ - **The blue logo is retired**: `public/logo.svg` removed, and Smíðja's *inline*
1918
+ copy in `App.vue` — the mark in its own header, still `#0f172a`/`#1e293b` —
1919
+ replaced with the ansuz rune in bronze. Rebuilt; no blue in the bundle.
1920
+
1921
+ ### The hearth stays, and spreads
1922
+
1923
+ Sessrúmnir's background fire is **`EmberBackground`** — 26 embers rising with a
1924
+ gentle sway on a canvas, *"the hearth of the landing page, carried into the
1925
+ chat"* — used by its home screen and chat panel. It stays. Next: one shared
1926
+ ember (a framework-neutral `midgard/design-system/ember.js` with a
1927
+ `prefers-reduced-motion` guard) so Hlidskjalf's shell, the login screen,
1928
+ Óðrerir's hall and the Smíðja chrome can warm the same fire.
1929
+
1930
+ ## 2026-09-16 — the hearth spreads: one fire, three more surfaces
1931
+
1932
+ Sessrúmnir's fire is now shared rather than copied. `midgard/design-system/ember.js`
1933
+ is the framework-neutral original (the land page's Ginnungagap embers: 30 rising
1934
+ with a gentle sway over six drifting haze pools), with `ember.d.ts` for TypeScript
1935
+ consumers and a reader who asked for less motion gets a **still frame** instead of
1936
+ an animation.
1937
+
1938
+ Wired, and built:
1939
+
1940
+ - **Hlidskjalf** — `src/components/EmberBackground.tsx` imports the shared module;
1941
+ mounted behind the *login* and behind the *shell*, so the door and the hall burn
1942
+ over one hearth.
1943
+ - **Smíðja visualizer** — a `<canvas class="ember-bg">` behind every view.
1944
+ - **Óðrerir** — a `data-ember` canvas in its shell, so the hall warms too.
1945
+ - Sessrúmnir keeps its own React port untouched (two ports of one fire, the same
1946
+ physics; consolidating them on the shared module is the next tidy).
1947
+
1948
+ And the reference for the missing furniture: **Óðrerir already carries it all** —
1949
+ four favicons, `mask-icon` in bronze, `site.webmanifest`, canonical, description,
1950
+ theme-colour and Open Graph. The other apps should be brought up to *it*.
1951
+
1952
+ ## 2026-09-16 — icons the operator can pin, and one pair of hands for the shells
1953
+
1954
+ **"Fail, no icons" — and the cause was a trap I had already been bitten by.**
1955
+ `bin/design-icon.sh install` writes an app's rune into the icon theme and a
1956
+ `.desktop` entry into the applications dir. It used `XDG_DATA_HOME`, and this
1957
+ agent session exports that as a **sandbox** (`…/opencode-rd`), so the icons went
1958
+ somewhere no desktop can see. It now targets the operator's real data dir
1959
+ (`$HOME/.local/share`, with `YMIR_DESKTOP_DATA_HOME` for a throwaway test) — the
1960
+ same class of bug that once hid `gh` from this session.
1961
+
1962
+ Installed now, for **every** app: hlidskjalf, **odrerir**, **sessrumnir**, the
1963
+ Smíðja visualizer and hlidskjalf-mobile — each with its rune icon and a dockable
1964
+ entry. Óðrerir and Sessrúmnir had **none**, which is exactly why they could not
1965
+ be pinned. The stale `ymir-smidja.desktop` (pointing at a repo PNG) is folded
1966
+ into the rune's own entry.
1967
+
1968
+ **One pair of hands for two lifecycles.** `scripts/start.sh` and
1969
+ `scripts/stop.sh` manage the web; `scripts/electron.sh` manages the shells — and
1970
+ they never met, which is why "the electrons are not running" was true while 28
1971
+ processes stood, and why a restart could leave a shell watching a dead port.
1972
+ `scripts/raise.sh` / `scripts/lower.sh` own both: lower takes the windows down
1973
+ **first**, so none is left watching a port that just vanished.
1974
+
1975
+ **A bug the restart surfaced:** `scripts/electron.sh stop` used `local` outside a
1976
+ function — it worked only because the assignment happened to be harmless, and it
1977
+ printed a shell error on every stop. Fixed.
1978
+
1979
+ **And the inventory is a tool:** `bin/feature-inventory.sh` — 132 tools, 5 jobs,
1980
+ 26 skills, 20 agents, 8 workflows, 6 apps, 4 migrations, 6 registered projects,
1981
+ each with the way it must be proven written beside it.
1982
+
1983
+ ## 2026-09-16 — the icons are real runes now (eight of them were drawings)
1984
+
1985
+ The Allfather looked at the icons and said what no check had: *"the icons must be
1986
+ based on real runes."* He was right, and the fault was in the **glyph set**, not
1987
+ just in the two icons I had minted.
1988
+
1989
+ `midgard/design-system/icons.md` claims every glyph is an Elder Futhark rune and
1990
+ gives each its unicode. **Eight of the twenty-one files were drawings** — a hall
1991
+ (`valhalla`, which Óðrerir was wearing), a horn, a shield, a spear, a well, a
1992
+ squirrel, a gate, a world-tree. Replaced with the real runes their own rows
1993
+ declare:
1994
+
1995
+ ```
1996
+ gjallarhorn ᚷ Gebo · gungnir ᚦ Thurisaz · heimdall ᚺ Hagall · mimirsbrunn ᛜ Ingwaz
1997
+ ratatoskr ᛒ Berkanan · utgard ᚢ Uruz · valhalla ᚹ Wunjo · yggdrasil ᛃ Jera
1998
+ ```
1999
+
2000
+ Gungnir was claiming Gebo, which Gjallarhorn already holds, so it takes
2001
+ **Thurisaz** (the thorn — the spear) and `icons.md` is corrected to match.
2002
+
2003
+ **Five apps, five different runes:** Hlidskjalf `ehwaz`, mobile `raidho` (the
2004
+ road), Óðrerir `valhalla`→ now genuinely `ᚹ Wunjo`, Sessrúmnir `sowilo`, Smíðja
2005
+ `kaunan` (the torch). The install table drives both mint and install now — it had
2006
+ been guessing paths, which is why the Smíðja visualizer had been given
2007
+ Hlidskjalf's icon.
2008
+
2009
+ Also this pass: the visualizer gained its furniture (description, canonical,
2010
+ manifest, OG + Twitter), Óðrerir's Electron shell asks for a window icon, and the
2011
+ apple-touch PNGs are rasterised (ImageMagick is present).
2012
+
2013
+ ## 2026-09-16 — the desktop never dials out: Cloudflare stays outside Electron
2014
+
2015
+ The Allfather's rule: **"electron should never have anything with cloudflare to
2016
+ do."** It was not quite true — three doors stood open:
2017
+
2018
+ - **Sessrúmnir's "To the Hall"** preferred the local hall and, when it did not
2019
+ answer, fell back to `https://hall.ymir.zerwiz.org`. A desktop seat that
2020
+ reaches for a public hostname when its own machine is quiet is a seat that
2021
+ leaves the building to do its errands. It is now **local only** — when the hall
2022
+ is not running, the seat says so.
2023
+ - **Hlidskjalf's shell** took `HLIDSKJALF_URL` and `SMIDJA_URL` from the
2024
+ environment, and **Óðrerir's** took `HALL_URL` — each able to point a desktop
2025
+ window at a tunnel with one exported variable.
2026
+ - Both now pass their URL through a **loopback guard**: anything that is not
2027
+ `127.0.0.1`, `localhost` or `::1` is refused with a warning and replaced by the
2028
+ local default. The shell loads the machine, never the internet.
2029
+
2030
+ **Why this is the same fight as the bridge:** a desktop app that dials out is a
2031
+ desktop app whose failures belong to somebody else's edge. The bridge disguised a
2032
+ cloud endpoint as `127.0.0.1` and cost a night; these three would have disguised a
2033
+ tunnel as a local seat.
2034
+
2035
+ ## 2026-09-16 — the panels that crashed, the gate that lied, and the well that would not open
2036
+
2037
+ - **Statistics now reports the HARNESSES** (pi + opencode), not the smithy's runs.
2038
+ `bin/hlidskjalf-usage.sh` aggregates opencode's SQLite token store and both pi
2039
+ stores, and emits the numbers under the names the gate renders (`gate{}`):
2040
+ totals, usage, providers local/online with per_model, by_chain, by_model.
2041
+ Verified: 21,497 runs (opencode 21,422 · pi 75) · 331.5M tokens ·
2042
+ local 239,623 / online 331,289,420 · cache-hit 91.5%.
2043
+ - **A failed fetch no longer takes the hall down.** Endpoints answering `{error}`
2044
+ were handed to panels as data — Rail read `.filter` on an object, Stats read
2045
+ `.totals` on a string, and both crashed. The store now runs every bootstrap
2046
+ answer through `ok()`: an error keeps the last good value.
2047
+ - **`/api/usage` was answering with a traceback** while the script passed by hand:
2048
+ the gate process had been started before the fix and caches a failure for 60s.
2049
+ Restarted; the endpoint serves the harness numbers.
2050
+ - **The engram MCP (`-32000: Connection closed`) is mended.** Not config, not the
2051
+ engram package: `python3.12 -c "import mcp"` failed because the system
2052
+ `python3-rpds-py` ships without its compiled `rpds.rpds` module, which
2053
+ `jsonschema` (inside `mcp` 1.x) imports. Mended with
2054
+ `python3.12 -m pip install --user --break-system-packages --force-reinstall rpds-py`
2055
+ (plus `mcp<2` under python3.12). The server now starts and waits on stdio.
2056
+ Eir should check this pair: `mcp` present for the interpreter that runs the
2057
+ binary, and `rpds` importable.
2058
+
2059
+ ## 2026-09-17 — Sessrúmnir stops wearing Pi's clothes
2060
+
2061
+ - **The naming law, broken at the root.** Clicking the Sessrúmnir icon opened a
2062
+ window titled **Pi Desktop**: `apps/sessrumnir` is the Pi Desktop shell adopted
2063
+ whole, and its product name was still Pi's —
2064
+ `export const PI_DESKTOP_PRODUCT_NAME = 'Pi Desktop'` — rendered as the heading on
2065
+ the home screen, the sidebar and the chat panel. That is the title the Allfather
2066
+ saw, twice, on every launch.
2067
+ - **Mended at the name:** `SESSRUMNIR_PRODUCT_NAME = 'Sessrúmnir'` (Odin's hall of
2068
+ many seats), the three rendering components walked, and the old constant kept as an
2069
+ alias so no unwalked import breaks. `tsc --noEmit` exit 0.
2070
+ - **Still owed:** 1,255 English strings, **86 of them naming Pi** ("Pi is working",
2071
+ "Quit Pi Desktop", "Pi Desktop v{{latestVersion}} is available", "Start Pi/OMP
2072
+ before planning with Council"), and Pi's logo on the home screen. The locale is the
2073
+ one place they live — that pass is the order, not a patch.
2074
+ - Also in the launcher this session: an icon click now raises THE SYSTEM (converge
2075
+ the service, reborn a windowless app, focus a live window), and `stop --view X`
2076
+ stops only X — the loop that killed every view is gone. The Hall's own outage had
2077
+ a cause: an Astro dev server on :4323 while the app waited on :4322.
2078
+
2079
+ ## 2026-09-17 — the smithy can see the well
2080
+
2081
+ - **Why the memory looked absent in the smithy:** the visualizer proxies the bridge's
2082
+ `/inspect`, and `/inspect` reported three fields — store, episodes, agents — while
2083
+ the well holds six layers. A panel told only the episode count cannot show that
2084
+ facts, entities and reflections exist. `/inspect` now reports them all:
2085
+ episodes, facts (active/superseded), entities, edges, reflections (with the last
2086
+ run's time) and the vector index.
2087
+ - **Through the smithy right now:**
2088
+ `store $YMIR_HOME/memory/kaia.engram · episodes 8 · facts 36 (35 active) ·
2089
+ entities 60 · edges 480 · reflections 1 (2026-09-16T21:01) · vec index 8`.
2090
+ - **A trap found while doing it:** restarting the bridge RAW (`python3 bin/mimir-bridge.py`)
2091
+ loses the env that carries the well's path, and the bridge silently re-points at the
2092
+ old store in the repo — 364 stale episodes and no facts. The well is the **blessed
2093
+ starter's** to raise: `bin/mimir-bridge.sh --start`. A raw restart is a different
2094
+ well wearing the same port.
2095
+
2096
+ ## 2026-09-17 — Skrymir opens the hoard, not the checkout
2097
+
2098
+ - **The file browser was rooted in the wrong tree.** `workspaceRoot()` built every
2099
+ realm path under `ROOT` — the checkout — so `work` resolved to a directory that does
2100
+ not exist and the walk fell back to the repo's `docs/`. That is why Skrymir showed
2101
+ `lore.md`, `Architecture.md`, `research/`, `runbooks/`, and why a file it had just
2102
+ listed answered **"not found"**: the listing came from one tree and the read from
2103
+ another.
2104
+ - **The hoard first now (Rule 04):** `$YMIR_HOME/svartalfaheim/<realm>` → company
2105
+ container → `$YMIR_HOME/workspace/<realm>` → the checkout only as legacy. An empty
2106
+ realm shows the hoard's home, never the repo's docs. Verified: `work` lists
2107
+ `memory/daily/2026-09-13.md` and `workspace/`, and a read returns its body.
2108
+ - Note for the next hand: the gate runs **without** `--watch` — a server edit is on
2109
+ disk and not in the process until `scripts/start.sh` raises it again.
2110
+
2111
+ ## 2026-09-17 — the Fleet was a rack of terminals; now it is the roster
2112
+
2113
+ - **We were tracking the tools.** `.agents/agents/` holds **21 agent definitions** —
2114
+ Brokk, Sindri, Bragi, Forseti, Mímir, Kvasir, Snotra, Huginn, Hnoss, Sága, Muninn,
2115
+ Frigg, Gróa, Jörð, Sýn, Týr, Galdr… — and the Fleet served **13 panes all named
2116
+ "OpenCode"**, role `opencode`. The board showed seats and never the smiths who might
2117
+ be standing in them.
2118
+ - **`bin/hlidskjalf-agents.sh` now reads the roster** (`.agents/agents/*.md`, canonical
2119
+ per RULES/02) and joins each agent to its standing pane: figure, craft, model from
2120
+ the definition's frontmatter, and `live{}` only when a pane answers to it. An
2121
+ **unseated agent is still an agent** — hiding it is what made the board a rack.
2122
+ - Harness seats that answer to no one on the roster are kept visible and named
2123
+ honestly, never dressed as agents.
2124
+ - Served: **34 entries — 21 agents (roster first) + 13 harness seats, 14 joined**.
2125
+
2126
+ ## 2026-09-17 — the extensions deployed without their modules
2127
+
2128
+ - **pi would not start a seated worker, and the reason was a half-deploy.**
2129
+ The four shared pi extensions all reported
2130
+ `Failed to load extension: Cannot find module ./lib/<module>.ts` — and every one
2131
+ of those modules was present in the repo, one level away under the extensions'
2132
+ own `lib/`. The loader copied the top-level extension files and never their
2133
+ supporting modules.
2134
+ - **Mended in two places.** (1) The missing modules are now beside the deployed
2135
+ extensions, so a running pi loads all four. (2) `bin/valknut-load.sh` — the
2136
+ loader — now deploys that lib alongside the extensions, idempotently (identical
2137
+ files untouched), so a fresh machine cannot hit it. A deploy that copies a file
2138
+ but not the module it imports is not a deploy.
2139
+ - **What this cost:** the Forseti audit was seated on a **local** model, and the
2140
+ pane fell back to a bare shell when pi could not start — which is why the brief
2141
+ was typed at a prompt and answered `bash: syntax error near unexpected token '('`.
2142
+ With the extensions mended, pi's remaining blocker is the model: the llama-router
2143
+ has no raisable seat tonight (`:8080` unbound, and the installed llama.cpp cannot
2144
+ load the Apodex weights), so a local-first dispatch has nothing to reach.
2145
+
2146
+ ## 2026-09-17 — the well as tools: Ymir's first custom pi extension
2147
+
2148
+ - **Why the extension tree was failing, from the upstream law:** pi auto-discovers
2149
+ extensions from BOTH the global home and the project-local tree. The same extension
2150
+ in both loads twice and pi refuses the duplicate tool —
2151
+ `Tool "gna_watch_arm" conflicts with …`. Ymir's own loader already says the shared
2152
+ extensions have **one** home; the tree contradicted it by carrying the extension
2153
+ files project-locally as well.
2154
+ - **Built: `ymir-well`** — the well, as tools inside every pi session.
2155
+ `well_recall(query, k)` reads Kaia's memory before work begins;
2156
+ `well_observe(content, tags, actors, salience)` writes the lesson after. Written in
2157
+ the house voice, honest about failure (a failed write says the lesson was NOT
2158
+ written), and it sends tags as LISTS — a comma-string is stored as an array of
2159
+ characters, which is exactly how a recall once returned `['r','u','n']`.
2160
+ - Authored in the SHARED source and deployed to the global home — one home, never a
2161
+ project copy. That is the pattern every future Ymir extension follows.
2162
+
2163
+ ## 2026-09-17 — the duplicate extensions removed from the project tree
2164
+
2165
+ - **The fault, now measured exactly.** The project-local extension tree carried four
2166
+ files that the shared source also carries — `gna-pi-watch.ts`, `ro.ts`,
2167
+ `skuld-branch-supervision.ts`, `syn-turnend-guard.ts`. pi auto-discovers both the
2168
+ global home and the project tree, so each loaded twice and pi refused the second
2169
+ copy of every tool: `Tool "gna_watch_arm" conflicts with …`. Every `pi` start in
2170
+ every worktree died on it, which is why a seated worker fell back to a shell.
2171
+ - **The fix:** those four files are gone from the project tree. What remains there is
2172
+ `lib/` — the modules the *global* extensions import — and its README. The
2173
+ extensions themselves live in the shared source and in the one global home.
2174
+ - The seatbelt refused the plain shapes (`rm`, `mv`, `>`-bearing commands) on this
2175
+ path and named the remedy: *a reviewed commit*. This is that commit.
2176
+
2177
+ ## 2026-09-17 — smoke test all UI: two checks that lied about their own subjects
2178
+
2179
+ - **The truth gate failed on my own change.** It demanded the connector's total equal
2180
+ the herdr pane count; the connector is roster-first now, so it legitimately reports
2181
+ agents AND seats (33 vs 13 panes). Panes are a subset of what the board must show.
2182
+ Corrected: a FAIL now means FEWER entries than panes — a standing seat hidden from
2183
+ the board — which is the fault worth catching. **Verdict: PASS.**
2184
+ - **The smoke test reported a present database absent.** It looked for the smithy's db
2185
+ at the repo path; the runtime keeps it under `$YMIR_HOME/smidja/`, which is where
2186
+ the bootstrap and the gate both resolve it. Same wound as Skrymir and the well: the
2187
+ check and the owner disagreeing about where the record lives. **Now: OK, 8 tables.**
2188
+ - **Smoke test all UI: 5/5 OK** — spa, api, well, smidja-db, loaders. Truth gate: PASS.
2189
+
2190
+ Also mended in the same pass: `StatusChip` read `STATUS_META[status].tone`, and the
2191
+ fleet now reports a status it was never taught (`seated`), so an unknown status threw
2192
+ and killed the gate. An unknown status can no longer take a panel down.
2193
+
2194
+ ## 2026-09-17 — Glitnir stops hanging, and one process is one row
2195
+
2196
+ - **`/api/reviews` ran two 60-second shell checks on every poll**, so the review board
2197
+ hung for a minute and read as a dead surface. It is memoised for a minute now, the
2198
+ same discipline the usage endpoint already follows: a board a human reads does not
2199
+ need an answer fresher than its own usefulness.
2200
+ - **`/api/processes` returns the same id twice** — `systemd:dbus` arrives from two
2201
+ sources, 45 entries for 44 processes. The panel dedupes by id, so a repeated process
2202
+ renders as the one process it is, and React is no longer handed a duplicate key.
2203
+ The duplicate at the source is recorded, not hidden.
2204
+ - `/api/stream` answers 200; the live stream is not broken, it was the window.
2205
+
2206
+ ## 2026-09-17 — the fleet stops colliding: a grid fan, and each smith's own house
2207
+
2208
+ - **The Fleet graph collided above ~8 agents.** `Fleet.tsx` fanned every
2209
+ non-hub agent into a single two-row line at ~4.4% pitch on 46px rings — with
2210
+ 20 roster cards the rings and labels overlapped into an unreadable pile.
2211
+ `layout()` now fans a square-ish grid (`cols = ceil(sqrt(n))`, hub at top,
2212
+ rows pitched past ring+label), so 20 agents render separated.
2213
+ - **Every ring showed the same rune.** `bin/hlidskjalf-agents.sh` hardcoded
2214
+ `domain: ymirlabs` for every card and its roster parser never read the
2215
+ `domain:` frontmatter each figure carries — so all twenty cards wore the
2216
+ anonymous ᛦ. The roster now parses `domain:` and passes it through;
2217
+ `galdr.md` names its house (`brokkforge`); the graph falls back to
2218
+ `DOMAINS.ymirlabs` only when a domain is genuinely unknown (was a crash)
2219
+ — and the `AgentCard` already fell back safely.
2220
+
2221
+ galdr-reread: `.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md` — the
2222
+ Fleet graph paragraph (grid fan, roster domains, fallback).
2223
+
2224
+ ## 2026-09-17 — the apps leave the monorepo; installation pulls them from their own repos
2225
+
2226
+ - **The app split lands in the installer.** The five apps (hlidskjalf,
2227
+ hlidskjalf-mobile, odrerir, sessrumnir, smidja) now live in their own repos
2228
+ registered in the home registry (`$HOARD/identity/projects.yaml`) — the
2229
+ monorepo never tracks them, and **`step_apps`** clones or fast-forwards each
2230
+ `apps/<path>` from its registered `git{}` block (never guessing a remote,
2231
+ always reading the registry). The smithy engine (`apps/smidja`) is stamped
2232
+ from the cloned factory's `templates/smidja`, exactly as `install.py` does
2233
+ for a target repo — so a fresh clone still gets a working smithy.
2234
+ - **The vendored app trees leave the index.** `apps/{hlidskjalf,
2235
+ hlidskjalf-mobile,odrerir,sessrumnir,smidja,smidja-factory}` are gitignored
2236
+ and `git rm --cached`-ed (751 files, 135k lines); `apps/README.md` stays.
2237
+ A dir present that is not a git clone is reported (`move it aside and
2238
+ re-run`) rather than silently replaced.
2239
+
2240
+ galdr-reread: `.agents/skills/galdr-ymirsystem/assets/installation.md` — the
2241
+ step table (`22 steps`, `24` `step_*` functions), the new `apps` row, and the
2242
+ Sessrúmnir rows now say "its own repo", not "vendored".