@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
@@ -26,6 +26,19 @@ set -u
26
26
  VERSION="1.0.0"
27
27
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28
28
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
29
+
30
+ # The roots that live OUTSIDE the code tree: this machine's records and the
31
+ # runtime state belong to the home the operator chose at installation, never in
32
+ # the tree — a packaged install replaces its tree on upgrade (Rule 04).
33
+ if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
34
+ _yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
35
+ for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
36
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
37
+ done
38
+ unset _yr _yc
39
+ fi
40
+ hoard_state_dir YMIR_STATE_DIR
41
+ hoard_data_dir YMIR_DATA_DIR
29
42
  YES=0
30
43
 
31
44
  case "${1-}" in
@@ -77,7 +90,7 @@ is_installed() { # <id>
77
90
  }
78
91
 
79
92
  repo_for() { # <id> — read the repo from the registry's own catalog when reachable
80
- local id=$1 cache="$ROOT/state/omarchy-catalog.json"
93
+ local id=$1 cache="$YMIR_STATE_DIR/omarchy-catalog.json"
81
94
  if [ -r "$cache" ]; then
82
95
  python3 - "$cache" "$id" <<'PY' 2>/dev/null
83
96
  import json,sys
@@ -135,7 +148,7 @@ case "$ACTION" in
135
148
  printf 'omarchy-plugins[1]{id,state}:\n "%s","already installed"\n' "$id"; exit 0
136
149
  fi
137
150
  # Resolve the repo from the registry catalog; refresh it if absent.
138
- cache="$ROOT/state/omarchy-catalog.json"
151
+ cache="$YMIR_STATE_DIR/omarchy-catalog.json"
139
152
  if [ ! -r "$cache" ] && have curl; then
140
153
  mkdir -p "$(dirname "$cache")"
141
154
  curl -fsS --max-time 25 https://plugins.omarchy.org/catalog.json -o "$cache" 2>/dev/null || true
@@ -21,7 +21,20 @@ set -u
21
21
  VERSION="1.0.0"
22
22
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23
23
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
24
- STATE_DIR="${BROKK_STATE_OVERRIDE:-$ROOT/state}"
24
+
25
+ # The roots that live OUTSIDE the code tree: this machine's records and the
26
+ # runtime state belong to the home the operator chose at installation, never in
27
+ # the tree — a packaged install replaces its tree on upgrade (Rule 04).
28
+ if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
29
+ _yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
30
+ for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
31
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
32
+ done
33
+ unset _yr _yc
34
+ fi
35
+ hoard_state_dir YMIR_STATE_DIR
36
+ hoard_data_dir YMIR_DATA_DIR
37
+ STATE_DIR="${BROKK_STATE_OVERRIDE:-$YMIR_STATE_DIR}"
25
38
  SNAP="$STATE_DIR/omarchy-setup.json"
26
39
 
27
40
  # Rule 05: this is the OMARCHY layer's sensor, so it is gated on its host. Ymir
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ # pi-model-agnostic — pi must not EXPECT any model. Local and online are peers.
3
+ #
4
+ # The law (Allfather, 2026-09-17): a seat that answers is used; a seat that does not
5
+ # must not stop pi from starting. pi was failing at launch because providers pointed at
6
+ # local ports with nothing behind them:
7
+ # [llama-cpp] failed to reach http://localhost:8080/v1/models: fetch failed
8
+ # An unreachable local seat is a SKIP, not an error.
9
+ #
10
+ # This prunes unreachable LOCAL providers from pi's models config, keeping every seat
11
+ # that answers and every online provider untouched.
12
+ #
13
+ # bin/pi-model-agnostic.sh # report and prune
14
+ # bin/pi-model-agnostic.sh --dry-run # report only
15
+ set -euo pipefail
16
+
17
+ CFG="${PI_MODELS_JSON:-$HOME/.pi/agent/models.json}"
18
+ DRY=0
19
+ [ "${1:-}" = "--dry-run" ] && DRY=1
20
+ [ -f "$CFG" ] || { printf 'error: no pi models config at %s\nhelp: set PI_MODELS_JSON\n' "$CFG" >&2; exit 1; }
21
+
22
+ CFG="$CFG" DRY="$DRY" python3 - <<'INNER'
23
+ import json, os, shutil, sys, time, urllib.request
24
+
25
+ cfg = os.environ["CFG"]
26
+ dry = os.environ.get("DRY") == "1"
27
+ d = json.load(open(cfg))
28
+ providers = d.get("providers", {})
29
+
30
+ def reachable(url):
31
+ if not url:
32
+ return False
33
+ probe = url.rstrip("/")
34
+ if not probe.endswith("/v1"):
35
+ probe += "/v1"
36
+ try:
37
+ with urllib.request.urlopen(probe + "/models", timeout=3) as r:
38
+ return r.status == 200
39
+ except Exception:
40
+ return False
41
+
42
+ kept, dropped = [], []
43
+ for name, prov in providers.items():
44
+ base = str(prov.get("baseUrl", ""))
45
+ local = ("127.0.0.1" in base) or ("localhost" in base) or ("0.0.0.0" in base)
46
+ if local and not reachable(base):
47
+ dropped.append((name, base))
48
+ else:
49
+ kept.append(name)
50
+
51
+ print("pi_models[%d]{provider,base,verdict}:" % (len(kept) + len(dropped)))
52
+ for n in kept:
53
+ print(' "%s","%s","kept"' % (n, providers[n].get("baseUrl", "")))
54
+ for n, b in dropped:
55
+ print(' "%s","%s","unreachable local seat - pruned"' % (n, b))
56
+
57
+ if dropped and not dry:
58
+ shutil.copy2(cfg, cfg + ".bak-" + time.strftime("%Y%m%d%H%M%S"))
59
+ d["providers"] = {k: v for k, v in providers.items() if k in kept}
60
+ dm = d.get("defaultProvider")
61
+ if dm and dm not in kept and kept:
62
+ d["defaultProvider"] = kept[0]
63
+ ids = (d["providers"][kept[0]].get("models") or [{}])
64
+ if ids and ids[0].get("id"):
65
+ d["defaultModel"] = ids[0]["id"]
66
+ print(" default -> %s / %s" % (d.get("defaultProvider"), d.get("defaultModel")))
67
+ json.dump(d, open(cfg, "w"), indent=2)
68
+ print("pi_models: %d pruned, backup beside the config" % len(dropped))
69
+ elif dry:
70
+ print("pi_models: dry run - nothing written")
71
+ INNER
@@ -18,6 +18,20 @@ FORBID='^(hodd/|svartalfaheim/|state/|data/|workspace/(work|personal|memory|comp
18
18
  # Allowed tracked exceptions inside those roots.
19
19
  ALLOW='^(hodd/(README\.md|\.gitignore|AGENTS\.example\.md|[^/]+\.example(\.md)?)$|svartalfaheim/.*(README\.md|\.gitkeep|\.gitignore|\.example(\.md)?)$|state/\.gitkeep$|data/[^/]+\.example$|assets/reference/state/[^/]+\.example$|\.env\.example|\.env\.sample)$'
20
20
 
21
+ # Tenant DOCUMENTS — a wiki, policy set, vision, roadmap or strategy file is
22
+ # company/operator knowledge, not a public asset. It belongs at
23
+ # $YMIR_HOME/hodd/identity/companies/<company>/ and nowhere in this repo.
24
+ # Added 2026-09-17 after `midgard/company_wiki/` was found sitting in the
25
+ # public tree: it was an empty placeholder, but the shape invited the leak.
26
+ #
27
+ # The match is deliberately a DIRECTORY or a doc suffixed with tenant content —
28
+ # NOT the bare word. `svartalfaheim/examples/SECRETS.md` is a public how-to (it
29
+ # teaches where secrets go and holds no value), so a bare `secrets` match would
30
+ # be a false positive.
31
+ TENANT_DOC='((^|/)(company_wiki|wiki|policies|handbook|playbook|internal|confidential)(/|\.)|(^|/)(vision|strategy|roadmap|policies)[^/]*\.(md|txt|rst|pdf|docx?)$)'
32
+ # Public exceptions: a wiki-shaped name that is a genuine scaffold or doc.
33
+ TENANT_DOC_ALLOW='(\.example$|\.example\.md$|README\.md$|/\.gitkeep$)'
34
+
21
35
  scan_list() {
22
36
  local hit=0 f
23
37
  while IFS= read -r f; do
@@ -25,6 +39,12 @@ scan_list() {
25
39
  if printf '%s\n' "$f" | grep -Eq "$FORBID" && ! printf '%s\n' "$f" | grep -Eq "$ALLOW"; then
26
40
  printf 'private-guard: tracked private path: %s\n' "$f" >&2
27
41
  hit=1
42
+ continue
43
+ fi
44
+ if printf '%s\n' "$f" | grep -Eq "$TENANT_DOC" && ! printf '%s\n' "$f" | grep -Eq "$TENANT_DOC_ALLOW"; then
45
+ printf 'private-guard: tenant/company document in the public tree: %s\n' "$f" >&2
46
+ printf ' company knowledge belongs at $YMIR_HOME/hodd/identity/companies/, never here\n' >&2
47
+ hit=1
28
48
  fi
29
49
  done
30
50
  return "$hit"
@@ -4,12 +4,12 @@
4
4
  # remote. Auth is a REFERENCE (app|pat|ssh|gh), never a value.
5
5
  #
6
6
  # Usage:
7
- # project-git.sh <project-id> [--field host|owner|repo|remote|default_branch|auth]
7
+ # project-git.sh <project-id> [--field host|owner|repo|remote|default_branch|auth|machine]
8
8
  # project-git.sh list
9
9
  # project-git.sh --version
10
10
  set -u
11
11
 
12
- VERSION="1.0.0"
12
+ VERSION="1.2.0"
13
13
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
14
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
15
15
  # shellcheck source=bin/hoard-lib.sh
@@ -26,6 +26,14 @@ while [ $# -gt 0 ]; do case "$1" in --field) FIELD=${2-}; shift 2 ;; *) shift ;;
26
26
 
27
27
  if [ "$ID" = "list" ]; then
28
28
  n=$(grep -cE '^\s*-\s+id:' "$REG" 2>/dev/null || echo 0)
29
+ if [ "$FIELD" = "about" ]; then
30
+ printf 'projects[%s]{id,about}:\n' "$n"
31
+ awk '
32
+ /^[[:space:]]*-[[:space:]]+id:/ { sub(/^[[:space:]]*-[[:space:]]+id:[[:space:]]*/, ""); id=$0 }
33
+ /^[[:space:]]*about:/ { sub(/^[[:space:]]*about:[[:space:]]*"/, ""); sub(/"[[:space:]]*$/, ""); if (id != "") printf " \"%s\",\"%s\"\n", id, $0 }
34
+ ' "$REG"
35
+ exit 0
36
+ fi
29
37
  printf 'projects[%s]{id}:\n' "$n"
30
38
  grep -E '^\s*-\s+id:' "$REG" | sed -E 's/^\s*-\s+id:\s*/ "/; s/\s*$/" /'
31
39
  exit 0
@@ -41,15 +49,21 @@ block="$(awk -v id="$ID" '
41
49
 
42
50
  gitline="$(printf '%s\n' "$block" | grep -m1 'git:')"
43
51
  val() { printf '%s' "$gitline" | sed -nE "s/.*[ {,]?$1:[[:space:]]*([^,}]+).*/\1/p" | tr -d ' '; }
52
+ # non-git top-level keys resolve from the block itself, not the git line
53
+ blockval() { printf '%s' "$block" | sed -nE "s/^[[:space:]]*$1:[[:space:]]*([^,}]+).*/\1/p" | head -1 | tr -d ' '; }
44
54
 
45
55
  host="$(val host)"; owner="$(val owner)"; repo="$(val repo)"; remote="$(val remote)"; br="$(val default_branch)"; auth="$(val auth)"
56
+ machine="$(blockval machine)"; company="$(blockval company)"; workspace="$(blockval workspace)"
57
+ about="$(printf '%s' "$block" | sed -nE 's/^[[:space:]]*about:[[:space:]]*"(.*)"[[:space:]]*$/\1/p' | head -1)"
46
58
  if [ -n "$FIELD" ]; then
47
59
  case "$FIELD" in
48
60
  host) printf '%s\n' "$host" ;; owner) printf '%s\n' "$owner" ;; repo) printf '%s\n' "$repo" ;;
49
61
  remote) printf '%s\n' "$remote" ;; default_branch) printf '%s\n' "$br" ;; auth) printf '%s\n' "$auth" ;;
62
+ machine) printf '%s\n' "$machine" ;; company) printf '%s\n' "$company" ;; workspace) printf '%s\n' "$workspace" ;;
63
+ about) printf '%s\n' "$about" ;;
50
64
  *) printf 'error: unknown field %s\n' "$FIELD" >&2; exit 2 ;;
51
65
  esac
52
66
  exit 0
53
67
  fi
54
- printf 'project[1]{id,host,owner,repo,remote,default_branch,auth}:\n'
55
- printf ' "%s","%s","%s","%s","%s","%s","%s"\n' "$ID" "$host" "$owner" "$repo" "$remote" "$br" "$auth"
68
+ printf 'project[1]{id,host,owner,repo,remote,default_branch,auth,machine,company,workspace}:\n'
69
+ printf ' "%s","%s","%s","%s","%s","%s","%s","%s","%s","%s"\n' "$ID" "$host" "$owner" "$repo" "$remote" "$br" "$auth" "$machine" "$company" "$workspace"
@@ -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
 
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env bash
2
+ # smidja-board.sh — Smíðja's board, by one name.
3
+ #
4
+ # The visualizer can live in two very different trees: a clone's
5
+ # `apps/smidja-factory/apps/visualizer`, or the `@zerwiz/smidja-factory` package
6
+ # a user got from npm. Its database lives in the HOME (`$YMIR_HOME/smidja/smidja.db`),
7
+ # not in either tree. This door resolves all three so the operator never has to.
8
+ #
9
+ # smidja-board.sh build # install deps and build the UI (./dist)
10
+ # smidja-board.sh start # raise the API + UI, detached, ready to answer
11
+ # smidja-board.sh stop
12
+ # smidja-board.sh status
13
+ # smidja-board.sh --version | --help
14
+ #
15
+ # The operator's door is `ymir smidja [build|start|stop|status]` — this is what
16
+ # that door runs.
17
+ #
18
+ # Exit: 0 ok, 1 error, 2 usage.
19
+ set -u
20
+
21
+ VERSION="1.0.0"
22
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23
+ ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
24
+ YMIR_HOME="${YMIR_HOME:-$HOME/Documents/Ymir}"
25
+
26
+ # shellcheck source=bin/hoard-lib.sh
27
+ . "$SCRIPT_DIR/hoard-lib.sh"
28
+ ymir_home_root YMIR_HOME
29
+ hoard_state_dir STATE
30
+ # shellcheck source=bin/smidja-lib.sh
31
+ . "$SCRIPT_DIR/smidja-lib.sh"
32
+
33
+ PORT="${SMIDJA_VIZ_API_PORT:-8437}"
34
+ PID_FILE="$STATE/smidja-viz-api.pid"
35
+ LOG_FILE="$STATE/smidja-viz-api.log"
36
+
37
+ # The database follows the HOME; SMIDJA_DB overrides. A tree-local copy is the
38
+ # last resort, for a checkout that keeps its own.
39
+ SMIDJA_DB_PATH="${SMIDJA_DB:-}"
40
+ if [ -z "$SMIDJA_DB_PATH" ]; then
41
+ for _db in "$YMIR_HOME/smidja/smidja.db" "$ROOT/apps/smidja/smidja_data/smidja.db"; do
42
+ [ -f "$_db" ] && { SMIDJA_DB_PATH="$_db"; break; }
43
+ done
44
+ fi
45
+
46
+ viz_dir() { # <result-var>
47
+ local result_var=${1-} v
48
+ [ -n "$result_var" ] || return 2
49
+ if smidja_visualizer_dir v; then printf -v "$result_var" '%s' "$v"; return 0; fi
50
+ printf -v "$result_var" '%s' ""; return 1
51
+ }
52
+
53
+ alive() { [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE" 2>/dev/null)" 2>/dev/null; }
54
+
55
+ case "${1-}" in
56
+ -v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;;
57
+ -h|--help|"") sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
58
+ esac
59
+ ACTION="$1"
60
+
61
+ VIZ=""
62
+ if ! viz_dir VIZ; then
63
+ printf 'error: the visualizer is not installed in this tree\n'
64
+ printf 'help: npm i -g @zerwiz/ymir (the smithy arrives as @zerwiz/smidja-factory)\n'
65
+ exit 1
66
+ fi
67
+
68
+ case "$ACTION" in
69
+ build)
70
+ if ! command -v bun >/dev/null 2>&1; then
71
+ printf 'error: bun is needed to build the UI\nhelp: bin/prereq-ensure.sh bun\n' >&2; exit 1
72
+ fi
73
+ printf 'visualizer_build[1]{step,state}:\n'
74
+ if [ ! -d "$VIZ/node_modules" ]; then
75
+ if (cd "$VIZ" && bun install >/dev/null 2>&1); then printf ' "deps","installed"\n'
76
+ else printf ' "deps","FAILED — (cd %s && bun install)"\n' "$VIZ" >&2; exit 1; fi
77
+ else printf ' "deps","present"\n'; fi
78
+ if (cd "$VIZ" && bun run build >/dev/null 2>&1) || (cd "$VIZ" && bunx vite build >/dev/null 2>&1); then
79
+ printf ' "ui","%s/dist"\n' "$VIZ"
80
+ else
81
+ printf 'error: the UI build failed\nhelp: (cd %s && bun run build)\n' "$VIZ" >&2; exit 1
82
+ fi
83
+ ;;
84
+ start)
85
+ command -v bun >/dev/null 2>&1 || { printf 'error: bun is needed to run the visualizer\nhelp: bin/prereq-ensure.sh bun\n' >&2; exit 1; }
86
+ [ -n "$SMIDJA_DB_PATH" ] && [ -f "$SMIDJA_DB_PATH" ] || {
87
+ printf 'error: no smidja.db found\nhelp: bin/smidja-bootstrap.sh (creates $YMIR_HOME/smidja/smidja.db)\n' >&2; exit 1; }
88
+ [ -d "$VIZ/dist" ] || printf 'note: the UI is unbuilt — the API will answer and show no interface\nnote: mend it with: bin/ymir-visualizer.sh build\n' >&2
89
+ if alive; then
90
+ printf 'visualizer[1]{state,pid,url}:\n "already running","%s","http://127.0.0.1:%s/"\n' "$(cat "$PID_FILE")" "$PORT"
91
+ exit 0
92
+ fi
93
+ mkdir -p "$STATE"
94
+ # ymir_detach PRINTS the pid it spawned (setsid/nohup is its business, not
95
+ # ours); reading $! here would read a variable the helper already consumed.
96
+ pid=""
97
+ if command -v ymir_detach >/dev/null 2>&1; then
98
+ pid="$(ymir_detach env CMD_DB="$SMIDJA_DB_PATH" PORT="$PORT" bun run "$VIZ/server/index.ts" 2>>"$LOG_FILE")"
99
+ fi
100
+ if [ -z "$pid" ]; then
101
+ ( cd "$VIZ" && exec env CMD_DB="$SMIDJA_DB_PATH" PORT="$PORT" bun run server/index.ts ) >>"$LOG_FILE" 2>&1 &
102
+ pid="$!"
103
+ fi
104
+ [ -n "$pid" ] && printf '%s\n' "$pid" >"$PID_FILE"
105
+ for _i in $(seq 1 20); do
106
+ curl -s -o /dev/null --max-time 1 "http://127.0.0.1:$PORT/api/health" 2>/dev/null && break
107
+ sleep 0.5
108
+ done
109
+ if curl -s --max-time 2 "http://127.0.0.1:$PORT/api/health" >/dev/null 2>&1; then
110
+ printf 'visualizer[1]{state,db,url}:\n "up","%s","http://127.0.0.1:%s/"\n' "$SMIDJA_DB_PATH" "$PORT"
111
+ else
112
+ printf 'error: the visualizer did not answer on :%s\nhelp: see %s\n' "$PORT" "$LOG_FILE" >&2; exit 1
113
+ fi
114
+ ;;
115
+ stop)
116
+ if alive; then
117
+ kill "$(cat "$PID_FILE")" 2>/dev/null; rm -f "$PID_FILE"
118
+ printf 'visualizer[1]{state}:\n "stopped"\n'
119
+ else
120
+ rm -f "$PID_FILE"
121
+ printf 'visualizer[1]{state}:\n "not running"\n'
122
+ fi
123
+ ;;
124
+ status)
125
+ local_db="${SMIDJA_DB_PATH:-none}"
126
+ if alive && curl -s --max-time 2 "http://127.0.0.1:$PORT/api/health" >/dev/null 2>&1; then
127
+ printf 'visualizer[3]{state,pid,db,url}:\n "up","%s","%s","http://127.0.0.1:%s/"\n' "$(cat "$PID_FILE")" "$local_db" "$PORT"
128
+ elif alive; then
129
+ printf 'visualizer[2]{state,pid,note}:\n "starting or wedged","%s","answers nothing on :%s — see %s"\n' "$(cat "$PID_FILE")" "$PORT" "$LOG_FILE"
130
+ exit 1
131
+ else
132
+ printf 'visualizer[3]{state,tree,db,ui}:\n "down","%s","%s","%s"\n' "$VIZ" "$local_db" "$([ -d "$VIZ/dist" ] && echo built || echo unbuilt)"
133
+ fi
134
+ ;;
135
+ *) printf 'error: unknown action %s\nhelp: bin/smidja-board.sh [build|start|stop|status]\n' "$ACTION" >&2; exit 2 ;;
136
+ esac
@@ -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")
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bash
2
+ # smidja-lib.sh — where the smithy's parts actually live.
3
+ #
4
+ # Two installs, one name. In a clone the smithy sits at `apps/smidja-factory`
5
+ # (the registry's `repo: apps/<path>` block clones it there), and
6
+ # `.agents/skills/smidja-factory` is a symlink to it. In a packaged install the
7
+ # tree has no `apps/` at all: the smithy arrives as a dependency, at
8
+ # `node_modules/@zerwiz/smidja-factory`, and that symlink dangles.
9
+ #
10
+ # Every script that needs the visualizer resolves it HERE, so the two shapes can
11
+ # never disagree (Rule 07). Source-safe; defines functions only.
12
+ #
13
+ # smidja_factory_dir <result-var> the smithy: skill, skills/, templates/, apps/
14
+ # smidja_visualizer_dir <result-var> the board: its server, its UI source, its dist
15
+ set -u
16
+
17
+ # A surface has a name the operator knows and a package has a name npm serves;
18
+ # they are not always the same, and the smithy is the pair that proves it.
19
+ SMIDJA_SURFACE="smidja"
20
+ SMIDJA_PACKAGE="smidja-factory"
21
+
22
+ smidja_factory_dir() { # <result-var> — the smithy's directory, or empty
23
+ local result_var=${1-} root="${YMIR_ROOT_DIR:-}" c
24
+ [ -n "$result_var" ] || return 2
25
+ if [ -z "$root" ]; then
26
+ root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
27
+ fi
28
+ for c in \
29
+ "$root/apps/$SMIDJA_PACKAGE" \
30
+ "$root/node_modules/@zerwiz/$SMIDJA_PACKAGE" \
31
+ "$root/.agents/skills/$SMIDJA_PACKAGE"
32
+ do
33
+ # A dangling symlink is not a home: the visualizer must be readable there.
34
+ [ -d "$c/apps/visualizer" ] && { printf -v "$result_var" '%s' "$c"; return 0; }
35
+ done
36
+ printf -v "$result_var" '%s' ""
37
+ return 1
38
+ }
39
+
40
+ smidja_visualizer_dir() { # <result-var> — the visualizer (server · UI source · dist)
41
+ local result_var=${1-} factory
42
+ [ -n "$result_var" ] || return 2
43
+ if smidja_factory_dir factory; then
44
+ printf -v "$result_var" '%s' "$factory/apps/visualizer"
45
+ return 0
46
+ fi
47
+ printf -v "$result_var" '%s' ""
48
+ return 1
49
+ }
@@ -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 ;;