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