@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
@@ -28,6 +28,9 @@ Quick rules:
28
28
  ## Location & stack
29
29
 
30
30
  - App: `apps/hlidskjalf` — **React 19 + Vite + TypeScript**, state via **Zustand**.
31
+ - The app lives in **its own repo** (`zerwiz/hlidskjalf`, registered in the home
32
+ registry) — the monorepo never tracks it; `bin/ymir-install.sh`'s `apps` step
33
+ clones it into `apps/hlidskjalf` at install, or fast-forwards a present clone.
31
34
  - Gate API: `apps/hlidskjalf/server/index.ts` — **Bun + `bun:sqlite`**, read-only,
32
35
  bound to the runtime and the smithy trace; Vite proxies `/api` → `:3889`.
33
36
  - Raise everything with the scripts (do not hand-start each process):
@@ -95,6 +98,13 @@ Rules that hold it honest:
95
98
 
96
99
  - `.shell` grid: `236px rail | 1fr`; rows `56px topbar · stage · stream`
97
100
  (the stream height is draggable, persisted in `streamHeight`).
101
+ - The **Fleet graph** (`gates/Fleet.tsx`) lays the roster on a 100×100 canvas:
102
+ Brokk as the hub, everyone else in a square-ish grid fan (`cols =
103
+ ceil(sqrt(n))`, pitch ≥ one ring + label) so 20 agents never collide; rings
104
+ carry the house glyph from `DOMAINS[agent.domain]` with a `ymirlabs`
105
+ fallback, and the live cards come from `bin/hlidskjalf-agents.sh` — which
106
+ reads each figure's `domain:` frontmatter (never a hardcoded `ymirlabs` for
107
+ everyone).
98
108
  - `.rail` (brand · gates · tenants · status), `.topbar` (realm chip · search ·
99
109
  accent · density · trace index · account), `main.stage` (the gate), `.stream`
100
110
  (Ratatoskr + Runes, pausable).
@@ -156,6 +166,18 @@ router `:8080`, Bifrost `:4603`), never only the first — a dead engine falls
156
166
  through to a live one. Recall reads `.agents/memory/well/episodes.jsonl`
157
167
  directly, so restoring that file revives the chat's memory without a restart.
158
168
 
169
+ ### The EmberBackground hearth (the shared fire)
170
+
171
+ The animated glow behind the shell, the login, and Sessrúmnir's chat comes from
172
+ **one** hearth, not four copies: `midgard/design-system/ember.js` (`startEmbers`),
173
+ with Sessrúmnir's `ember-background.tsx` as its React port. Both draw the same
174
+ embers + haze, respect reduced-motion, and — since the Sep 17 mend — re-seed
175
+ via **ResizeObserver on the container**, not just `window.resize`: a split pane
176
+ or toggled sidebar changes the chat column's size without a window resize, and a
177
+ canvas that sized itself once at a stale dimension never shows the fire. The
178
+ shared module watches the host; the port watches its parent; re-seeding fires
179
+ only when the measured size actually changed (never per-frame).
180
+
159
181
  ### The gate API reads cheaply (added 2026-09-16)
160
182
 
161
183
  The gate reads append-only ledgers (Runes, the well's `episodes.jsonl`, masterplan
@@ -201,7 +223,10 @@ API on the same port (`scripts/start.sh` raises it). Details: `assets/smidja.md`
201
223
  ## Agents, skills & mythological naming
202
224
 
203
225
  - The Forge gate (`src/gates/Forge.tsx`) creates/edits **Eindri** and **skills**,
204
- and reads/edits the smithy's prompts.
226
+ and reads/edits the smithy's prompts. The skill list shows each skill's own
227
+ text: the name + aett rune on the head row, and the description line
228
+ underneath (`forge-item-desc`, dimmed until hover) — the description comes
229
+ from the live `/api/skills` index, truncated to 240 chars by the gate API.
205
230
  - Naming law: `src/data/mythology.ts` maps a craft/capability → the Norse figure
206
231
  whose myth matches it (smith→Sindri, skald→Bragi, sage→Huginn, judge→Tyr,
207
232
  forger→Brokk …). Skills take an **aett** prefix.
@@ -253,6 +278,17 @@ npm run build # must be green
253
278
  - When the shell, tokens, gates, or the gate API change, update this asset and
254
279
  `docs/design.md` together.
255
280
 
281
+ ### The agent path the Fleet gate reads (2026-09-17)
282
+
283
+ `server/index.ts` decides whether a figure is *registered* by testing for its
284
+ profile in the OpenCode agent directory. That directory is **`.opencode/agents/`
285
+ — PLURAL**, which is what OpenCode actually loads. The singular
286
+ `.opencode/agent/` was never read by the harness, so twenty correct symlinks sat
287
+ in a directory no loader opened and the gate reported figures unregistered while
288
+ the tree held them all. Any code that resolves an agent profile must use the
289
+ plural path; the loader (`bin/valknut-load.sh`) migrates a legacy singular dir
290
+ forward and binds the plural one.
291
+
256
292
  ## Domains, not houses (Rule 01/03)
257
293
 
258
294
  The eight Labs are **domains (Greinar)** — the knowledge axes — not houses. A
@@ -292,6 +328,13 @@ the password comes from `HLIDSKJALF_AUTH` in `.env.local`, never inline. A
292
328
  hardcoded name here is both a leak into the public tree and wrong for any other
293
329
  operator — `bin/public-guard.sh` exists to catch exactly that class of mistake.
294
330
 
331
+ **The gate must actually receive it.** `scripts/start.sh` loads `.env.local`
332
+ (mode `0600`, gitignored) into the environment before raising the gate, because
333
+ the bun process reads `process.env.HLIDSKJALF_AUTH` and nothing else was loading
334
+ it — so a credential set by `bin/ymir-setup-auth.sh` never took effect, and an
335
+ empty `GATE_AUTH` makes the gate treat every request as authenticated. After
336
+ setting a credential, restart the gate: `scripts/stop.sh; scripts/start.sh`.
337
+
295
338
  ### One login, and it is the one with the lore (2026-09-12)
296
339
 
297
340
  There used to be **two** ways in, and the second was a mistake:
@@ -5,12 +5,13 @@
5
5
  > page is the map.
6
6
 
7
7
  One command sets up the whole system for the user; it self-heals what it can and
8
- reports what it cannot. It **asks for consent first** (accept the plan, or pass
9
- `--yes` for non-interactive use), and it **validates at the end** that what it
8
+ reports what it cannot. It **asks for consent first** with a plan it computes on
9
+ this host, not a recited paragraph — and it **validates at the end** that what it
10
10
  claims is actually running.
11
11
 
12
12
  ```
13
13
  bin/ymir-install.sh # the first setup (idempotent; asks to proceed)
14
+ bin/ymir-install.sh --plan # the plan, probed — changes nothing (--json too)
14
15
  bin/ymir-install.sh --check # report only, no writes, no prompt
15
16
  bin/ymir-install.sh --yes # non-interactive (accept the plan)
16
17
  bin/ymir-install.sh --skip-engines --skip-services
@@ -18,25 +19,267 @@ bin/ymir-install.sh --no-desktop # don't open the desktop apps at the end
18
19
  bin/ymir-install.sh --status # alias of --check
19
20
  ```
20
21
 
22
+ ## The plan comes first — `bin/ymir-plan.sh`
23
+
24
+ The consent a real install asks for is a **computed plan**, one row per step, each
25
+ carrying its state and the reason for it. A hardcoded paragraph cannot know the
26
+ host: the old one named an Omarchy version on a Mac, promised a workspace tree that
27
+ already stood, and never mentioned that no application had been installed at all.
28
+
29
+ ```
30
+ bin/ymir-plan.sh # the plan (TOON)
31
+ bin/ymir-plan.sh --json # the same, for automation
32
+ bin/ymir-plan.sh --phase 5 # one phase
33
+ bin/ymir-plan.sh --blocked # only what cannot proceed, and why
34
+ ```
35
+
36
+ ```
37
+ plan_states[5]{state,means}:
38
+ "DO","a change will be made"
39
+ "SKIP","already satisfied — nothing to do"
40
+ "INFO","a fact about this host, discovered; no change implied"
41
+ "BLOCKED","cannot run — the reason names what is missing"
42
+ "CONSENT","needs the operator's word (a credential, an invite, the shells)"
43
+ ```
44
+
45
+ ```
46
+ plan_phases[9]{n,name,gate}:
47
+ "0","resolve","this host, the container engine, the code root, and the home"
48
+ "1","code","the tree is intact — and holds nothing of the operator's"
49
+ "2","home","the operator's world, outside the tree"
50
+ "3","runtimes","git/python3 · bun/uv/mcp<2> · pi · hermes · the terminal backend"
51
+ "4","engines","treehouse · no-mistakes · sandcastle · the Utgard image"
52
+ "5","apps","hlidskjalf · odrerir · sessrumnir · smidja — web build required, shell gated"
53
+ "6","wire","the way in (auth · invite) and the launcher entries"
54
+ "7","raise","every port listening, then the desktop apps"
55
+ "8","verify","what stands, honestly"
56
+ ```
57
+
58
+ `ymir-install.sh` prints the plan at the consent prompt; `--plan`/`--dry-run` prints
59
+ it and exits without writing. The plan is recomputed on every run, so it cannot
60
+ drift behind the code the way a paragraph did. `bin/ymir-plan.sh` is the ward for
61
+ the law below: its phase-1 `purity` row names any of the operator's things found in
62
+ the code tree.
63
+
64
+ ## Where things live — the package is code, the home is the operator's
65
+
66
+ **Law (Rule 04):** the package carries the **core systems** — everything needed to
67
+ run the programs. Everything the **operator** owns goes to `$YMIR_HOME`: their
68
+ info, their records, their documents, their settings, their state, their
69
+ credentials. A packaged install (npm) treats its tree as read-only; the next
70
+ upgrade replaces it, so anything of theirs kept there is kept at its peril.
71
+
72
+ ```
73
+ roots[5]{root,resolves_from,holds}:
74
+ "ymir_home_root","$YMIR_HOME → the choice recorded at installation → one documented default","everything private the operator owns"
75
+ "hoard_root","$YMIR_HOARD → <home>/hodd","docs · secrets · identity · tenants · memory"
76
+ "hoard_data_dir","$YMIR_DATA_DIR → <hoard>/data","this machine's records: operator, fleet, machines, the host profile"
77
+ "hoard_state_dir","$YMIR_STATE_DIR → <home>/state","runtime state: pids, logs, locks, caches"
78
+ "hoard_settings_dir","$YMIR_SETTINGS_DIR → <home>/config","settings: agents.yaml, cron.yaml, tailscale-sync, the wedge channel"
79
+ ```
80
+
81
+ Plus `hoard_local_env` → `$YMIR_HOME/.env.local`: the operator's **credentials**
82
+ (`HLIDSKJALF_AUTH`, OAuth keys, tokens), never in the tree.
83
+
84
+ **The home is chosen, not assumed.** A real interactive install asks once
85
+ (`step_home`), records the answer as machine state under `~/.config/ymir/home`
86
+ (the same place `engram-python` and `accounts.json` live), and every later script
87
+ resolves it through `bin/hoard-lib.sh`. `--check` never writes; `--yes` takes what
88
+ is recorded, else the documented default.
89
+
90
+ ```bash
91
+ bin/hoard-lib.sh # the lib (source-safe; functions only)
92
+ ymir_home_root HOME # → the home
93
+ hoard_settings_dir SETTINGS # → <home>/config
94
+ hoard_local_env ENVFILE # → <home>/.env.local
95
+ ```
96
+
97
+ **Rule for new code:** never write `$ROOT/data`, `$ROOT/state`, `$ROOT/config`,
98
+ `$ROOT/.env.local` or `$ROOT/workspace`. Resolve the root through the lib. A script
99
+ that points an operator's thing into the tree is the bug the `purity` row exists to
100
+ catch.
101
+
102
+ ### Carrying an existing tree home — migration 0005
103
+
104
+ Every *writer* resolves the home now, but an installation made before this change
105
+ still holds the operator's things in the tree. `.agents/migrations/0005-roots-out-of-tree.sh`
106
+ carries them:
107
+
108
+ ```
109
+ what moves from to
110
+ ------------------ --------------------- ----------------------------
111
+ this machine's data/ <hoard>/data
112
+ records
113
+ runtime state state/ <home>/state
114
+ (pids · logs · (including the (running services keep their open
115
+ locks · the applied-migrations fds across the move — `mv` keeps
116
+ applied-marker) marker) the inode)
117
+ credentials the local env file $YMIR_HOME/.env.local (0600)
118
+ settings .agents/config/* <home>/config
119
+ that git does NOT track
120
+ ```
121
+
122
+ One name, two files — the migration decides by evidence, never by assumption:
123
+
124
+ ```
125
+ collision[3]{case,what_happens,nothing_lost}:
126
+ "identical content","the tree's copy is removed","the content is provably at home already"
127
+ "different content","the home's keeps the name; the tree's is carried beside it as <name>.stale-<UTC>","both are real, so both are kept — never merged, never discarded"
128
+ "not a plain file","left in place and reported","a directory is not silently swallowed"
129
+ ```
130
+
131
+ Templates and defaults never move: `*.example` (and `*.example.*`), `.gitkeep`, and
132
+ any settings file **git tracks** — that is the distro's shipped default, not the
133
+ operator's. The plan's `purity` row applies the same rule, asking git about the
134
+ real path (the tree's `config` is a symlink into `.agents/config`, and git tracks
135
+ what the index holds, not the link). Idempotent: a second run changes nothing.
136
+
137
+ ```bash
138
+ bin/ymir-migrate.sh status # pending / applied
139
+ bin/ymir-migrate.sh apply --dry-run # name the migration, touch nothing
140
+ bin/ymir-migrate.sh apply # carry it
141
+ ```
142
+
143
+ Stop the runtime first if you want no stale pid files; nothing is lost either way.
144
+
145
+ ## Operating it afterwards — the doors, the cloth, and the shapes
146
+
147
+ ### The doors (the CLI), named for the figure who does the work
148
+
149
+ `npm install -g @zerwiz/ymir` puts **two** commands on PATH: `ymir` (the front
150
+ door) and `ymir-install` (the bare installer). Everything else the operator
151
+ needs is a door on `ymir` — never a script path inside `node_modules`.
152
+
153
+ ```
154
+ doors[14]{verb,door,what}:
155
+ "ymir","first setup (a bare call installs)","the plan, then consent, then the work"
156
+ "ymir install [...]","bin/ymir-install.sh","the same, said out loud"
157
+ "ymir plan","bin/ymir-plan.sh","what an install would do here — writes nothing"
158
+ "ymir raise / lower","scripts/start.sh · stop.sh","lift the hall, or lay it down"
159
+ "ymir eir","bin/eir-doctor.sh","what stands, and mend what does not (the healer)"
160
+ "ymir groa","bin/groa-update.sh","take the latest, then mend this home forward"
161
+ "ymir groa migrate","bin/ymir-migrate.sh","heal this home's structure"
162
+ "ymir heimdall","bin/ymir-setup-auth.sh","the way in — status · set · github (the guardian)"
163
+ "ymir invite","bin/ymir-invite.sh","let someone else in — mint · list · revoke"
164
+ "ymir smidja","bin/smidja-board.sh","the smithy's board on :8437 — build · start · stop · status"
165
+ "ymir hlidskjalf","scripts/electron.sh start --view hlidskjalf","the high seat's window"
166
+ "ymir sessrumnir","scripts/electron.sh start --view sessrumnir","the seat-hall's window"
167
+ "ymir mimir","bin/mimir.sh","the memory well"
168
+ "ymir sense","bin/host-sense.sh","what THIS machine is"
169
+ ```
170
+
171
+ A name the law has not given a home is still answered, once, with the name that
172
+ has it: `ymir doctor` → *the door is named `ymir eir` now*. Colour appears only
173
+ where a human watches.
174
+
175
+ ### The cloth — `bin/ymir-style.sh`
176
+
177
+ Ymir had correct output and no design. The cloth is cut from the same stone as
178
+ the halls (`midgard/design-system/tokens.css`): **bone** `#cfc3a9` for words,
179
+ **bronze** `#c9973f` for what acts, **steel** `#96a0a8` for what stands, **blood**
180
+ `#c2584a` for what is wrong, and a mark per state (`◆` do · `·` already · `—`
181
+ fact · `✕` blocked · `?` needs your word · `✓` proved).
182
+
183
+ ```
184
+ cloth_rules[4]{rule,why}:
185
+ "colour and marks only when stderr is a terminal and NO_COLOR is unset","a pipeline never parses a decoration"
186
+ "data on stdout, the human rendering on stderr","the TOON row is the data; the coloured line is for the eye"
187
+ "the words are never conditional — only the colour is","hiding information to save colour is the wrong trade"
188
+ "no banner over four lines, no rule longer than its text","density first (Monoline TUI · cli-guidelines)"
189
+ ```
190
+
191
+ `bin/ymir-plan.sh --colour` renders the plan in the cloth on stderr while the
192
+ TOON stays on stdout. The installer prints it at the consent, and ends with the
193
+ next steps — *a reaction for every action, a next step for every ending*.
194
+
195
+ ### Two libs the packaged tree needs
196
+
197
+ ```
198
+ libs[2]{path,resolves}:
199
+ "bin/smidja-lib.sh","the smithy: apps/smidja-factory in a clone, node_modules/@zerwiz/smidja-factory in a package — `smidja_factory_dir`, `smidja_visualizer_dir`"
200
+ "bin/electron-lib.sh","whether a shell's runtime VERIFIES: `electron_runtime_state` (ok · partial · absent) and the exact remedy"
201
+ ```
202
+
203
+ **Why they exist.** A packaged tree has no `apps/`: the smithy arrives as a
204
+ dependency and the skill symlink dangles. Four call sites assumed the clone's
205
+ layout, so the visualizer read as unbuildable in a package. They resolve through
206
+ `bin/smidja-lib.sh` now.
207
+
208
+ **And a shell is never declared ready on a directory's presence.** npm gates
209
+ install scripts; a skipped Electron postinstall leaves `dist/` partial and
210
+ `path.txt` unwritten, so the web app builds, every check passes, and the window
211
+ never opens. `electron_runtime_state` verifies, and the plan's `electron` row
212
+ says *the runtime is PARTIAL* with the command that mends it.
213
+
214
+ ## The app packages — how the four surfaces arrive
215
+
216
+ The distro depends on the surfaces as their own npm packages, so one
217
+ `npm install -g @zerwiz/ymir` fetches them into the tree:
218
+
219
+ ```
220
+ app_packages[4]{package,repo,what}:
221
+ "@zerwiz/hlidskjalf","zerwiz/hlidskjalf","the control plane — dist/ ships built, served on :3888"
222
+ "@zerwiz/odrerir","zerwiz/odrerir","the live hall — dist/ ships built"
223
+ "@zerwiz/sessrumnir","zerwiz/sessrumnir","the seat-hall desktop — out/ ships built (a fork of pi-desktop)"
224
+ "@zerwiz/smidja-factory","zerwiz/smidja","the smithy and its visualizer — the factory plus the UI's source, built at install"
225
+ ```
226
+
227
+ They are **optionalDependencies**, deliberately: a broken app package must never
228
+ stop the CORE from installing, and a package not yet on the registry is skipped by
229
+ npm and starts arriving the moment it is published. The plan's phase-5 rows report
230
+ each surface separately — installed, declared-but-not-fetched, or no package at all
231
+ — so a silent skip cannot hide.
232
+
233
+ A global install nests them under the distro's own `node_modules`
234
+ (`<prefix>/lib/node_modules/@zerwiz/ymir/node_modules/@zerwiz/<app>`); a local one
235
+ hoists them to `node_modules/@zerwiz/<app>`. The plan checks both shapes.
236
+
237
+ ```
238
+ bin/npm-publish.sh # the platform only
239
+ bin/npm-publish.sh --all # the platform + every app package
240
+ bin/npm-publish.sh --dry-run --all # what would go out, and from where
241
+ bin/npm-publish.sh --unpublish @zerwiz/ymir@0.1.5 # take ONE version back
242
+ ```
243
+
244
+ **A version can be taken back, and the window is short.** npm permits unpublishing
245
+ **one version for 72 hours** after it was published; past that it is npm support's
246
+ door. So `--unpublish` demands a spec that names the version — never a bare name —
247
+ and the row says where the CDN may still serve the tarball for a while afterwards.
248
+ Publishing a newer version is the other half of the repair: it moves the `latest`
249
+ tag off the bad build at once, even before the removal propagates.
250
+
251
+ **A `files[]` entry that names a directory overrides `.gitignore`.** This is how
252
+ `@zerwiz/ymir@0.1.5` shipped the memory well: `files: [".agents/"]` packed that
253
+ subtree *including* the gitignored stores, and the repo stayed clean while the
254
+ artefact did not. Name what ships, exclude what must not, and prove it with
255
+ `npm pack --dry-run` — the repo's cleanliness says nothing about the tarball.
256
+
257
+ The app repos are cloned into `apps/` by the install's `apps` step (from the
258
+ registry's `repo: apps/<path>` blocks), which is where `--all` reads their
259
+ manifests. The token comes from the hoard, never from `~/.npmrc`.
260
+
21
261
  ## The steps
22
262
 
23
263
  ```
24
- install[20]{step,what,self-heals}:
264
+ install[23]{step,what,self-heals}:
25
265
  "panes","the run shown in a herdr pane","bin/herdr-run.sh sits a pane beside the caller when inside herdr; inline otherwise — a pane that cannot be raised never loses the work"
26
266
  "prereqs","git python3 bun docker|podman gh · mcp<2","bin/prereq-ensure.sh installs bun+uv+mcp in user space; engram is an honest optional SKIP"
27
267
  "memory-well","the engram engine (Mimirsbrunn)","optional; reported with the exact next command, never a fake fix"
28
- "tree","workspace/{work,personal}/<domains>, companies/, workspaces.yaml, projects.yaml, and the hoard OUTSIDE the repo (secrets/ · docs/ · identity/ · tenants/ at $YMIR_HOARD, else $YMIR_HOME)","creates if missing; hoard_root resolves through bin/hoard-lib.sh so no script can point the hoard inside the checkout (Rule 04), and an empty secrets/platform.env (0600) is seeded so bin/hodd.sh emit resolves"
268
+ "home","the home the operator CHOOSES, recorded under ~/.config/ymir/home","asks once, records the answer; --check never writes, --yes takes what is recorded, else the documented default"
269
+ "tree","workspace/{work,personal}/<domains>, companies/, workspaces.yaml, projects.yaml, and the hoard OUTSIDE the repo (secrets/ · docs/ · identity/ · tenants/ at the hoard root under the chosen home)","creates if missing; hoard_root resolves through bin/hoard-lib.sh so no script can point the hoard inside the checkout (Rule 04), and an empty secrets/platform.env (0600) is seeded so bin/hodd.sh emit resolves"
270
+ "apps","the app repos (the app split) — hlidskjalf · hlidskjalf-mobile · odrerir · sessrumnir · smidja","reads $HOARD/identity/projects.yaml (never guesses a remote); clones a missing apps/<path> from its registered git{} block, fast-forwards a present one, and stamps the smithy engine (apps/smidja) from the cloned factory's templates"
29
271
  "engines","treehouse · sandcastle · no-mistakes","installs treehouse + no-mistakes from their installers"
30
272
  "hermes","the Nous Research agent runtime","installs via bin/hermes-ensure.sh when absent"
31
- "sessrumnir","the Sessrúmnir desktop GUI (vendored pi-desktop at apps/sessrumnir)","bin/sessrumnir-ensure.sh installs deps + builds on first run (deps are never committed); launch via bin/sessrumnir.sh"
273
+ "sessrumnir","the Sessrúmnir desktop GUI (its own repo; lands via the `apps` step at apps/sessrumnir)","bin/sessrumnir-ensure.sh installs deps + builds on first run (deps are never committed); launch via bin/sessrumnir.sh"
32
274
  "backend","Þjazi — herdr (protocol 14+) or tmux","bin/herdr-ensure.sh detects/tests version, installs via the pinned installer or falls back to tmux"
33
- "host","this machine — learnt on EVERY host","bin/omarchy-sense.sh learns the setup, bin/desktop-place.sh places the apps, the Omarchy post-update hook (when Omarchy), the wedge-alarm channel, and (on Omarchy) an OFFER of the suggested shell plugins — listed, never installed unbidden; seeds the private config/agents.yaml from its example"
275
+ "host","this machine — sensed on EVERY host","bin/host-sense.sh senses the setup on ANY host (Rule 05); the Omarchy layer then RECORDS it (bin/omarchy-sense.sh observe), places the apps (bin/desktop-place.sh), installs the post-update hook and the wedge-alarm channel, and (on Omarchy) offers the suggested shell plugins — listed, never installed unbidden; seeds the private config/agents.yaml from its example"
34
276
  "sandbox","utgard-runner:latest image","builds via bin/utgard.sh build on Docker or rootless Podman; distinguishes an unreachable engine from a build failure"
35
277
  "memory","engram store + harness MCP registrations","raises the bridge; reports MCP coverage"
36
278
  "smidja","smidja/smidja_data/smidja.db","bin/smidja-bootstrap.sh creates it from the tracer schema + a bootstrap session"
37
279
  "visualizer","the Smíðja visualizer UI (Vue, served on :8437)","builds ./dist with bun when absent — the API serves the UI from dist, and without it the API answers but shows no interface"
38
280
  "loaders","agents/skills into the harnesses","runs bin/valknut-load.sh"
39
281
  "gates","the git delivery gates — secret-guard (pre-commit), branch-guard + changelog-guard (pre-push)","bin/secret-guard.sh --install and bin/changelog-guard.sh --install seat the versioned guards into .git/hooks, so the gate is live from the first commit of a fresh clone; idempotent"
282
+ "marks","each app's rune icon + .desktop entry into the operator's own desktop, and the Ymir contract into pi's agent home","bin/design-icon.sh mint --all + install writes to $HOME/.local/share (never a session sandbox), so every app is dockable and pinnable; the contract symlink means every pi session, in ANY folder, loads Brokk"
40
283
  "invite","the way in for anyone else — an invite code","bin/ymir-invite.sh ensure mints one only when nothing is live, so the step is idempotent; the code is printed at the end of the run and again in workspace/INSTALL.md"
41
284
  "register","workspace/INSTALL.md","writes the record"
42
285
  "services","gate API, SPA, Nornir, bridges, visualizer","raises via scripts/start.sh (which builds the visualizer UI when ./dist is absent)"
@@ -44,7 +287,7 @@ install[20]{step,what,self-heals}:
44
287
  "validate","the running system","bin/ymir-validate.sh — live port/store/process checks"
45
288
  ```
46
289
 
47
- **22** `step_*` functions are defined. A step is not a row: one step may emit
290
+ **25** `step_*` functions are defined (`home` asks, `tree` builds). A step is not a row: one step may emit
48
291
  several. `prereqs` also emits `memory-well`, `host` also emits `agents-config`,
49
292
  `smidja` also emits `visualizer`, and `spa` also emits `hlidskjalf`. `--check`
50
293
  skips the runtime-only steps (`services`, `desktop`, `validate`), which have
@@ -94,8 +337,10 @@ now build it when absent:
94
337
  (cd .agents/skills/smidja-factory/apps/visualizer && bun run build) # vue-tsc + vite
95
338
  ```
96
339
 
97
- `bin/ymir-validate.sh` reports `visualizer` FAIL when `./dist` is missing, so the
98
- gap cannot silently return.
340
+ `bin/ymir-validate.sh` reports `visualizer` FAIL when `./dist` is missing **and**
341
+ when the build exists but nothing is listening on `:8437`. A PASS means the UI is
342
+ built *and* the API is up — so a built-but-dead visualizer (a bad `CMD_DB`, a
343
+ crashed API) can no longer read as green.
99
344
 
100
345
  ## Desktop placement (Omarchy desktops, not monitors)
101
346
 
@@ -163,13 +408,25 @@ non-interactive callers; without it a non-interactive `add` refuses with exit 3)
163
408
  The installer only *offers*.
164
409
 
165
410
  ## Consent
166
- A real install prints its plan and waits for `[y/N]`. Declining changes nothing
167
- (exit 3). `--check` never prompts. A non-interactive caller without `--yes` is
168
- refused rather than silently proceeding.
169
-
170
- The plan names every change, including the terminal backend (herdr/tmux), the
171
- host learning, and the desktop placement so the operator accepts what is
172
- actually done, not a shorter list that drifted behind the code.
411
+ A real install prints **the plan it computed** (`bin/ymir-plan.sh`) and waits for
412
+ `[y/N]`. Declining changes nothing (exit 3). `--check` and `--plan` never prompt.
413
+ A non-interactive caller without `--yes` is refused rather than silently
414
+ proceeding.
415
+
416
+ The plan names every step with its state and the reason for it — including the
417
+ home the operator is asked to choose, the four app surfaces and whether each can
418
+ be installed at all, the terminal backend, and what will be skipped and why. The
419
+ state vocabulary is `DO · SKIP · INFO · BLOCKED · CONSENT`.
420
+
421
+ `--check` writes nothing — and that includes the migrations. `bin/ymir-migrate.sh
422
+ apply` **moves private data**, so the step chain runs it only on a real run;
423
+ a preview leaves the home exactly as it found it. (It used to apply them even
424
+ under `--check`, which moved a home during a "report only" pass.)
425
+
426
+ `--check` writes nothing — and that includes the migrations. `bin/ymir-migrate.sh
427
+ apply` **moves private data**, so the step chain runs it only on a real run;
428
+ a preview leaves the home exactly as it found it. (It used to apply them even
429
+ under `--check`, which moved a home during a "report only" pass.)
173
430
 
174
431
  ## The operator's way in (auth) — `bin/ymir-setup-auth.sh`
175
432
 
@@ -225,7 +482,7 @@ part is off. `--json` for machine consumption.
225
482
  | **sandcastle** (`mattpocock/sandcastle`) | Utgard | Docker/Podman/Vercel sandboxes |
226
483
  | **no-mistakes** (`kunchenguid/no-mistakes`) | Mjollnir · Glitnir | clean-PR validation gate |
227
484
  | **Hermes** (`NousResearch/hermes-agent`, MIT) | — (product name) | worker agent runtime: own brain, memory, skills, subagents, sandbox backends |
228
- | **pi-desktop** (`FaqFirebase/pi-desktop`, Apache-2.0) | **Sessrúmnir** (vendored at `apps/sessrumnir`) | desktop GUI for the Pi/OMP coding agents — the seat-hall, themed with the Ymir way-of palette |
485
+ | **pi-desktop** (`FaqFirebase/pi-desktop`, Apache-2.0) | **Sessrúmnir** (cloned from its own repo to `apps/sessrumnir` at install) | desktop GUI for the Pi/OMP coding agents — the seat-hall, themed with the Ymir way-of palette |
229
486
 
230
487
  ## Missing dependencies
231
488
 
@@ -298,14 +555,16 @@ company is the operator's to name; Ymir ships no default.
298
555
  bin/ymir-install.sh --check # all steps OK/WARN
299
556
  bin/ymir-validate.sh # the running system actually works
300
557
  bin/herdr-ensure.sh status # the Þjazi backend and its protocol floor
301
- bin/omarchy-sense.sh status # what Ymir has learnt about this host
558
+ bin/host-sense.sh # sense THIS machine (any host)
559
+ bin/omarchy-sense.sh status # the Omarchy recording (Omarchy hosts)
302
560
  bin/saga-session-start.sh # the session digest
303
561
  bash .agents/skills/galdr-ymirsystem/scripts/compliance-check.sh
304
562
  ```
305
563
 
306
- On an **Omarchy** host the installer also learns the machine
307
- (`bin/omarchy-sense.sh`) and installs a `post-update` hook so Ymir re-learns it
308
- every time Omarchy updates. On a non-Omarchy host that step is a clean SKIP.
564
+ On **every** host the installer senses the machine with `bin/host-sense.sh`. On
565
+ an **Omarchy** host the layer also *records* it (`bin/omarchy-sense.sh observe`)
566
+ and installs a `post-update` hook so Ymir re-learns it every time Omarchy
567
+ updates. On a non-Omarchy host that layer step is a clean SKIP.
309
568
 
310
569
  Rule: the installer is **idempotent** — running it again changes nothing but
311
570
  fills gaps. It never overwrites real user data.
@@ -470,7 +729,7 @@ machines may run the runtime.
470
729
  | Layer | Runs on | What it owns |
471
730
  |---|---|---|
472
731
  | **Portable core** | Linux, macOS, Windows (WSL2; MSYS best-effort) | the runtime — session digest, lock, watch, cron, skills — the installer's user-space prerequisites, the capability shim, generated machine config |
473
- | **Omarchy layer** | Omarchy only | learning the host (`omarchy-sense`), placing each app on its own numbered Hyprland desktop (`desktop-place`), the launcher entries (`.desktop`), the suggested shell plugins, the post-update hook |
732
+ | **Omarchy layer** | Omarchy only | recording the host (`omarchy-sense observe`), placing each app on its own numbered Hyprland desktop (`desktop-place`), the launcher entries (`.desktop`), the suggested shell plugins, the post-update hook |
474
733
 
475
734
  The core never grows a Hyprland branch, and the Omarchy layer is never stretched
476
735
  into pretending it is portable. An Omarchy-specific step is gated on the host and
@@ -480,8 +739,15 @@ reports a clean skip anywhere else:
480
739
  [ -d /usr/share/omarchy ] && ... || add host OK "... (not an Omarchy host)"
481
740
  ```
482
741
 
483
- That is why `step_host` and `step_desktop` are the only steps that mention
484
- Omarchy: everywhere else, the same code path runs on any host.
742
+ That is why the only Omarchy branches left in the core installer are the ones
743
+ that call the layer (`step_omarchy`) and desktop placement (`step_desktop`):
744
+ `step_host` senses the host portably with `host-sense`, and everywhere else the
745
+ same code path runs on any host.
746
+
747
+ The **consent preamble** the installer prints before it acts says the same thing —
748
+ *"learn this machine (OS, desktop, packages, configs, monitors, scale — Omarchy
749
+ hosts recorded first-class)"* — so the operator is told what will happen in the
750
+ portable terms the code now uses, not the Omarchy-only terms it used before.
485
751
 
486
752
  ### What the Omarchy layer installs
487
753
 
@@ -154,7 +154,9 @@ hardcoded timeline is gone.
154
154
  - **First `observe` slow (~20–30 s)** → the embedding model is warming; later
155
155
  writes are instant.
156
156
  - **Bridge down / `COLD`** → `bin/mimir-bridge.sh --start`; check
157
- `state/mimir-bridge.log`.
157
+ `$YMIR_STATE_DIR/mimir-bridge.log` — runtime state lives in the home the
158
+ operator chose (`hoard_state_dir` via `bin/hoard-lib.sh`), never in the code
159
+ tree: a packaged install replaces its tree on upgrade.
158
160
 
159
161
  **Portability.** The bridge signals processes through `ymir_kill_matching` from
160
162
  `bin/ymir-platform.sh` rather than calling `pkill` directly, because `pkill` is
@@ -62,7 +62,7 @@ same day. That is deliberate: cron is idempotent-by-date, not retrying-by-failur
62
62
  |---|---|---|
63
63
  | `BROKK_ROOT_OVERRIDE` | script's parent | Root when `BROKK_HOME` unset. |
64
64
  | `BROKK_HOME` | `$ROOT` | Home owning the jobs and state. |
65
- | `BROKK_STATE_OVERRIDE` | `$BROKK_HOME/state` | Scheduler state directory. |
65
+ | `BROKK_STATE_OVERRIDE` | `$YMIR_STATE_DIR` (`<home>/state`, via `bin/hoard-lib.sh`) | Scheduler state directory — in the home the operator chose, never in the code tree. |
66
66
  | `BROKK_CONFIG_OVERRIDE` | `$BROKK_HOME/config` | Where `cron.yaml` lives. |
67
67
  | `BROKK_CRON_LOG_MAX_BYTES` | `1048576` | Rotation threshold. |
68
68
  | `BROKK_REALM` | `""` | Realm exported to jobs (jobs also fall back to `data/realm.md`). |
@@ -153,6 +153,44 @@ removed and the failure is reported plainly.
153
153
  | `BROKK_MEMORY_PRUNE` | `0` (disabled) |
154
154
  | `BROKK_MEMORY_PRUNE_DAYS` | `7` |
155
155
 
156
+ ### 3.5 Óðrerir — Live Hall snapshot (`bin/nornir-job-hall-snapshot.sh`, 08:00)
157
+
158
+ The Live Hall is a glass: it reads `apps/odrerir/public/livehall.json`
159
+ (same-origin, `cache: no-store`). This job writes that snapshot from real state
160
+ via `bin/hall-snapshot.sh` — runes, the project registry, the cron gauge, the
161
+ wake queue, standing smiths, armed `when-` sources, and the landed errands —
162
+ then carves Rune `odrerir / hall.snapshot`. Scheduled at 08:00 so it follows
163
+ the 06:00 observer and the 07:00 briefing: the morning board carries the day's
164
+ fresh runes.
165
+
166
+ | Reads | Writes |
167
+ |---|---|
168
+ | runes ledger · `hodd/identity/projects.yaml` · `config/cron.yaml` · `state/.wake-queue` · herdr agent list · armed when-sources · `state/eindri-reports/archive/` | `apps/odrerir/public/livehall.json` (gitignored runtime) · Rune `odrerir / hall.snapshot` |
169
+
170
+ - The snapshot is **runtime, never repo**: `apps/odrerir/public/livehall.json`
171
+ is gitignored, so the job never dirties a branch.
172
+ - Absent `livehall.json` on the Hall is *not* a build failure — the page paints
173
+ the saga's own count and says so. This job is what makes the board true.
174
+ - Idempotent by nature: each run rewrites the same snapshot from the same
175
+ inputs; the cron date-guard suppresses repeat dispatch within a day.
176
+
177
+ ### 3.6 Tyr — NSR compliance round (`bin/nornir-job-nsr-compliance.sh`, 02:30)
178
+
179
+ The NorthStar deterministic gate, run nightly in the quiet hours so sunrise
180
+ finds the doors mended or the Rune already says which broke. Runs every
181
+ `.compliance/gates/check_*.sh` (danger · env · paths · platform · wiring)
182
+ and carves one Rune with the verdict — `nornir / nsr.compliance` on a clean
183
+ round, `nornir / nsr.compliance.failed` (exit 1) naming the failed gates.
184
+
185
+ | Reads | Writes |
186
+ |---|---|
187
+ | `.compliance/gates/check_*.sh` (deterministic, no network) | Rune `nornir / nsr.compliance[.failed]` · `state/last` line |
188
+
189
+ - The morning briefing reads the ledger, so a FAIL is seen at 07:00, not
190
+ found by accident.
191
+ - Idempotent: gates are pure checks; the cron date-guard suppresses repeat
192
+ dispatch within a day; safe to invoke by hand (`bash bin/nornir-job-nsr-compliance.sh`).
193
+
156
194
  ### 3.4 Yggdrasil — git sync (`bin/nornir-job-git-sync.sh`, 00:00)
157
195
 
158
196
  The world-tree kept in order. Two modes, both non-destructive:
@@ -110,6 +110,10 @@ snapshot[1]{command,out}:
110
110
  - **Ports are sacred:** `:4322` is Óðrerir's alone. Do not move it into a
111
111
  Hlidskjalf route or a launcher call — its door is a plain anchor in a new tab
112
112
  (`.hall-btn`, rune **ᛟ**, "To the Hall") and its own window.
113
+ - **`.astro/dev.json` is runtime state, never tracked.** Astro rewrites it on
114
+ every start with the live `pid` and the host's own addresses (LAN + tailnet),
115
+ so committing it leaks a private IP per run. It is gitignored; the generated
116
+ types beside it (`content.d.ts`, `types.d.ts`) stay tracked.
113
117
 
114
118
  ## 5. Files you will touch
115
119
 
@@ -1,4 +1,9 @@
1
- # PI Profile — Default Brokk Harness Profile for Ymir
1
+ # PI Profile — the SHAPE of the Brokk harness profile, not a machine's values.
2
+ #
3
+ # Rule 07A: a model id is the USER's, not the repo's. Every operator's differ, so
4
+ # this template carries a placeholder; the live values come from the user's own
5
+ # roster at $YMIR_HOME/hodd/config/agents.yaml, resolved through
6
+ # bin/agents-config.sh. Never commit a real model id here.
2
7
 
3
8
  profile:
4
9
  name: "pi"
@@ -6,7 +11,7 @@ profile:
6
11
  version: "1.0.0"
7
12
 
8
13
  agent:
9
- model: "lmstudio/qwen3.6-35b-a3b"
14
+ model: "<your-model-id>" # resolved from $YMIR_HOME/hodd/config/agents.yaml
10
15
  temperature: 0.7
11
16
  max_tokens: 8192
12
17
  system_prompt: "BROKK — SYSTEM OPERATING MANUAL (see .opencode/agents/brokk.md)"
@@ -292,7 +292,7 @@ The house law lives in `RULES/`; Galdr must know it (Rule 01/02/03):
292
292
 
293
293
  - **Eindri are the specialists** (marketer, builder, researcher, planner,
294
294
  reviewer, documenter, scout, …); each names its **domain** and its **craft**.
295
- - **Agents live only in `.agents/agents/*.md`.** `.opencode/agent` and
295
+ - **Agents live only in `.agents/agents/*.md`.** `.opencode/agents` and
296
296
  `.pi/agents` are **symlinks** to the canonical profiles — never edit them;
297
297
  edit `.agents/agents/<profile>.md` and run `bin/valknut-load.sh --all`.
298
298
  - **No mock agents.** Ids, names, domains, models, status are real and sourced;
@@ -32,7 +32,7 @@ siblings of this file (`eindri-orchestration.md`, `nornir-jobs.md`). The runtime
32
32
  | 11 | Secrets never committed | secret scan + ignore audit | No secret literal; ignore rules cover env files. |
33
33
  | 12 | Governed assets current | `compliance-check.sh` (`assets` check) | A governed path changed in the working tree has its owning asset changed too. |
34
34
  | 13 | **Governed paths resolve** | `compliance-check.sh` (`governed` check) | Every path in `AGENTS.md`'s `governed[]` table exists (or its glob matches something). |
35
- | 14 | **Harness surfaces resolve** | `compliance-check.sh` (`harnesses` check) | Every link in `.claude/agents`, `.codex/agents`, `.cursor/agents`, `.pi/agents`, `.opencode/agent` lands (one hop) in `.agents/agents/`, and no nested `SKILL.md` carries frontmatter — a phantom skill. |
35
+ | 14 | **Harness surfaces resolve** | `compliance-check.sh` (`harnesses` check) | Every link in `.claude/agents`, `.codex/agents`, `.cursor/agents`, `.pi/agents`, `.opencode/agents` lands (one hop) in `.agents/agents/`, and no nested `SKILL.md` carries frontmatter — a phantom skill. |
36
36
  | 15 | **Skill index true** | `compliance-check.sh` (`skillindex` check) | Every real skill dir is named in `.agents/skills/README.md`, and every `.agents/skills/<name>` path the assets cite exists. |
37
37
 
38
38
  ### Governed paths — load the asset before you edit
@@ -88,7 +88,7 @@ Two failures hide in the harness layer, and neither is visible from the code:
88
88
 
89
89
  1. **A symlink that no longer resolves.** Rule 02 says `.agents/agents` is
90
90
  canonical and every harness dir (`.claude/agents`, `.codex/agents`,
91
- `.cursor/agents`, `.pi/agents`, `.opencode/agent`) is a link into it. The
91
+ `.cursor/agents`, `.pi/agents`, `.opencode/agents`) is a link into it. The
92
92
  `galdr-cli` → `galdr-ymirsystem` rename left `.agents/agents/galdr.md`
93
93
  dangling, so Galdr's agent surface did not exist in *any* harness — and
94
94
  nothing failed loudly; the harness simply had no such agent.