@zerwiz/ymir 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/.agents/agents/huginn-researcher.md +1 -1
  2. package/.agents/agents/kvasir-scout.md +1 -1
  3. package/.agents/agents/sindri-developer.md +1 -1
  4. package/.agents/backend/README.md +74 -0
  5. package/.agents/backend/model-bridge.py +74 -4
  6. package/.agents/config/cron.yaml +2 -0
  7. package/.agents/config/eindri-dispatch.json +6 -6
  8. package/.agents/migrations/0003-private-data-separation.sh +11 -1
  9. package/.agents/migrations/0005-roots-out-of-tree.sh +158 -0
  10. package/.agents/skills/README.md +1 -1
  11. package/.agents/skills/galdr-ymirsystem/SKILL.md +2 -1
  12. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +73 -10
  13. package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +44 -1
  14. package/.agents/skills/galdr-ymirsystem/assets/installation.md +215 -23
  15. package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +3 -1
  16. package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +39 -1
  17. package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +4 -0
  18. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +7 -2
  19. package/.agents/skills/galdr-ymirsystem/assets/registry.md +1 -1
  20. package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +2 -2
  21. package/.agents/skills/galdr-ymirsystem/assets/smidja.md +16 -11
  22. package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +147 -4
  23. package/.agents/skills/groa-update/SKILL.md +20 -0
  24. package/.agents/skills/hamr-adapters/SKILL.md +2 -2
  25. package/.agents/skills/lifecycle/smoke_test.sh +7 -2
  26. package/.agents/skills/ymir-host/SKILL.md +2 -1
  27. package/.agents/skills/ymir-host/assets/install.md +69 -0
  28. package/AGENTS.md +78 -34
  29. package/CHANGELOG.md +1033 -1
  30. package/README.md +1 -1
  31. package/RULES/02-agents.md +2 -2
  32. package/RULES/04-hoard.md +32 -0
  33. package/RULES/06-append-only.md +50 -0
  34. package/RULES/07-config.md +56 -0
  35. package/RULES/09-electron.md +58 -0
  36. package/RULES/README.md +2 -0
  37. package/bin/a2a-mcp.sh +42 -7
  38. package/bin/agents-config.sh +30 -7
  39. package/bin/apodex-smoke-test.sh +14 -1
  40. package/bin/bifrost-bridge.sh +25 -6
  41. package/bin/bootstrap-macos.sh +6 -1
  42. package/bin/brokk-send.sh +14 -1
  43. package/bin/changelog-assemble.sh +154 -0
  44. package/bin/changelog-guard.sh +39 -7
  45. package/bin/crash-sense.sh +14 -1
  46. package/bin/design-check.sh +80 -0
  47. package/bin/design-icon.sh +153 -0
  48. package/bin/docs-guard.sh +2 -2
  49. package/bin/editor-place.sh +33 -15
  50. package/bin/eindri-acclaim.sh +14 -1
  51. package/bin/eindri-seat.sh +83 -0
  52. package/bin/eindri-seen.sh +14 -1
  53. package/bin/eindri-start.sh +14 -1
  54. package/bin/eir-doctor.sh +47 -1
  55. package/bin/feature-inventory.sh +73 -0
  56. package/bin/fleet-apply.sh +15 -2
  57. package/bin/github-deploy.sh +14 -1
  58. package/bin/gjallarhorn-expose.sh +43 -9
  59. package/bin/gjallarhorn-notify.sh +14 -1
  60. package/bin/gjallarhorn-purge.sh +14 -1
  61. package/bin/gjallarhorn-tunnel.sh +16 -3
  62. package/bin/groa-update.sh +9 -1
  63. package/bin/hall-snapshot.sh +21 -2
  64. package/bin/herdr-run.sh +23 -2
  65. package/bin/hlidskjalf-agents.sh +172 -0
  66. package/bin/hlidskjalf-usage.sh +196 -0
  67. package/bin/hoard-guard.sh +135 -0
  68. package/bin/hoard-lib.sh +91 -11
  69. package/bin/hodd.sh +28 -4
  70. package/bin/huginn-research-worker.sh +14 -1
  71. package/bin/local-model-lock.sh +22 -3
  72. package/bin/mimir-bridge.py +33 -0
  73. package/bin/mimir-bridge.sh +14 -1
  74. package/bin/mimir-reflect.sh +27 -0
  75. package/bin/mjollnir-webhook.sh +14 -1
  76. package/bin/mjollnir.sh +22 -3
  77. package/bin/model-hardware.sh +17 -3
  78. package/bin/model-resolve.sh +14 -1
  79. package/bin/nornir-job-hall-snapshot.sh +28 -0
  80. package/bin/nornir-job-nsr-compliance.sh +54 -0
  81. package/bin/npm-publish.sh +136 -0
  82. package/bin/omarchy-install.sh +18 -5
  83. package/bin/omarchy-plugins.sh +15 -2
  84. package/bin/omarchy-sense.sh +14 -1
  85. package/bin/pi-model-agnostic.sh +71 -0
  86. package/bin/private-guard.sh +20 -0
  87. package/bin/project-git.sh +18 -4
  88. package/bin/public-guard.sh +3 -2
  89. package/bin/sessrumnir-ensure.sh +1 -1
  90. package/bin/sessrumnir.sh +16 -3
  91. package/bin/smidja-bootstrap.sh +5 -1
  92. package/bin/syn-asset-pretool-check.sh +14 -1
  93. package/bin/syn-spawn-pretool-check.sh +58 -0
  94. package/bin/tailscale-sync.sh +15 -2
  95. package/bin/telegram-bot.sh +14 -1
  96. package/bin/ui-truth-check.sh +75 -0
  97. package/bin/valknut-load.sh +42 -4
  98. package/bin/wedge-notify.sh +14 -1
  99. package/bin/workspace-rag.sh +14 -1
  100. package/bin/ymir-install.sh +219 -54
  101. package/bin/ymir-plan.sh +356 -0
  102. package/bin/ymir-say.sh +14 -1
  103. package/bin/ymir-setup-auth.sh +12 -1
  104. package/bin/ymir-validate.sh +28 -8
  105. package/docs/lore.md +5 -1
  106. package/docs/runbooks/agents.md +1 -1
  107. package/package.json +13 -3
  108. package/scripts/electron.sh +74 -15
  109. package/scripts/lower.sh +34 -0
  110. package/scripts/raise.sh +42 -0
  111. package/scripts/start.sh +52 -2
  112. package/.agents/config/agents.yaml +0 -50
  113. package/.agents/config/tailscale-sync.yaml +0 -27
  114. package/.agents/memory/kaia.engram +0 -0
  115. package/.agents/memory/kaia.engram-shm +0 -0
  116. package/.agents/memory/kaia.engram-wal +0 -0
  117. package/.agents/memory/well/episodes.jsonl +0 -367
  118. package/.agents/memory/well/workspace.jsonl +0 -27
@@ -0,0 +1,356 @@
1
+ #!/usr/bin/env bash
2
+ # ymir-plan.sh — THE INSTALL PLAN. Probe this machine, print what would change,
3
+ # change nothing.
4
+ #
5
+ # The consent a real install asks for used to be a hardcoded paragraph, and a
6
+ # paragraph cannot know the host: it named an Omarchy version on a Mac, promised
7
+ # a workspace tree that already stood, and never mentioned that no application
8
+ # had been installed at all. This is the replacement: a plan COMPUTED from the
9
+ # same host the install will act on, one row per step, each row carrying its
10
+ # state and the reason for it.
11
+ #
12
+ # DO a change will be made
13
+ # SKIP already satisfied — nothing to do
14
+ # DO · (with a note) the step is available but conditional
15
+ # BLOCKED cannot run; the reason names what is missing
16
+ # CONSENT needs the operator's word before it may run
17
+ # INFO a fact about this host, discovered — no change implied
18
+ #
19
+ # Usage:
20
+ # bin/ymir-plan.sh # the plan (TOON)
21
+ # bin/ymir-plan.sh --json # the same, as JSON, for automation
22
+ # bin/ymir-plan.sh --phase 5 # one phase
23
+ # bin/ymir-plan.sh --blocked # only what cannot proceed, and why
24
+ #
25
+ # Exit: 0 the plan was printed (a BLOCKED row is a fact, not a failure), 1 error,
26
+ # 2 usage.
27
+ set -u
28
+
29
+ # --- portability shim: bin/ymir-platform.sh --------------------------------
30
+ if [ -z "${YMIR_PLATFORM_LOADED:-}" ]; then
31
+ _ymir_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
32
+ for _ymir_c in "$_ymir_dir/ymir-platform.sh" "$(dirname "$_ymir_dir")/bin/ymir-platform.sh"; do
33
+ [ -r "$_ymir_c" ] && { . "$_ymir_c"; YMIR_PLATFORM_LOADED=1; break; }
34
+ done
35
+ unset _ymir_dir _ymir_c
36
+ fi
37
+
38
+ VERSION="1.0.0"
39
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
40
+ ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
41
+ # Whether the home was named explicitly matters: a home that was chosen is not
42
+ # the same as one we defaulted to, and the plan says which.
43
+ YMIR_HOME_WAS_SET=0; [ -n "${YMIR_HOME:-}" ] && YMIR_HOME_WAS_SET=1
44
+ # shellcheck source=bin/hoard-lib.sh
45
+ . "$SCRIPT_DIR/hoard-lib.sh"
46
+ ymir_home_root YMIR_HOME
47
+ hoard_root HOARD
48
+
49
+ JSON=0; ONLY_PHASE=""; ONLY_BLOCKED=0
50
+ case "${1-}" in
51
+ -v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;;
52
+ -h|--help) sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
53
+ esac
54
+ while [ $# -gt 0 ]; do
55
+ case "$1" in
56
+ --json) JSON=1; shift ;;
57
+ --phase) ONLY_PHASE="${2-}"; shift 2 ;;
58
+ --blocked) ONLY_BLOCKED=1; shift ;;
59
+ *) printf 'error: unknown flag %s\nhelp: bin/ymir-plan.sh [--json] [--phase N] [--blocked]\n' "$1" >&2; exit 2 ;;
60
+ esac
61
+ done
62
+
63
+ declare -a P_N P_NAME P_STEP P_STATE P_WHY
64
+ emit() { P_N+=("$1"); P_NAME+=("$2"); P_STEP+=("$3"); P_STATE+=("$4"); P_WHY+=("$5"); }
65
+
66
+ have() { command -v "$1" >/dev/null 2>&1; }
67
+ port_up() { # <port>
68
+ if have curl; then curl -s -o /dev/null --max-time 1 "http://127.0.0.1:$1/" 2>/dev/null && return 0; fi
69
+ if have ss; then ss -ltn 2>/dev/null | grep -q ":$1 " && return 0; fi
70
+ return 1
71
+ }
72
+ # An app is ours by one of two shapes: a source checkout under apps/, or a
73
+ # published package under node_modules/@zerwiz/. Neither is guessed.
74
+ app_dir() { # <name> -> path, or empty
75
+ local n="$1" c
76
+ for c in "$ROOT/apps/$n" "$ROOT/node_modules/@zerwiz/$n"; do
77
+ [ -d "$c" ] && { printf '%s' "$c"; return 0; }
78
+ done
79
+ return 1
80
+ }
81
+
82
+ # ── phase 0 · resolve — where this runs, and against what ────────────────────
83
+ resolve_phase() {
84
+ local os="" desk="terminal"
85
+ os="$(ymir_os 2>/dev/null || printf unknown)"
86
+ [ -n "${WAYLAND_DISPLAY:-}" ] && desk="wayland"
87
+ [ -n "${DISPLAY:-}" ] && desk="x11"
88
+ [ -d /usr/share/omarchy ] && desk="$desk · omarchy first-class"
89
+ case "$os" in
90
+ wsl) desk="$desk · wsl2" ;;
91
+ msys) desk="$desk · msys (best-effort)" ;;
92
+ esac
93
+ emit 0 resolve os INFO "$os · $desk"
94
+
95
+ local eng=""
96
+ if command -v ymir_container_engine >/dev/null 2>&1; then eng="$(ymir_container_engine_name 2>/dev/null || true)"; fi
97
+ [ -z "$eng" ] && have docker && eng=docker
98
+ [ -z "$eng" ] && have podman && eng=podman
99
+ if [ -n "$eng" ]; then emit 0 resolve engine INFO "$eng"
100
+ else emit 0 resolve engine BLOCKED "no docker or podman on PATH — the Utgard image cannot be built"; fi
101
+
102
+ case "$ROOT" in
103
+ */node_modules/*) emit 0 resolve code INFO "$ROOT — a package install (read-only at runtime)" ;;
104
+ *) emit 0 resolve code INFO "$ROOT — a source checkout" ;;
105
+ esac
106
+
107
+ case "$HOARD" in
108
+ "$ROOT"/*) emit 0 resolve home BLOCKED "the home ($HOARD) sits inside the code tree — private data would live in the package" ;;
109
+ *) emit 0 resolve home INFO "$HOARD (the home = $YMIR_HOME)" ;;
110
+ esac
111
+
112
+ # The home is the OPERATOR's to choose. Until they have, it is only a default,
113
+ # and the install will ask — so the plan says so rather than pretending.
114
+ local rec=""
115
+ ymir_home_record rec
116
+ if [ -n "$rec" ]; then
117
+ emit 0 resolve chosen SKIP "the home was chosen at installation: $rec"
118
+ elif [ "$YMIR_HOME_WAS_SET" = 1 ]; then
119
+ emit 0 resolve chosen SKIP "the home is set by \$YMIR_HOME: $YMIR_HOME"
120
+ else
121
+ emit 0 resolve chosen CONSENT "no home chosen yet — the install asks, then records it (default $YMIR_HOME_DEFAULT)"
122
+ fi
123
+ }
124
+
125
+ # ── phase 1 · code — the tree itself, and its purity ────────────────────────
126
+ code_phase() {
127
+ local n=0
128
+ [ -x "$ROOT/bin/ymir-install.sh" ] && n=$(find "$ROOT/bin" -maxdepth 1 -name '*.sh' 2>/dev/null | wc -l)
129
+ if [ "$n" -gt 0 ] && [ -d "$ROOT/.agents/skills" ] && [ -d "$ROOT/RULES" ]; then
130
+ emit 1 code integrity SKIP "tree intact — $n scripts, .agents/skills, RULES/"
131
+ else
132
+ emit 1 code integrity BLOCKED "bin/, .agents/skills or RULES/ is missing — reinstall the package or re-clone"
133
+ fi
134
+
135
+ # The law this row enforces: the package is the code that RUNS the programs.
136
+ # Everything the operator owns — their records, their state, their settings,
137
+ # their credentials — lives in the home they chose. A packaged install replaces
138
+ # its tree on upgrade, so anything of theirs kept here is kept at its peril.
139
+ # A settings file GIT TRACKS is not theirs: that is the distro's shipped
140
+ # default, and it stays where the code ships it.
141
+ local leaked="" f n
142
+ n="$(ls -A "$ROOT/data" 2>/dev/null | grep -vc '^\.gitkeep$' || true)"
143
+ [ -d "$ROOT/data" ] && [ "${n:-0}" -gt 0 ] && leaked="$leaked data/"
144
+ n="$(ls -A "$ROOT/state" 2>/dev/null | grep -vc '^\.gitkeep$' || true)"
145
+ [ -d "$ROOT/state" ] && [ "${n:-0}" -gt 0 ] && leaked="$leaked state/"
146
+ for f in config/agents.yaml config/cron.yaml config/tailscale-sync.yaml config/wedge-alarm .env.local; do
147
+ [ -e "$ROOT/$f" ] || continue
148
+ # `config` is a symlink into .agents/config, and git tracks the REAL path —
149
+ # ask it about the name the index holds, not the link the tree shows.
150
+ local ask="$f"
151
+ case "$f" in config/*) [ -L "$ROOT/config" ] && ask=".agents/config/${f#config/}" ;; esac
152
+ if command -v git >/dev/null 2>&1 && git -C "$ROOT" ls-files --error-unmatch "$ask" >/dev/null 2>&1; then
153
+ continue # the distro ships this one — code, not the operator's own
154
+ fi
155
+ leaked="$leaked $f"
156
+ done
157
+ if [ -n "$leaked" ]; then
158
+ emit 1 code purity DO "the operator's own things sit in the code tree ($leaked) — they belong in $YMIR_HOME, or the next upgrade will erase them"
159
+ else
160
+ emit 1 code purity SKIP "nothing of the operator's is written into the code tree"
161
+ fi
162
+ }
163
+
164
+ # ── phase 2 · home — the operator's world, outside the tree ─────────────────
165
+ home_phase() {
166
+ if [ -d "$HOARD/identity" ] && [ -d "$HOARD/secrets" ]; then
167
+ emit 2 home hoard SKIP "hodd/{identity,secrets,memory} present"
168
+ else
169
+ emit 2 home hoard DO "create hodd/{identity,secrets,docs,tenants,memory} — all private data lives here"
170
+ fi
171
+ if [ -d "$YMIR_HOME/workspaces" ]; then
172
+ emit 2 home workspaces SKIP "$YMIR_HOME/workspaces present"
173
+ else
174
+ emit 2 home workspaces DO "create workspaces/{work,personal} and the registries"
175
+ fi
176
+ # Settings are the operator's: they live in the home, never in the package.
177
+ local settings; hoard_settings_dir settings
178
+ if [ -e "$settings/agents.yaml" ]; then
179
+ emit 2 home agents-config SKIP "the agent set is seeded ($settings/agents.yaml)"
180
+ else
181
+ emit 2 home agents-config DO "seed the private agent set into $settings/agents.yaml (harness + model per agent)"
182
+ fi
183
+ }
184
+
185
+ # ── phase 3 · runtimes — every one proven by a probe, never assumed ─────────
186
+ runtimes_phase() {
187
+ local missing=""
188
+ for c in git python3; do have "$c" || missing="$missing $c"; done
189
+ if [ -n "$missing" ]; then
190
+ emit 3 runtimes system BLOCKED "missing$missing — a system package (apt/dnf/brew), not user space"
191
+ else
192
+ emit 3 runtimes system SKIP "git and python3 present"
193
+ fi
194
+
195
+ local user=""
196
+ for c in bun uv; do have "$c" || user="$user $c"; done
197
+ if [ -n "$user" ]; then emit 3 runtimes userspace DO "install in user space (no sudo):$user"
198
+ else emit 3 runtimes userspace SKIP "bun and uv present"; fi
199
+
200
+ if have mcp; then emit 3 runtimes mcp SKIP "mcp<2> present"; else emit 3 runtimes mcp DO "install mcp<2> via pip"; fi
201
+ if have pi; then emit 3 runtimes pi SKIP "the Pi harness present"; else emit 3 runtimes pi DO "install the Pi harness"; fi
202
+ if have hermes; then emit 3 runtimes hermes SKIP "the Hermes worker runtime present"; else emit 3 runtimes hermes DO "install the Hermes worker runtime"; fi
203
+
204
+ if have herdr; then emit 3 runtimes backend SKIP "herdr — Þjazi (the preferred terminal backend)"
205
+ elif have tmux; then emit 3 runtimes backend SKIP "tmux — the accepted reference backend"
206
+ else emit 3 runtimes backend DO "install herdr (Þjazi), else tmux — without a backend no Eindri can be seated"; fi
207
+ }
208
+
209
+ # ── phase 4 · engines — the OSS anvils Ymir wraps ───────────────────────────
210
+ engine_row() { # <bin> <label>
211
+ if have "$1"; then emit 4 engines "$1" SKIP "$2 present"
212
+ else emit 4 engines "$1" DO "install $2 ($1)"; fi
213
+ }
214
+ engines_phase() {
215
+ engine_row treehouse "Yggdrasil (the worktree engine)"
216
+ engine_row no-mistakes "the clean-PR gate (Mjollnir · Glitnir)"
217
+ if have sandcastle; then emit 4 engines sandcastle SKIP "Utgard present"
218
+ else emit 4 engines sandcastle DO "install sandcastle (Utgard) if available"; fi
219
+
220
+ if have docker || have podman; then
221
+ local img
222
+ if have docker && docker image inspect utgard-runner:latest >/dev/null 2>&1; then
223
+ emit 4 engines utgard-image SKIP "utgard-runner:latest present"
224
+ elif have podman && podman image exists utgard-runner:latest >/dev/null 2>&1; then
225
+ emit 4 engines utgard-image SKIP "utgard-runner:latest present (podman)"
226
+ else
227
+ emit 4 engines utgard-image DO "build the sandbox image utgard-runner:latest"
228
+ fi
229
+ else
230
+ emit 4 engines utgard-image BLOCKED "no container engine — the sandbox cannot be built"
231
+ fi
232
+ }
233
+
234
+ # ── phase 5 · apps — the surfaces the operator actually sees ────────────────
235
+ # Four surfaces, one shape each: web build required, Electron shell gated.
236
+ # The apps are their own packages (@zerwiz/<app>) — the distro depends on them.
237
+ app_row() { # <name> <about>
238
+ local name="$1" about="$2" dir
239
+ if dir="$(app_dir "$name")"; then
240
+ if [ -d "$dir/dist" ] || [ -d "$dir/out" ]; then
241
+ emit 5 apps "$name" SKIP "$about — installed, and its build shipped with it"
242
+ else
243
+ emit 5 apps "$name" DO "$about — installed from source; the web build is still to make"
244
+ fi
245
+ elif grep -q "\"@zerwiz/$name\"" "$ROOT/package.json" 2>/dev/null; then
246
+ # Declared as a dependency of the distro but not present: the package exists,
247
+ # the tree simply has not fetched it. That is a DO, not a dead end.
248
+ emit 5 apps "$name" DO "$about — declared as a dependency but not fetched (npm i -g @zerwiz/ymir fetches it)"
249
+ else
250
+ emit 5 apps "$name" BLOCKED "$about — no apps/$name, no @zerwiz/$name package, and the distro does not depend on it"
251
+ fi
252
+ }
253
+ apps_phase() {
254
+ app_row hlidskjalf "the control plane (the high seat), served on :3888"
255
+ app_row odrerir "the live hall (chat + council)"
256
+ app_row sessrumnir "the seat-hall desktop"
257
+ app_row smidja "the smithy and its visualizer (:8437)"
258
+
259
+ local shells=0 d
260
+ for d in hlidskjalf odrerir sessrumnir; do
261
+ dir="$(app_dir "$d" 2>/dev/null || true)"
262
+ if [ -n "$dir" ] && [ -d "$dir/node_modules/electron/dist" ]; then shells=$((shells+1)); fi
263
+ done
264
+ if [ "$shells" -ge 3 ]; then
265
+ emit 5 apps electron SKIP "the three desktop shells are built (electron runtime present)"
266
+ elif [ "${YMIR_NO_DESKTOP:-0}" = 1 ]; then
267
+ emit 5 apps electron SKIP "declined (--no-desktop) — the web surfaces stand without the shells"
268
+ else
269
+ emit 5 apps electron CONSENT "build the desktop shells? each pulls a ~100 MB Electron runtime — the web surfaces need none of it"
270
+ fi
271
+ }
272
+
273
+ # ── phase 6 · wire — the way in, and the agent set ──────────────────────────
274
+ wire_phase() {
275
+ local door="none"
276
+ if [ -x "$ROOT/bin/ymir-setup-auth.sh" ]; then
277
+ door="$(bash "$ROOT/bin/ymir-setup-auth.sh" status 2>/dev/null | sed -n '2p' | cut -d, -f1 | tr -d ' "')"
278
+ fi
279
+ if [ -n "$door" ] && [ "$door" != none ]; then
280
+ emit 6 wire auth SKIP "an operator credential is set ($door)"
281
+ else
282
+ emit 6 wire auth CONSENT "no credential — a password or GitHub sign-in; without it the gate has no door"
283
+ fi
284
+
285
+ local codes=""
286
+ [ -x "$ROOT/bin/ymir-invite.sh" ] && codes="$(bash "$ROOT/bin/ymir-invite.sh" list 2>/dev/null | grep -c '^ "' || true)"
287
+ if [ "${codes:-0}" -gt 0 ]; then emit 6 wire invite SKIP "a live invite code exists"
288
+ else emit 6 wire invite CONSENT "mint an invite code — how anyone else is let in (registration stays closed without one)"; fi
289
+
290
+ if [ -d /usr/share/omarchy ]; then
291
+ emit 6 wire launchers DO "place each app on its own numbered desktop + write the launcher entries"
292
+ else
293
+ emit 6 wire launchers SKIP "not an Omarchy host — the Omarchy layer skips cleanly (Rule 05)"
294
+ fi
295
+ }
296
+
297
+ # ── phase 7 · raise — the services, then what the operator sees ─────────────
298
+ raise_phase() {
299
+ local down="" p
300
+ for p in 3888:Hlidskjalf 3889:gate-API 4603:Bifrost 8437:Smidja-visualizer; do
301
+ port_up "${p%%:*}" || down="$down ${p#*:}"
302
+ done
303
+ if [ -n "$down" ]; then emit 7 raise services DO "raise:$down"
304
+ else emit 7 raise services SKIP "every service is listening (SPA · gate API · Bifrost · visualizer)"; fi
305
+ if have hermes; then emit 7 raise well INFO "the memory well (engram) is provisioned by its own step"; fi
306
+ emit 7 raise desktop CONSENT "open the desktop apps so they are seen, not merely installed"
307
+ }
308
+
309
+ # ── phase 8 · verify — what stands, honestly ────────────────────────────────
310
+ verify_phase() {
311
+ if [ -x "$SCRIPT_DIR/ymir-validate.sh" ]; then
312
+ emit 8 verify validate DO "observe the result: ports, stores, processes, the sandbox image"
313
+ else
314
+ emit 8 verify validate BLOCKED "bin/ymir-validate.sh is absent — the install could not be proven"
315
+ fi
316
+ }
317
+
318
+ resolve_phase; code_phase; home_phase; runtimes_phase; engines_phase; apps_phase; wire_phase; raise_phase; verify_phase
319
+
320
+ # ── output ──────────────────────────────────────────────────────────────────
321
+ if [ "$JSON" = 1 ]; then
322
+ printf '['
323
+ local_i=0
324
+ for i in "${!P_STEP[@]}"; do
325
+ [ -n "$ONLY_PHASE" ] && [ "${P_N[$i]}" != "$ONLY_PHASE" ] && continue
326
+ [ "$ONLY_BLOCKED" = 1 ] && [ "${P_STATE[$i]}" != BLOCKED ] && continue
327
+ [ "$local_i" -gt 0 ] && printf ','
328
+ printf '\n {"phase":%s,"name":"%s","step":"%s","state":"%s","why":"%s"}' \
329
+ "${P_N[$i]}" "${P_NAME[$i]}" "${P_STEP[$i]}" "${P_STATE[$i]}" "${P_WHY[$i]}"
330
+ local_i=$((local_i+1))
331
+ done
332
+ [ "$local_i" -gt 0 ] && printf '\n'
333
+ printf ']\n'
334
+ exit 0
335
+ fi
336
+
337
+ shown=0
338
+ for i in "${!P_STEP[@]}"; do
339
+ [ -n "$ONLY_PHASE" ] && [ "${P_N[$i]}" != "$ONLY_PHASE" ] && continue
340
+ [ "$ONLY_BLOCKED" = 1 ] && [ "${P_STATE[$i]}" != BLOCKED ] && continue
341
+ shown=$((shown+1))
342
+ done
343
+
344
+ if [ "$shown" -eq 0 ]; then
345
+ if [ "$ONLY_BLOCKED" = 1 ]; then printf 'plan: 0 blocked steps — nothing is holding the install back\n'
346
+ else printf 'plan: 0 rows for phase %s\n' "$ONLY_PHASE"; fi
347
+ exit 0
348
+ fi
349
+
350
+ printf 'plan[%d]{phase,name,step,state,why}:\n' "$shown"
351
+ for i in "${!P_STEP[@]}"; do
352
+ [ -n "$ONLY_PHASE" ] && [ "${P_N[$i]}" != "$ONLY_PHASE" ] && continue
353
+ [ "$ONLY_BLOCKED" = 1 ] && [ "${P_STATE[$i]}" != BLOCKED ] && continue
354
+ printf ' "%s","%s","%s","%s","%s"\n' "${P_N[$i]}" "${P_NAME[$i]}" "${P_STEP[$i]}" "${P_STATE[$i]}" "${P_WHY[$i]}"
355
+ done
356
+ exit 0
package/bin/ymir-say.sh CHANGED
@@ -24,7 +24,20 @@ set -u
24
24
  VERSION="1.0.0"
25
25
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
26
26
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
27
- STATE_DIR="${BROKK_STATE_OVERRIDE:-$ROOT/state}"
27
+
28
+ # The roots that live OUTSIDE the code tree: this machine's records and the
29
+ # runtime state belong to the home the operator chose at installation, never in
30
+ # the tree — a packaged install replaces its tree on upgrade (Rule 04).
31
+ if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
32
+ _yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
33
+ for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
34
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
35
+ done
36
+ unset _yr _yc
37
+ fi
38
+ hoard_state_dir YMIR_STATE_DIR
39
+ hoard_data_dir YMIR_DATA_DIR
40
+ STATE_DIR="${BROKK_STATE_OVERRIDE:-$YMIR_STATE_DIR}"
28
41
  LOG="$STATE_DIR/ymir-said.log"
29
42
 
30
43
  case "${1-}" in
@@ -18,7 +18,18 @@ set -u
18
18
  VERSION="1.0.0"
19
19
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
20
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
21
- ENV_FILE="${YMIR_ENV_FILE:-$ROOT/.env.local}"
21
+
22
+ # The operator's credentials are theirs and live in the HOME they chose — never
23
+ # in the code tree, which a packaged install replaces on upgrade (Rule 04).
24
+ if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
25
+ _yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
26
+ for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
27
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
28
+ done
29
+ unset _yr _yc
30
+ fi
31
+ hoard_local_env _YMIR_ENV_DEFAULT
32
+ ENV_FILE="${YMIR_ENV_FILE:-$_YMIR_ENV_DEFAULT}"
22
33
 
23
34
  usage() { sed -n '2,17p' "$0" | sed 's/^# \{0,1\}//'; }
24
35
  die() { printf 'error: %s\n' "$1" >&2; exit 1; }
@@ -99,21 +99,35 @@ else
99
99
  fi
100
100
 
101
101
  # ── 7. smidja db (visualizer readiness) ─────────────────────────────────────
102
+ # The same pair scripts/start.sh and bin/smidja-bootstrap.sh resolve: an existing
103
+ # $YMIR_HOME/smidja/smidja.db first, then an in-repo copy.
102
104
  YMIR_HOME="${YMIR_HOME:-$HOME/Documents/Ymir}"
103
- SMIDJA_DB_PATH="${YMIR_HOME:+$YMIR_HOME/smidja/smidja.db}"
104
- SMIDJA_DB_PATH="${SMIDJA_DB_PATH:-$ROOT/apps/smidja/smidja_data/smidja.db}"
105
+ SMIDJA_DB_PATH="${SMIDJA_DB:-}"
106
+ if [ -z "$SMIDJA_DB_PATH" ]; then
107
+ for c in "$YMIR_HOME/smidja/smidja.db" "$ROOT/apps/smidja/smidja_data/smidja.db"; do
108
+ [ -f "$c" ] && { SMIDJA_DB_PATH="$c"; break; }
109
+ done
110
+ SMIDJA_DB_PATH="${SMIDJA_DB_PATH:-$YMIR_HOME/smidja/smidja.db}"
111
+ fi
105
112
  if [ -f "$SMIDJA_DB_PATH" ]; then
106
113
  add smidja-db PASS "smidja.db present"
107
114
  else
108
115
  add smidja-db FAIL "smidja.db missing — run bin/smidja-bootstrap.sh"
109
116
  fi
110
117
 
111
- # ── 7b. visualizer UI (the API serves it from ./dist) ───────────────────
118
+ # ── 7b. visualizer (built ./dist AND its API actually listening) ────────────
119
+ # A PASS must mean the thing is UP. Probe the port, not just the build: a
120
+ # built-but-dead visualizer (a bad CMD_DB, a crashed API) is a FAIL, not green.
112
121
  VIZ="$ROOT/.agents/skills/smidja-factory/apps/visualizer"
113
- if [ -d "$VIZ/dist" ]; then
114
- add visualizer PASS "UI built and served on :8437"
115
- elif [ -d "$VIZ" ]; then
122
+ VIZ_PORT="${SMIDJA_VIZ_API_PORT:-8437}"
123
+ if [ ! -d "$VIZ" ]; then
124
+ add visualizer SKIP "no visualizer tree at $VIZ"
125
+ elif [ ! -d "$VIZ/dist" ]; then
116
126
  add visualizer FAIL "UI not built (./dist missing) — (cd $VIZ && bun run build)"
127
+ elif port_open "$VIZ_PORT"; then
128
+ add visualizer PASS "UI built and served on :$VIZ_PORT"
129
+ else
130
+ add visualizer FAIL "UI built but no API listening on :$VIZ_PORT — run scripts/start.sh"
117
131
  fi
118
132
 
119
133
  # ── 8. desktop apps (both Electron windows) ─────────────────────────────────
@@ -133,8 +147,14 @@ if port_open 4602; then add memory PASS "Mimir bridge up on :4602"
133
147
  else add memory WARN "well off (:4602) — needs the engram engine; platform runs without it"; fi
134
148
 
135
149
  # ── 10. audit ledger intact ─────────────────────────────────────────────────
136
- RUNES_LEDGER="${YMIR_HOME:+$YMIR_HOME/memory/runes_audit.md}"
137
- RUNES_LEDGER="${RUNES_LEDGER:-$ROOT/workspace/memory/runes_audit.md}"
150
+ RUNES_LEDGER=""
151
+ # The ledger follows the hoard (Rule 06): 0004-hoard-and-realms moved it under
152
+ # hodd/memory/. Try the hoard first, then the pre-move locations.
153
+ for c in "${YMIR_HOME:+${YMIR_HOARD:-$YMIR_HOME/hodd}/memory/runes_audit.md}" \
154
+ "${YMIR_HOME:+$YMIR_HOME/memory/runes_audit.md}" \
155
+ "$ROOT/workspace/memory/runes_audit.md"; do
156
+ [ -n "$c" ] && [ -r "$c" ] && { RUNES_LEDGER="$c"; break; }
157
+ done
138
158
  if [ -r "$RUNES_LEDGER" ]; then
139
159
  add runes PASS "audit ledger readable ($(wc -l <"$RUNES_LEDGER" | tr -d ' ') lines)"
140
160
  else
package/docs/lore.md CHANGED
@@ -818,9 +818,13 @@ what belongs to all houses, nothing to one.
818
818
  | Path | What it holds |
819
819
  |------|---------------|
820
820
  | `midgard/design-system/` | Theme tokens, icons, rune glyphs, component rules |
821
- | `midgard/company_wiki/` | Cross-tenant company knowledge |
822
821
  | `midgard/github_org_repos/` | GitHub organization repo inventory |
823
822
  | `midgard/shared-packages/` | Shared npm/Python packages |
823
+
824
+ Midgard holds **public-by-design** cross-tenant assets only. Company knowledge —
825
+ policies, vision, specs, client names — is **tenant data** and lives at
826
+ `$YMIR_HOME/hodd/identity/companies/`. A company wiki here would publish that
827
+ company's internals in a public repo.
824
828
  | `midgard/infrastructure/` | DB schemas, Caddyfile, cloudflared, oauth2-proxy |
825
829
 
826
830
  **Rule:** Midgard is shared. No realm may mutate another realm's Midgard assets.
@@ -6,7 +6,7 @@ symlinked into each harness.
6
6
  ## Where agents live
7
7
 
8
8
  - **Canonical:** `.agents/agents/<name>-<craft>.md` (edit these only).
9
- - **Bindings:** `.opencode/agent/*.md` and `.pi/agents/*.md` are symlinks.
9
+ - **Bindings:** `.opencode/agents/*.md` and `.pi/agents/*.md` are symlinks.
10
10
 
11
11
  Rebind after any change:
12
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerwiz/ymir",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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",
@@ -29,7 +29,6 @@
29
29
  "bin/",
30
30
  ".agents/",
31
31
  "scripts/",
32
- "config/",
33
32
  "RULES/",
34
33
  "docs/",
35
34
  "packaging/",
@@ -38,7 +37,12 @@
38
37
  "CHANGELOG.md",
39
38
  "LICENSE",
40
39
  "NOTICE",
41
- "!**/node_modules/**"
40
+ "!**/node_modules/**",
41
+ "!.agents/memory/kaia.engram*",
42
+ "!.agents/memory/well/*.jsonl",
43
+ "!.agents/memory/*.db",
44
+ "!**/__pycache__/",
45
+ "!**/*.pyc"
42
46
  ],
43
47
  "engines": {
44
48
  "node": ">=20"
@@ -53,5 +57,11 @@
53
57
  "scripts": {
54
58
  "test": "bash .agents/skills/galdr-ymirsystem/scripts/compliance-check.sh",
55
59
  "compliance": "bash .agents/skills/galdr-ymirsystem/scripts/compliance-check.sh"
60
+ },
61
+ "optionalDependencies": {
62
+ "@zerwiz/hlidskjalf": "^0.1.0",
63
+ "@zerwiz/odrerir": "^0.1.0",
64
+ "@zerwiz/sessrumnir": "^0.1.8-alpha",
65
+ "@zerwiz/smidja-factory": "^0.1.0"
56
66
  }
57
67
  }
@@ -26,6 +26,19 @@ fi
26
26
  VERSION="1.1.0"
27
27
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28
28
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
29
+
30
+ # The roots that live OUTSIDE the code tree: this machine's records and the
31
+ # runtime state belong to the home the operator chose at installation, never in
32
+ # the tree — a packaged install replaces its tree on upgrade (Rule 04).
33
+ if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
34
+ _yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
35
+ for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
36
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
37
+ done
38
+ unset _yr _yc
39
+ fi
40
+ hoard_state_dir YMIR_STATE_DIR
41
+ hoard_data_dir YMIR_DATA_DIR
29
42
  APP="$ROOT/apps/hlidskjalf"
30
43
  Odrerir_app="$ROOT/apps/odrerir"
31
44
  NO_INSTALL=0
@@ -85,6 +98,23 @@ pid_of() {
85
98
  printf '%s' "$(printf '%s\n' "$pids" | head -1)"
86
99
  }
87
100
 
101
+ stop_view() { # stop ONE view's pids - and only that view's
102
+ local v="$1" pids
103
+ pids="$(view_pids "$v")"
104
+ if [ -z "$pids" ]; then
105
+ printf 'electron: %s already stopped\n' "$v"
106
+ rm -f "$(pid_file "$v")"
107
+ return 0
108
+ fi
109
+ local list; list="$(printf '%s' "$pids" | tr '\n' ',')"
110
+ printf '%s\n' "$pids" | xargs -r kill 2>/dev/null || true
111
+ sleep 0.5
112
+ local left; left="$(view_pids "$v")"
113
+ [ -z "$left" ] || printf '%s\n' "$left" | xargs -r kill -KILL 2>/dev/null || true
114
+ rm -f "$(pid_file "$v")"
115
+ printf 'electron: stopped %s pid(s)=%s\n' "$v" "${list%,}"
116
+ }
117
+
88
118
  case "$ACTION" in
89
119
  status)
90
120
  printf 'electron[%s]{view,state,pid}:\n' "${#VIEWS[@]}"
@@ -93,15 +123,13 @@ case "$ACTION" in
93
123
  done
94
124
  exit 0 ;;
95
125
  stop)
96
- for v in "${VIEWS[@]}"; do
97
- local pids; pids="$(view_pids "$v")"
98
- if [ -n "$pids" ]; then
99
- # Kill every live pid for this view (the shim era could leave several).
100
- printf '%s\n' "$pids" | while IFS= read -r p; do [ -n "$p" ] && kill "$p" 2>/dev/null || true; done
101
- printf 'electron: stopped %s pid(s)=%s\n' "$v" "$(printf '%s' "$pids" | tr '\n' ',' | sed 's/,$//')"
102
- fi
103
- rm -f "$(pid_file "$v")"
104
- done
126
+ # ONLY the view asked for. Walking every view here is why `stop --view odrerir`
127
+ # used to take the smithy's window down with it.
128
+ if [ "$VIEW" = both ]; then
129
+ for v in "${VIEWS[@]}"; do stop_view "$v"; done
130
+ else
131
+ stop_view "$VIEW"
132
+ fi
105
133
  printf 'electron: stopped\n'
106
134
  exit 0 ;;
107
135
  start|hlidskjalf|smidja|odrerir) [ "$ACTION" = smidja ] && VIEW=smidja; [ "$ACTION" = odrerir ] && VIEW=odrerir ;;
@@ -109,7 +137,7 @@ case "$ACTION" in
109
137
  esac
110
138
 
111
139
  command -v npm >/dev/null 2>&1 || { printf 'error: npm not found\nhelp: install node/npm to run the desktop shell\n' >&2; exit 1; }
112
- mkdir -p "$ROOT/state"
140
+ mkdir -p "$YMIR_STATE_DIR"
113
141
 
114
142
  if [ ! -x "$APP/node_modules/.bin/electron" ]; then
115
143
  if [ "$NO_INSTALL" = 1 ]; then
@@ -151,14 +179,39 @@ fi
151
179
  app_dir() { case "$1" in odrerir) printf '%s' "$Odrerir_app" ;; *) printf '%s' "$APP" ;; esac; }
152
180
  view_host() { case "$1" in smidja) printf '%s' 'http://127.0.0.1:8437/' ;; odrerir) printf '%s' 'http://127.0.0.1:4322/' ;; *) printf '%s' 'http://127.0.0.1:3888/' ;; esac; }
153
181
 
182
+ service_up() { curl -s -o /dev/null --max-time 3 "$1" 2>/dev/null; }
183
+ wait_for_service() {
184
+ local u="$1" i
185
+ for i in $(seq 1 120); do service_up "$u" && return 0; sleep 0.5; done
186
+ return 1
187
+ }
188
+ # Does the compositor hold a window of this class at all? A process with no window
189
+ # cannot be focused, and focusing nothing is exactly the silence a click must not get.
190
+ window_present() {
191
+ command -v hyprctl >/dev/null 2>&1 || return 0 # no compositor to ask: assume yes
192
+ hyprctl clients 2>/dev/null | grep -qi "$1"
193
+ }
194
+
154
195
  start_one() {
155
196
  local v="$1" f l
156
197
  f="$(pid_file "$v")"; l="$(log_file "$v")"
198
+ # THE SYSTEM FIRST. A shell whose backing service is down draws an empty window,
199
+ # and a window that never mapped cannot be focused. Clicking an icon must raise
200
+ # THE SYSTEM, so the service is converged before the window is judged.
201
+ local host; host="$(view_host "$v")"
202
+ if ! service_up "$host"; then
203
+ printf 'electron: %s has no service at %s — raising the system\n' "$v" "$host" >&2
204
+ ( nohup bash "$ROOT/scripts/start.sh" >/dev/null 2>&1 </dev/null & )
205
+ if ! wait_for_service "$host"; then
206
+ printf 'electron: %s still does not answer at %s — see $YMIR_STATE_DIR/\n' "$v" "$host" >&2
207
+ fi
208
+ # A process that lived through the outage is windowless; it must be reborn.
209
+ if is_running "$v"; then stop_view "$v"; fi
210
+ fi
157
211
  if is_running "$v"; then
158
- # A speed-start must RAISE the app, not report that it is already up. The
212
+ # A bound key must RAISE the app, never report that it is already up. The
159
213
  # window lives on its own numbered desktop by design, so "nothing happens"
160
- # is exactly what a bound key must not do. focuswindow also switches to that
161
- # desktop when the window is elsewhere.
214
+ # is exactly what a click must not do. focuswindow also switches desktops.
162
215
  local cls state=already-up
163
216
  cls="$(view_mark "$v")" # the window class, from the one place that defines it
164
217
  if command -v hyprctl >/dev/null 2>&1; then
@@ -167,8 +220,14 @@ start_one() {
167
220
  # string form is a syntax error there. This is the working idiom.
168
221
  hyprctl dispatch "hl.dsp.focus({window=\"class:^${cls}\$\"})" >/dev/null 2>&1 && state=raised
169
222
  fi
170
- printf 'electron[1]{view,state,pid}:\n "%s","%s",%s\n' "$v" "$state" "$(pid_of "$v")"
171
- return 0
223
+ # A process whose window is gone cannot be focused — reborn, not silent.
224
+ if [ "$state" = already-up ] && ! window_present "$cls"; then
225
+ printf 'electron: %s is running without a window — restarting it\n' "$v" >&2
226
+ stop_view "$v"
227
+ else
228
+ printf 'electron[1]{view,state,pid}:\n "%s","%s",%s\n' "$v" "$state" "$(pid_of "$v")"
229
+ return 0
230
+ fi
172
231
  fi
173
232
  # Launch the REAL Electron binary, not the .bin node shim, so the recorded pid
174
233
  # is the app itself (the shim spawns and would leave a stale/incorrect pid).