@zerwiz/ymir 0.1.5 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/.agents/agents/huginn-researcher.md +1 -1
  2. package/.agents/agents/kvasir-scout.md +1 -1
  3. package/.agents/agents/sindri-developer.md +1 -1
  4. package/.agents/backend/README.md +74 -0
  5. package/.agents/backend/model-bridge.py +74 -4
  6. package/.agents/config/cron.yaml +2 -0
  7. package/.agents/config/eindri-dispatch.json +6 -6
  8. package/.agents/migrations/0003-private-data-separation.sh +11 -1
  9. package/.agents/migrations/0005-roots-out-of-tree.sh +158 -0
  10. package/.agents/skills/README.md +1 -1
  11. package/.agents/skills/galdr-ymirsystem/SKILL.md +2 -1
  12. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +73 -10
  13. package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +44 -1
  14. package/.agents/skills/galdr-ymirsystem/assets/installation.md +290 -24
  15. package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +3 -1
  16. package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +39 -1
  17. package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +4 -0
  18. package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +7 -2
  19. package/.agents/skills/galdr-ymirsystem/assets/registry.md +1 -1
  20. package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +2 -2
  21. package/.agents/skills/galdr-ymirsystem/assets/smidja.md +16 -11
  22. package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +147 -4
  23. package/.agents/skills/groa-update/SKILL.md +20 -0
  24. package/.agents/skills/hamr-adapters/SKILL.md +2 -2
  25. package/.agents/skills/lifecycle/smoke_test.sh +7 -2
  26. package/.agents/skills/ymir-host/SKILL.md +2 -1
  27. package/.agents/skills/ymir-host/assets/install.md +69 -0
  28. package/AGENTS.md +78 -34
  29. package/CHANGELOG.md +1362 -1
  30. package/README.md +1 -1
  31. package/RULES/02-agents.md +2 -2
  32. package/RULES/04-hoard.md +32 -0
  33. package/RULES/06-append-only.md +50 -0
  34. package/RULES/07-config.md +56 -0
  35. package/RULES/09-electron.md +58 -0
  36. package/RULES/README.md +2 -0
  37. package/bin/a2a-mcp.sh +42 -7
  38. package/bin/agents-config.sh +30 -7
  39. package/bin/apodex-smoke-test.sh +14 -1
  40. package/bin/bifrost-bridge.sh +25 -6
  41. package/bin/bootstrap-macos.sh +6 -1
  42. package/bin/brokk-send.sh +14 -1
  43. package/bin/changelog-assemble.sh +154 -0
  44. package/bin/changelog-guard.sh +39 -7
  45. package/bin/crash-sense.sh +14 -1
  46. package/bin/design-check.sh +80 -0
  47. package/bin/design-icon.sh +156 -0
  48. package/bin/docs-guard.sh +2 -2
  49. package/bin/editor-place.sh +33 -15
  50. package/bin/eindri-acclaim.sh +14 -1
  51. package/bin/eindri-seat.sh +83 -0
  52. package/bin/eindri-seen.sh +14 -1
  53. package/bin/eindri-start.sh +14 -1
  54. package/bin/einherjar-spawn.sh +9 -3
  55. package/bin/eir-doctor.sh +89 -1
  56. package/bin/electron-lib.sh +34 -0
  57. package/bin/feature-inventory.sh +73 -0
  58. package/bin/fleet-apply.sh +15 -2
  59. package/bin/github-deploy.sh +14 -1
  60. package/bin/gjallarhorn-expose.sh +43 -9
  61. package/bin/gjallarhorn-notify.sh +14 -1
  62. package/bin/gjallarhorn-purge.sh +14 -1
  63. package/bin/gjallarhorn-tunnel.sh +16 -3
  64. package/bin/groa-update.sh +9 -1
  65. package/bin/hall-snapshot.sh +21 -2
  66. package/bin/herdr-run.sh +23 -2
  67. package/bin/hlidskjalf-agents.sh +172 -0
  68. package/bin/hlidskjalf-usage.sh +196 -0
  69. package/bin/hoard-guard.sh +135 -0
  70. package/bin/hoard-lib.sh +91 -11
  71. package/bin/hodd.sh +28 -4
  72. package/bin/huginn-research-worker.sh +14 -1
  73. package/bin/local-model-lock.sh +22 -3
  74. package/bin/mimir-bridge.py +33 -0
  75. package/bin/mimir-bridge.sh +14 -1
  76. package/bin/mimir-reflect.sh +27 -0
  77. package/bin/mjollnir-webhook.sh +14 -1
  78. package/bin/mjollnir.sh +22 -3
  79. package/bin/model-hardware.sh +17 -3
  80. package/bin/model-resolve.sh +14 -1
  81. package/bin/nornir-job-hall-snapshot.sh +28 -0
  82. package/bin/nornir-job-nsr-compliance.sh +54 -0
  83. package/bin/npm-publish.sh +136 -0
  84. package/bin/omarchy-install.sh +18 -5
  85. package/bin/omarchy-plugins.sh +15 -2
  86. package/bin/omarchy-sense.sh +14 -1
  87. package/bin/pi-model-agnostic.sh +71 -0
  88. package/bin/private-guard.sh +20 -0
  89. package/bin/project-git.sh +18 -4
  90. package/bin/public-guard.sh +3 -2
  91. package/bin/sessrumnir-ensure.sh +1 -1
  92. package/bin/sessrumnir.sh +16 -3
  93. package/bin/smidja-board.sh +136 -0
  94. package/bin/smidja-bootstrap.sh +5 -1
  95. package/bin/smidja-lib.sh +49 -0
  96. package/bin/syn-asset-pretool-check.sh +14 -1
  97. package/bin/syn-spawn-pretool-check.sh +58 -0
  98. package/bin/tailscale-sync.sh +15 -2
  99. package/bin/telegram-bot.sh +14 -1
  100. package/bin/ui-truth-check.sh +75 -0
  101. package/bin/valknut-load.sh +42 -4
  102. package/bin/wedge-notify.sh +14 -1
  103. package/bin/workspace-rag.sh +14 -1
  104. package/bin/ymir-install.sh +257 -57
  105. package/bin/ymir-plan.sh +390 -0
  106. package/bin/ymir-say.sh +14 -1
  107. package/bin/ymir-setup-auth.sh +12 -1
  108. package/bin/ymir-style.sh +108 -0
  109. package/bin/ymir-validate.sh +64 -11
  110. package/bin/ymir.js +82 -34
  111. package/docs/design.md +2 -2
  112. package/docs/lore.md +5 -1
  113. package/docs/runbooks/agents.md +1 -1
  114. package/package.json +13 -3
  115. package/scripts/electron.sh +74 -15
  116. package/scripts/lower.sh +34 -0
  117. package/scripts/raise.sh +42 -0
  118. package/scripts/start.sh +65 -3
  119. package/.agents/config/agents.yaml +0 -50
  120. package/.agents/config/tailscale-sync.yaml +0 -27
  121. package/.agents/memory/kaia.engram +0 -0
  122. package/.agents/memory/kaia.engram-shm +0 -0
  123. package/.agents/memory/kaia.engram-wal +0 -0
  124. package/.agents/memory/well/episodes.jsonl +0 -367
  125. package/.agents/memory/well/workspace.jsonl +0 -27
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env bash
2
+ # ui-truth-check.sh — the panel's number must equal the machine's number.
3
+ #
4
+ # The wiring order's last row, and the one that makes the rest trustworthy: a
5
+ # board can render beautifully and still lie. This compares what the UI is SERVED
6
+ # against what the machine actually holds, per source, and FAILS on a mismatch.
7
+ #
8
+ # bin/ui-truth-check.sh # compare, TOON, exit 0/1
9
+ # bin/ui-truth-check.sh --help
10
+ #
11
+ # It reads endpoints as the DESKTOP SEAT (loopback + the marker), which is the
12
+ # trusted seat rule 09 allows — the web door would answer 401 and prove nothing
13
+ # about the data.
14
+ set -u
15
+
16
+ VERSION="1.0.0"
17
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
+ ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
19
+ GATE="${HLIDSKJALF_API_PORT:-3889}"
20
+ # shellcheck source=bin/hoard-lib.sh
21
+ . "$SCRIPT_DIR/hoard-lib.sh"
22
+
23
+ case "${1-}" in
24
+ -v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;;
25
+ -h|--help) sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
26
+ esac
27
+
28
+ have() { command -v "$1" >/dev/null 2>&1; }
29
+ have curl || { printf 'error: curl is required\nhelp: install curl\n' >&2; exit 1; }
30
+
31
+ json_len() { python3 -c 'import json,sys
32
+ try: print(len(json.load(sys.stdin)))
33
+ except Exception: print(0)' 2>/dev/null || echo 0; }
34
+
35
+ verdict="PASS"; details=""
36
+
37
+ # ── the fleet: herdr vs the connector vs the endpoint ─────────────────────────
38
+ SRC_JSON="$("$SCRIPT_DIR/hlidskjalf-agents.sh" 2>/dev/null || printf '[]')"
39
+ src_n=$(printf '%s' "$SRC_JSON" | json_len)
40
+ UI_JSON="$(curl -s --max-time 8 -H 'x-ymir-surface: desktop' "http://127.0.0.1:${GATE}/api/agents" 2>/dev/null || printf '[]')"
41
+ ui_n=$(printf '%s' "$UI_JSON" | json_len)
42
+ if have "${HERDR_BIN_PATH:-herdr}"; then
43
+ herd_n="$("${HERDR_BIN_PATH:-herdr}" pane list 2>/dev/null | python3 -c 'import json,sys
44
+ try: d=json.load(sys.stdin)
45
+ except Exception: print(0); raise SystemExit
46
+ panes=(d.get("result") or {}).get("panes") or d.get("panes") or []
47
+ print(sum(1 for p in panes if p.get("agent")))' 2>/dev/null || echo 0)"
48
+ else
49
+ herd_n="unknown"
50
+ fi
51
+ [ "$src_n" = "$ui_n" ] || { verdict="FAIL"; details="$details fleet:connector=$src_n,served=$ui_n"; }
52
+ # The connector is ROSTER-FIRST: it reports the agents (the who) and the seats (the
53
+ # where), so its total legitimately exceeds the pane count. What must not happen is
54
+ # FEWER entries than panes - a standing seat hidden from the board. Panes are a subset.
55
+ [ "$herd_n" = "unknown" ] || [ "$src_n" -ge "$herd_n" ] || { verdict="FAIL"; details="$details fleet:connector=$src_n,herdr=$herd_n"; }
56
+
57
+ # ── the ledger: the hoard's, against what the gate serves ─────────────────────
58
+ hoard_root _hoard
59
+ LEDGER="$_hoard/memory/runes_audit.md"
60
+ ledger_n=0
61
+ [ -r "$LEDGER" ] && ledger_n=$(grep -c '^[[:space:]]*{' "$LEDGER" 2>/dev/null || echo 0)
62
+ UI_RUNES="$(curl -s --max-time 8 -H 'x-ymir-surface: desktop' "http://127.0.0.1:${GATE}/api/runes" 2>/dev/null || printf '[]')"
63
+ ui_runes_n=$(printf '%s' "$UI_RUNES" | json_len)
64
+ [ "$ledger_n" = "$ui_runes_n" ] || { verdict="FAIL"; details="$details ledger=$ledger_n,served=$ui_runes_n"; }
65
+
66
+ printf 'ui_truth[6]{source,count,note}:\n'
67
+ printf ' "herdr panes","%s","what the machine holds"\n' "$herd_n"
68
+ printf ' "the connector","%s","bin/hlidskjalf-agents.sh"\n' "$src_n"
69
+ printf ' "the endpoint","%s","/api/agents as the desktop seat"\n' "$ui_n"
70
+ printf ' "ledger entries","%s","the hoard'"'"'s ledger"\n' "$ledger_n"
71
+ printf ' "ledger served","%s","/api/runes as the desktop seat"\n' "$ui_runes_n"
72
+ printf ' "verdict","%s","%s"\n' "$verdict" "$([ -n "$details" ] && printf 'mismatch:%s' "$details" || printf 'the board may be believed')"
73
+
74
+ [ "$verdict" = "PASS" ] || { printf 'error: the UI and the machine disagree\nhelp: %s\n' "${details:-see the rows above}" >&2; exit 1; }
75
+ exit 0
@@ -26,7 +26,7 @@ PI_GLOBAL="${HOME}/.pi/agent/agents"
26
26
  # seated. This loader DEPLOYS that source into the single home.
27
27
  PI_EXT_SRC="$ROOT/.pi/shared/extensions"
28
28
  PI_EXT_HOME="${HOME}/.pi/agent/extensions"
29
- OC_LOCAL="$ROOT/.opencode/agent"
29
+ OC_LOCAL="$ROOT/.opencode/agents"
30
30
  SKILLS="$ROOT/.agents/skills"
31
31
 
32
32
  usage() {
@@ -167,6 +167,18 @@ fi
167
167
 
168
168
  if [ "$MODE_OPENCODE" = 1 ]; then
169
169
  add opencode-config "$ROOT/opencode.json" "$(config_out "$ROOT/opencode.json.example" "$ROOT/opencode.json")"
170
+ # OpenCode reads `.opencode/agents/` (PLURAL). The singular `.opencode/agent/`
171
+ # was never read by the harness at all — twenty correct symlinks in a directory
172
+ # no loader opens, which is why only the agents declared by hand in
173
+ # opencode.json ever appeared. Migrate a legacy singular dir forward so an old
174
+ # home heals instead of silently keeping its agents invisible.
175
+ if [ -d "$ROOT/.opencode/agent" ] && [ ! -e "$ROOT/.opencode/agents" ]; then
176
+ mv "$ROOT/.opencode/agent" "$ROOT/.opencode/agents" 2>/dev/null \
177
+ && add opencode-migrate "$ROOT/.opencode/agents" "legacy .opencode/agent/ moved to .opencode/agents/" \
178
+ || add opencode-migrate "$ROOT/.opencode/agents" "ERROR could not migrate"
179
+ fi
180
+ # Count what is bound here; the binding itself happens further down, after
181
+ # `link_agents` is defined (this block runs before that definition).
170
182
  if [ -d "$OC_LOCAL" ]; then
171
183
  n=$(ls "$OC_LOCAL"/*.md 2>/dev/null | wc -l | tr -d ' ')
172
184
  add opencode "$OC_LOCAL" "native ($n agents)"
@@ -190,6 +202,17 @@ if [ "$MODE_PI" = 1 ]; then
190
202
  # Deploy the shared extensions into their single home. Copy, not link: a
191
203
  # broken link would silently disable a tool, and pi reads the file directly.
192
204
  # Idempotent — identical files are left untouched.
205
+ # The CONTRACT, deployed globally: pi reads an AGENTS.md from its agent home in
206
+ # every session, whatever the workspace. Without this, Sessrumnir (or any pi
207
+ # session) started in another project loads THAT project's rules and knows
208
+ # nothing of Ymir - no Brokk, no laws, no lore. Symlink, so the tree stays the
209
+ # single source and an update is picked up with no re-install.
210
+ if [ -d "$HOME/.pi/agent" ]; then
211
+ ln -sfn "$ROOT/AGENTS.md" "$HOME/.pi/agent/AGENTS.md" 2>/dev/null \
212
+ && add pi-global-contract "$HOME/.pi/agent/AGENTS.md" "the Ymir contract, loaded in every pi session" \
213
+ || add pi-global-contract "$HOME/.pi/agent/AGENTS.md" "ERROR"
214
+ fi
215
+
193
216
  if [ -d "$PI_EXT_SRC" ]; then
194
217
  mkdir -p "$PI_EXT_HOME" 2>/dev/null
195
218
  dep_n=0
@@ -199,7 +222,22 @@ if [ "$MODE_PI" = 1 ]; then
199
222
  if [ -f "$PI_EXT_HOME/$b" ] && cmp -s "$f" "$PI_EXT_HOME/$b"; then continue; fi
200
223
  cp -f "$f" "$PI_EXT_HOME/$b" && dep_n=$((dep_n+1))
201
224
  done
202
- add pi-extensions "$PI_EXT_HOME" "$dep_n deployed (shared single home)"
225
+ # Their supporting modules are one level away, under the extensions' own lib:
226
+ # the shared extensions require sibling lib modules, and deploying the
227
+ # top-level files ALONE ships extensions that cannot load. That is exactly
228
+ # what pi reported: Failed to load extension, Cannot find module ./lib/....
229
+ # A deploy that copies a file but not the module it imports is not a deploy.
230
+ PI_EXT_LIB="$ROOT/.pi/extensions/lib"
231
+ if [ -d "$PI_EXT_LIB" ]; then
232
+ mkdir -p "$PI_EXT_HOME/lib" 2>/dev/null
233
+ for f in "$PI_EXT_LIB"/*; do
234
+ [ -e "$f" ] || continue
235
+ b=$(basename "$f")
236
+ if [ -f "$PI_EXT_HOME/lib/$b" ] && cmp -s "$f" "$PI_EXT_HOME/lib/$b"; then continue; fi
237
+ cp -f "$f" "$PI_EXT_HOME/lib/$b" && dep_n=$((dep_n+1))
238
+ done
239
+ fi
240
+ add pi-extensions "$PI_EXT_HOME" "$dep_n deployed (shared single home, lib included)"
203
241
  fi
204
242
  fi
205
243
 
@@ -248,8 +286,8 @@ if [ "$MODE_STATUS" = 0 ]; then
248
286
  add "${hd#.}-agents" "$ROOT/$hd/agents" "bound ($n)"
249
287
  done
250
288
  if [ -d "$ROOT/.opencode" ]; then
251
- n=$(link_agents "$ROOT/.opencode/agent" short) || n=0
252
- add opencode-agents "$ROOT/.opencode/agent" "bound ($n)"
289
+ n=$(link_agents "$OC_LOCAL" short) || n=0
290
+ add opencode-agents "$OC_LOCAL" "bound ($n)"
253
291
  fi
254
292
  fi
255
293
 
@@ -20,7 +20,20 @@ CHANNEL="${1:-unknown}"
20
20
  SUMMARY="${2:-}"
21
21
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
22
22
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
23
- STATE_DIR="${BROKK_STATE_OVERRIDE:-$ROOT/state}"
23
+
24
+ # The roots that live OUTSIDE the code tree: this machine's records and the
25
+ # runtime state belong to the home the operator chose at installation, never in
26
+ # the tree — a packaged install replaces its tree on upgrade (Rule 04).
27
+ if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
28
+ _yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
29
+ for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
30
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
31
+ done
32
+ unset _yr _yc
33
+ fi
34
+ hoard_state_dir YMIR_STATE_DIR
35
+ hoard_data_dir YMIR_DATA_DIR
36
+ STATE_DIR="${BROKK_STATE_OVERRIDE:-$YMIR_STATE_DIR}"
24
37
  LOG="$STATE_DIR/wedge-alarm.log"
25
38
 
26
39
  # "discard" is the library-mode default so no test posts a real notification.
@@ -16,8 +16,21 @@ set -u
16
16
  VERSION="1.0.0"
17
17
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
18
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
19
+
20
+ # The roots that live OUTSIDE the code tree: this machine's records and the
21
+ # runtime state belong to the home the operator chose at installation, never in
22
+ # the tree — a packaged install replaces its tree on upgrade (Rule 04).
23
+ if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
24
+ _yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
+ for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
26
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
27
+ done
28
+ unset _yr _yc
29
+ fi
30
+ hoard_state_dir YMIR_STATE_DIR
31
+ hoard_data_dir YMIR_DATA_DIR
19
32
  STORE="$ROOT/.agents/memory/well/workspace.jsonl"
20
- REALM_FILE="$ROOT/data/realm.md"
33
+ REALM_FILE="$YMIR_DATA_DIR/realm.md"
21
34
  DEFAULT_REALM="$(head -n1 "$REALM_FILE" 2>/dev/null | tr -d '[:space:]')"
22
35
  # Never assume the company's slug: resolve the operator's realm neutrally.
23
36
  if [ -z "$DEFAULT_REALM" ]; then . "$SCRIPT_DIR/realm-lib.sh"; ymir_active_realm "$ROOT" DEFAULT_REALM; fi
@@ -6,13 +6,17 @@
6
6
  #
7
7
  # Usage:
8
8
  # bin/ymir-install.sh [--check] [--skip-engines] [--skip-services] [--no-desktop] [--yes]
9
+ # bin/ymir-install.sh --plan [--json] # the plan, computed — changes nothing
10
+ # bin/ymir-install.sh --yes | --non-interactive | --accept-all-defaults
9
11
  # bin/ymir-install.sh --status
10
12
  # bin/ymir-install.sh --version
11
13
  #
12
- # Consent: a real install (not --check) asks the operator to accept the plan
13
- # before any change is made. Non-interactive callers must pass --yes.
14
+ # The consent a real install asks for is not a recited paragraph: it is the PLAN
15
+ # printed by bin/ymir-plan.sh probed on THIS host, one row per step, each with
16
+ # its state and the reason for it. A paragraph drifts behind the code; a plan is
17
+ # computed from it every run.
14
18
  #
15
- # Exit: 0 all good (or --check), 1 a step failed, 2 usage, 3 declined.
19
+ # Exit: 0 all good (or --check/--plan), 1 a step failed, 2 usage, 3 declined.
16
20
  set -u
17
21
 
18
22
  # --- portability shim: bin/ymir-platform.sh --------------------------------
@@ -27,30 +31,72 @@ fi
27
31
  VERSION="1.0.0"
28
32
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
29
33
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
30
- YMIR_HOME="${YMIR_HOME:-$HOME/Documents/Ymir}"
31
- WORKSPACE="${YMIR_WORKSPACE:-$YMIR_HOME/workspaces}"
34
+
35
+ # The operator's settings and secrets live in the home they chose, never in the
36
+ # code tree — a packaged install replaces its tree on upgrade, and a credential
37
+ # must never sit in a tree that ships (Rule 04).
38
+ if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
39
+ _yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
40
+ for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
41
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
42
+ done
43
+ unset _yr _yc
44
+ fi
45
+ hoard_settings_dir YMIR_SETTINGS_DIR
46
+ hoard_local_env YMIR_ENV_FILE
47
+ # The home is the OPERATOR's to choose, never ours to assume. Resolution:
48
+ # $YMIR_HOME (explicit) → the choice recorded at a previous installation →
49
+ # the one documented default. An interactive run ASKS and RECORDS the answer
50
+ # (step_home); --check never writes, --yes takes what is recorded.
32
51
  # The hoard resolves through the shared lib, so the installer can never disagree
33
52
  # with bin/hodd.sh about where private data lives — and never points inside the
34
53
  # repo (Rule 04).
35
54
  # shellcheck source=bin/hoard-lib.sh
36
55
  . "$SCRIPT_DIR/hoard-lib.sh"
56
+ ymir_home_root YMIR_HOME
57
+ # Where the smithy's parts live: apps/smidja-factory in a clone, or the
58
+ # @zerwiz/smidja-factory package in an npm install (bin/smidja-lib.sh).
59
+ if [ -z "${YMIR_SMIDJA_LIB_LOADED:-}" ]; then
60
+ _ys="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
61
+ for _yc in "$_ys/smidja-lib.sh" "$(dirname "$_ys")/bin/smidja-lib.sh"; do
62
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_SMIDJA_LIB_LOADED=1; break; }
63
+ done
64
+ unset _ys _yc
65
+ fi
66
+ smidja_visualizer_dir SMIDJA_VIZ
67
+ smidja_factory_dir SMIDJA_FACTORY
68
+
69
+ WORKSPACE="${YMIR_WORKSPACE:-$YMIR_HOME/workspaces}"
37
70
  hoard_root HOARD
38
71
  DOMAINS="company marketing development life me"
39
72
 
40
- CHECK=0; SKIP_ENGINES=0; SKIP_SERVICES=0; ASSUME_YES=0; NO_DESKTOP=0
41
- case "${1-}" in -v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;; -h|--help) sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; esac
73
+ CHECK=0; SKIP_ENGINES=0; SKIP_SERVICES=0; ASSUME_YES=0; NO_DESKTOP=0; PLAN_ONLY=0; PLAN_ARGS=()
74
+ case "${1-}" in -v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;; -h|--help) sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; esac
42
75
  while [ $# -gt 0 ]; do
43
76
  case "$1" in
44
77
  --check) CHECK=1; shift ;;
78
+ --plan|--dry-run) PLAN_ONLY=1; shift ;;
79
+ --json|--blocked) PLAN_ARGS+=("$1"); shift ;;
80
+ --phase) PLAN_ARGS+=("$1" "${2-}"); shift 2 ;; # --phase carries its number
45
81
  --skip-engines) SKIP_ENGINES=1; shift ;;
46
82
  --skip-services) SKIP_SERVICES=1; shift ;;
47
83
  --no-desktop) NO_DESKTOP=1; shift ;;
48
- --yes|-y) ASSUME_YES=1; shift ;;
84
+ --yes|-y|--non-interactive|--accept-all-defaults) ASSUME_YES=1; shift ;;
49
85
  --status) exec "$SCRIPT_DIR/ymir-install.sh" --check ;;
50
- *) printf 'error: unknown flag %s\nhelp: bin/ymir-install.sh [--check|--skip-engines|--skip-services|--no-desktop|--yes]\n' "$1" >&2; exit 2 ;;
86
+ *) printf 'error: unknown flag %s\nhelp: bin/ymir-install.sh [--check|--plan|--skip-engines|--skip-services|--no-desktop|--yes]\n' "$1" >&2; exit 2 ;;
51
87
  esac
52
88
  done
53
89
 
90
+ # The plan is a question, not a change: it prints and exits without writing.
91
+ if [ "$PLAN_ONLY" = 1 ]; then
92
+ exec "$SCRIPT_DIR/ymir-plan.sh" ${PLAN_ARGS[@]+"${PLAN_ARGS[@]}"}
93
+ fi
94
+
95
+ # The cloth (bin/ymir-style.sh): colour, marks and spacing for the human, while
96
+ # every row of data stays TOON on stdout.
97
+ . "$SCRIPT_DIR/ymir-style.sh"
98
+ style_init
99
+
54
100
  declare -a IDS STATUS DETAIL
55
101
  # The code minted for this install, reported at the end so it is not lost in the
56
102
  # step table. Empty on --check, and when no code could be minted.
@@ -62,33 +108,28 @@ TOON="install[0]{step,status,detail}:"
62
108
  # ── consent ──────────────────────────────────────────────────────────────
63
109
  # Show exactly what will change and require explicit acceptance. A real
64
110
  # install touches the machine (packages, a docker image, raised services),
65
- # so it never proceeds on silence.
111
+ # so it never proceeds on silence. The plan is COMPUTED here (bin/ymir-plan.sh
112
+ # probes this host) — never a paragraph that can drift behind the code.
66
113
  confirm_install() {
67
114
  [ "$ASSUME_YES" = 1 ] && return 0
68
115
  if [ ! -t 0 ]; then
69
- printf 'error: refusing a non-interactive install without --yes\nhelp: re-run with --yes to accept non-interactively, or --check to preview\n' >&2
116
+ printf 'error: refusing a non-interactive install without --yes\nhelp: re-run with --yes to accept non-interactively, or --plan / --check to preview\n' >&2
70
117
  exit 3
71
118
  fi
119
+ style_title "Ymir first setup" "the plan below is probed on this machine, not recited"
72
120
  cat <<'PLAN'
73
- Ymir first setup this will make the following changes:
74
-
75
- install in USER SPACE (no sudo): bun, uv — and 'mcp<2>' via pip
76
- create the workspace tree (work/ · personal/ · companies · registries)
77
- install the OSS engines: treehouse, no-mistakes (+ sandcastle if present)
78
- ensure the Hermes worker runtime
79
- ensure a terminal backend (herdr Þjazi — preferred, else tmux)
80
- • learn this machine (Omarchy version, packages, configs, monitors, scale)
81
- • place the desktop apps on their own numbered desktops
82
- • build the Utgard sandbox image 'utgard-runner:latest' (needs docker access)
83
- • create the Smiðja database and build the visualizer UI
84
- • load agents/skills and write workspace/INSTALL.md
85
- • install the git delivery gates (secret · branch · changelog)
86
- • raise the runtime services (Hlidskjalf SPA + gate API + bridges)
87
- • open BOTH desktop apps so you see them: Hlidskjalf + Smíðja
88
- • verify the running system and report what stands
121
+ Each row is a phase, a step, its state, and why.
122
+
123
+ DO a change will be made
124
+ SKIP already satisfied nothing to do
125
+ INFO a fact about this host; no change implied
126
+ BLOCKED cannot run the reason names what is missing
127
+ CONSENT needs your word (a credential, an invite, the desktop shells)
89
128
 
90
129
  Nothing is deleted. Every step is idempotent.
91
130
  PLAN
131
+ printf '\n'
132
+ bash "$SCRIPT_DIR/ymir-plan.sh" --colour >/dev/null 2>&1 || true
92
133
  printf '\nProceed with the install? [y/N] '
93
134
  read -r reply || reply=""
94
135
  case "$reply" in
@@ -197,6 +238,112 @@ YAML
197
238
  add tree OK "workspace/{personal} · companies · registries · hoard (outside the repo) (created $created)"
198
239
  }
199
240
 
241
+ # ── 2a. the home the operator chooses ────────────────────────────────────────
242
+ # Everything private — docs, secrets, identity, projects, workspaces, memory —
243
+ # lives in ONE place the operator owns, and it is the operator's to name. A real
244
+ # interactive run asks once and records the answer under ~/.config/ymir/home, so
245
+ # every later script resolves the same home without being told again. --check
246
+ # never writes; --yes takes what is already recorded (or the documented default).
247
+ step_home() {
248
+ local rec="" chosen=""
249
+ ymir_home_record rec
250
+ if [ -n "${YMIR_HOME:-}" ] && [ "$CHECK" = 1 ]; then add home OK "home: $YMIR_HOME"; return; fi
251
+ if [ -n "$rec" ]; then
252
+ add home OK "home: $YMIR_HOME (the choice recorded at installation)"
253
+ return
254
+ fi
255
+ if [ "$ASSUME_YES" = 1 ] || [ ! -t 0 ]; then
256
+ if ymir_home_record_set "$YMIR_HOME" 2>/dev/null; then
257
+ add home OK "home: $YMIR_HOME (default recorded — change it any time with YMIR_HOME=<path> bin/ymir-install.sh)"
258
+ else
259
+ add home WARN "home: $YMIR_HOME (could not record the choice under $YMIR_CONFIG_DIR — pass YMIR_HOME=<path> to every run)"
260
+ fi
261
+ return
262
+ fi
263
+ printf '\nWhere shall your home live?\n'
264
+ printf 'Everything private is kept there — docs, secrets, identity, projects,\n'
265
+ printf 'workspaces, memory — never in the code tree, never in this package.\n'
266
+ printf 'Home [%s]: ' "$YMIR_HOME"
267
+ IFS= read -r chosen || true
268
+ [ -n "$chosen" ] && YMIR_HOME="${chosen/#\~/$HOME}"
269
+ if [ -e "$YMIR_HOME" ]; then
270
+ printf 'using %s — what is already there is kept; nothing is deleted.\n' "$YMIR_HOME"
271
+ fi
272
+ # Re-resolve the roots the choice moved: the hoard within the home, and the
273
+ # workspace tree beneath it. A second run finds the record and never asks again.
274
+ hoard_root HOARD
275
+ WORKSPACE="${YMIR_WORKSPACE:-$YMIR_HOME/workspaces}"
276
+ if ymir_home_record_set "$YMIR_HOME"; then
277
+ add home OK "home: $YMIR_HOME (recorded — later runs need not ask)"
278
+ else
279
+ add home WARN "home: $YMIR_HOME (could not record the choice; pass YMIR_HOME=<path> to every run)"
280
+ fi
281
+ }
282
+
283
+ # ── 2b. app repos (the app split) ────────────────────────────────────────────
284
+ # The apps live in their OWN repos (zerwiz/hlidskjalf · hlidskjalf-mobile ·
285
+ # odrerir · sessrumnir · smidja), registered in the home registry with a
286
+ # `repo: apps/<path>` block. The monorepo never tracks them; installation
287
+ # clones each into the tree — never guessing a remote, always reading the
288
+ # registry. A present repo is fast-forwarded; a missing one cloned. The smithy
289
+ # engine (apps/smidja) is stamped from the cloned factory's templates, exactly
290
+ # as install.py does for a target repo.
291
+ step_apps() {
292
+ local reg="$HOARD/identity/projects.yaml"
293
+ if [ ! -r "$reg" ]; then
294
+ add apps SKIP "no registry — no app repos to pull ($reg)"
295
+ return 0
296
+ fi
297
+ # Parse blocks that carry `repo: apps/<path>` and a `git:` inline dict.
298
+ local entries
299
+ entries=$(awk '
300
+ /^ - id:/ { repo="" }
301
+ /^ repo: apps\// { repo=substr($2, 6) }
302
+ /^ git: / {
303
+ line=$0; host=""; owner=""; grepo=""
304
+ if (match(line, /host: [A-Za-z0-9._-]+/)) host=substr(line, RSTART+6, RLENGTH-6)
305
+ if (match(line, /owner: [A-Za-z0-9_-]+/)) owner=substr(line, RSTART+7, RLENGTH-7)
306
+ if (match(line, /repo: [A-Za-z0-9._-]+/)) grepo=substr(line, RSTART+6, RLENGTH-6)
307
+ if (repo != "" && owner != "" && grepo != "") print repo "|" host "|" owner "|" grepo
308
+ repo=""
309
+ }' "$reg")
310
+ if [ -z "$entries" ]; then
311
+ add apps OK "no apps/ projects registered — nothing to pull"
312
+ return 0
313
+ fi
314
+ local ok=0 skip=0 fail=0 missing=0
315
+ while IFS='|' read -r rel host owner grepo; do
316
+ [ -n "$rel" ] || continue
317
+ local path="$ROOT/apps/$rel"
318
+ if [ "$CHECK" = 1 ]; then
319
+ if [ -d "$path/.git" ]; then add apps OK "$rel present ($owner/$grepo)"; ok=$((ok+1));
320
+ elif [ -d "$path" ]; then add apps WARN "$rel present but not a git clone — re-install to replace"; skip=$((skip+1));
321
+ else add apps WARN "$rel absent — a full run clones $owner/$grepo"; missing=$((missing+1)); fi
322
+ continue
323
+ fi
324
+ if [ -d "$path/.git" ]; then
325
+ if (cd "$path" && git fetch origin 2>/dev/null && git merge --ff-only origin/main >/dev/null 2>&1); then add apps OK "$rel up to date ($owner/$grepo)"; ok=$((ok+1));
326
+ else add apps OK "$rel present ($owner/$grepo; pull refused — local changes?)"; skip=$((skip+1)); fi
327
+ elif [ -d "$path" ]; then
328
+ add apps WARN "$rel present but not a git clone — move $path aside and re-run"; skip=$((skip+1))
329
+ else
330
+ local url="https://github.com/$owner/$grepo.git"
331
+ if git clone -q "$url" "$path" 2>/dev/null; then add apps OK "$rel cloned ($owner/$grepo)"; ok=$((ok+1));
332
+ else add apps WARN "$rel clone failed — run: git clone $url $path"; fail=$((fail+1)); fi
333
+ fi
334
+ done <<< "$entries"
335
+ # The smithy engine: apps/smidja is stamped from the cloned factory's
336
+ # templates (the monorepo tracks neither). The config template follows.
337
+ local factory="$ROOT/apps/smidja-factory" eng="$ROOT/apps/smidja"
338
+ if [ "$CHECK" = 0 ] && [ -d "$factory/templates/smidja" ] && [ ! -e "$eng/smidja_modules" ]; then
339
+ mkdir -p "$eng"
340
+ cp -rn "$factory/templates/smidja/." "$eng/" 2>/dev/null
341
+ [ -f "$factory/templates/smidja.config.yaml" ] && { mkdir -p "$eng/smidja_smidja_config"; cp -n "$factory/templates/smidja.config.yaml" "$eng/smidja_smidja_config/smidja.config.yaml" 2>/dev/null; }
342
+ add apps OK "smidja engine stamped into apps/smidja"
343
+ fi
344
+ if [ "$fail" -gt 0 ]; then add apps WARN "$fail app clone(s) failed (offline?)"; fi
345
+ }
346
+
200
347
  # ── 3. engines ───────────────────────────────────────────────────────────────
201
348
  step_engines() {
202
349
  [ "$SKIP_ENGINES" = 1 ] && { add engines SKIP "--skip-engines"; return; }
@@ -240,7 +387,6 @@ step_models() {
240
387
  if [ ! -f "$HOME/.pi/agent/models.json" ]; then
241
388
  "$SCRIPT_DIR/models-detect.sh" --write >/dev/null 2>&1 && add models OK "seeded ~/.pi/agent/models.json (was absent)"
242
389
  fi
243
- printf 'models: local-model testing is first-class — load the modeltesting skill; llama.cpp (llama-server/llama-swap) is fastest on the GPU; research your hardware best settings online; for coding use >= 80000 context.\n'
244
390
  fi
245
391
  if [ -x "$SCRIPT_DIR/model-hardware.sh" ]; then
246
392
  "$SCRIPT_DIR/model-hardware.sh" >/dev/null 2>&1 && add models OK "profiled this machine (data/local-models.md; online research left for Brokk)"
@@ -323,35 +469,37 @@ step_omarchy() {
323
469
  }
324
470
 
325
471
  step_host() {
326
- # Core host learning: record THIS machine so the agent helps with the real one.
327
- # The Omarchy-specific extras (desktop placement, the post-update hook, the
328
- # plugin offer, the desktop alarm channel) belong to the Omarchy layer and are
329
- # applied by step_omarchy; nothing here assumes Omarchy.
330
- local learned=no
331
- if [ "$CHECK" = 1 ]; then
332
- if [ -x "$SCRIPT_DIR/omarchy-sense.sh" ]; then
333
- local snap; snap="$("$SCRIPT_DIR/omarchy-sense.sh" status 2>&1 | sed -n '2p' | tr -d '"' | cut -c1-60)"
334
- learned="${snap:-no snapshot yet}"
335
- fi
336
- add host OK "host snapshot: ${learned}"
337
- else
338
- if [ -x "$SCRIPT_DIR/omarchy-sense.sh" ]; then
339
- "$SCRIPT_DIR/omarchy-sense.sh" observe --quiet >/dev/null 2>&1 && learned=yes
340
- fi
341
- add host OK "host learnt=${learned}"
472
+ # Core host learning: sense THIS machine on EVERY host with the portable
473
+ # sensor (Rule 05). The Omarchy-specific RECORDING (omarchy-sense observe), the
474
+ # post-update hook, desktop placement, the plugin offer, and the desktop alarm
475
+ # channel belong to the Omarchy layer and are applied by step_omarchy; nothing
476
+ # here assumes Omarchy.
477
+ local seen="unknown"
478
+ if [ -x "$SCRIPT_DIR/host-sense.sh" ]; then
479
+ # host-sense TOON row 2 is: os, id, family, session, desktop
480
+ local row; row="$("$SCRIPT_DIR/host-sense.sh" 2>/dev/null | sed -n '2p')"
481
+ [ -n "$row" ] && seen="$(printf '%s' "$row" | sed -E 's/^[[:space:]]*"//; s/"[[:space:]]*$//; s/","/ \/ /g')"
342
482
  fi
343
-
344
- # The Allfather's own agent setup, seeded from the tracked template. Idempotent:
345
- # config/agents.yaml is private and is never overwritten once set. --check never
346
- # writes, so it only reports.
347
- if [ -e "$ROOT/config/agents.yaml" ]; then
483
+ add host OK "host sensed: ${seen}"
484
+
485
+ # The operator's own agent setup, seeded from the tracked template into the
486
+ # home's settings dir — settings are the operator's, never the package's.
487
+ # Idempotent: the seeded file is private and is never overwritten once set.
488
+ # --check writes nothing, so it only reports. The template is code, so it is
489
+ # read from wherever the tree keeps it: `config/` in a clone (a symlink the
490
+ # npm tarball cannot carry), else `.agents/config/`.
491
+ if [ -e "$YMIR_SETTINGS_DIR/agents.yaml" ]; then
348
492
  add agents-config OK "kept (private, never overwritten)"
349
- elif [ "$CHECK" = 1 ]; then
350
- add agents-config WARN "not seeded (run without --check)"
351
- elif [ -r "$ROOT/config/agents.yaml.example" ] && cp "$ROOT/config/agents.yaml.example" "$ROOT/config/agents.yaml" 2>/dev/null; then
352
- add agents-config OK "seeded from the template"
493
+ return 0
494
+ fi
495
+ local tpl="$ROOT/config/agents.yaml.example"
496
+ [ -r "$tpl" ] || tpl="$ROOT/.agents/config/agents.yaml.example"
497
+ if [ "$CHECK" = 1 ]; then
498
+ add agents-config WARN "not seeded yet — a real run writes $YMIR_SETTINGS_DIR/agents.yaml"
499
+ elif [ -r "$tpl" ] && mkdir -p "$YMIR_SETTINGS_DIR" && cp "$tpl" "$YMIR_SETTINGS_DIR/agents.yaml" 2>/dev/null; then
500
+ add agents-config OK "seeded $YMIR_SETTINGS_DIR/agents.yaml from the template"
353
501
  else
354
- add agents-config WARN "could not seed config/agents.yaml"
502
+ add agents-config WARN "could not seed $YMIR_SETTINGS_DIR/agents.yaml"
355
503
  fi
356
504
  }
357
505
 
@@ -398,7 +546,7 @@ step_smidja() {
398
546
  if [ "$CHECK" = 1 ]; then
399
547
  local dbok vizok
400
548
  [ -f "$YMIR_HOME/smidja/smidja.db" ] && dbok=present || dbok=missing
401
- [ -d "$ROOT/.agents/skills/smidja-factory/apps/visualizer/dist" ] && vizok=built || vizok=unbuilt
549
+ [ -n "${SMIDJA_VIZ:-}" ] && [ -d "$SMIDJA_VIZ/dist" ] && vizok=built || vizok=unbuilt
402
550
  if [ "$dbok" = missing ]; then
403
551
  add smidja WARN "smidja.db missing — a full run (without --check) creates it and seeds one bootstrap session"
404
552
  else
@@ -414,7 +562,7 @@ step_smidja() {
414
562
  else add smidja SKIP "no smidja-bootstrap.sh"; fi
415
563
  # The visualizer API serves its UI from ./dist — without a build it answers
416
564
  # the API but shows "No ./dist build found". Build it once when absent.
417
- local viz="$ROOT/.agents/skills/smidja-factory/apps/visualizer"
565
+ local viz="${SMIDJA_VIZ:-}"; [ -n "$viz" ] || return 0
418
566
  [ -d "$viz" ] || return 0
419
567
  if [ -d "$viz/dist" ]; then
420
568
  add visualizer OK "UI built (served on :8437)"
@@ -487,12 +635,44 @@ step_gates() {
487
635
  else
488
636
  add gates WARN "hooks not installed — bin/secret-guard.sh --install && bin/changelog-guard.sh --install"
489
637
  fi
638
+ # The home repo is where the private data lives, so its ward matters more
639
+ # than this repo's four. Report it separately so a dormant vault is visible.
640
+ local hh; hh="${YMIR_HOME:-$HOME/Documents/Ymir}/.git/hooks/pre-commit"
641
+ if [ -x "$hh" ]; then add hoard-gate OK "home pre-commit seated";
642
+ else add hoard-gate WARN "home hooks not installed — bin/hoard-guard.sh --install"; fi
490
643
  return
491
644
  fi
492
645
  [ -x "$SCRIPT_DIR/secret-guard.sh" ] && "$SCRIPT_DIR/secret-guard.sh" --install >/dev/null 2>&1 || true
493
646
  [ -x "$SCRIPT_DIR/changelog-guard.sh" ] && "$SCRIPT_DIR/changelog-guard.sh" --install >/dev/null 2>&1 || true
647
+ # Seat the ward on the PRIVATE home too — every install layer, every machine.
648
+ [ -x "$SCRIPT_DIR/hoard-guard.sh" ] && "$SCRIPT_DIR/hoard-guard.sh" --install >/dev/null 2>&1 || true
494
649
  if [ -x "$pre_commit" ] && [ -x "$pre_push" ]; then add gates OK "pre-commit + pre-push installed"
495
650
  else add gates WARN "could not write .git/hooks — gates are dormant"; fi
651
+ local hh; hh="${YMIR_HOME:-$HOME/Documents/Ymir}/.git/hooks/pre-commit"
652
+ if [ -x "$hh" ]; then add hoard-gate OK "home pre-commit seated";
653
+ else add hoard-gate WARN "home hooks not seated — bin/hoard-guard.sh --install"; fi
654
+ }
655
+
656
+ # ── 6c. desktop marks (the rune, the entry, the contract) ────────────────────
657
+ #
658
+ # Every app wears its OWN rune in the operator's desktop: the icon into the icon
659
+ # theme, the .desktop entry into their applications dir (so every app is
660
+ # dockable and pinnable), and the Ymir contract deployed into pi's agent home so
661
+ # every session - in ANY folder - loads Brokk. Idempotent, and it writes to the
662
+ # operator's real data dir, never a sandbox one.
663
+ step_marks() {
664
+ if [ ! -x "$SCRIPT_DIR/design-icon.sh" ]; then add marks SKIP "no design-icon.sh"; return; fi
665
+ if [ "$CHECK" = 1 ]; then
666
+ n=$(ls "$HOME/.local/share"/applications/ymir-*.desktop 2>/dev/null | wc -l | tr -d ' ')
667
+ add marks OK "$n desktop app marks installed"
668
+ return
669
+ fi
670
+ "$SCRIPT_DIR/design-icon.sh" mint --all >/dev/null 2>&1 || true
671
+ n=$("$SCRIPT_DIR/design-icon.sh" install 2>/dev/null | grep -c '"ymir-') || n=0
672
+ [ -r "$HOME/.pi/agent/AGENTS.md" ] || [ -d "$HOME/.pi/agent" ] && {
673
+ ln -sfn "$ROOT/AGENTS.md" "$HOME/.pi/agent/AGENTS.md" 2>/dev/null || true
674
+ }
675
+ add marks OK "$n app marks (rune icon + entry) · Ymir contract in the pi agent home"
496
676
  }
497
677
 
498
678
  # ── 7. services ──────────────────────────────────────────────────────────────
@@ -521,6 +701,22 @@ step_desktop() {
521
701
  fi
522
702
  # Placement is the Omarchy layer's job and has already run (step_omarchy runs
523
703
  # before this step), so here we only launch.
704
+ # Verify the runtime before claiming anything: a skipped Electron postinstall
705
+ # leaves a partial runtime that fails to launch while every build still passes.
706
+ if [ -z "${YMIR_ELECTRON_LIB_LOADED:-}" ] && [ -r "$SCRIPT_DIR/electron-lib.sh" ]; then
707
+ . "$SCRIPT_DIR/electron-lib.sh"; YMIR_ELECTRON_LIB_LOADED=1
708
+ fi
709
+ local shell partial=""
710
+ for shell in hlidskjalf odrerir sessrumnir; do
711
+ local dir="$ROOT/apps/$shell" d
712
+ [ -d "$dir" ] || dir="$(cd "$ROOT" && npm root 2>/dev/null)/@zerwiz/$shell"
713
+ d="$(electron_runtime_state "$dir" 2>/dev/null || true)"
714
+ [ "$d" = partial ] && partial="$partial $shell"
715
+ done
716
+ if [ -n "$partial" ]; then
717
+ add desktop WARN "the Electron runtime is PARTIAL for:$partial — the web surfaces stand; approve and rebuild to launch the shells"
718
+ return 0
719
+ fi
524
720
  if "$ROOT/scripts/electron.sh" start --both >/dev/null 2>&1; then
525
721
  add desktop OK "raised Hlidskjalf + Smíðja"
526
722
  else
@@ -624,7 +820,11 @@ step_panes() {
624
820
  # Ask before touching the machine; --check only previews and never asks.
625
821
  [ "$CHECK" = 0 ] && confirm_install
626
822
 
627
- step_panes; step_prereqs; step_tree; step_engines; step_models; step_hermes; step_sessrumnir; step_backend; step_host; step_sandbox; step_memory; step_smidja; step_spa; step_omarchy; step_loaders; step_gates; bin/ymir-migrate.sh apply >/dev/null 2>&1 || true; step_auth; step_invite; step_register
823
+ step_panes; step_prereqs; step_home; step_tree; step_apps; step_engines; step_models; step_hermes; step_sessrumnir; step_backend; step_host; step_sandbox; step_memory; step_smidja; step_spa; step_omarchy; step_loaders; step_gates; step_marks
824
+ # Migrations MOVE private data — that is a write, and `--check` promises none.
825
+ # Only a real run heals the home forward; the preview leaves it untouched.
826
+ if [ "$CHECK" = 0 ]; then bin/ymir-migrate.sh apply >/dev/null 2>&1 || true; fi
827
+ step_auth; step_invite; step_register
628
828
  [ "$CHECK" = 0 ] && step_services
629
829
  [ "$CHECK" = 0 ] && step_desktop
630
830
  [ "$CHECK" = 0 ] && step_validate