@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
package/bin/eir-doctor.sh CHANGED
@@ -25,7 +25,7 @@ have() { command -v "$1" >/dev/null 2>&1; }
25
25
  say_ok() { return 0; }
26
26
 
27
27
  # Each surface: `s_<name>` = healthy? (exit 0), `f_<name>` = the repair.
28
- SURFACES=(floors herdr a2abridge hermes sessrumnir well mcp lock migrations)
28
+ SURFACES=(floors herdr a2abridge hermes sessrumnir well mcp lock migrations hoard)
29
29
 
30
30
  s_floors() { [ -x "$SCRIPT_DIR/prereq-ensure.sh" ] && "$SCRIPT_DIR/prereq-ensure.sh" status >/dev/null 2>&1; }
31
31
  f_floors() { "$SCRIPT_DIR/prereq-ensure.sh" ensure --install >/dev/null 2>&1; }
@@ -69,6 +69,51 @@ f_lock() {
69
69
  }
70
70
  s_migrations(){ [ ! -x "$SCRIPT_DIR/ymir-migrate.sh" ] || "$SCRIPT_DIR/ymir-migrate.sh" status >/dev/null 2>&1; }
71
71
  f_migrations(){ [ -x "$SCRIPT_DIR/ymir-migrate.sh" ] && "$SCRIPT_DIR/ymir-migrate.sh" apply >/dev/null 2>&1; }
72
+ # Hoard: private data sits under the hoard, and .ymir-layout.yaml tells the truth.
73
+ # Two failures this catches: (1) a declared layout path that does not exist — a
74
+ # stale map is what lets private work land outside the hoard; (2) a flat
75
+ # $YMIR_HOME/{identity,data,docs,secrets,tenants} duplicate beside hodd/ — the
76
+ # drift that RULES/04-hoard.md (correction 2026-09-17) forbids.
77
+ _hoard_root() { local r; if [ -x "$SCRIPT_DIR/hoard-lib.sh" ]; then . "$SCRIPT_DIR/hoard-lib.sh"; hoard_root r; printf '%s' "$r"; else printf '%s' "${YMIR_HOARD:-$YMIR_HOME/hodd}"; fi; }
78
+ s_hoard() {
79
+ local h; h="$(_hoard_root)"
80
+ [ -d "$h" ] || return 1
81
+ local lay="$YMIR_HOME/.ymir-layout.yaml" k p
82
+ if [ -f "$lay" ]; then
83
+ for k in config secrets identity workspaces memory smidja state data; do
84
+ p="$(sed -nE "s/^ $k: \"([^\"]+)\".*/\1/p" "$lay" | head -1)"
85
+ [ -n "$p" ] && [ ! -d "$p" ] && return 1
86
+ done
87
+ fi
88
+ for k in identity data docs secrets tenants; do
89
+ [ -d "$YMIR_HOME/$k" ] && return 1
90
+ done
91
+ return 0
92
+ }
93
+ f_hoard() {
94
+ local h; h="$(_hoard_root)"
95
+ mkdir -p "$h" 2>/dev/null || return 1
96
+ # A flat duplicate beside hodd/ is the drift: merge it in, never clobber.
97
+ local k
98
+ for k in identity data docs secrets tenants; do
99
+ local src="$YMIR_HOME/$k"
100
+ [ -d "$src" ] || continue
101
+ mkdir -p "$h/$k" 2>/dev/null || continue
102
+ cp -an "$src/." "$h/$k/" 2>/dev/null || true
103
+ rm -rf "$src" 2>/dev/null || true
104
+ done
105
+ # A layout entry naming a nonexistent dir is repointed at the hoard.
106
+ local lay="$YMIR_HOME/.ymir-layout.yaml" k2 p
107
+ if [ -f "$lay" ] && [ -d "$h" ]; then
108
+ for k2 in identity data secrets docs tenants; do
109
+ p="$(sed -nE "s/^ $k2: \"([^\"]+)\".*/\1/p" "$lay" | head -1)"
110
+ if [ -n "$p" ] && [ ! -d "$p" ]; then
111
+ sed -i "s|^ $k2: .*| $k2: \"$h/$k2\"|" "$lay"
112
+ fi
113
+ done
114
+ fi
115
+ s_hoard
116
+ }
72
117
 
73
118
  detail() { # <name> -> one short fact
74
119
  case "$1" in
@@ -81,6 +126,7 @@ detail() { # <name> -> one short fact
81
126
  mcp) "echo 'a2abridge + engram'" ;;
82
127
  lock) "cat $STATE/.lock 2>/dev/null | tr -d '[:space:]' | sed 's/^/pid /' || echo none" ;;
83
128
  migrations)"echo 'structure'" ;;
129
+ hoard) "printf 'hoard %s' \"$(_hoard_root)\" ; [ -d \"$YMIR_HOME/identity\" ] && printf ' +flat-duplicate' ; printf '\\n'" ;;
84
130
  esac
85
131
  }
86
132
 
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env bash
2
+ # feature-inventory.sh — every feature the system CLAIMS, in one checklist.
3
+ #
4
+ # "Every single feature must work" is a hope until there is a list. This builds
5
+ # the list from the tree itself rather than from anyone's memory: every tool, job,
6
+ # skill, agent, app, workflow, service and registered project. It asserts nothing
7
+ # about whether they work — that is the verification pass, and it needs this
8
+ # inventory to exist first.
9
+ #
10
+ # bin/feature-inventory.sh # the inventory, as TOON
11
+ # bin/feature-inventory.sh --counts # just the tallies
12
+ # bin/feature-inventory.sh --verify-plan # the inventory as a checklist to fill in
13
+ set -u
14
+
15
+ VERSION="1.0.0"
16
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
+ ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
18
+ HOARD="${YMIR_HOARD:-${YMIR_HOME:-$HOME/Documents/Ymir}/hodd}"
19
+
20
+ case "${1-}" in
21
+ -v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;;
22
+ -h|--help|"") sed -n '2,14p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
23
+ esac
24
+ MODE="${1:-all}"
25
+
26
+ n_tools=$(find "$ROOT/bin" -maxdepth 1 -type f \( -name '*.sh' -o -name '*.py' -o -name '*.js' \) 2>/dev/null | wc -l | tr -d ' ')
27
+ n_jobs=$(ls "$ROOT"/bin/nornir-job-*.sh 2>/dev/null | wc -l | tr -d ' ')
28
+ n_skills=$(find -L "$ROOT/.agents/skills" -mindepth 2 -maxdepth 2 -name SKILL.md 2>/dev/null | wc -l | tr -d ' ')
29
+ n_agents=$(ls "$ROOT"/.agents/agents/*.md 2>/dev/null | wc -l | tr -d ' ')
30
+ n_workflows=$(ls "$ROOT"/.github/workflows/*.yml 2>/dev/null | wc -l | tr -d ' ')
31
+ n_apps=$(find "$ROOT/apps" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l | tr -d ' ')
32
+ n_migrations=$(ls "$ROOT"/.agents/migrations/*.sh 2>/dev/null | wc -l | tr -d ' ')
33
+ n_projects=0
34
+ [ -r "$HOARD/identity/projects.yaml" ] && n_projects=$(grep -c '^ - id:' "$HOARD/identity/projects.yaml" 2>/dev/null | tr -d ' ')
35
+
36
+ if [ "$MODE" = "--counts" ]; then
37
+ printf 'features[8]{kind,count}:\n'
38
+ printf ' "tools (bin/)",%s\n' "$n_tools"
39
+ printf ' "nornir jobs",%s\n' "$n_jobs"
40
+ printf ' "skills",%s\n' "$n_skills"
41
+ printf ' "agents",%s\n' "$n_agents"
42
+ printf ' "workflows (CI)",%s\n' "$n_workflows"
43
+ printf ' "apps",%s\n' "$n_apps"
44
+ printf ' "migrations",%s\n' "$n_migrations"
45
+ printf ' "registered projects",%s\n' "$n_projects"
46
+ exit 0
47
+ fi
48
+
49
+ # The checklist: one row per thing that must be proven.
50
+ printf 'inventory[8]{kind,count,how_proven}:\n'
51
+ printf ' "tools (bin/)",%s,"bash -n + --version/--help runs, exit 0"\n' "$n_tools"
52
+ printf ' "nornir jobs",%s,"one run each, --check or dry-run, writes as documented"\n' "$n_jobs"
53
+ printf ' "skills",%s,"its scripts run; its asset paths resolve (skillindex gate)"\n' "$n_skills"
54
+ printf ' "agents",%s,"bound into all five harnesses (harnesses gate) + a dispatch"\n' "$n_agents"
55
+ printf ' "workflows (CI)",%s,"YAML parses + the job runs green on the branch"\n' "$n_workflows"
56
+ printf ' "apps",%s,"builds, serves, wears its rune, installs its entry, opens in Electron"\n' "$n_apps"
57
+ printf ' "migrations",%s,"idempotent; re-run leaves the home unchanged"\n' "$n_migrations"
58
+ printf ' "registered projects",%s,"gh resolves the repo; PR ledger reports its PRs"\n' "$n_projects"
59
+
60
+ printf 'tools[%s]{tool}:\n' "$n_tools"
61
+ for f in "$ROOT"/bin/*; do
62
+ [ -f "$f" ] || continue
63
+ case "$f" in *.sh|*.py|*.js) printf ' "%s"\n' "$(basename "$f")" ;; esac
64
+ done
65
+
66
+ printf 'jobs[%s]{job}:\n' "$n_jobs"
67
+ for f in "$ROOT"/bin/nornir-job-*.sh; do [ -f "$f" ] && printf ' "%s"\n' "$(basename "$f")"; done
68
+
69
+ printf 'apps[%s]{app}:\n' "$n_apps"
70
+ for d in "$ROOT"/apps/*/; do [ -d "$d" ] && printf ' "%s"\n' "$(basename "$d")"; done
71
+
72
+ printf 'skills[%s]{skill,owner}:\n' "$n_skills"
73
+ grep -oE '^ "[a-z0-9-]+","[^"]*","[a-z]+"' "$ROOT/.agents/skills/README.md" 2>/dev/null | sed 's/^ //' || true
@@ -14,8 +14,21 @@ 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
- REG="${BROKK_EINDRI_HOMES:-$ROOT/data/eindri-homes.md}"
18
- PREFS="${BROKK_FLEET_PREFS:-$ROOT/data/fleet.md}"
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
30
+ REG="${BROKK_EINDRI_HOMES:-$YMIR_DATA_DIR/eindri-homes.md}"
31
+ PREFS="${BROKK_FLEET_PREFS:-$YMIR_DATA_DIR/fleet.md}"
19
32
  CHECK=0
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
@@ -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
- ENV_FILE="${BROKK_ENV_FILE:-$ROOT/.env.local}"
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
+ ENV_FILE="${BROKK_ENV_FILE:-$YMIR_ENV_FILE}"
20
33
  SECRETS="NETLIFY_AUTH_TOKEN NETLIFY_SITE_ID VERCEL_TOKEN GITHUB_TOKEN TELEGRAM_BOT_TOKEN"
21
34
 
22
35
  case "${1-}" in
@@ -17,6 +17,19 @@ set -u
17
17
  VERSION="1.0.0"
18
18
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
19
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
20
+
21
+ # The roots that live OUTSIDE the code tree: this machine's records and the
22
+ # runtime state belong to the home the operator chose at installation, never in
23
+ # the tree — a packaged install replaces its tree 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_state_dir YMIR_STATE_DIR
32
+ hoard_data_dir YMIR_DATA_DIR
20
33
  TUNNEL="${YMIR_TUNNEL_NAME:-ymir}"
21
34
  DOMAIN="${YMIR_TUNNEL_DOMAIN:-zerwiz.org}"
22
35
  SUFFIX="${YMIR_TUNNEL_SUFFIX:-dell}"
@@ -31,13 +44,16 @@ case "${1:-plan}" in
31
44
  esac
32
45
  ACTION="${1:-plan}"
33
46
 
34
- # host_key port app-path
47
+ # Every public hostname points at the GATE (:3889), never at an app's own port:
48
+ # the gate holds the login and proxies on by host, so the web door is locked for
49
+ # every app. An app port here would be a hole straight past the login.
50
+ GATE_PORT="${HLIDSKJALF_API_PORT:-3889}"
35
51
  APPS=(
36
- "ymir:$(( ${#SUFFIX} ? 3889 : 3889 ))"
37
- "hlidskjalf:3889"
38
- "odrerir:4322"
39
- "smidja:8437"
40
- "smidjafactory:8437"
52
+ "ymir:$GATE_PORT"
53
+ "hlidskjalf:$GATE_PORT"
54
+ "odrerir:$GATE_PORT"
55
+ "smidja:$GATE_PORT"
56
+ "smidjafactory:$GATE_PORT"
41
57
  )
42
58
 
43
59
  if [ -z "$SUFFIX" ]; then
@@ -71,7 +87,9 @@ mkdir -p "$(dirname "$CONFIG")"
71
87
  {
72
88
  printf '# Gjallarhorn — this machine (%s). Generated by bin/gjallarhorn-expose.sh.\n' "$SUFFIX"
73
89
  printf 'tunnel: %s\n' "$TUNNEL"
74
- printf 'credentials-file: %s/.cloudflared/%s.json\n' "$HOME" "$TUNNEL"
90
+ # No credentials-file line: cloudflared resolves <UUID>.json for a named
91
+ # tunnel itself. Guessing the filename (ymir.json) is what made it refuse to
92
+ # start, and a tunnel that will not start answers 530 to the world.
75
93
  printf 'ingress:\n'
76
94
  for row in "${APPS[@]}"; do
77
95
  app="${row%%:*}"; port="${row##*:}"
@@ -81,16 +99,31 @@ mkdir -p "$(dirname "$CONFIG")"
81
99
  } >"$CONFIG"
82
100
 
83
101
  # 2. the routes (idempotent: an existing record is left alone)
102
+ # Route by the tunnel's own UUID, never by its name. `route dns <name> <host>`
103
+ # resolves the name through the account certificate, which can point at a
104
+ # DIFFERENT (even deleted) tunnel: every hostname then answers 530 while the
105
+ # named tunnel runs happily beside it. Resolve the UUID first, then route.
106
+ TUNNEL_ID="$(cloudflared tunnel info "$TUNNEL" 2>/dev/null | grep -m1 -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')"
107
+ [ -n "$TUNNEL_ID" ] || TUNNEL_ID="$TUNNEL"
84
108
  made=0
85
109
  for row in "${APPS[@]}"; do
86
110
  app="${row%%:*}"
87
- cloudflared tunnel route dns "$TUNNEL" "$app$SUFFIX.$DOMAIN" >/dev/null 2>&1 && made=$((made+1))
111
+ cloudflared tunnel route dns --overwrite-dns "$TUNNEL_ID" "$app$SUFFIX.$DOMAIN" >/dev/null 2>&1 && made=$((made+1))
88
112
  done
89
113
 
114
+ # 2b. the gate must know which host belongs to which app (it routes by host)
115
+ HOSTMAP="$YMIR_STATE_DIR/gjallarhorn-hosts.env"
116
+ {
117
+ printf '# Written by bin/gjallarhorn-expose.sh — the gate routes these hosts.\n'
118
+ printf 'YMIR_PRIMARY_HOST=ymir%s.%s\n' "$SUFFIX" "$DOMAIN"
119
+ printf 'SMIDJA_HOST=smidja%s.%s\n' "$SUFFIX" "$DOMAIN"
120
+ printf 'ODRERIR_HOST=odrerir%s.%s\n' "$SUFFIX" "$DOMAIN"
121
+ } >"$HOSTMAP"
122
+
90
123
  # 3. raise it
91
124
  started="already"
92
125
  if ! pgrep -f "cloudflared.*tunnel.*--config $CONFIG" >/dev/null 2>&1; then
93
- nohup cloudflared tunnel --config "$CONFIG" run "$TUNNEL" >"$ROOT/state/gjallarhorn.log" 2>&1 &
126
+ nohup cloudflared tunnel --config "$CONFIG" run "$TUNNEL" >"$YMIR_STATE_DIR/gjallarhorn.log" 2>&1 &
94
127
  started="pid $!"
95
128
  sleep 4
96
129
  fi
@@ -98,5 +131,6 @@ fi
98
131
  printf 'gjallarhorn_apply[3]{what,detail,count}:\n'
99
132
  printf ' "config","%s","%s"\n' "${CONFIG#"$HOME"/}" "${#APPS[@]}"
100
133
  printf ' "routes","%s%s.%s","%s"\n' "" "$SUFFIX" "$DOMAIN" "$made"
134
+ printf ' "hostmap","%s","the gate routes by host"\n' "${HOSTMAP#"$ROOT"/}"
101
135
  printf ' "tunnel","%s","%s"\n' "$started" "$TUNNEL"
102
136
  plan
@@ -13,7 +13,20 @@ set -u
13
13
  VERSION="1.0.0"
14
14
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
15
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
16
- ENV_FILE="${BROKK_ENV_FILE:-$ROOT/.env.local}"
16
+
17
+ # The operator's settings and secrets live in the home they chose, never in the
18
+ # code tree — a packaged install replaces its tree on upgrade, and a credential
19
+ # must never sit in a tree that ships (Rule 04).
20
+ if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
21
+ _yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
22
+ for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
23
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
24
+ done
25
+ unset _yr _yc
26
+ fi
27
+ hoard_settings_dir YMIR_SETTINGS_DIR
28
+ hoard_local_env YMIR_ENV_FILE
29
+ ENV_FILE="${BROKK_ENV_FILE:-$YMIR_ENV_FILE}"
17
30
 
18
31
  usage() { sed -n '2,11p' "$0" | sed 's/^# \{0,1\}//'; }
19
32
 
@@ -11,7 +11,20 @@ set -u
11
11
  VERSION="1.0.0"
12
12
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
13
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
14
- ENV_FILE="${BROKK_ENV_FILE:-$ROOT/.env.local}"
14
+
15
+ # The operator's settings and secrets live in the home they chose, never in the
16
+ # code tree — a packaged install replaces its tree on upgrade, and a credential
17
+ # must never sit in a tree that ships (Rule 04).
18
+ if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
19
+ _yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
+ for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
21
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
22
+ done
23
+ unset _yr _yc
24
+ fi
25
+ hoard_settings_dir YMIR_SETTINGS_DIR
26
+ hoard_local_env YMIR_ENV_FILE
27
+ ENV_FILE="${BROKK_ENV_FILE:-$YMIR_ENV_FILE}"
15
28
  DOMAIN="${YMIR_DOMAIN:-${YMIR_TUNNEL_HOST:-}}"
16
29
  [ -n "$DOMAIN" ] || { printf 'gjallarhorn-purge: set YMIR_DOMAIN (or YMIR_TUNNEL_HOST)\n' >&2; exit 2; }
17
30
  ZONE_NAME="${CLOUDFLARE_ZONE_NAME:-}"
@@ -19,10 +19,23 @@ fi
19
19
  VERSION="1.0.0"
20
20
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
21
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
22
+
23
+ # The roots that live OUTSIDE the code tree: this machine's records and the
24
+ # runtime state belong to the home the operator chose at installation, never in
25
+ # the tree — a packaged install replaces its tree on upgrade (Rule 04).
26
+ if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
27
+ _yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28
+ for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
29
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
30
+ done
31
+ unset _yr _yc
32
+ fi
33
+ hoard_state_dir YMIR_STATE_DIR
34
+ hoard_data_dir YMIR_DATA_DIR
22
35
  CONFIG="${YMIR_TUNNEL_CONFIG:-$HOME/.cloudflared/config-ymir.yml}"
23
36
  TUNNEL="${YMIR_TUNNEL_NAME:-ymir}"
24
- PID_FILE="$ROOT/state/gjallarhorn.pid"
25
- LOG_FILE="$ROOT/state/gjallarhorn.log"
37
+ PID_FILE="$YMIR_STATE_DIR/gjallarhorn.pid"
38
+ LOG_FILE="$YMIR_STATE_DIR/gjallarhorn.log"
26
39
 
27
40
  case "${1-}" in -v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;; -h|--help|"") sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; esac
28
41
  ACTION="${1:-status}"; shift || true
@@ -59,7 +72,7 @@ esac
59
72
 
60
73
  command -v cloudflared >/dev/null 2>&1 || { printf 'error: cloudflared not found\nhelp: install cloudflared\n' >&2; exit 1; }
61
74
  [ -r "$CONFIG" ] || { printf 'error: tunnel config not found: %s\nhelp: cp midgard/infrastructure/ingress/cloudflared-ymir.yml ~/.cloudflared/config-ymir.yml\n' "$CONFIG" >&2; exit 1; }
62
- mkdir -p "$ROOT/state"
75
+ mkdir -p "$YMIR_STATE_DIR"
63
76
  if running; then printf 'gjallarhorn[1]{state,host}:\n "already up","%s"\n' "$HOST"; exit 0; fi
64
77
 
65
78
  nohup cloudflared tunnel --config "$CONFIG" run "$TUNNEL" >"$LOG_FILE" 2>&1 &
@@ -25,7 +25,15 @@ VERSION="2.0.0"
25
25
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
26
26
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
27
27
  YMIR_HOME="${YMIR_HOME:-$HOME/Documents/Ymir}"
28
- REG="${BROKK_EINDRI_HOMES:-$YMIR_HOME/data/eindri-homes.md}"
28
+ # The registry lives in the hoard (RULES/04-hoard.md; correction 2026-09-17).
29
+ # Resolve through hoard-lib so the path can never drift again.
30
+ if [ -r "$SCRIPT_DIR/hoard-lib.sh" ]; then
31
+ # shellcheck source=bin/hoard-lib.sh
32
+ . "$SCRIPT_DIR/hoard-lib.sh"; hoard_root _hoard
33
+ else
34
+ _hoard="${YMIR_HOARD:-$YMIR_HOME/hodd}"
35
+ fi
36
+ REG="${BROKK_EINDRI_HOMES:-$_hoard/data/eindri-homes.md}"
29
37
  REMOTE=""; BRANCH=""; CHECK=0
30
38
 
31
39
  case "${1-}" in -v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;; -h|--help) sed -n '2,21p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; esac
@@ -17,13 +17,32 @@
17
17
  set -u
18
18
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
19
  ROOT="${BROKK_ROOT_OVERRIDE:-$(cd "$SCRIPT_DIR/.." && pwd)}"
20
- STATE="${BROKK_STATE_OVERRIDE:-$ROOT/state}"
20
+
21
+ # The operator's settings and secrets live in the home they chose, never in the
22
+ # code tree — a packaged install replaces its tree on upgrade, and a credential
23
+ # must never sit in a tree that ships (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_settings_dir YMIR_SETTINGS_DIR
32
+ hoard_local_env YMIR_ENV_FILE
33
+ hoard_state_dir YMIR_STATE_DIR
34
+ hoard_data_dir YMIR_DATA_DIR
35
+
36
+ # The roots that live OUTSIDE the code tree: this machine's records and the
37
+ # runtime state belong to the home the operator chose at installation, never in
38
+ # the tree — a packaged install replaces its tree on upgrade (Rule 04).
39
+ STATE="${BROKK_STATE_OVERRIDE:-$YMIR_STATE_DIR}"
21
40
  OUT="${1:-$ROOT/apps/odrerir/public/livehall.json}"
22
41
 
23
42
  runes_file="${BROKK_RUNES_FILE:-${YMIR_HOME:-$HOME/Documents/Ymir}/hodd/memory/runes_audit.md}"
24
43
  PROJECTS_FILE="${YMIR_HOME:-$HOME/Documents/Ymir}/hodd/identity/projects.yaml"
25
44
  projects_file="$PROJECTS_FILE"
26
- cron_file="$ROOT/config/cron.yaml"
45
+ cron_file="$YMIR_SETTINGS_DIR/cron.yaml"
27
46
 
28
47
  # --- tally the pieces ---
29
48
  runes=$(grep -c '^{' "$runes_file" 2>/dev/null || echo 0)
package/bin/herdr-run.sh CHANGED
@@ -37,7 +37,20 @@ set -u
37
37
  VERSION="2.0.0"
38
38
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
39
39
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
40
- STATE_DIR="${BROKK_STATE_OVERRIDE:-$ROOT/state}"
40
+
41
+ # The roots that live OUTSIDE the code tree: this machine's records and the
42
+ # runtime state belong to the home the operator chose at installation, never in
43
+ # the tree — a packaged install replaces its tree on upgrade (Rule 04).
44
+ if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
45
+ _yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
46
+ for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
47
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
48
+ done
49
+ unset _yr _yc
50
+ fi
51
+ hoard_state_dir YMIR_STATE_DIR
52
+ hoard_data_dir YMIR_DATA_DIR
53
+ STATE_DIR="${BROKK_STATE_OVERRIDE:-$YMIR_STATE_DIR}"
41
54
  TAB_LOG="$STATE_DIR/herdr-seats"
42
55
  SETTLE="${YMIR_HERDR_SETTLE:-300}"
43
56
 
@@ -302,7 +315,15 @@ case "$ACTION" in
302
315
  h="$(printf '%s' "$r" | cut -d'"' -f6)"
303
316
  pv="$(printf '%s' "$r" | cut -d'"' -f8)"
304
317
  md="$(printf '%s' "$r" | cut -d'"' -f10)"
305
- [ -n "$h" ] && KIND="$h"
318
+ # The harness FOLLOWS the model: a local provider is pi's work, an
319
+ # online one is opencode's. Trusting a field position here seated
320
+ # pi with an online model id, so the worker started and sat IDLE -
321
+ # "sent but never started" - while the dispatch row reported success.
322
+ case "${md:-}:${pv:-}:${h:-}" in
323
+ *llama*|*lmstudio*|*ollama*) KIND=pi ;;
324
+ "") KIND=pi ;; # nothing resolved: stay local
325
+ *) KIND=opencode ;;
326
+ esac
306
327
  [ -n "$md" ] && MODEL_ARGS=(-- --model "$pv/$md")
307
328
  printf 'herdr-run[1]{model,harness,provider,model_id}:\n "%s","%s","%s","%s"\n' "$MODEL_REQ" "$h" "$pv" "$md" >&2 ;;
308
329
  esac
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env bash
2
+ # hlidskjalf-agents.sh — WHO IS ACTUALLY STANDING, and what they are doing.
3
+ #
4
+ # The control plane showed the ROSTER: the twenty profiles that *could* run.
5
+ # A roster is a leaflet; the panes are the truth. This reads the live pane list
6
+ # (herdr) and emits the sessions that exist right now, so the Fleet gate shows a
7
+ # fleet rather than a painting of one — every opencode and pi session, with its
8
+ # kind, its state, its pane and where it works.
9
+ #
10
+ # bin/hlidskjalf-agents.sh # JSON (what /api/agents serves)
11
+ # bin/hlidskjalf-agents.sh --toon # TOON (for a human at a terminal)
12
+ #
13
+ # State comes from the pane's own reported status. Nothing here infers liveness
14
+ # from a process list or from prose: if herdr does not say an agent is there, it
15
+ # is not on the board.
16
+ set -u
17
+
18
+ VERSION="1.0.0"
19
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
+ ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
21
+ HERDR="${HERDR_BIN_PATH:-herdr}"
22
+ MODE="json"
23
+ [ "${1:-}" = "--toon" ] && MODE="toon"
24
+ case "${1-}" in -v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;;
25
+ -h|--help) sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; esac
26
+
27
+ command -v "$HERDR" >/dev/null 2>&1 || { [ "$MODE" = json ] && printf '[]\n' || printf 'agents[0]{name}:\n'; exit 0; }
28
+
29
+ raw="$("$HERDR" pane list 2>/dev/null)"
30
+
31
+ MODE="$MODE" RAW="$raw" python3 - <<'PY'
32
+ import json, os, sys
33
+
34
+ mode = os.environ.get("MODE", "json")
35
+ raw = os.environ.get("RAW", "")
36
+
37
+ ROSTER_DIR = os.environ.get("ROSTER_DIR", ".agents/agents")
38
+
39
+ def panes():
40
+ """The STANDING seats: a herdr pane an agent runs in. A pane is the WHERE."""
41
+ try:
42
+ d = json.loads(raw)
43
+ except Exception:
44
+ return []
45
+ pl = (d.get("result") or {}).get("panes") or d.get("panes") or []
46
+ out = []
47
+ for p in pl:
48
+ agent = p.get("agent") or ""
49
+ if not agent and p.get("agent_status") in (None, "", "unknown"):
50
+ continue
51
+ title = (p.get("terminal_title_stripped") or p.get("terminal_title") or "").strip()
52
+ out.append({
53
+ "id": p.get("pane_id", ""),
54
+ "name": title.split("|")[0].strip() or agent or p.get("pane_id", ""),
55
+ "harness": agent or "agent",
56
+ "state": p.get("agent_status") or "unknown",
57
+ "title": title,
58
+ "cwd": p.get("foreground_cwd") or p.get("cwd") or "",
59
+ "workspace": p.get("workspace_id", ""),
60
+ "task": title.split("|", 1)[1].strip() if "|" in title else "",
61
+ "matched": False,
62
+ })
63
+ return out
64
+
65
+
66
+ def roster():
67
+ """The AGENTS, from the canonical roster (.agents/agents/*.md).
68
+
69
+ A pane is the where; this is the WHO. Emitting panes alone is why the Fleet
70
+ showed thirteen OpenCode terminals and not one of the smiths who might be
71
+ standing in them. The roster is canonical (RULES/02); a harness seat is not an
72
+ agent, and an agent is not its seat.
73
+ """
74
+ try:
75
+ files = sorted(f for f in os.listdir(ROSTER_DIR) if f.endswith(".md"))
76
+ except Exception:
77
+ return []
78
+ out = []
79
+ for f in files:
80
+ stem = f[:-3]
81
+ parts = stem.split("-")
82
+ model = mode = domain = ""
83
+ try:
84
+ with open(os.path.join(ROSTER_DIR, f)) as fh:
85
+ txt = fh.read(6000)
86
+ if txt.startswith("---"):
87
+ for line in txt.split("---", 2)[1].splitlines():
88
+ if line.startswith("model:") and not model:
89
+ model = line.split(":", 1)[1].strip()
90
+ if line.startswith("mode:") and not mode:
91
+ mode = line.split(":", 1)[1].strip()
92
+ if line.startswith("domain:") and not domain:
93
+ domain = line.split(":", 1)[1].strip()
94
+ except Exception:
95
+ pass
96
+ out.append({
97
+ "stem": stem,
98
+ "figure": parts[0],
99
+ "craft": parts[1] if len(parts) > 1 else "agent",
100
+ "model": model,
101
+ "mode": mode,
102
+ "domain": domain,
103
+ })
104
+ return out
105
+
106
+
107
+ ps = panes()
108
+ rs = roster()
109
+ rows = []
110
+
111
+ def card(name, role, model, state, domain="ymirlabs", pane=None, task="", where=""):
112
+ status = {"working": "nominal", "idle": "nominal", "unseated": "seated"}.get(state, "degraded")
113
+ return {
114
+ "id": pane["id"] if pane else "roster:" + name,
115
+ "name": name,
116
+ "role": role,
117
+ "realm": "work",
118
+ "domain": domain,
119
+ "status": status,
120
+ "capabilities": [],
121
+ "skills": [],
122
+ "interface": {
123
+ "protocol": "a2a/1.0",
124
+ "endpoint": ("local://%s" % pane["id"]) if pane else "",
125
+ "signed": False,
126
+ },
127
+ "model": model or (pane["harness"] if pane else ""),
128
+ "uptime": 0,
129
+ "tasksDone": 0,
130
+ "kind": role,
131
+ "state": state,
132
+ "pane": pane["id"] if pane else "",
133
+ "cwd": (pane["cwd"] if pane else where),
134
+ "task": task,
135
+ "live": ({
136
+ "kind": pane["harness"], "state": state, "pane": pane["id"],
137
+ "workspace": pane["workspace"], "cwd": pane["cwd"], "task": pane["task"],
138
+ } if pane else None),
139
+ }
140
+
141
+ # THE AGENTS FIRST — every smith on the roster, seated or not. An unseated agent is
142
+ # still an agent; hiding it made the board look like a rack of terminals.
143
+ for a in rs:
144
+ hit = None
145
+ for p in ps:
146
+ hay = ("%s %s %s" % (p["name"], p["harness"], p["title"])).lower()
147
+ if a["figure"] and a["figure"] in hay:
148
+ p["matched"] = True
149
+ hit = p
150
+ break
151
+ rows.append(card(a["figure"], a["craft"], a["model"],
152
+ hit["state"] if hit else "unseated",
153
+ domain=a["domain"] or "ymirlabs",
154
+ pane=hit, task=hit["task"] if hit else ""))
155
+
156
+ # Harness seats that answer to no one on the roster: kept visible, named honestly.
157
+ for p in ps:
158
+ if not p["matched"]:
159
+ rows.append(card(p["name"], p["harness"], p["harness"], p["state"], pane=p,
160
+ task=p["task"]))
161
+
162
+ if mode == "json":
163
+ print(json.dumps(rows))
164
+ raise SystemExit
165
+
166
+ print("agents[%d]{name,kind,state,pane,cwd,task}:" % len(rows))
167
+ for r in rows:
168
+ q = lambda s: '"%s"' % str(s).replace('"', '\\"')
169
+ print(" %s,%s,%s,%s,%s,%s" % (q(r["name"]), q(r["kind"]), q(r["state"]), q(r["pane"]), q(r["cwd"]), q(r["task"])))
170
+ if not rows:
171
+ print("agents: 0 live sessions — nothing is standing")
172
+ PY