@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
@@ -25,6 +25,11 @@ set -u
25
25
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
26
26
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
27
27
  CHANGELOG="CHANGELOG.md"
28
+ # A change may tell its story either way: an entry appended to the ledger, or a
29
+ # fragment in CHANGELOG.d/ that the assembler will fold in. The fragment is the
30
+ # preferred form — two branches never touch the same fragment file, so the duty
31
+ # is met without the O(N^2) conflict every branch appending to one file causes.
32
+ FRAGMENT_DIR="CHANGELOG.d"
28
33
  ZERO="0000000000000000000000000000000000000000"
29
34
 
30
35
  usage() {
@@ -32,7 +37,8 @@ usage() {
32
37
  exit 2
33
38
  }
34
39
 
35
- # changes_touch_changelog <base> <head> — exit 0 when CHANGELOG.md differs
40
+ # changes_touch_changelog <base> <head> — exit 0 when the change TELLS ITS STORY,
41
+ # either by appending to CHANGELOG.md or by adding a CHANGELOG.d/ fragment.
36
42
  changes_touch_changelog() {
37
43
  local base=${1-} head=${2-} f
38
44
  [ -n "$head" ] || return 1
@@ -40,9 +46,16 @@ changes_touch_changelog() {
40
46
  # No base to compare against (root commit range): look at every commit.
41
47
  f="$(git -C "$ROOT" ls-tree -r --name-only "$head" -- "$CHANGELOG" 2>/dev/null)"
42
48
  [ -n "$f" ] && return 0
49
+ f="$(git -C "$ROOT" ls-tree -r --name-only "$head" -- "$FRAGMENT_DIR" 2>/dev/null | grep -v '/README.md$' | grep '\.md$')"
50
+ [ -n "$f" ] && return 0
43
51
  return 1
44
52
  fi
45
53
  git -C "$ROOT" diff --name-only "$base" "$head" -- "$CHANGELOG" 2>/dev/null | grep -q . && return 0
54
+ # A fragment ADDED in the range satisfies the duty. A fragment merely deleted
55
+ # (the assembler folding it into the ledger) does not count on its own — but in
56
+ # that same push the ledger changed, which the test above already caught.
57
+ git -C "$ROOT" diff --name-only --diff-filter=A "$base" "$head" -- "$FRAGMENT_DIR" 2>/dev/null \
58
+ | grep -v '/README.md$' | grep -q '\.md$' && return 0
46
59
  return 1
47
60
  }
48
61
 
@@ -90,15 +103,22 @@ verdict() { # <ok|blocked>
90
103
  cat >&2 <<'EOF'
91
104
  changelog-guard: blocked — this push carries no CHANGELOG entry.
92
105
 
93
- Every change that ships is a change that was told. Append an entry to
94
- CHANGELOG.md under the current date (append-only — never rewrite an
95
- existing entry; RULES/06-append-only.md), commit it, and push again.
106
+ Every change that ships is a change that was told. Tell it either way:
107
+
108
+ PREFERRED add a fragment (no conflict with any other branch):
109
+ CHANGELOG.d/<YYYY-MM-DD>-<slug>.md starting with "## YYYY-MM-DD — title"
110
+ bin/changelog-assemble.sh folds it into CHANGELOG.md
111
+
112
+ OR — append an entry to CHANGELOG.md under the current date
113
+ (append-only — never rewrite an existing entry; RULES/06-append-only.md).
114
+
115
+ Commit it, and push again.
96
116
 
97
117
  Deliberate override, when an entry truly does not belong:
98
118
  YMIR_SKIP_CHANGELOG_GUARD=1 git push ...
99
119
  EOF
100
- printf 'error: changelog-guard blocked the push (no CHANGELOG.md change in range)\n' >&2
101
- printf 'help: append a CHANGELOG entry, or set YMIR_SKIP_CHANGELOG_GUARD=1\n' >&2
120
+ printf 'error: changelog-guard blocked the push (no CHANGELOG entry or fragment in range)\n' >&2
121
+ printf 'help: add a CHANGELOG.d/ fragment (preferred), or append to CHANGELOG.md; override with YMIR_SKIP_CHANGELOG_GUARD=1\n' >&2
102
122
  exit 1
103
123
  }
104
124
 
@@ -111,12 +131,24 @@ case "${1:-}" in
111
131
  cat >"$hook" <<EOF
112
132
  #!/usr/bin/env bash
113
133
  # pre-push hook — the delivery gate. Installed by bin/changelog-guard.sh --install.
134
+ # 0. changelog-assemble — fold CHANGELOG.d/ fragments into CHANGELOG.md, so the
135
+ # ledger is never behind what the fragments already tell. Folding is an
136
+ # append: existing entries are copied verbatim, never reordered.
114
137
  # 1. branch-guard — Rule 08: never push a protected branch
115
- # 2. changelog-guard — every push carries a CHANGELOG entry
138
+ # 2. changelog-guard — every push carries a CHANGELOG entry (or a fragment)
116
139
  set -u
117
140
  refs="\$(mktemp)"
118
141
  trap 'rm -f "\$refs"' EXIT
119
142
  cat >"\$refs"
143
+ if [ -x "$SCRIPT_DIR/changelog-assemble.sh" ]; then
144
+ "$SCRIPT_DIR/changelog-assemble.sh" >/dev/null 2>&1 || true
145
+ # If folding changed the ledger, it is now a working-tree change the push does
146
+ # not carry. Say so loudly rather than pushing a ledger the fragments are not in.
147
+ if ! git -C "$ROOT" diff --quiet -- "$CHANGELOG" 2>/dev/null; then
148
+ printf 'changelog-assemble[1]{state}:\n "folded fragments into %s — commit it, then push again"\n' "$CHANGELOG" >&2
149
+ exit 1
150
+ fi
151
+ fi
120
152
  "$SCRIPT_DIR/branch-guard.sh" <"\$refs" || exit 1
121
153
  "$SCRIPT_DIR/changelog-guard.sh" <"\$refs" || exit 1
122
154
  exit 0
@@ -20,7 +20,20 @@ set -u
20
20
  VERSION="1.0.0"
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
  SEEN="$STATE_DIR/crash-seen.json"
25
38
  QUIET=0; NOTIFY=1
26
39
 
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env bash
2
+ # design-check.sh — one identity, two renderers: they must agree.
3
+ #
4
+ # The design language is the landing page's cloth (stone · bone · bronze ·
5
+ # blood). It is carried in TWO places, on purpose:
6
+ #
7
+ # midgard/design-system/tokens.css the CSS variables every web surface imports
8
+ # apps/sessrumnir/…/themes/fensalir.json the seven SEEDS the seat derives ~40
9
+ # tokens from (the reference form)
10
+ #
11
+ # Two carriers of one identity drift the moment someone edits one. This is the
12
+ # guard: the same value must appear in both, or the build says so.
13
+ #
14
+ # Usage:
15
+ # bin/design-check.sh # compare, TOON, exit 0/1
16
+ # bin/design-check.sh --fix-hint
17
+ set -u
18
+
19
+ VERSION="1.0.0"
20
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
+ ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
22
+ TOKENS="$ROOT/midgard/design-system/tokens.css"
23
+ SEEDS="$ROOT/apps/sessrumnir/src/renderer/src/themes/fensalir.json"
24
+
25
+ case "${1-}" in
26
+ -v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;;
27
+ -h|--help) sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
28
+ esac
29
+
30
+ [ -r "$TOKENS" ] || { printf 'error: tokens not found: %s\n' "${TOKENS#"$ROOT"/}" >&2; exit 1; }
31
+ # The seat's cloth lives in the Sessrúmnir app repo (the apps split moved it out
32
+ # of the monorepo; `step_apps` clones it). In a fresh clone or a worktree it is
33
+ # absent until that step runs — so an absent seat is a clean SKIP, not a failure.
34
+ # Two carriers can only be compared when both are present; demanding the app's
35
+ # file would fail every worktree for a reason that is not drift.
36
+ if [ ! -r "$SEEDS" ]; then
37
+ printf 'design[1]{state,reason}:\n'
38
+ printf ' "SKIP","the seat'"'"'s cloth is app-provided (%s) — run bin/ymir-install.sh step_apps"\n' "${SEEDS#"$ROOT"/}"
39
+ exit 0
40
+ fi
41
+
42
+ # the pairs that MUST agree: CSS token <-> seed
43
+ python3 - "$TOKENS" "$SEEDS" <<'PY'
44
+ import json, re, sys
45
+
46
+ tokens_path, seeds_path = sys.argv[1], sys.argv[2]
47
+ css = open(tokens_path).read()
48
+ seeds = json.load(open(seeds_path))["seeds"]
49
+
50
+ def token(name):
51
+ m = re.search(r"--%s:\s*([^;]+);" % re.escape(name), css)
52
+ return m.group(1).strip() if m else None
53
+
54
+ PAIRS = [
55
+ ("ymir-bg-0", "app", "the ground"),
56
+ ("ymir-bg-1", "surface", "the panel"),
57
+ ("ymir-text-0", "text", "the bone"),
58
+ ("ymir-cyan-1", "accent", "the bronze"),
59
+ ("ymir-ok", "success", "the metal proved true"),
60
+ ("ymir-warn", "warning", "hot, heed it"),
61
+ ("ymir-danger", "error", "the wound"),
62
+ ]
63
+
64
+ bad = []
65
+ print("design[%d]{css_token,seed,value,agreement}:" % len(PAIRS))
66
+ for tok, seed, label in PAIRS:
67
+ a, b = token(tok), (seeds.get(seed) or "").lower()
68
+ ok = a is not None and a.lower() == b
69
+ if not ok:
70
+ bad.append((tok, seed, a, b))
71
+ print(' "%s","%s","%s","%s"' % (tok, seed, a or "-", "ok" if ok else "MISMATCH (seed %s)" % (b or "-")))
72
+
73
+ if bad:
74
+ print("error: the two carriers of the cloth disagree", file=sys.stderr)
75
+ for tok, seed, a, b in bad:
76
+ print(" --%s = %s ≠ seeds.%s = %s" % (tok, a, seed, b), file=sys.stderr)
77
+ print("help: change the value in midgard/design-system/tokens.css AND fensalir.json, or neither", file=sys.stderr)
78
+ sys.exit(1)
79
+ print("design-check: the cloth is one — %d pairs agree" % len(PAIRS))
80
+ PY
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env bash
2
+ # design-icon.sh — every app wears its own rune.
3
+ #
4
+ # The icon set is runecoded (midgard/design-system/icons.md): an Elder Futhark
5
+ # rune, stroked at the chisel bevel, tinted by the app's HOUSE colour. Not a
6
+ # generic globe, not an emoji, not a screenshot of a UI.
7
+ #
8
+ # bin/design-icon.sh list # the app -> rune -> house mapping
9
+ # bin/design-icon.sh mint [app] # write <app>/public/icon.svg (+ the link hint)
10
+ # bin/design-icon.sh mint --all
11
+ #
12
+ # The tile is stone (--ymir-bg-0) with the rune in the house tint; the SVG is
13
+ # self-contained, so a favicon needs no build step and no raster asset.
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
+ ICONS="$ROOT/midgard/design-system/icons"
20
+ STONE="#0e0c09"
21
+
22
+ case "${1-}" in
23
+ -v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;;
24
+ -h|--help|"") sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
25
+ esac
26
+ ACTION="${1:-list}"; shift || true
27
+
28
+ # dir|glyph|tint|says|icon-name|exec|wm-class
29
+ # FIVE UI surfaces, FIVE DIFFERENT glyphs — no app borrows another's rune.
30
+ APPS=(
31
+ "apps/hlidskjalf|ehwaz|#c9973f|the seat — Hlidskjalf, the high seat of the control plane|ymir-hlidskjalf|scripts/electron.sh start --view hlidskjalf|ymir-hlidskjalf|Ymir · Hlidskjalf"
32
+ "apps/hlidskjalf-mobile|raidho|#c9973f|the road — the seat carried, Hlidskjalf on a phone|ymir-hlidskjalf-mobile|scripts/electron.sh start --view hlidskjalf|ymir-hlidskjalf-mobile|Ymir · Hlidskjalf Mobile"
33
+ "apps/odrerir|ansuz|#c9973f|the mead — Óðrerir, the Live Hall (Odin's breath, inspiration)|ymir-odrerir|scripts/electron.sh start --view odrerir|ymir-odrerir|Ymir · Óðrerir"
34
+ "apps/sessrumnir|othala|#c9973f|the hall — Sessrúmnir, the seat that holds the cloth|ymir-sessrumnir|bin/sessrumnir.sh start|ymir-sessrumnir|Ymir · Sessrúmnir"
35
+ "apps/smidja-factory/apps/visualizer|kaunan|#f59e0b|the torch — the forge's eye, Smíðja's trace|ymir-smidja|scripts/electron.sh start --view smidja|ymir-smidja|Ymir · Smíðja"
36
+ )
37
+
38
+ list() {
39
+ printf 'app_icons[%d]{app,rune,tint,says}:\n' "${#APPS[@]}"
40
+ for row in "${APPS[@]}"; do
41
+ IFS='|' read -r dir glyph tint says icon _exec _klass _name <<<"$row"
42
+ # The surface's name is the one the desktop knows it by (ymir-smidja →
43
+ # smidja), never the directory's leaf — a path is not a name (the naming law).
44
+ surface="${icon#ymir-}"
45
+ printf ' "%s","%s","%s","%s"\n' "$surface" "$glyph" "$tint" "$says"
46
+ done
47
+ printf 'runes[6]{glyph,name,meaning}:\n'
48
+ printf ' "algiz","ᛉ","the Ymir emblem — the platform itself"\n'
49
+ printf ' "ehwaz","ᛖ","horse/journey — the seat and its transit"\n'
50
+ printf ' "ansuz","ᚨ","Odin'"'"'s breath — the forge"\n'
51
+ printf ' "sowilo","ᛊ","sun — light on the tree"\n'
52
+ printf ' "valhalla","ᚹ","the hall"\n'
53
+ printf ' "gjallarhorn","ᚷ","the horn — the tunnel"\n'
54
+ }
55
+
56
+ mint() { # <app-dir> <glyph> <tint> <label>
57
+ local dir=$1 glyph=$2 tint=$3 label=$4
58
+ local g="$ICONS/$glyph.svg"
59
+ [ -r "$g" ] || { printf 'error: no glyph %s in midgard/design-system/icons/\nhelp: bin/design-icon.sh list\n' "$glyph" >&2; return 1; }
60
+ local path
61
+ path="$(sed -n 's/.*<path d="\([^"]*\)".*/\1/p' "$g" | head -1)"
62
+ [ -n "$path" ] || { printf 'error: %s carries no <path> to reuse\n' "$glyph" >&2; return 1; }
63
+ local outdir="$ROOT/$dir/public"
64
+ [ -d "$(dirname "$outdir")" ] || { printf ' "%s","SKIP (no such app)"\n' "$dir"; return 0; }
65
+ mkdir -p "$outdir"
66
+ {
67
+ printf '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" role="img" aria-label="%s">\n' "$label"
68
+ printf ' <title>%s</title>\n' "$label"
69
+ printf ' <rect width="32" height="32" rx="7" fill="%s"/>\n' "$STONE"
70
+ printf ' <g transform="translate(4 4) scale(1.0)" fill="none" stroke="%s" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter">\n' "$tint"
71
+ printf ' <path d="%s"/>\n' "$path"
72
+ printf ' </g>\n</svg>\n'
73
+ } >"$outdir/icon.svg"
74
+ printf ' "%s","%s","%s","<link rel=\\"icon\\" href=\\"/icon.svg\\" type=\\"image/svg+xml\\">"\n' \
75
+ "$(basename "$dir")" "$glyph" "${outdir#"$ROOT"/}/icon.svg"
76
+ }
77
+
78
+
79
+ # ── install into the user's system ───────────────────────────────────────────
80
+ # An icon that only exists in the checkout is an icon the operator cannot pin.
81
+ # This writes the rune into the user's icon theme and a .desktop entry per app,
82
+ # rendered with THIS machine's root (never a hardcoded home), so every app is
83
+ # dockable and carries the house mark. Idempotent.
84
+ # The DESKTOP the operator actually sees, not whatever a session happened to
85
+ # export: an agent harness may set XDG_DATA_HOME to a sandbox (this one does),
86
+ # and an icon written there is an icon nobody can pin. YMIR_DESKTOP_DATA_HOME
87
+ # exists for a test that genuinely wants a throwaway target.
88
+ data_home="${YMIR_DESKTOP_DATA_HOME:-$HOME/.local/share}"
89
+ icons_dir="$data_home/icons/hicolor/scalable/apps"
90
+ apps_dir="$data_home/applications"
91
+ # app | entry name | Exec | StartupWMClass
92
+ ENTRIES=(
93
+ "hlidskjalf|Ymir · Hlidskjalf|scripts/electron.sh start --view hlidskjalf|ymir-hlidskjalf"
94
+ "odrerir|Ymir · Óðrerir|scripts/electron.sh start --view odrerir|ymir-odrerir"
95
+ "sessrumnir|Ymir · Sessrúmnir|bin/sessrumnir.sh start|ymir-sessrumnir"
96
+ "visualizer|Ymir · Smíðja|scripts/electron.sh start --view smidja|ymir-smidja"
97
+ "hlidskjalf-mobile|Ymir · Hlidskjalf Mobile|scripts/electron.sh start --view hlidskjalf|ymir-hlidskjalf-mobile"
98
+ )
99
+ install_all() {
100
+ # A phone app gets an icon for its own bundle, never a desktop entry.
101
+ local EXCLUDE=" hlidskjalf-mobile "
102
+
103
+ mkdir -p "$icons_dir" "$apps_dir" || { printf 'error: cannot write %s / %s\n' "$icons_dir" "$apps_dir" >&2; exit 1; }
104
+ printf 'installed[%d]{app,icon,entry}:\n' "${#APPS[@]}"
105
+ local row dir glyph tint says iconname exec klass src
106
+ for row in "${APPS[@]}"; do
107
+ IFS='|' read -r dir glyph tint says iconname exec klass display <<<"$row"
108
+ case "$EXCLUDE" in *" $(basename "$dir") "*) continue ;; esac
109
+ src="$ROOT/$dir/public/icon.svg"
110
+ if [ ! -f "$src" ]; then
111
+ # mint it first — the icon is defined by this table, so it is never absent
112
+ if [ -r "$ICONS/$glyph.svg" ]; then
113
+ mkdir -p "$(dirname "$src")" 2>/dev/null
114
+ path="$(sed -n 's/.*<path d="\([^"]*\)".*/\1/p' "$ICONS/$glyph.svg" | head -1)"
115
+ { printf '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" role="img" aria-label="%s">\n' "$says"
116
+ printf ' <title>%s</title>\n <rect width="32" height="32" rx="7" fill="%s"/>\n' "$says" "$STONE"
117
+ printf ' <g transform="translate(4 4)" fill="none" stroke="%s" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter">\n' "$tint"
118
+ printf ' <path d="%s"/>\n </g>\n</svg>\n' "$path"
119
+ } >"$src" 2>/dev/null || true
120
+ fi
121
+ fi
122
+ cp -f "$src" "$icons_dir/$iconname.svg" 2>/dev/null || { printf ' "%s","FAILED","-","no icon at %s"\n' "$dir" "${src#"$ROOT"/}"; continue; }
123
+ {
124
+ printf '[Desktop Entry]\nType=Application\nVersion=1.0\n'
125
+ printf 'Name=%s\n' "$display"
126
+ printf 'Comment=%s\n' "$says"
127
+ printf 'Exec=bash %s/%s\n' "$ROOT" "$exec"
128
+ printf 'Icon=%s\n' "$iconname"
129
+ printf 'Terminal=false\nCategories=Development;Utility;\n'
130
+ printf 'StartupWMClass=%s\nStartupNotify=true\n' "$klass"
131
+ } >"$apps_dir/$iconname.desktop"
132
+ printf ' "%s","%s.svg","%s.desktop"\n' "$iconname" "$iconname" "$iconname"
133
+ done
134
+ [ -f "$apps_dir/ymir-smidja.desktop" ] && rm -f "$apps_dir/ymir-smidja.desktop"
135
+ for stale in ymir-hlidskjalf-mobile; do rm -f "$apps_dir/$stale.desktop" "$icons_dir/$stale.svg"; done
136
+ command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$apps_dir" >/dev/null 2>&1 || true
137
+ }
138
+
139
+ case "$ACTION" in
140
+ list) list ;;
141
+ install) install_all ;;
142
+ mint)
143
+ if [ "${1:-}" = "--all" ]; then
144
+ printf 'minted[%d]{app,rune,file,link}:\n' "${#APPS[@]}"
145
+ for row in "${APPS[@]}"; do IFS='|' read -r d g t l _i _e _k _n <<<"$row"; mint "$d" "$g" "$t" "$l"; done
146
+ else
147
+ want="${1:-}"; found=0
148
+ printf 'minted[1]{app,rune,file,link}:\n'
149
+ for row in "${APPS[@]}"; do
150
+ IFS='|' read -r d g t l _i _e _k _n <<<"$row"
151
+ case "$(basename "$d")" in "$want") mint "$d" "$g" "$t" "$l"; found=1 ;; esac
152
+ done
153
+ [ "$found" = 1 ] || { printf 'error: unknown app %s\nhelp: bin/design-icon.sh list\n' "$want" >&2; exit 2; }
154
+ fi ;;
155
+ *) printf 'error: unknown action %s\nhelp: bin/design-icon.sh [list|mint] [app|--all]\n' "$ACTION" >&2; exit 2 ;;
156
+ esac
package/bin/docs-guard.sh CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  # docs-guard.sh — docs/ is the PUBLIC, user-facing tree. Block operator-private
3
3
  # documents (plans, strategy, roadmaps, the masterplan) before they are staged.
4
- # Those live at $YMIR_HOME/docs/ (Rule 04-hoard.md).
4
+ # Those live at $YMIR_HOME/hodd/docs/ (Rule 04-hoard.md).
5
5
  #
6
6
  # bin/docs-guard.sh # scan the staged change (used as pre-commit)
7
7
  # bin/docs-guard.sh --all # scan every tracked file
@@ -35,5 +35,5 @@ if [ "${1:-}" = "--all" ]; then
35
35
  scan_list < <(git -C "$ROOT" ls-files)
36
36
  else
37
37
  scan_list < <(git -C "$ROOT" diff --cached --name-only --diff-filter=ACM)
38
- fi || { printf 'docs-guard: blocked — docs/ is public; move it to $YMIR_HOME/docs/ (Rule 04)\n' >&2; exit 1; }
38
+ fi || { printf 'docs-guard: blocked — docs/ is public; move it to $YMIR_HOME/hodd/docs/ (Rule 04)\n' >&2; exit 1; }
39
39
  exit 0
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env bash
2
- # editor-place.sh — the editor opens on its OWN desktop, never on yours.
2
+ # editor-place.sh — the editor is UNMANAGED by default: it opens on the
3
+ # desktop you are on, focused and clickable, like any other app.
3
4
  #
4
- # The Allfather reads and works on a desktop. An editor that maps on top of that
5
- # steals the place he was in. So the editor gets a desktop of its own, exactly as
6
- # the Ymir apps do (bin/desktop-place.sh) — and it uses the SAME mechanism, which
7
- # is the one proven on this machine:
5
+ # It CAN pin the editor to its own desktop never on yours the same way the
6
+ # Ymir apps do (bin/desktop-place.sh), the mechanism proven on this machine:
7
+ # YMIR_EDITOR_PIN=1 bin/editor-place.sh apply
8
+ # The pin is opt-in because the old always-on behavior made the editor
9
+ # un-focusable (no_focus) and dropped it onto a different desktop, which the
10
+ # Allfather found unusable.
8
11
  #
9
12
  # Hyprland applies window rules at MAP time, so the window never appears on the
10
13
  # working desktop at all. Writing `o.window({ class = ... }, { workspace = ... })`
@@ -22,6 +25,7 @@
22
25
  # bin/editor-place.sh --version
23
26
  #
24
27
  # Env: YMIR_EDITOR_CLASS (default: the resolved editor, else `code`)
28
+ # YMIR_EDITOR_PIN (1 = pin the editor to its own desktop; default unmanaged)
25
29
  set -u
26
30
 
27
31
  VERSION="1.0.0"
@@ -142,12 +146,13 @@ write_rule() { # <desktop>
142
146
  done
143
147
  )"
144
148
  fi
145
- local editorline
146
- # `no_focus = true` is Omarchy's own idiom (default/hypr/windows.lua) for mapping
147
- # a window without stealing focus. `workspace` sends it to its own desktop; the
148
- # pair means the editor appears where it belongs and the Allfather keeps the
149
- # desk he was reading at.
150
- editorline="$(printf 'o.window({ class = "^%s$" }, { workspace = "%s", no_focus = true })\n' "$cls" "$d")"
149
+ local editorline=""
150
+ # Unmanaged by default: the editor opens on the active desktop, focused and
151
+ # clickable. Only pin it away (the old `no_focus` behavior) when the operator
152
+ # opts in with YMIR_EDITOR_PIN=1.
153
+ if [ "${YMIR_EDITOR_PIN:-0}" = "1" ]; then
154
+ editorline="$(printf 'o.window({ class = "^%s$" }, { workspace = "%s", no_focus = true })\n' "$cls" "$d")"
155
+ fi
151
156
  if [ "$DRY" = 1 ]; then
152
157
  printf '%s' "$header"
153
158
  [ -n "$applines" ] && printf '%s\n' "$applines"
@@ -174,8 +179,13 @@ include_rule_file() {
174
179
 
175
180
  case "$ACTION" in
176
181
  plan)
177
- cls="$(editor_class)"; d="$(plan_desktop)"
178
- printf 'editor-place[1]{class,desktop}:\n "%s",%s\n' "$cls" "$d"
182
+ cls="$(editor_class)"
183
+ if [ "${YMIR_EDITOR_PIN:-0}" = "1" ]; then
184
+ d="$(plan_desktop)"
185
+ printf 'editor-place[1]{class,desktop}:\n "%s",%s\n' "$cls" "$d"
186
+ else
187
+ printf 'editor-place[1]{class,desktop}:\n "%s","unmanaged (YMIR_EDITOR_PIN=1 to pin)"\n' "$cls"
188
+ fi
179
189
  ;;
180
190
  apply)
181
191
  have hyprctl || { printf 'editor-place[1]{state}:\n "skipped — no hyprctl (not a Hyprland host)"\n'; exit 0; }
@@ -190,9 +200,17 @@ case "$ACTION" in
190
200
  printf 'editor-place[1]{state,desktop,configerrors}:\n "written, but Hyprland reported errors","%s","%s"\n' "$d" "$(printf '%s' "$errs" | tr '\n' ' ')"
191
201
  exit 1
192
202
  fi
193
- printf 'editor-place[1]{state,class,desktop}:\n "written","%s","%s"\n' "$(editor_class)" "$d"
203
+ if [ "${YMIR_EDITOR_PIN:-0}" = "1" ]; then
204
+ printf 'editor-place[1]{state,class,desktop}:\n "written","%s","%s"\n' "$(editor_class)" "$d"
205
+ else
206
+ printf 'editor-place[1]{state,class}:\n "written (editor unmanaged)","%s"\n' "$(editor_class)"
207
+ fi
194
208
  else
195
- printf 'editor-place[1]{state,desktop}:\n "dry-run","%s"\n' "$d"
209
+ if [ "${YMIR_EDITOR_PIN:-0}" = "1" ]; then
210
+ printf 'editor-place[1]{state,desktop}:\n "dry-run","%s"\n' "$d"
211
+ else
212
+ printf 'editor-place[1]{state}:\n "dry-run (editor unmanaged)"\n'
213
+ fi
196
214
  fi
197
215
  ;;
198
216
  status)
@@ -11,7 +11,20 @@
11
11
  set -u
12
12
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
13
  ROOT="${BROKK_ROOT_OVERRIDE:-$(cd "$SCRIPT_DIR/.." && pwd)}"
14
- STATE="${BROKK_STATE_OVERRIDE:-$ROOT/state}"
14
+
15
+ # The roots that live OUTSIDE the code tree: this machine's records and the
16
+ # runtime state belong to the home the operator chose at installation, never in
17
+ # the tree — a packaged install replaces its tree on upgrade (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_state_dir YMIR_STATE_DIR
26
+ hoard_data_dir YMIR_DATA_DIR
27
+ STATE="${BROKK_STATE_OVERRIDE:-$YMIR_STATE_DIR}"
15
28
  AGENT="${1:-}"
16
29
  WORKTREE="${2:-}"
17
30
  [ -n "$AGENT" ] || { echo "usage: eindri-acclaim.sh <agent> [<worktree-root>]" >&2; exit 2; }
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env bash
2
+ # eindri-seat.sh — seat a worker and PROVE it, in one move.
3
+ #
4
+ # The lesson from firstmate (bin/fm-spawn.sh + fm-send.sh + fm-peek.sh): a spawn
5
+ # is not a seat until the brief is delivered AND the agent has been read. Ymir
6
+ # had ported only the launcher, so a seat could be reported while the worker sat
7
+ # idle — "sent but never started". This is the missing half, in our idiom.
8
+ #
9
+ # bin/eindri-seat.sh <name> --role <role> [--model <model>] -- "<brief>"
10
+ #
11
+ # It seats through bin/herdr-run.sh (the house door, so the model->harness rule
12
+ # and the pane bookkeeping stay in one place), waits for the pane to settle,
13
+ # reads it, and reports what the agent ACTUALLY is:
14
+ #
15
+ # state: working — it has begun (exit 0)
16
+ # state: idle — seated but nothing ran (exit 3: a seat, not an errand)
17
+ # state: error — the pane shows a refusal (the 403/1010 class) (exit 1)
18
+ #
19
+ # Nothing is claimed that the pane does not show.
20
+ set -u
21
+
22
+ VERSION="1.0.0"
23
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24
+ ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
25
+ SETTLE="${YMIR_SEAT_SETTLE:-35}"
26
+
27
+ case "${1-}" in
28
+ -v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;;
29
+ -h|--help|"") sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
30
+ esac
31
+
32
+ NAME="${1:-}"; shift || true
33
+ [ -n "$NAME" ] || { printf 'error: a name is required\nhelp: bin/eindri-seat.sh <name> --role <role> -- "<brief>"\n' >&2; exit 2; }
34
+
35
+ # hand the rest to the house door, which owns the harness rule and the pane log
36
+ SEAT_OUT="$("$SCRIPT_DIR/herdr-run.sh" eindri "$NAME" "$@" 2>&1)"
37
+ RC=$?
38
+ printf '%s\n' "$SEAT_OUT"
39
+
40
+ # The pane id is the door's own answer; without it there is nothing to read.
41
+ # The door answers with the TAB; the pane must be resolved from it (its row
42
+ # carries the eindri's name in the pane field, which is how I once read a name
43
+ # as a pane and reported an empty tail as an idle agent).
44
+ TAB="$(printf '%s\n' "$SEAT_OUT" | sed -n 's/.*","\(w[0-9]*:t[0-9A-Za-z]*\)","[a-z]*".*/\1/p' | head -1)"
45
+ PANE=""
46
+ if [ -n "$TAB" ] && [ -x "${HERDR_BIN_PATH:-}" ]; then
47
+ PANE="$("${HERDR_BIN_PATH}" pane list 2>/dev/null | python3 -c "
48
+ import json,sys
49
+ try:
50
+ d=json.load(sys.stdin)
51
+ except Exception:
52
+ raise SystemExit
53
+ tab=sys.argv[1] if len(sys.argv)>1 else ''
54
+ for p in d.get('result',{}).get('panes',[]) or []:
55
+ if p.get('tab_id')==tab and p.get('agent'):
56
+ print(p.get('pane_id','')); break
57
+ " "$TAB")"
58
+ fi
59
+ if [ -z "$PANE" ]; then
60
+ # herdr absent or the door refused: report what it said, claim nothing more.
61
+ printf 'eindri[1]{name,state,detail}:\n "%s","unseated","the door did not return a pane"\n' "$NAME"
62
+ exit "$([ "$RC" = 0 ] && echo 3 || echo 1)"
63
+ fi
64
+
65
+ sleep "$SETTLE"
66
+ TAIL="$("${HERDR_BIN_PATH:-herdr}" pane read "$PANE" 2>/dev/null | tail -25)"
67
+
68
+ STATE="idle"; WHY="seated, but the pane shows no work yet"
69
+ case "$TAIL" in
70
+ *"Error: 403"*|*"error code: 1010"*|*"Access denied"*)
71
+ STATE="blocked"; WHY="the provider refused the client (403/1010 class) — see the pane" ;;
72
+ *working*|*"esc interrupt"*|*"thinking"*|*"✻"*|*"▣"*)
73
+ STATE="working"; WHY="the agent has begun" ;;
74
+ esac
75
+
76
+ printf 'eindri[1]{name,pane,state,detail}:\n "%s","%s","%s","%s"\n' "$NAME" "$PANE" "$STATE" "$WHY"
77
+ printf 'pane_tail[1]{last_lines}:\n "%s"\n' "$(printf '%s' "$TAIL" | tail -3 | tr '\n' '~' | sed 's/"/\\"/g' | cut -c1-200)"
78
+
79
+ case "$STATE" in
80
+ working) exit 0 ;;
81
+ blocked) exit 1 ;;
82
+ *) exit 3 ;;
83
+ esac
@@ -13,7 +13,20 @@
13
13
  set -u
14
14
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
15
  ROOT="${BROKK_ROOT_OVERRIDE:-$(cd "$SCRIPT_DIR/.." && pwd)}"
16
- STATE="${BROKK_STATE_OVERRIDE:-$ROOT/state}"
16
+
17
+ # The roots that live OUTSIDE the code tree: this machine's records and the
18
+ # runtime state belong to the home the operator chose at installation, never in
19
+ # the tree — a packaged install replaces its tree on upgrade (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_state_dir YMIR_STATE_DIR
28
+ hoard_data_dir YMIR_DATA_DIR
29
+ STATE="${BROKK_STATE_OVERRIDE:-$YMIR_STATE_DIR}"
17
30
  AGENT="${1:-}"
18
31
  WORKTREE="${2:-}"
19
32
  [ -n "$AGENT" ] || { echo "usage: eindri-seen.sh <agent> [<worktree-root>]" >&2; exit 2; }
@@ -15,7 +15,20 @@ set -u
15
15
  VERSION="1.0.0"
16
16
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
17
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
18
- STATE="${BROKK_STATE_OVERRIDE:-$ROOT/state}"
18
+
19
+ # The roots that live OUTSIDE the code tree: this machine's records and the
20
+ # runtime state belong to the home the operator chose at installation, never in
21
+ # the tree — a packaged install replaces its tree on upgrade (Rule 04).
22
+ if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
23
+ _yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24
+ for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
25
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
26
+ done
27
+ unset _yr _yc
28
+ fi
29
+ hoard_state_dir YMIR_STATE_DIR
30
+ hoard_data_dir YMIR_DATA_DIR
31
+ STATE="${BROKK_STATE_OVERRIDE:-$YMIR_STATE_DIR}"
19
32
  SEATS="$STATE/herdr-seats"
20
33
  SESSION="${YMIR_HERDR_SESSION:-brokk}"
21
34
 
@@ -342,6 +342,12 @@ else
342
342
  fi
343
343
 
344
344
  # --- isolation resolution ----------------------------------------------------
345
+ # A spawned Eindri is a MODEL-DRIVEN worker: it must reach a model endpoint to
346
+ # think at all. Utgard runs with `--network none`, so a sandboxed agent cannot
347
+ # reach either a cloud model (OpenCode Go) or a local one (llama.cpp) — it dies
348
+ # silently at launch, leaving an empty pane and no status line. Utgard is for
349
+ # untrusted CODE, not for the agent's own brain. So `auto` keeps the worker in
350
+ # its worktree, and `on` is an explicit choice that warns.
345
351
  ISOLATION_EFFECTIVE=off
346
352
  case "$ISOLATION" in
347
353
  on)
@@ -351,14 +357,14 @@ case "$ISOLATION" in
351
357
  exit 1
352
358
  }
353
359
  ISOLATION_EFFECTIVE=on
360
+ echo "warning: Utgard runs with --network none — the worker's model endpoint must be reachable INSIDE the sandbox, or the agent dies silently. Pass --isolation off to run in the worktree." >&2
354
361
  ;;
355
362
  off) ISOLATION_EFFECTIVE=off ;;
356
363
  auto)
364
+ ISOLATION_EFFECTIVE=off
357
365
  if [ -n "$ENGINE" ] && "$ENGINE" image inspect utgard-runner:latest >/dev/null 2>&1; then
358
- ISOLATION_EFFECTIVE=on
359
- echo "isolation: on (Utgard image present; pass --isolation off to run in the worktree)" >&2
366
+ echo "isolation: off (Utgard image present, but a model-driven worker needs its endpoint and Utgard has no network — pass --isolation on only for a sandbox that can reach a model)" >&2
360
367
  else
361
- ISOLATION_EFFECTIVE=off
362
368
  echo "isolation: off (no Utgard image; pass --isolation on after building it)" >&2
363
369
  fi
364
370
  ;;