@zerwiz/ymir 0.1.10 → 0.1.11

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 (40) hide show
  1. package/.agents/skills/galdr-ymirsystem/assets/installation.md +29 -0
  2. package/CHANGELOG.md +65 -0
  3. package/bin/design-icon.sh +18 -3
  4. package/bin/ymir-config.sh +76 -0
  5. package/bin/ymir-install.sh +15 -9
  6. package/bin/ymir-style.sh +24 -0
  7. package/bin/ymir.js +16 -1
  8. package/midgard/README.md +16 -0
  9. package/midgard/design-system/.gitkeep +0 -0
  10. package/midgard/design-system/ember.d.ts +13 -0
  11. package/midgard/design-system/ember.js +183 -0
  12. package/midgard/design-system/icons/algiz.svg +4 -0
  13. package/midgard/design-system/icons/ansuz.svg +4 -0
  14. package/midgard/design-system/icons/berkana.svg +4 -0
  15. package/midgard/design-system/icons/dagaz.svg +4 -0
  16. package/midgard/design-system/icons/ehwaz.svg +4 -0
  17. package/midgard/design-system/icons/fehu.svg +4 -0
  18. package/midgard/design-system/icons/gjallarhorn.svg +4 -0
  19. package/midgard/design-system/icons/gungnir.svg +4 -0
  20. package/midgard/design-system/icons/heimdall.svg +4 -0
  21. package/midgard/design-system/icons/ingwaz.svg +4 -0
  22. package/midgard/design-system/icons/jera.svg +4 -0
  23. package/midgard/design-system/icons/kaunan.svg +4 -0
  24. package/midgard/design-system/icons/midgard.svg +4 -0
  25. package/midgard/design-system/icons/mimirsbrunn.svg +4 -0
  26. package/midgard/design-system/icons/othala.svg +4 -0
  27. package/midgard/design-system/icons/raidho.svg +4 -0
  28. package/midgard/design-system/icons/ratatoskr.svg +4 -0
  29. package/midgard/design-system/icons/sowilo.svg +4 -0
  30. package/midgard/design-system/icons/tiwaz.svg +4 -0
  31. package/midgard/design-system/icons/utgard.svg +4 -0
  32. package/midgard/design-system/icons/valhalla.svg +4 -0
  33. package/midgard/design-system/icons/yggdrasil.svg +4 -0
  34. package/midgard/design-system/icons.md +59 -0
  35. package/midgard/design-system/runes.md +78 -0
  36. package/midgard/design-system/tokens.css +109 -0
  37. package/midgard/design-system/ymir-mark.svg +62 -0
  38. package/package.json +2 -1
  39. package/scripts/electron.sh +18 -2
  40. package/scripts/start.sh +50 -6
@@ -259,6 +259,35 @@ clone's marks already existed. `bin/desktop-place.sh entries` is the launcher ha
259
259
  alone (any Linux desktop), the installer's `marks` step calls it, and both the
260
260
  desktop database and the icon cache are refreshed after.
261
261
 
262
+ ### Every notice can be silenced — `ymir config`
263
+
264
+ Some lines are information the first time and noise the tenth. A user must be able
265
+ to say **not again**, once, and be believed. The preferences live in the operator's
266
+ settings (`<home>/config/notices.conf`, one `key=on|off` per name) and the door is:
267
+
268
+ ```
269
+ ymir config # what is shown, and what is not
270
+ ymir config notice version off # silence one; `on` restores it
271
+ judgment[4]{key,what}:
272
+ "version","the line that says which version moved to which"
273
+ "patience","the long-hour words before a slow install or build"
274
+ "next","the where-to-go-from-here block at the end"
275
+ "hints","the one-line helpers that say how to hide a notice"
276
+ ```
277
+
278
+ Every notice that can be silenced **says so once**, in the cloth's faint voice —
279
+ `(not again: ymir config notice version off)` — so the way out is discoverable
280
+ without a manual. Absent means on; a preference is honoured by the shell cloth
281
+ (`notice_wanted`, `notice_hint`) and by the CLI alike. `ymir-install.sh`'s marks
282
+ and the raise path honour it too.
283
+
284
+ ### Four surfaces, one raise
285
+
286
+ `ymir raise` lifts the whole hall — the SPA (`:3888`), Óðrerir (`:4322`), the
287
+ board (`:8437`), and the commit-hall Sessrúmnir rises in the same motion (it is an
288
+ Electron app of its own, not a browser surface). `ymir-install.sh`'s desktop step
289
+ opens **one window per surface** rather than a single "both".
290
+
262
291
  ### What a user went from, and to
263
292
 
264
293
  `npm install -g` prints *"changed 266 packages"* and no versions. The CLI records
package/CHANGELOG.md CHANGED
@@ -1,4 +1,69 @@
1
1
 
2
+ ## 2026-09-17 — 0.1.11: four surfaces, their marks, and a door that mends
3
+
4
+ - **All four surfaces rise from one npm install** — Hlidskjalf `:3888`, Óðrerir
5
+ `:4322`, the board `:8437`, and Sessrúmnir in its own window; the installer's
6
+ desktop step opens one window per surface.
7
+ - **All four launcher marks land** — the rune glyphs ship in the package
8
+ (`midgard/design-system/`), the mint resolves a packaged app, the apps'
9
+ templates are written before the rune marks so the themed name stands, and a
10
+ retired name is swept from the launcher.
11
+ - **`ymir hlidskjalf` mends the Electron runtime itself** before refusing, because
12
+ npm gates install scripts and a fresh user could otherwise have no window at all.
13
+ - **Every notice can be silenced once and remembered** — `ymir config notice
14
+ version|patience|next|hints off`.
15
+ - **Óðrerir serves its build** from a package, and says what failed when it cannot.
16
+
17
+ ## 2026-09-17 — the sweep, and the door that mends itself
18
+
19
+ - **A retired name is swept from the launcher.** `ymir-visualizer.desktop` and its
20
+ icon pointed at a glyph nobody ships — a blank square for anyone who knew the old
21
+ name. `design-icon.sh install` removes retired entries now (`ymir-visualizer`,
22
+ and `ymir-hlidskjalf-mobile` where that app is not installed).
23
+ - **`ymir hlidskjalf` mends the Electron runtime itself.** npm gates install
24
+ scripts by default, so a fresh install can build the web app perfectly and have
25
+ no window. The door now runs the repair it used to *describe*
26
+ (`npm install-scripts approve electron` + `npm rebuild electron`), and only then
27
+ refuses — with both the manual command and the honest alternative
28
+ (`ymir install --no-desktop`).
29
+ - **Two faults are provably in the app repos, not here** (recorded so nobody hunts
30
+ them in Ymir again):
31
+ - **the fourth hall** — the landing's list is a hardcoded SPA component
32
+ (`HallsSwitcher`; the bundle says *"three halls of Ymir — one seat, three
33
+ roofs"*), not data from the gate API. Óðrerir must be added in
34
+ `zerwiz/hlidskjalf` and the SPA rebuilt.
35
+ - **the ember glow** — the seat ships `emberBootTheme` in its built renderer but
36
+ no themed cloth beside it; the house's framework-neutral
37
+ `midgard/design-system/ember.js` exists precisely so every surface can share
38
+ one fire, and the seat must be told to light it (`zerwiz/sessrumnir`).
39
+
40
+ ## 2026-09-17 — "not again", and four surfaces in one raise
41
+
42
+ - **Every notice can be silenced.** A line is information once and noise the
43
+ tenth: the version that moved, the long-hour words, the next-steps block, the
44
+ hints. Each now says so **once** in the cloth's faint voice — *not again:
45
+ `ymir config notice version off`* — and the preference is recorded in the
46
+ operator's settings, honoured by the shell cloth and the CLI alike. `ymir config
47
+ show` is the door; four keys: `version · patience · next · hints`.
48
+ - **The package carries the design system.** It never shipped `midgard/`, so the
49
+ rune glyphs — the icons themselves — were absent and every mark failed with
50
+ *no glyph ansuz*. `midgard/design-system/` is in `files[]` now: 30 files, 22
51
+ glyphs.
52
+ - **The marks are written in the right order.** The apps' own launcher templates
53
+ landed *after* the rune marks and overwrote the themed name with an absolute
54
+ `.png`; the templates go first now and the rune marks stand over them.
55
+ - **Óðrerir rises from a package.** Its published tarball carries `dist` and a
56
+ `dev` command that calls a script it does not carry, so `npm run dev` could never
57
+ work from a package — the hall serves its **build** (`astro preview`) where there
58
+ is one, exactly as the SPA does. And its dependencies' absence is no longer
59
+ silent: the hall says what failed and why.
60
+ - **Four surfaces, one raise.** `ymir raise` lifts the SPA, Óðrerir, the board and
61
+ Sessrúmnir together; the installer's desktop step opens **one window per
62
+ surface**, not a single "both".
63
+ - **A library must not end a function on a failing test.** `style_init` did, and
64
+ `scripts/start.sh` runs under `set -e` — so the cloth took the whole raise down
65
+ with it, silently. `return 0`, and the reason is written beside it.
66
+
2
67
  ## 2026-09-17 — 0.1.10: the hall answers on its port, and the marks land
3
68
 
4
69
  - **The SPA listens where it is asked.** A bare `npm run dev` let Vite take 5173,
@@ -75,8 +75,18 @@ mint() { # <app-dir> <glyph> <tint> <label>
75
75
  local path
76
76
  path="$(sed -n 's/.*<path d="\([^"]*\)".*/\1/p' "$g" | head -1)"
77
77
  [ -n "$path" ] || { printf 'error: %s carries no <path> to reuse\n' "$glyph" >&2; return 1; }
78
- local outdir="$ROOT/$dir/public"
79
- [ -d "$(dirname "$outdir")" ] || { printf ' "%s","SKIP (no such app)"\n' "$dir"; return 0; }
78
+ # A clone keeps the app at $ROOT/apps/<name>; a package keeps it under
79
+ # node_modules/@zerwiz/<package>. The mint resolves both, so a packaged install
80
+ # gets its favicon too — the same resolver every other script uses.
81
+ local base outdir surface
82
+ base="$ROOT/$dir"
83
+ if [ ! -d "$base" ]; then
84
+ surface="$(basename "$dir")"
85
+ case "$surface" in smidja-factory) surface=smidja ;; esac
86
+ app_dir "$surface" base 2>/dev/null || base=""
87
+ fi
88
+ [ -n "$base" ] && [ -d "$base" ] || { printf ' "%s","SKIP (no such app)"\n' "$dir"; return 0; }
89
+ outdir="$base/public"
80
90
  mkdir -p "$outdir"
81
91
  {
82
92
  printf '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" role="img" aria-label="%s">\n' "$label"
@@ -147,7 +157,12 @@ install_all() {
147
157
  printf ' "%s","%s.svg","%s.desktop"\n' "$iconname" "$iconname" "$iconname"
148
158
  done
149
159
  [ -f "$apps_dir/ymir-smidja.desktop" ] && rm -f "$apps_dir/ymir-smidja.desktop"
150
- for stale in ymir-hlidskjalf-mobile; do rm -f "$apps_dir/$stale.desktop" "$icons_dir/$stale.svg"; done
160
+ # Names we have retired, swept on every install: a launcher entry that points at
161
+ # an icon nobody ships is a blank square, and the naming law does not let an old
162
+ # name linger beside the new one. (ymir-visualizer → ymir-smidja.)
163
+ for stale in ymir-hlidskjalf-mobile ymir-visualizer; do
164
+ rm -f "$apps_dir/$stale.desktop" "$icons_dir/$stale.svg"
165
+ done
151
166
  command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$apps_dir" >/dev/null 2>&1 || true
152
167
  }
153
168
 
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env bash
2
+ # ymir-config.sh — the operator's own preferences, honoured and remembered.
3
+ #
4
+ # Some lines a script prints are information the first time and noise the tenth:
5
+ # the version that moved, the long-hour words, the hints. A user must be able to
6
+ # say "not again" — once — and be believed. That is what this door is for.
7
+ #
8
+ # ymir-config.sh show # every preference and its state
9
+ # ymir-config.sh notice <key> off|on # silence or restore one notice
10
+ # ymir-config.sh notice <key> # what it is now
11
+ #
12
+ # The store is the operator's settings, in the home they chose — never the code
13
+ # tree (Rule 04) — one `key=on|off` line per name:
14
+ #
15
+ # <home>/config/notices.conf
16
+ #
17
+ # keys: version the "the tree moved x → y" line
18
+ # patience the long-hour words before a slow install or build
19
+ # next the "where to go from here" block
20
+ # hints the one-line helpers ("hide this next time: …")
21
+ #
22
+ # Exit: 0 ok, 2 usage.
23
+ set -u
24
+
25
+ VERSION="1.0.0"
26
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
27
+ # shellcheck source=bin/hoard-lib.sh
28
+ . "$SCRIPT_DIR/hoard-lib.sh"
29
+ hoard_settings_dir SETTINGS
30
+ STORE="$SETTINGS/notices.conf"
31
+ KNOWN="version patience next hints"
32
+
33
+ usage() { sed -n '2,22p' "$0" | sed 's/^# \{0,1\}//'; }
34
+
35
+ state_of() { # <key> → on|off (absent = on)
36
+ local key="$1" line
37
+ [ -r "$STORE" ] || { printf 'on'; return 0; }
38
+ line="$(grep -m1 "^${key}=" "$STORE" 2>/dev/null || true)"
39
+ [ -n "$line" ] && printf '%s' "${line#*=}" || printf 'on'
40
+ }
41
+
42
+ set_state() { # <key> <on|off>
43
+ local key="$1" value="$2" tmp
44
+ case "$value" in on|off) ;; *) printf 'error: %s is not on or off\nhelp: ymir config notice %s off\n' "$value" "$key" >&2; exit 2 ;; esac
45
+ mkdir -p "$SETTINGS" 2>/dev/null || { printf 'error: cannot write %s\n' "$SETTINGS" >&2; exit 1; }
46
+ tmp="$(mktemp)"; chmod 600 "$tmp"
47
+ if [ -r "$STORE" ]; then grep -v "^${key}=" "$STORE" >"$tmp" 2>/dev/null || true; fi
48
+ printf '%s=%s\n' "$key" "$value" >>"$tmp"
49
+ mv "$tmp" "$STORE"
50
+ }
51
+
52
+ case "${1-}" in
53
+ -v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;;
54
+ -h|--help|"") usage; exit 0 ;;
55
+ show)
56
+ printf 'notices[%d]{key,state,what}:\n' 4
57
+ printf ' "version","%s","the line that says which version moved to which"\n' "$(state_of version)"
58
+ printf ' "patience","%s","the long-hour words before a slow install or build"\n' "$(state_of patience)"
59
+ printf ' "next","%s","the where-to-go-from-here block at the end"\n' "$(state_of next)"
60
+ printf ' "hints","%s","the one-line helpers that say how to hide a notice"\n' "$(state_of hints)"
61
+ printf 'store[1]{path}:\n "%s"\n' "$STORE"
62
+ ;;
63
+ notice)
64
+ key="${2-}"; value="${3-}"
65
+ [ -n "$key" ] || { printf 'error: which notice?\nhelp: ymir config notice <version|patience|next|hints> off\n' >&2; exit 2; }
66
+ case " $KNOWN " in *" $key "*) ;; *) printf 'error: unknown notice %s\nhelp: one of: %s\n' "$key" "$KNOWN" >&2; exit 2 ;; esac
67
+ if [ -z "$value" ]; then
68
+ printf 'notices[1]{key,state}:\n "%s","%s"\n' "$key" "$(state_of "$key")"
69
+ else
70
+ set_state "$key" "$value"
71
+ printf 'notices[1]{key,state}:\n "%s","%s"\n' "$key" "$value"
72
+ [ "$value" = off ] && printf 'this notice will not be shown again — restore it with: ymir config notice %s on\n' "$key"
73
+ fi
74
+ ;;
75
+ *) printf 'error: unknown action %s\nhelp: bin/ymir-config.sh [show|notice <key> on|off]\n' "$1" >&2; exit 2 ;;
76
+ esac
@@ -670,15 +670,15 @@ step_marks() {
670
670
  add marks OK "$n desktop app marks installed"
671
671
  return
672
672
  fi
673
- "$SCRIPT_DIR/design-icon.sh" mint --all >/dev/null 2>&1 || true
674
- n=$("$SCRIPT_DIR/design-icon.sh" install 2>/dev/null | grep -c '"ymir-') || n=0
675
- # The launcher entries are the freedesktop half of desktop integration, and they
676
- # belong to ANY Linux desktop — not only to Omarchy's window rules. A GNOME
677
- # operator gets entries and icons and no window rules; before this, both halves
678
- # sat behind the Omarchy gate and a packaged install placed nothing at all.
673
+ # The apps' own launcher templates land first (the freedesktop half of desktop
674
+ # integration, for ANY Linux desktop), and the rune marks are minted and
675
+ # installed over them so the themed rune name wins for the surfaces that have
676
+ # one, instead of a template's absolute .png.
679
677
  if [ -x "$SCRIPT_DIR/desktop-place.sh" ]; then
680
678
  "$SCRIPT_DIR/desktop-place.sh" entries >/dev/null 2>&1 || true
681
679
  fi
680
+ "$SCRIPT_DIR/design-icon.sh" mint --all >/dev/null 2>&1 || true
681
+ n=$("$SCRIPT_DIR/design-icon.sh" install 2>/dev/null | grep -c '"ymir-') || n=0
682
682
  [ -r "$HOME/.pi/agent/AGENTS.md" ] || [ -d "$HOME/.pi/agent" ] && {
683
683
  ln -sfn "$ROOT/AGENTS.md" "$HOME/.pi/agent/AGENTS.md" 2>/dev/null || true
684
684
  }
@@ -727,10 +727,16 @@ step_desktop() {
727
727
  add desktop WARN "the Electron runtime is PARTIAL for:$partial — the web surfaces stand; approve and rebuild to launch the shells"
728
728
  return 0
729
729
  fi
730
- if "$ROOT/scripts/electron.sh" start --both >/dev/null 2>&1; then
731
- add desktop OK "raised Hlidskjalf + Smíðja"
730
+ # One window per surface that is here: the halls are not one app.
731
+ raised=0
732
+ for _v in hlidskjalf smidja odrerir; do
733
+ "$ROOT/scripts/electron.sh" start --view "$_v" >/dev/null 2>&1 && raised=$((raised+1))
734
+ done
735
+ [ -x "$ROOT/bin/sessrumnir.sh" ] && "$ROOT/bin/sessrumnir.sh" start >/dev/null 2>&1 && raised=$((raised+1))
736
+ if [ "$raised" -ge 2 ]; then
737
+ add desktop OK "raised $raised window(s) — Hlidskjalf · Smíðja · Óðrerir · Sessrúmnir"
732
738
  else
733
- add desktop WARN "could not raise the desktop apps — run scripts/electron.sh start --both"
739
+ add desktop WARN "could not raise the desktop apps — ymir hlidskjalf | smidja | sessrumnir"
734
740
  fi
735
741
  }
736
742
 
package/bin/ymir-style.sh CHANGED
@@ -38,6 +38,10 @@ style_init() {
38
38
  fi
39
39
  C_OFF=""
40
40
  [ "$STYLE_ON" = 1 ] && C_OFF=$'\033[0m'
41
+ # A sourced library must never end a function on a FAILING test: callers run
42
+ # under `set -e` (scripts/start.sh does), and the last command's status becomes
43
+ # the function's — so a caller raised the whole hall and died at the cloth.
44
+ return 0
41
45
  }
42
46
 
43
47
  # The forge's marks, one per state a thing can be in.
@@ -64,6 +68,23 @@ style_colour() { # <state> → the colour for it
64
68
  esac
65
69
  }
66
70
 
71
+ # --- preferences: a user must be able to say "not again", once --------------
72
+ # The store is the operator's settings (see bin/ymir-config.sh); absent means on.
73
+ notice_state() { # <key> → on|off
74
+ local key="$1" store line
75
+ if [ -n "${YMIR_SETTINGS_DIR:-}" ]; then store="$YMIR_SETTINGS_DIR/notices.conf"
76
+ else store="${YMIR_HOME:-$HOME/Documents/Ymir}/config/notices.conf"; fi
77
+ [ -r "$store" ] || { printf 'on'; return 0; }
78
+ line="$(grep -m1 "^${key}=" "$store" 2>/dev/null || true)"
79
+ [ -n "$line" ] && printf '%s' "${line#*=}" || printf 'on'
80
+ }
81
+ notice_wanted() { [ "$(notice_state "${1-}")" != off ]; }
82
+ notice_hint() { # <key> — said once, and never when the hints are silenced
83
+ [ "$(notice_state hints)" = off ] && return 0
84
+ printf '%s
85
+ ' "${C_FAINT} (not again: ymir config notice ${1-} off)${C_OFF}" >&2
86
+ }
87
+
67
88
  # --- the pieces -------------------------------------------------------------
68
89
 
69
90
  # The mark: four lines at most. The WORDS show even in a pipe — only the colour
@@ -99,18 +120,21 @@ style_heading() { printf '\n%s%s%s\n' "$C_BRONZE" "$1" "$C_OFF" >&2; }
99
120
  # knows the halls are being set right waits; a user watching a silent cursor
100
121
  # wonders whether it has broken.
101
122
  style_patience() { # [what is being set right]
123
+ notice_wanted patience || return 0
102
124
  local what="${1:-the halls are being set right}"
103
125
  printf '\n' >&2
104
126
  style_line DO "much moves" "$what"
105
127
  style_hint " this hour is long, and nothing of yours is lost in it —"
106
128
  style_hint " roots come home, shapes are re-cut, names are set true again."
107
129
  style_hint " Your patience is noted, and it is earned."
130
+ notice_hint patience
108
131
  printf '\n' >&2
109
132
  }
110
133
 
111
134
  # The ending: what stands, then exactly what to type next. Every CLI deserves
112
135
  # to leave the operator with the next step and nothing else to guess.
113
136
  style_next() { # one command per line, as "verb — what it does"
137
+ notice_wanted next || return 0
114
138
  printf '\n%s\n' "${C_BOLD}Where to go from here${C_OFF}" >&2
115
139
  while [ $# -gt 0 ]; do
116
140
  local cmd="${1%% — *}" what="${1#* — }"
package/bin/ymir.js CHANGED
@@ -54,8 +54,20 @@ const bold = wrap('1');
54
54
  // cannot see what moved. We can: record the version we last ran, and say the
55
55
  // transition once, on the first run after an update.
56
56
  const STATE_DIR = process.env.YMIR_STATE_DIR || path.join(process.env.YMIR_HOME || path.join(require('node:os').homedir(), 'Documents', 'Ymir'), 'state');
57
+ function noticeState(key) {
58
+ try {
59
+ const s = process.env.YMIR_SETTINGS_DIR
60
+ || path.join(process.env.YMIR_HOME || path.join(require('node:os').homedir(), 'Documents', 'Ymir'), 'config');
61
+ const f = path.join(s, 'notices.conf');
62
+ if (!fs.existsSync(f)) return 'on';
63
+ const line = fs.readFileSync(f, 'utf8').split('\n').find((l) => l.startsWith(key + '='));
64
+ return line ? line.slice(key.length + 1).trim() : 'on';
65
+ } catch { return 'on'; }
66
+ }
67
+
57
68
  function versionNotice() {
58
69
  try {
70
+ if (noticeState('version') === 'off') return;
59
71
  const f = path.join(STATE_DIR, 'version');
60
72
  const seen = fs.existsSync(f) ? fs.readFileSync(f, 'utf8').trim() : '';
61
73
  if (seen === pkg.version) return;
@@ -64,7 +76,9 @@ function versionNotice() {
64
76
  if (!seen) return; // a first install has nothing to compare
65
77
  process.stderr.write(
66
78
  bone(` the tree moved `) + faint(`${seen} → `) + bronze(`${pkg.version}`) + '\n' +
67
- faint(` run \`ymir eir\` to see what stands, \`ymir raise\` to lift the hall\n\n`));
79
+ faint(` run \`ymir eir\` to see what stands, \`ymir raise\` to lift the hall\n`) +
80
+ (noticeState('hints') === 'off' ? '' : faint(` (not again: ymir config notice version off)\n`)) +
81
+ '\n');
68
82
  } catch { /* a version notice must never break a door */ }
69
83
  }
70
84
 
@@ -83,6 +97,7 @@ const DOORS = {
83
97
  hlidskjalf: { script: 'scripts/electron.sh', about: "the high seat's window", args: ['start', '--view', 'hlidskjalf'] },
84
98
  sessrumnir: { script: 'scripts/electron.sh', about: "the seat-hall's window", args: ['start', '--view', 'sessrumnir'] },
85
99
  mimir: { script: 'bin/mimir.sh', about: 'the memory well' },
100
+ config: { script: 'bin/ymir-config.sh', about: 'your preferences — which notices are shown (stay silent with `notice <key> off`)' },
86
101
  sense: { script: 'bin/host-sense.sh', about: 'what THIS machine is' },
87
102
  plan: { script: 'bin/ymir-plan.sh', about: 'what an install would do here — writes nothing' },
88
103
  migrate: { script: 'bin/ymir-migrate.sh', about: "heal this home's structure forward (Gr\u00f3a's mend)" },
@@ -0,0 +1,16 @@
1
+ # MIDGARD — Shared Design Assets
2
+
3
+ Cross-tenant assets that are **public by design**: the design system and the
4
+ shared libraries any realm may pull.
5
+
6
+ **What does NOT belong here.** Company knowledge — policies, vision, specs,
7
+ client names, wiki pages. That is tenant data, scoped per company, and lives at
8
+ `$YMIR_HOME/hodd/identity/companies/`. This repo is public; a company wiki in it
9
+ would publish that company's internals.
10
+
11
+ ## Contents
12
+ - `design-system/` — shared UI components, design tokens, icon set
13
+ - `shared-packages/` — internal npm/cargo/python libraries (public)
14
+ - `infrastructure/` — shared Terraform / Docker / Kubernetes configs
15
+ - `github_org_repos/` — central repositories
16
+ - `shared-core-api/` — example shared repo (with `.yggdrasil/` worktrees)
File without changes
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Type declarations for the shared hearth (ember.js).
3
+ *
4
+ * The implementation is plain JS so any surface — React, Vue, Astro, or a bare
5
+ * page — can import the same fire. These types keep TypeScript consumers honest
6
+ * without forcing the module itself into a build step.
7
+ */
8
+
9
+ /** Start the hearth on <host> (a canvas, or an element to receive one). Returns a stop function. */
10
+ export function startEmbers(host: Element | null): () => void;
11
+
12
+ /** Light every element marked `data-ember`. Returns one stop function per hearth. */
13
+ export function startAllEmbers(root?: ParentNode): Array<() => void>;
@@ -0,0 +1,183 @@
1
+ /**
2
+ * ember.js — the hearth, in one place.
3
+ *
4
+ * The fire that warms Sessrúmnir's chat came from the landing page's hero
5
+ * (Ginnungagap): embers rising slowly with a gentle sway, over a few drifting
6
+ * haze pools. Sessrúmnir carries a React port; this is the framework-neutral
7
+ * original, so every surface can share one fire instead of four copies.
8
+ *
9
+ * import { startEmbers } from '.../midgard/design-system/ember.js'
10
+ * const stop = startEmbers(document.querySelector('[data-ember]'))
11
+ *
12
+ * Or simply mark an element and let it find its own:
13
+ *
14
+ * <canvas data-ember></canvas>
15
+ * <script type="module" src="/ember.js"></script>
16
+ *
17
+ * It is decorative: the canvas is given `pointer-events: none` and
18
+ * `aria-hidden`, and a reader who has asked their machine for less motion gets a
19
+ * still, single-frame hearth instead of an animation.
20
+ */
21
+
22
+ const EMBERS = 30;
23
+ const HAZE = 6;
24
+ const EMBER_RGB = '236,166,84';
25
+ const HAZE_RGB = '214,138,64';
26
+
27
+ /** Draw one still frame — also the whole effect under reduced motion. */
28
+ function paint(ctx, W, H, embers, haze) {
29
+ ctx.clearRect(0, 0, W, H);
30
+ for (const ha of haze) {
31
+ const g = ctx.createRadialGradient(ha.x, ha.y, 0, ha.x, ha.y, ha.r);
32
+ g.addColorStop(0, `rgba(${HAZE_RGB},0.030)`);
33
+ g.addColorStop(1, 'rgba(0,0,0,0)');
34
+ ctx.fillStyle = g;
35
+ ctx.beginPath();
36
+ ctx.arc(ha.x, ha.y, ha.r, 0, 6.283);
37
+ ctx.fill();
38
+ }
39
+ for (const e of embers) {
40
+ ctx.fillStyle = `rgba(${EMBER_RGB},${e.a.toFixed(3)})`;
41
+ ctx.beginPath();
42
+ ctx.arc(e.x, e.y, e.r, 0, 6.283);
43
+ ctx.fill();
44
+ }
45
+ }
46
+
47
+ function seed(W, H) {
48
+ const embers = [];
49
+ for (let i = 0; i < EMBERS; i++) {
50
+ embers.push({
51
+ x: Math.random() * W,
52
+ y: H + Math.random() * H * 0.3 - 6,
53
+ r: 1.0 + Math.random() * 1.6,
54
+ v: 0.14 + Math.random() * 0.34,
55
+ ph: Math.random() * 6.28,
56
+ sp: 0.6 + Math.random() * 1.4,
57
+ a: 0.26,
58
+ });
59
+ }
60
+ const haze = [];
61
+ for (let i = 0; i < HAZE; i++) {
62
+ haze.push({ x: Math.random() * W, y: H * 0.3 + Math.random() * H * 0.7, r: 130 + Math.random() * 170, v: (Math.random() - 0.5) * 0.15 });
63
+ }
64
+ return { embers, haze };
65
+ }
66
+
67
+ /**
68
+ * Start the hearth on <host>. Returns a stop function. Safe to call on a host
69
+ * that has no box yet (it simply draws nothing until it has one).
70
+ */
71
+ export function startEmbers(host) {
72
+ if (!host) return () => {};
73
+ const canvas = host.tagName === 'CANVAS' ? host : document.createElement('canvas');
74
+ if (canvas !== host) {
75
+ canvas.style.position = 'absolute';
76
+ canvas.style.inset = '0';
77
+ host.appendChild(canvas);
78
+ }
79
+ canvas.setAttribute('aria-hidden', 'true');
80
+ canvas.style.pointerEvents = 'none';
81
+
82
+ const ctx = canvas.getContext('2d');
83
+ if (!ctx) return () => {};
84
+
85
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
86
+ let W = 0;
87
+ let H = 0;
88
+ let t = 0;
89
+ let raf = 0;
90
+ let { embers, haze } = seed(1, 1);
91
+
92
+ const reduced = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true;
93
+
94
+ function size() {
95
+ const rect = canvas.getBoundingClientRect();
96
+ W = rect.width || canvas.clientWidth || 0;
97
+ H = rect.height || canvas.clientHeight || 0;
98
+ canvas.width = Math.max(1, W * dpr);
99
+ canvas.height = Math.max(1, H * dpr);
100
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
101
+ }
102
+
103
+ function reseed() {
104
+ ({ embers, haze } = seed(W, H));
105
+ if (reduced) {
106
+ for (const e of embers) e.a = 0.3 + 0.4 * Math.random();
107
+ paint(ctx, W, H, embers, haze);
108
+ }
109
+ }
110
+
111
+ function frame() {
112
+ t += 0.016;
113
+ for (const ha of haze) {
114
+ ha.x += ha.v;
115
+ if (ha.x < -ha.r) ha.x = W + ha.r;
116
+ if (ha.x > W + ha.r) ha.x = -ha.r;
117
+ }
118
+ for (const e of embers) {
119
+ e.y -= e.v;
120
+ e.x += Math.sin(t * e.sp + e.ph) * 0.18;
121
+ if (e.y < -6) {
122
+ e.y = H + 6;
123
+ e.x = Math.random() * W;
124
+ }
125
+ e.a = 0.26 + 0.4 * (0.5 + 0.5 * Math.sin(t * e.sp * 2 + e.ph));
126
+ }
127
+ paint(ctx, W, H, embers, haze);
128
+ }
129
+
130
+ function loop() {
131
+ frame();
132
+ raf = requestAnimationFrame(loop);
133
+ }
134
+
135
+ size();
136
+ reseed();
137
+ if (!reduced) raf = requestAnimationFrame(loop);
138
+
139
+ const onResize = () => {
140
+ size();
141
+ reseed();
142
+ };
143
+ window.addEventListener('resize', onResize);
144
+
145
+ // The hearth warms a *container*, and containers change without a window
146
+ // resize (a split pane, a toggled sidebar). Watch the host: re-size and
147
+ // re-seed only when its dimensions actually changed, never per-frame.
148
+ let ro = null;
149
+ if (typeof ResizeObserver !== 'undefined' && canvas.parentElement) {
150
+ const host = canvas.parentElement;
151
+ let lastW = 0;
152
+ let lastH = 0;
153
+ ro = new ResizeObserver((entries) => {
154
+ const e = entries[0];
155
+ if (!e) return;
156
+ const w = e.contentRect?.width ?? e.contentBoxSize?.[0]?.inlineSize ?? 0;
157
+ const h = e.contentRect?.height ?? e.contentBoxSize?.[0]?.blockSize ?? 0;
158
+ if (Math.abs(w - lastW) > 0.5 || Math.abs(h - lastH) > 0.5) {
159
+ lastW = w;
160
+ lastH = h;
161
+ onResize();
162
+ }
163
+ });
164
+ ro.observe(host);
165
+ }
166
+
167
+ return () => {
168
+ if (raf) cancelAnimationFrame(raf);
169
+ window.removeEventListener('resize', onResize);
170
+ if (ro) ro.disconnect();
171
+ };
172
+ }
173
+
174
+ /** Every element marked `data-ember` lights its own hearth. */
175
+ export function startAllEmbers(root = document) {
176
+ return Array.from(root.querySelectorAll('[data-ember]')).map(startEmbers);
177
+ }
178
+
179
+ if (typeof document !== 'undefined') {
180
+ const boot = () => startAllEmbers();
181
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot, { once: true });
182
+ else boot();
183
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter" role="img" aria-label="Algiz — Reviews / protection">
2
+ <title>Algiz — Reviews / protection</title>
3
+ <path d="M12 20V10 M12 10L5 3 M12 10L19 3" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter" role="img" aria-label="Ansuz — Forge">
2
+ <title>Ansuz — Forge</title>
3
+ <path d="M12 4V20 M12 9L7 4 M12 9L17 4" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter" role="img" aria-label="Berkanan — Profile">
2
+ <title>Berkanan — Profile</title>
3
+ <path d="M9 4V20 M9 4L17 8V14L9 18" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter" role="img" aria-label="Dagaz — Processes">
2
+ <title>Dagaz — Processes</title>
3
+ <path d="M5 5L19 19 M19 5L5 19 M5 5V19 M19 5V19" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter" role="img" aria-label="Ehwaz — Runtime">
2
+ <title>Ehwaz — Runtime</title>
3
+ <path d="M4 20V8L12 14L20 8V20" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter" role="img" aria-label="Fehu — Fleet">
2
+ <title>Fehu — Fleet</title>
3
+ <path d="M12 20V4 M12 8L6 4 M12 8L18 4" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter" role="img" aria-label="Gjallarhorn — tunnel">
2
+ <title>Gjallarhorn — tunnel</title>
3
+ <path d="M6 5L18 19 M18 5L6 19" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter" role="img" aria-label="Gungnir — skills">
2
+ <title>Gungnir — skills</title>
3
+ <path d="M7 20V4 M7 6L17 12L7 18" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter" role="img" aria-label="Heimdall — auth">
2
+ <title>Heimdall — auth</title>
3
+ <path d="M7 4V20 M17 4V20 M7 12H17" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter" role="img" aria-label="Ingwaz — Well">
2
+ <title>Ingwaz — Well</title>
3
+ <path d="M12 4L20 12L12 20L4 12Z" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter" role="img" aria-label="Jera — Cron">
2
+ <title>Jera — Cron</title>
3
+ <path d="M6 20L12 12L6 4 M18 4L12 12L18 20" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter" role="img" aria-label="Kaunan — OmniChat">
2
+ <title>Kaunan — OmniChat</title>
3
+ <path d="M18 4L6 12L18 20" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter" role="img" aria-label="Midgard — shared">
2
+ <title>Midgard — shared</title>
3
+ <path d="M4 12H20 M12 4V20 M6 6L18 18 M18 6L6 18" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter" role="img" aria-label="Mimirsbrunn — memory">
2
+ <title>Mimirsbrunn — memory</title>
3
+ <path d="M12 4L20 12L12 20L4 12Z" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter" role="img" aria-label="Othala — inheritance, the hall">
2
+ <title>Othala — inheritance, the hall</title>
3
+ <path d="M12 4L19 9L12 14L5 9Z M9 12L7 20 M15 12L17 20" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter" role="img" aria-label="Raidho — Runes">
2
+ <title>Raidho — Runes</title>
3
+ <path d="M8 20V4H14L18 8L14 12H8 M14 12L18 20" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter" role="img" aria-label="Ratatoskr — A2A bus">
2
+ <title>Ratatoskr — A2A bus</title>
3
+ <path d="M9 4V20 M9 4L17 8V14L9 18" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter" role="img" aria-label="Sowilo — the sun">
2
+ <title>Sowilo — the sun</title>
3
+ <path d="M16 4L8 4L16 20L8 20" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter" role="img" aria-label="Tiwaz — Tasks">
2
+ <title>Tiwaz — Tasks</title>
3
+ <path d="M12 20V4 M12 4L6 11 M12 4L18 11" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter" role="img" aria-label="Utgard — sandbox">
2
+ <title>Utgard — sandbox</title>
3
+ <path d="M8 20V8C8 5 16 5 16 8V20" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter" role="img" aria-label="Valhalla — processes">
2
+ <title>Valhalla — processes</title>
3
+ <path d="M7 20V4 M7 4L17 8L7 12" />
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter" role="img" aria-label="Yggdrasil — worktrees">
2
+ <title>Yggdrasil — worktrees</title>
3
+ <path d="M6 20L12 12L6 4 M18 4L12 12L18 20" />
4
+ </svg>
@@ -0,0 +1,59 @@
1
+ # Rune-Glyph Icon Map
2
+
3
+ **Runecoded, not emoji.** Every icon in Ymir is an Elder Futhark rune drawn as a
4
+ 24px stroke glyph, colored by `currentColor`. Emoji are never used as icons.
5
+
6
+ ## Set
7
+
8
+ The glyphs live in `midgard/design-system/icons/*.svg` (24×24 viewBox; render at
9
+ 16px or 24px). Stroke uses `currentColor`, square caps, miter joins — the chisel
10
+ bevel. The primary seat may add a 1px inner bevel via the `--ymir-bevel` token.
11
+
12
+ ```
13
+ icons[21]{glyph,name,unicode,figure,used_for}:
14
+ "fehu","Fehu","ᚠ","Ymir (cattle/wealth)","Fleet — the herd of agents"
15
+ "tiwaz","Tiwaz","ᛏ","Týr (justice)","Tasks — the A2A lifecycle board"
16
+ "ingwaz","Ingwaz","ᛜ","Ing (the well)","Well — Mimirsbrunn recall"
17
+ "raidho","Raidho","ᚱ","ride/journey","Runes — the ledger (a road carved)"
18
+ "othala","Othala","ᛟ","inheritance, the hall","Sessrúmnir — the seat that holds the cloth"
19
+ "algiz","Algiz","ᛉ","protection","Reviews / Glitnir — the protective gate; the Ymir emblem"
20
+ "dagaz","Dagaz","ᛞ","day/dawn","Processes — Valhalla supervision"
21
+ "sowilo","Sowilo","ᛊ","sun","Files — Skrymir (light on the tree)"
22
+ "kaunan","Kaunan","ᚴ","torch","OmniChat — Kaia's flame"
23
+ "ansuz","Ansuz","ᚨ","Odin's breath","Óðrerir — the mead of poetry, the Live Hall"
24
+ "ehwaz","Ehwaz","ᛖ","horse/journey","Runtime — the seat and its transit"
25
+ "jera","Jera","ᛃ","year/harvest","Cron — the Nornir's seasons"
26
+ "berkana","Berkanan","ᛒ","birch","Profile — the self"
27
+ "midgard","Midgard","—","the shared world","shared workspace / cross-tenant assets"
28
+ "utgard","Utgard","ᚢ","the outer realm","Utgard — the sandbox barrier"
29
+ "yggdrasil","Yggdrasil","ᛃ","the world-tree","Yggdrasil — worktrees"
30
+ "ratatoskr","Ratatoskr","ᛒ","the squirrel","Ratatoskr — the A2A bus"
31
+ "mimirsbrunn","Mimirsbrunn","ᛜ","the well of wisdom","Mimirsbrunn — memory"
32
+ "valhalla","Wunjo","ᚹ","joy","Valhalla — process health (the glyph ᚹ is drawn here)"
33
+ "gungnir","Gungnir","ᚦ","Odin's spear","Gungnir — skill synthesis"
34
+ "heimdall","Heimdall","ᚺ","the watchman","Heimdall — auth / the gate"
35
+ "gjallarhorn","Gjallarhorn","ᚷ","the horn","Gjallarhorn — the tunnel and its call"
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ - Reference a glyph as an inline `<svg>` (copy the path) or `<img src>`; stroke
41
+ with `currentColor`, never a raw hex.
42
+ - Render at **24px** in panels, **16px** inline in tables and chips.
43
+ - A status glyph is always paired with **glyph + colour + text** — never
44
+ colour-only state.
45
+ - The active realm/accent may tint glyphs via `--realm-tint`.
46
+
47
+ ## Verification
48
+
49
+ ```bash
50
+ ls midgard/design-system/icons/*.svg | wc -l # 21
51
+ # no emoji used as icons in the UI:
52
+ rg -n "[\x{1F300}-\x{1FAFF}\x{2600}-\x{27BF}]" apps/hlidskjalf/src || echo "no emoji icons"
53
+ ```
54
+
55
+ ## Maintaining this
56
+
57
+ - **Owner:** Brokk. **Tokens:** `midgard/design-system/tokens.css`.
58
+ - Add a rune here and its glyph in `icons/` in the same pass; keep the count in the
59
+ `icons[N]` block true (Galdr TOON check: `.agents/skills/galdr-ymirsystem/scripts/toon-check.py`).
@@ -0,0 +1,78 @@
1
+ # The Rune Library
2
+
3
+ **Runecoded, not emoji.** Every icon in Ymir is an Elder Futhark rune drawn as a
4
+ 24×24 stroke glyph, coloured by `currentColor` (or the app's house tint). This is
5
+ the library: the full futhark, what each rune means, and where Ymir uses it.
6
+
7
+ - **Glyphs:** `midgard/design-system/icons/*.svg` (24×24 viewBox, stroke 1.8,
8
+ square caps, miter joins — the chisel bevel).
9
+ - **Names in code:** the file name is the rune's name, lower-case (`ehwaz.svg`).
10
+ - **Usage rules:** inline `<svg>` or `<img src>`; stroke with `currentColor`,
11
+ never a raw hex; 24px in panels, 16px inline; a status glyph is always paired
12
+ with **glyph + colour + text**, never colour alone.
13
+ - **The app runes:** five UI surfaces, five different glyphs — see the foot.
14
+
15
+ ## The futhark
16
+
17
+ ```
18
+ runes[24]{glyph,name,sound,meaning,ymir_use,file}:
19
+ "ᚠ","Fehu","f","cattle, wealth","Fleet — the herd of agents","fehu"
20
+ "ᚢ","Uruz","u","aurochs, strength","Utgard — the sandbox barrier","utgard"
21
+ "ᚦ","Thurisaz","th","thorn, giant","Gungnir — skill synthesis","gungnir"
22
+ "ᚨ","Ansuz","a","Odin's breath","Óðrerir — the mead of poetry, the Live Hall","ansuz"
23
+ "ᚱ","Raido","r","ride, journey","Runes — the ledger (a road carved)","raidho"
24
+ "ᚲ","Kaunan","k","torch, ulcer","Smíðja's eye — the trace of the forge, OmniChat's flame","kaunan"
25
+ "ᚷ","Gebo","g","gift","Gjallarhorn — the tunnel and its call","gjallarhorn"
26
+ "ᚹ","Wunjo","w","joy","Valhalla — the hall of the supervisor","valhalla"
27
+ "ᚺ","Hagall","h","hail","Heimdall — auth, the gate","heimdall"
28
+ "ᚾ","Naudiz","n","need, constraint","(no glyph yet)","—"
29
+ "ᛁ","Isaz","i","ice","(no glyph yet)","—"
30
+ "ᛃ","Jera","j","year, harvest","Nornir — the seasons; Yggdrasil — the world-tree's cycle","jera"
31
+ "ᛇ","Eihwaz","ï","yew","(no glyph yet)","—"
32
+ "ᛈ","Perthro","p","lot, fate","(no glyph yet)","—"
33
+ "ᛉ","Algiz","z","protection","Glitnir's reviews — the protective gate; **the Ymir emblem**","algiz"
34
+ "ᛊ","Sowilo","s","sun","Skrymir — files (light on the tree)","sowilo"
35
+ "ᛏ","Tiwaz","t","Týr, justice","Tasks — the A2A lifecycle board","tiwaz"
36
+ "ᛒ","Berkanan","b","birch","Profile — the self; Ratatoskr — the messenger","berkana"
37
+ "ᛖ","Ehwaz","e","horse","Runtime — the seat and its transit; Hlidskjalf","ehwaz"
38
+ "ᛗ","Mannaz","m","man, self","(no glyph yet)","—"
39
+ "ᛚ","Laguz","l","water, flow","(no glyph yet)","—"
40
+ "ᛜ","Ingwaz","ng","Ing, the seed","Mimirsbrunn — the well of wisdom","ingwaz"
41
+ "ᛞ","Dagaz","d","day, dawn","Valhalla — process health","dagaz"
42
+ "ᛟ","Othala","o","inheritance, estate","(no glyph yet)","—"
43
+ ```
44
+
45
+ **Twenty-one of the twenty-four are drawn; three are not yet:** Naudiz, Isaz,
46
+ Eihwaz, Perthro, Mannaz, Laguz and Othala have no file — add the glyph and its
47
+ row in `icons.md` in the same pass.
48
+
49
+ **Every file is a rune.** It was not always so: eight files used to be *drawings*
50
+ of the thing they were named after (a hall, a horn, a shield, a spear, a well, a
51
+ squirrel, a gate, a world-tree) while `icons.md` claimed a rune for each. Corrected
52
+ 2026-09-16 — Gungnir took Thurisaz, since Gebo was already Gjallarhorn's.
53
+
54
+ ## The app runes
55
+
56
+ Five UI surfaces, five different glyphs — no app borrows another's rune:
57
+
58
+ ```
59
+ app_runes[5]{app,glyph,rune,house_tint}:
60
+ "Hlidskjalf","ᛖ","ehwaz — the seat","#c9973f bronze (ymirlabs)"
61
+ "Hlidskjalf Mobile","ᚱ","raidho — the road, the seat carried","#c9973f bronze"
62
+ "Óðrerir","ᚨ","ansuz — Odin's breath, the mead of poetry","#c9973f bronze (ymirlabs)"
63
+ "Sessrúmnir","ᛟ","othala — inheritance, the hall that holds what is kept","#c9973f bronze (ymirlabs)"
64
+ "Smíðja (visualizer)","ᚲ","kaunan — the torch, the forge's eye","#f59e0b amber (brokkforge)"
65
+ ```
66
+
67
+ Minted and installed by `bin/design-icon.sh` (`list` · `mint --all` · `install`):
68
+ the icon into the user's icon theme, the `.desktop` entry into their applications
69
+ dir, so every app is dockable and carries the house mark.
70
+
71
+ ## Adding a rune
72
+
73
+ 1. Draw it in `icons/<name>.svg` — 24×24, stroke 1.8, square caps, miter joins,
74
+ `stroke="currentColor"`, a `<title>` naming the rune and its use.
75
+ 2. Add its row to `icons.md` **and** to the futhark table above, in the same
76
+ change. Keep both counts true (the TOON check counts rows).
77
+ 3. If an app should wear it, add the app to `bin/design-icon.sh`'s table and run
78
+ `mint --all && install`.
@@ -0,0 +1,109 @@
1
+ /* ============================================================
2
+ YMIR — Design Tokens · Canonical (the carved cloth of the halls)
3
+ Source of truth: docs/design.md §4.
4
+ The cloth is cut from the landing page (CodeP/ymir-homepage/src/lore.html
5
+ :root): stone and bone, bronze and blood, and the three faces Cormorant /
6
+ Newsreader / IBM Plex Mono. The landing is the reference — a token change
7
+ here re-themes every hall (Hlidskjalf, Smíðja, and the seat).
8
+ House accents are heraldry, not chrome: they stay, exactly as the landing
9
+ keeps its house seals.
10
+ Consume via CSS variables — never hardcode hex in components.
11
+ ============================================================ */
12
+
13
+ :root {
14
+ /* --- Canvas: the stone ------------------------------- */
15
+ --ymir-bg-0: #0e0c09; /* stone — the ground */
16
+ --ymir-bg-1: #151209; /* panel */
17
+ --ymir-bg-2: #1a1610; /* panel2 — elevated surface, cards */
18
+ --ymir-bg-3: #221d14; /* line-soft — hover, wells */
19
+
20
+ /* --- Accents: the bronze light of the forge ---------- */
21
+ --ymir-cyan-1: #c9973f; /* bronze — primary accent, active states, focus, links */
22
+ --ymir-cyan-2: #7d5f2a; /* bronze-deep — hover/darker accent */
23
+ --ymir-violet-1: #96a0a8; /* steel — realm identity, secondary accent */
24
+ --ymir-violet-2: #5f686e; /* steel-dim */
25
+
26
+ /* --- Structure: bone and brass ----------------------- */
27
+ --ymir-steel-1: #2b241a; /* line — borders, bevel exteriors */
28
+ --ymir-steel-2: #7d5f2a; /* bronze-deep — stronger rules, dividers */
29
+ --ymir-steel-3: #9a8f75; /* bone-dim — secondary text, muted glyphs */
30
+ --ymir-chisel: #c9973f; /* bronze — the lit chisel edge (emblem, focus) */
31
+
32
+ /* --- Semantics: state of the forge ------------------- */
33
+ --ymir-ok: #96a0a8; /* steel — the metal proved true */
34
+ --ymir-warn: #c9973f; /* bronze — hot, heed it */
35
+ --ymir-danger: #c2584a; /* blood-lit — the wound */
36
+ --ymir-info: #9a8f75; /* bone-dim — the note */
37
+ --ymir-ok-dim: rgba(150, 160, 168, 0.15);
38
+ --ymir-warn-dim: rgba(201, 151, 79, 0.15);
39
+ --ymir-danger-dim: rgba(194, 88, 74, 0.15);
40
+ --ymir-info-dim: rgba(154, 143, 117, 0.15);
41
+
42
+ /* --- Text on stone ----------------------------------- */
43
+ --ymir-text-0: #cfc3a9; /* bone — primary text */
44
+ --ymir-text-1: rgba(207, 195, 169, 0.82); /* bone, blunted — secondary text */
45
+ --ymir-text-2: #9a8f75; /* bone-dim — tertiary, disabled */
46
+ --ymir-text-3: #6b6250; /* bone-faint — placeholder */
47
+
48
+ /* --- House accents (structural only, never chrome) ---------- */
49
+ --ymir-house-ymirlabs: #c9973f; /* bronze — the platform's own house wears the forge accent (was blue) */
50
+ --ymir-house-brokkforge: #f59e0b;
51
+ --ymir-house-runestone: #f43f5e;
52
+ --ymir-house-muninn: #8b5cf6;
53
+ --ymir-house-dvalin: #94a3b8;
54
+ --ymir-house-utgard: #d946ef;
55
+ --ymir-house-askr: #34d399;
56
+ --ymir-house-mannheim: #64748b;
57
+
58
+ /* --- Type: the cloth's three faces --------------------------- */
59
+ /* Runic glyphs fall through to Noto Sans Runic inside every stack, so a rune
60
+ renders in the rune family wherever it is written. */
61
+ --ymir-font-head: 'Cormorant', 'Noto Sans Runic', Georgia, serif;
62
+ --ymir-font-mono: 'IBM Plex Mono', 'Noto Sans Runic', ui-monospace, 'Cascadia Mono', monospace;
63
+ --ymir-font-body: 'Newsreader', Georgia, 'Noto Sans Runic', serif;
64
+
65
+ --ymir-s-mono-s: 12px / 16px;
66
+ --ymir-s-mono-m: 14px / 20px;
67
+ --ymir-s-mono-l: 18px / 24px;
68
+ --ymir-s-display: clamp(28px, 4vw, 44px) / 1.1;
69
+ --ymir-s-body: 15px / 22px;
70
+ --ymir-s-caption: 13px / 18px;
71
+
72
+ /* --- Space & structure ----------------------------------------- */
73
+ --ymir-space-1: 4px;
74
+ --ymir-space-2: 8px;
75
+ --ymir-space-3: 12px;
76
+ --ymir-space-4: 16px;
77
+ --ymir-space-5: 24px;
78
+ --ymir-space-6: 32px;
79
+ --ymir-space-7: 48px;
80
+
81
+ --ymir-radius: 8px;
82
+ --ymir-radius-panel: 12px;
83
+ --ymir-border: 1px solid var(--ymir-steel-1);
84
+
85
+ /* Bevel: sell the carved edge — a brass hairline inside the stone */
86
+ --ymir-bevel: inset 0 0 0 1px rgba(201, 151, 79, 0.12);
87
+ --ymir-shadow-panel: 0 1px 2px rgba(0, 0, 0, 0.5);
88
+ --ymir-shadow-raise: 0 8px 24px rgba(0, 0, 0, 0.55);
89
+
90
+ /* --- Motion ------------------------------------------------------- */
91
+ --ymir-dur-fast: 120ms;
92
+ --ymir-dur-task: 180ms;
93
+ --ymir-dur-hop: 220ms;
94
+ --ymir-dur-roll: 240ms;
95
+ --ymir-ease: cubic-bezier(0.4, 0, 0.2, 1);
96
+ }
97
+
98
+ /* Numeric safety for all telemetry */
99
+ .tabular { font-variant-numeric: tabular-nums; font-family: var(--ymir-font-mono); }
100
+
101
+ /* Status chips: color + rune + label — never color alone */
102
+ .status-ok { color: var(--ymir-ok); background: var(--ymir-ok-dim); }
103
+ .status-warn { color: var(--ymir-warn); background: var(--ymir-warn-dim); }
104
+ .status-danger{ color: var(--ymir-danger); background: var(--ymir-danger-dim); }
105
+ .status-info { color: var(--ymir-info); background: var(--ymir-info-dim); }
106
+
107
+ @media (prefers-reduced-motion: reduce) {
108
+ * { transition-duration: 120ms !important; animation-duration: 120ms !important; }
109
+ }
@@ -0,0 +1,62 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="100%" height="100%">
2
+ <defs>
3
+ <!-- Background Dark Steel Gradient -->
4
+ <linearGradient id="bg-grad" x1="0%" y1="0%" x2="100%" y2="100%">
5
+ <stop offset="0%" stop-color="#0f172a" />
6
+ <stop offset="100%" stop-color="#020617" />
7
+ </linearGradient>
8
+
9
+ <!-- Base Metal Color for the Rune -->
10
+ <linearGradient id="metal-base" x1="0%" y1="0%" x2="0%" y2="100%">
11
+ <stop offset="0%" stop-color="#38bdf8" />
12
+ <stop offset="50%" stop-color="#475569" />
13
+ <stop offset="100%" stop-color="#1e293b" />
14
+ </linearGradient>
15
+
16
+ <!-- Advanced 3D Forge Filter -->
17
+ <filter id="heavy-metal-3d" x="-20%" y="-20%" width="140%" height="140%">
18
+ <!-- 1. Generate Hammered Iron Texture -->
19
+ <feTurbulence type="fractalNoise" baseFrequency="0.04" numOctaves="4" result="noise" />
20
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.15 0" in="noise" result="coloredNoise" />
21
+
22
+ <!-- 2. Blend Texture with Base Color -->
23
+ <feComposite operator="in" in="coloredNoise" in2="SourceGraphic" result="textured" />
24
+ <feBlend mode="multiply" in="textured" in2="SourceGraphic" result="baseTex" />
25
+
26
+ <!-- 3. Create 3D Bevel / Depth Map -->
27
+ <feGaussianBlur in="SourceAlpha" stdDeviation="4" result="blur" />
28
+
29
+ <!-- 4. Apply 3D Lighting (Cyan/Blue Specular Highlight) -->
30
+ <feSpecularLighting in="blur" surfaceScale="6" specularConstant="1.2" specularExponent="25" lighting-color="#bae6fd" result="specOut">
31
+ <!-- Light Source Positioned Top-Left -->
32
+ <fePointLight x="-100" y="-100" z="300" />
33
+ </feSpecularLighting>
34
+
35
+ <!-- 5. Mask Lighting to the Shape -->
36
+ <feComposite operator="in" in="specOut" in2="SourceAlpha" result="specularMasked" />
37
+
38
+ <!-- 6. Combine Lighting and Textured Base -->
39
+ <feComposite operator="arithmetic" k1="0" k2="1" k3="1" k4="0" in="specularMasked" in2="baseTex" result="litModel" />
40
+
41
+ <!-- 7. Heavy Drop Shadow for Canvas Depth -->
42
+ <feDropShadow in="litModel" dx="0" dy="16" stdDeviation="12" flood-opacity="0.85" flood-color="#000000" result="finalShadow" />
43
+ </filter>
44
+ </defs>
45
+
46
+ <!-- Canvas -->
47
+ <rect width="512" height="512" rx="40" fill="url(#bg-grad)" stroke="#1e293b" stroke-width="4"/>
48
+
49
+ <!-- Master Group with 3D Rendering Filter Applied -->
50
+ <g filter="url(#heavy-metal-3d)" fill="url(#metal-base)">
51
+
52
+ <!-- Algiz Y-Stem (Solid Geometric Polygon) -->
53
+ <polygon points="120,100 168,100 256,188 344,100 392,100 276,216 276,340 236,340 236,216" />
54
+
55
+ <!-- Top Anvil Bar -->
56
+ <polygon points="100,340 412,340 400,388 112,388" />
57
+
58
+ <!-- Bottom Anvil Bar (Slightly narrower base) -->
59
+ <polygon points="132,408 380,408 368,440 144,440" />
60
+
61
+ </g>
62
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerwiz/ymir",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Ymir \u2014 the single-tenant agent operating system. Omarchy-first, host-aware, self-healing.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/zerwiz/ymir",
@@ -30,6 +30,7 @@
30
30
  ".agents/",
31
31
  "scripts/",
32
32
  "RULES/",
33
+ "midgard/design-system/",
33
34
  "docs/",
34
35
  "packaging/",
35
36
  "AGENTS.md",
@@ -184,9 +184,25 @@ ensure_electron_binary() {
184
184
  return 1
185
185
  }
186
186
 
187
+ # npm gates install scripts by default (11.16+ warns, 12 refuses), and Electron's
188
+ # postinstall is what downloads the runtime — so a fresh install can build the web
189
+ # app perfectly and still have no window. Before refusing, do the mending a user
190
+ # would otherwise be told to do by hand: approve the script and rebuild.
191
+ repair_electron() { # <app-dir>
192
+ local dir="$1"
193
+ command -v npm >/dev/null 2>&1 || return 1
194
+ ( cd "$dir" && npm install-scripts approve electron >/dev/null 2>&1 || true
195
+ cd "$dir" && npm rebuild electron >/dev/null 2>&1 )
196
+ }
187
197
  if ! ensure_electron_binary; then
188
- printf 'error: the Electron binary is missing (the package installed but its postinstall was blocked)\nhelp: cd apps/hlidskjalf && node node_modules/electron/install.js\nhelp: if npm blocks install scripts, run: npm install --foreground-scripts\n' >&2
189
- exit 1
198
+ # Try the mending ourselves before telling the user to do it by hand: npm's
199
+ # gating is the cause, and the cure is one command we can run.
200
+ echo "the Electron runtime is partial — mending it (npm rebuild electron)…" >&2
201
+ repair_electron "$APP" >/dev/null 2>&1 || true
202
+ ensure_electron_binary || {
203
+ printf 'error: the Electron runtime could not be mended\nhelp: cd <the app> && npm install-scripts approve electron && npm rebuild electron\nhelp: or run the web surfaces only: ymir install --no-desktop\n' >&2
204
+ exit 1
205
+ }
190
206
  fi
191
207
 
192
208
  app_dir() { case "$1" in odrerir) printf '%s' "$Odrerir_app" ;; *) printf '%s' "$APP" ;; esac; }
package/scripts/start.sh CHANGED
@@ -98,7 +98,21 @@ fi
98
98
  if [[ ! -d "$APP/node_modules" ]]; then
99
99
  style_patience "the seat's own dependencies are being fetched, then the hall is raised"
100
100
  echo "Installing dependencies…"
101
- (cd "$APP" && npm install --no-audit --no-fund)
101
+ (# Sessrúmnir the seat-hall. Its own Electron app (not a browser surface), so it
102
+ # rises here rather than with the SPA: four surfaces, one raise.
103
+ if [ -x "$ROOT/bin/sessrumnir.sh" ]; then
104
+ if app_dir sessrumnir APP_SESSRUMNIR; then
105
+ if "$ROOT/bin/sessrumnir.sh" start >/dev/null 2>&1; then
106
+ echo "Sessrúmnir — the seat-hall raised → (its own window)"
107
+ else
108
+ echo "Sessrúmnir — the seat-hall did not rise (run: bin/sessrumnir.sh start)" >&2
109
+ fi
110
+ else
111
+ echo "Sessrúmnir — skipped (the sessrumnir surface is not present)" >&2
112
+ fi
113
+ fi
114
+
115
+ cd "$APP" && npm install --no-audit --no-fund)
102
116
  fi
103
117
 
104
118
  # NOTE: when the SPA is already up we still raise the API and the services
@@ -201,15 +215,45 @@ app_dir odrerir HALL_DIR || HALL_DIR=""
201
215
  HALL_PORT="${ODRERIR_PORT:-4322}"
202
216
  HALL_PID_FILE="$RUN/odrerir.pid"
203
217
  HALL_LOG="$RUN/odrerir.log"
204
- if [ -d "$HALL_DIR" ]; then
205
- [ -d "$HALL_DIR/node_modules" ] || (cd "$HALL_DIR" && npm install --no-audit --no-fund >/dev/null 2>&1 || true)
218
+ if [ -d "$HALL_DIR" ] && [ -n "$HALL_DIR" ]; then
219
+ # Its dependencies are not optional and their absence is not silent: an Astro
220
+ # dev server without them dies with MODULE_NOT_FOUND while the raise claims it
221
+ # was "raised" — the failure the truth-telling above exists to prevent.
222
+ if [ ! -d "$HALL_DIR/node_modules" ]; then
223
+ echo "Óðrerir — fetching its dependencies…"
224
+ if ! (cd "$HALL_DIR" && npm install --no-audit --no-fund >/dev/null 2>&1); then
225
+ echo "Óðrerir — Live Hall NOT raised: its dependencies could not be installed" >&2
226
+ echo "help: (cd ${HALL_DIR#"$ROOT"/} && npm install)" >&2
227
+ HALL_DIR=""
228
+ fi
229
+ fi
230
+ fi
231
+ if [ -n "$HALL_DIR" ] && [ -d "$HALL_DIR" ]; then
206
232
  if [ -f "$HALL_PID_FILE" ] && kill -0 "$(cat "$HALL_PID_FILE")" 2>/dev/null; then
207
233
  echo "Óðrerir — Live Hall already running (pid $(cat "$HALL_PID_FILE")) → http://127.0.0.1:${HALL_PORT}/"
208
234
  else
209
- ymir_detach bash -c "cd '$HALL_DIR' && exec npm run dev -- --port '$HALL_PORT' --host" >"$HALL_LOG" 2>&1
235
+ # A packaged app ships a BUILD (odrerir's tarball carries dist/, and its dev
236
+ # command calls a script the tarball does not carry — so `npm run dev` could
237
+ # never work from a package). Serve the build where there is one; develop
238
+ # where there is not.
239
+ if [ -d "$HALL_DIR/dist" ]; then
240
+ ymir_detach bash -c "cd '$HALL_DIR' && exec npx --no-install astro preview --port '$HALL_PORT' --host" >"$HALL_LOG" 2>&1
241
+ else
242
+ ymir_detach bash -c "cd '$HALL_DIR' && exec npm run dev -- --port '$HALL_PORT' --host" >"$HALL_LOG" 2>&1
243
+ fi
210
244
  echo $! > "$HALL_PID_FILE"
211
- for _ in $(seq 1 30); do curl -s -o /dev/null "http://127.0.0.1:${HALL_PORT}/" && break; sleep 0.5; done
212
- echo "Óðrerir Live Hall raised (pid $(cat "$HALL_PID_FILE")) → http://127.0.0.1:${HALL_PORT}/"
245
+ _hall_up=0
246
+ for _ in $(seq 1 40); do
247
+ curl -s -o /dev/null "http://127.0.0.1:${HALL_PORT}/" && { _hall_up=1; break; }
248
+ kill -0 "$(cat "$HALL_PID_FILE")" 2>/dev/null || break
249
+ sleep 0.5
250
+ done
251
+ if [ "$_hall_up" = 1 ]; then
252
+ echo "Óðrerir — Live Hall raised (pid $(cat "$HALL_PID_FILE")) → http://127.0.0.1:${HALL_PORT}/"
253
+ else
254
+ echo "Óðrerir — Live Hall started (pid $(cat "$HALL_PID_FILE")) but :${HALL_PORT} did not answer — see $HALL_LOG" >&2
255
+ tail -5 "$HALL_LOG" >&2 2>/dev/null || true
256
+ fi
213
257
  fi
214
258
  else
215
259
  echo "Óðrerir — Live Hall skipped (the odrerir surface is not present)." >&2