@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
@@ -43,8 +43,9 @@ leaks() { # stdin → prints masked hits, returns 1 on any
43
43
  scan_one() { # $1 = path in the index; prints a finding, returns 1 when clean
44
44
  local f="$1" out
45
45
  # This file necessarily contains the patterns it looks for; scanning it would
46
- # always fail. Exempt by name, and keep the patterns here honest.
47
- case "$f" in bin/public-guard.sh|bin/secret-guard.sh) return 0 ;; esac
46
+ # always fail. The same is true of every guard whose job is to name a secret
47
+ # shape, so all three are exempt by name — and the patterns in them stay honest.
48
+ case "$f" in bin/public-guard.sh|bin/secret-guard.sh|bin/hoard-guard.sh) return 0 ;; esac
48
49
  printf '%s\n' "$f" | grep -Eq "$PUBLIC" || return 0
49
50
  printf '%s\n' "$f" | grep -Eq "$APPEND_ONLY" && return 0
50
51
  out="$(git -C "$ROOT" show ":$f" 2>/dev/null | leaks)" || true
@@ -90,7 +90,7 @@ case "$CMD" in
90
90
  if [ "$DO_INSTALL" = 1 ]; then
91
91
  npm_install || true
92
92
  ensure_electron_binary || true
93
- [ "$built_present" ] || build_app || true
93
+ built_present || build_app || true
94
94
  status
95
95
  exit $?
96
96
  fi
package/bin/sessrumnir.sh CHANGED
@@ -14,9 +14,22 @@ set -u
14
14
  VERSION="1.0.0"
15
15
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
16
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
17
+
18
+ # The roots that live OUTSIDE the code tree: this machine's records and the
19
+ # runtime state belong to the home the operator chose at installation, never in
20
+ # the tree — a packaged install replaces its tree on upgrade (Rule 04).
21
+ if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
22
+ _yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23
+ for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
24
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
25
+ done
26
+ unset _yr _yc
27
+ fi
28
+ hoard_state_dir YMIR_STATE_DIR
29
+ hoard_data_dir YMIR_DATA_DIR
17
30
  APP="$ROOT/apps/sessrumnir"
18
- PID_FILE="$ROOT/state/sessrumnir.pid"
19
- LOG_FILE="$ROOT/state/sessrumnir.log"
31
+ PID_FILE="$YMIR_STATE_DIR/sessrumnir.pid"
32
+ LOG_FILE="$YMIR_STATE_DIR/sessrumnir.log"
20
33
 
21
34
  case "${1-}" in -v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;; -h|--help|"") sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; esac
22
35
  ACTION="${1:-start}"; shift || true
@@ -72,7 +85,7 @@ fi
72
85
  exit 1
73
86
  }
74
87
 
75
- mkdir -p "$ROOT/state"
88
+ mkdir -p "$YMIR_STATE_DIR"
76
89
  args=()
77
90
  [ -n "$WORKSPACE" ] && args+=("$WORKSPACE")
78
91
 
@@ -69,7 +69,11 @@ if command -v uv >/dev/null 2>&1; then
69
69
  python3 -c '
70
70
  import os, sys
71
71
  from pathlib import Path
72
- sys.path.insert(0, str(Path.cwd() / "smidja"))
72
+ root = Path.cwd()
73
+ for sub in ("apps/smidja", "smidja"):
74
+ p = root / sub
75
+ if (p / "smidja_modules").is_dir():
76
+ sys.path.insert(0, str(p)); break
73
77
  from smidja_modules.tracer import Tracer
74
78
  db = os.environ["SMIDJA_BS_DB"]
75
79
  events = str(Path(db).parent / "sessions" / "bootstrap" / "events.jsonl")
@@ -16,7 +16,20 @@ set -u
16
16
 
17
17
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
18
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
19
- READS="${SYN_ASSET_READS:-$ROOT/state/asset-reads}"
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
32
+ READS="${SYN_ASSET_READS:-$YMIR_STATE_DIR/asset-reads}"
20
33
  TARGET=""; NOTE=""
21
34
  while [ $# -gt 0 ]; do
22
35
  case "$1" in
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env bash
2
+ # syn-spawn-pretool-check.sh - PreToolUse seatbelt for spawning and for typing.
3
+ #
4
+ # Sýn guards how workers are raised. The failure this exists to end, observed
5
+ # live: an agent (me) hand-fired `herdr-run.sh eindri`, then delivered the brief
6
+ # by TYPING it into the pane — and when the agent had already exited the brief
7
+ # was typed into **bash**, where it sat as a command. Workers were "sent" and
8
+ # never started, and text was executed in a shell. One door exists for this and
9
+ # it is `bin/einherjar-spawn.sh` (firstmate's `fm-spawn.sh`, ported): it creates
10
+ # the worktree, records `state/<id>.meta`, and fails closed on an unverified
11
+ # harness.
12
+ #
13
+ # Usage: syn-spawn-pretool-check.sh --command <bash command>
14
+ # Exit: 0 = allow, 2 = block (stderr carries the reason and the remedy)
15
+ set -u
16
+
17
+ command_text=""
18
+ while [ $# -gt 0 ]; do
19
+ case "$1" in
20
+ --command) command_text=${2-}; shift 2 ;;
21
+ *) shift ;;
22
+ esac
23
+ done
24
+
25
+ # 1. Never type into a pane. A brief is not keystrokes: typing lands in whatever
26
+ # owns the pane, and when the agent has exited that is the shell.
27
+ case "$command_text" in
28
+ *"pane input"*|*"send-keys"*|*"pane type"*)
29
+ printf 'denied: do not type into a pane — a brief delivered as keystrokes lands in whatever owns it (a shell, once the agent has exited)\n' >&2
30
+ printf 'help: seat the worker through the door: bin/einherjar-spawn.sh <task-id> <project-dir> --mode local-only --harness opencode --model <model> --backend herdr\n' >&2
31
+ exit 2 ;;
32
+ esac
33
+
34
+ # 2. Never hand-raise an Eindri. `herdr-run.sh run` (a command in a pane) is
35
+ # fine; raising a WORKER by hand is not — it skips the worktree, the meta
36
+ # record, and the harness verification.
37
+ case "$command_text" in
38
+ *"herdr-run.sh eindri"*|*"herdr-run.sh agent"*)
39
+ printf 'denied: an Eindri is raised through the door, not by hand — herdr-run.sh eindri skips the worktree, state/<id>.meta, and the harness check\n' >&2
40
+ printf 'help: bin/einherjar-spawn.sh <task-id> <project-dir> --mode <direct-PR|local-only|no-mistakes> [--harness <name>] [--model <name>] [--backend tmux|herdr]\n' >&2
41
+ exit 2 ;;
42
+ esac
43
+
44
+ # 3. A LOCAL model needs a live local server. Requesting one while the router is
45
+ # down is how a seat came up, errored, and left a shell behind.
46
+ case "$command_text" in
47
+ *"einherjar-spawn.sh"*)
48
+ case "$command_text" in
49
+ *"llama-cpp/"*|*"llama.cpp/"*|*"lmstudio/"*|*"ollama/"*)
50
+ if ! (exec 3<>/dev/tcp/127.0.0.1/8080) 2>/dev/null; then
51
+ printf 'denied: a local model was requested but no local server answers on :8080 — the worker will start, fail, and leave a shell\n' >&2
52
+ printf 'help: raise the llama-router (bin/models-detect.sh reports what is served), or seat the worker on an online model\n' >&2
53
+ exit 2
54
+ fi ;;
55
+ esac ;;
56
+ esac
57
+
58
+ exit 0
@@ -16,7 +16,20 @@ 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
- CFG="${YMIR_TAILSCALE_SYNC:-$ROOT/config/tailscale-sync.yaml}"
19
+
20
+ # The operator's settings and secrets live in the home they chose, never in the
21
+ # code tree — a packaged install replaces its tree on upgrade, and a credential
22
+ # must never sit in a tree that ships (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_settings_dir YMIR_SETTINGS_DIR
31
+ hoard_local_env YMIR_ENV_FILE
32
+ CFG="${YMIR_TAILSCALE_SYNC:-$YMIR_SETTINGS_DIR/tailscale-sync.yaml}"
20
33
  DRY=0; ACTION=""; PEER=""
21
34
 
22
35
  for a in "$@"; do
@@ -62,7 +75,7 @@ except Exception: pass' 2>/dev/null)"
62
75
  fi
63
76
 
64
77
  if [ "$ACTION" = init ]; then
65
- TPL="$ROOT/config/tailscale-sync.example.yaml"
78
+ TPL="$YMIR_SETTINGS_DIR/tailscale-sync.example.yaml"
66
79
  if [ -r "$CFG" ]; then printf 'tailscale-sync[1]{action,state}:\n "init","kept — %s exists"\n' "$CFG"; exit 0; fi
67
80
  [ -r "$TPL" ] || { printf 'error: template missing: %s\n' "$TPL" >&2; exit 1; }
68
81
  cp "$TPL" "$CFG" && printf 'tailscale-sync[1]{action,path}:\n "init","%s"\n' "$CFG"
@@ -12,7 +12,20 @@ set -u
12
12
  VERSION="1.0.0"
13
13
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
14
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
15
- ENV_FILE="${BROKK_ENV_FILE:-$ROOT/.env.local}"
15
+
16
+ # The operator's settings and secrets live in the home they chose, never in the
17
+ # code tree — a packaged install replaces its tree on upgrade, and a credential
18
+ # must never sit in a tree that ships (Rule 04).
19
+ if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
20
+ _yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
+ for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
22
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
23
+ done
24
+ unset _yr _yc
25
+ fi
26
+ hoard_settings_dir YMIR_SETTINGS_DIR
27
+ hoard_local_env YMIR_ENV_FILE
28
+ ENV_FILE="${BROKK_ENV_FILE:-$YMIR_ENV_FILE}"
16
29
 
17
30
  case "${1-}" in
18
31
  -v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;;
@@ -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