@zerwiz/ymir 0.1.9 → 0.1.10

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.
@@ -192,6 +192,99 @@ cloth_rules[4]{rule,why}:
192
192
  TOON stays on stdout. The installer prints it at the consent, and ends with the
193
193
  next steps — *a reaction for every action, a next step for every ending*.
194
194
 
195
+ ### A clone and a package are one tree — `bin/app-lib.sh`
196
+
197
+ The apps are the only thing that differs between the two shapes, and only in
198
+ where they live:
199
+
200
+ ```
201
+ surface a clone an npm install
202
+ --------------- ---------------------- ------------------------------------
203
+ hlidskjalf apps/hlidskjalf node_modules/@zerwiz/hlidskjalf
204
+ hlidskjalf-mobile apps/hlidskjalf-mobile node_modules/@zerwiz/hlidskjalf-mobile
205
+ odrerir apps/odrerir node_modules/@zerwiz/odrerir
206
+ sessrumnir apps/sessrumnir node_modules/@zerwiz/sessrumnir
207
+ smidja apps/smidja-factory node_modules/@zerwiz/smidja-factory
208
+ ```
209
+
210
+ `bin/app-lib.sh` answers for both — `app_dir <surface> <var>`, `app_pkg
211
+ <surface>` — and **eighteen files** were converted to it: the raise path
212
+ (`scripts/start.sh`), the windows (`scripts/electron.sh`), the invite door, the
213
+ seat-hall trio, Eir, the icon mint, the desktop placement, the hall snapshot, and
214
+ the installer's own SPA and shell steps. `bin/smidja-lib.sh` delegates to it, so
215
+ there is **one** truth about where things live.
216
+
217
+ **The trap that named itself.** `printf -v <name>` writes to the *function's*
218
+ scope. A helper whose scratch variable shares the caller's requested name
219
+ swallows the answer — `app_dir hlidskjalf c` returned nothing because the
220
+ helper's own `local c` held the path. Every scratch name in the resolvers is
221
+ function-prefixed (`_apd_c`, `_smd_c`, `_apr_root`) for exactly that reason, and a
222
+ new resolver must follow the rule.
223
+
224
+ **Why it matters beyond tidiness:** a packaged install shipped with eighteen
225
+ scripts looking in `apps/`, so `ymir raise` died on
226
+ `cd …/apps/hlidskjalf: No such file or directory` while every package had in fact
227
+ arrived. A shape assumption is a bug that only shows itself on the *other* shape.
228
+
229
+ ### The hall answers on the port it was given
230
+
231
+ `scripts/start.sh` raises the SPA, and the SPA must listen on
232
+ `$HLIDSKJALF_PORT` (3888 by default) — every other door looks for it there. Two
233
+ rules came out of a packaged install where it listened nowhere:
234
+
235
+ ```
236
+ spa_serving[2]{shape,how}:
237
+ "a packaged install","serves its built ./dist with `vite preview --port $PORT --strictPort`"
238
+ "a clone","serves the dev server, but STILL on $PORT — `npm run dev -- --port $PORT --strictPort`"
239
+ ```
240
+
241
+ A bare `npm run dev` lets Vite take 5173 and the hall silently answers on the
242
+ wrong port; the raise now prints the log's last lines when the port stays silent
243
+ rather than claiming success. Óðrerir resolves through `bin/app-lib.sh` like every
244
+ other surface (it read as `missing apps/odrerir` on a packaged install).
245
+
246
+ ### The marks land on any desktop — not only Omarchy's
247
+
248
+ `bin/desktop-place.sh` holds **two kinds of thing**, and they were behind one gate:
249
+
250
+ ```
251
+ desktop_halves[2]{half,who_reads_it}:
252
+ "the window rules (numbered desktops, the Lua rule)","Hyprland / Omarchy only"
253
+ "the launcher entries and the rune icons","the freedesktop standard — GNOME, KDE, Hyprland alike"
254
+ ```
255
+
256
+ A GNOME operator gets entries and icons and no window rules, which is the correct
257
+ answer — and the reason a packaged install on GNOME placed *nothing* while the
258
+ clone's marks already existed. `bin/desktop-place.sh entries` is the launcher half
259
+ alone (any Linux desktop), the installer's `marks` step calls it, and both the
260
+ desktop database and the icon cache are refreshed after.
261
+
262
+ ### What a user went from, and to
263
+
264
+ `npm install -g` prints *"changed 266 packages"* and no versions. The CLI records
265
+ the version it last ran in the home's state (`state/version`) and says the
266
+ transition **once**, on the first run after an update:
267
+
268
+ ```
269
+ the tree moved 0.1.7 → 0.1.9
270
+ run `ymir eir` to see what stands, `ymir raise` to lift the hall
271
+ ```
272
+
273
+ ### The patience words — `style_patience`
274
+
275
+ A long hour must say what it is doing. `bin/ymir-style.sh` carries the line, and
276
+ the installer's long chain and the raise path's build open with it:
277
+
278
+ ```
279
+ ◆ much moves the halls are being stood up for the first time
280
+ this hour is long, and nothing of yours is lost in it —
281
+ roots come home, shapes are re-cut, names are set true again.
282
+ Your patience is noted, and it is earned.
283
+ ```
284
+
285
+ Written in the house voice, kept under four lines (density), and shown on stderr
286
+ with everything else the cloth renders.
287
+
195
288
  ### Two libs the packaged tree needs
196
289
 
197
290
  ```
@@ -62,6 +62,15 @@ This touches only the Brokk repo and its own worktrees, never anything under `pr
62
62
  Summarize what landed without internal vocabulary: which parts of the fleet are now on the latest, and which were left as-is and why.
63
63
  Surface any skipped target whose reason needs the Allfather's attention (a home with un-landed changes, local edits).
64
64
 
65
+ 5. **Re-check where the apps live — an update can move the shape.**
66
+ `bin/app-lib.sh` resolves a surface in a clone (`apps/<surface>`) or in a
67
+ package (`node_modules/@zerwiz/<package>`); `bin/smidja-lib.sh` and
68
+ `bin/electron-lib.sh` do the same for the smithy and a shell's runtime. After a
69
+ fast-forward, run `bin/ymir-plan.sh --phase 5` — if a surface that stood
70
+ installed now reads `BLOCKED`, the layout assumption moved and the resolver
71
+ needs the new shape. A long update should also say so: `style_patience` from
72
+ `bin/ymir-style.sh` opens the slow part in the house's own voice.
73
+
65
74
  5. **Read the plan after an update — the new version may expect more of this host.**
66
75
  A tracked change can add a step, a root, or a setting the running machine has not
67
76
  met yet. After a fast-forward that moved the instruction surface, ask what the
@@ -48,6 +48,35 @@ To move an existing home to a new place, re-run the install with the new value:
48
48
  YMIR_HOME=/path/to/new-home bin/ymir-install.sh
49
49
  ```
50
50
 
51
+ ## The doors — what the operator types afterwards
52
+
53
+ Two commands land on PATH (`ymir`, `ymir-install`); everything else is a door on
54
+ `ymir`, named for the figure who does the work. `ymir --help` lists them all.
55
+
56
+ ```
57
+ ymir raise | lower lift the hall, or lay it down
58
+ ymir hlidskjalf | sessrumnir the two windows
59
+ ymir smidja the board on :8437 — build · start · stop · status
60
+ ymir heimdall [invite] the way in, and letting someone else in
61
+ ymir eir what stands, and mend what does not
62
+ ymir groa [migrate] take the latest, and mend this home forward
63
+ ymir mimir · sense · plan
64
+ ```
65
+
66
+ ## Both shapes — a clone and an npm install
67
+
68
+ The operator may have cloned the tree or installed the package; the scripts must
69
+ not care. `bin/app-lib.sh` resolves a surface either way (`apps/<surface>`, else
70
+ `node_modules/@zerwiz/<package>`), `bin/smidja-lib.sh` resolves the smithy, and
71
+ `bin/electron-lib.sh` verifies a shell's runtime. To exercise the *other* shape
72
+ without a second machine:
73
+
74
+ ```bash
75
+ YMIR_ROOT_DIR=/path/to/an/npm/@zerwiz/ymir bash bin/app-lib.sh # resolve against it
76
+ ```
77
+
78
+ That variable is how a resolver is tested against a package from a clone.
79
+
51
80
  ## Which root holds what
52
81
 
53
82
  ```
package/CHANGELOG.md CHANGED
@@ -1,4 +1,58 @@
1
1
 
2
+ ## 2026-09-17 — 0.1.10: the hall answers on its port, and the marks land
3
+
4
+ - **The SPA listens where it is asked.** A bare `npm run dev` let Vite take 5173,
5
+ so `:3888` answered nothing in either shape; a packaged tree now serves its built
6
+ `./dist` and every shape passes `--port "$HLIDSKJALF_PORT" --strictPort`.
7
+ - **Óðrerir resolves** through `bin/app-lib.sh` instead of the clone's `apps/`.
8
+ - **The launcher entries and rune icons land on any Linux desktop** — the
9
+ freedesktop half of desktop integration is no longer behind Omarchy's gate, so a
10
+ GNOME operator gets marks at all.
11
+ - **The version a user went from and to** is said once, on the first run after an
12
+ update: *the tree moved 0.1.9 → 0.1.10*.
13
+
14
+ ## 2026-09-17 — npm-only: the hall answers on its own port, and the marks land on any desktop
15
+
16
+ The Allfather turned off every service running from the clone and asked for the
17
+ npm path alone to be true.
18
+
19
+ - **The SPA never listened on `:3888`.** `scripts/start.sh` raised it with a bare
20
+ `npm run dev`, so Vite took 5173 — then 5174 — while `ymir-validate.sh` and
21
+ every door looked for `:3888`. The port is now our decision
22
+ (`--port "$HLIDSKJALF_PORT" --strictPort`), a packaged install serves its built
23
+ `./dist` through `vite preview` instead of a dev server, and the raise prints
24
+ the last lines of the log when the port does not answer instead of claiming
25
+ success.
26
+ - **Óðrerir was skipped on a packaged install** — `HALL_DIR="$ROOT/apps/odrerir"`,
27
+ the clone's layout again. It resolves through `bin/app-lib.sh` now
28
+ (`app_dir odrerir`), like every other surface.
29
+ - **The launcher entries and the icons are the freedesktop half, not Omarchy's.**
30
+ Both halves of `bin/desktop-place.sh` sat behind the Omarchy gate, so a GNOME
31
+ operator got *nothing* — no entries, no rune icons — which is why the desktop
32
+ marks from the clone existed and the ones from npm never appeared. `entries` is
33
+ its own verb now (any Linux desktop), the installer's `marks` step calls it, and
34
+ the desktop database and icon cache are refreshed afterwards.
35
+ - **A user can see what moved.** `npm install -g` says *"changed 266 packages"* and
36
+ no versions. The CLI records the version it last ran and says the transition
37
+ once: *the tree moved 0.1.7 → 0.1.9 · run `ymir eir` to see what stands*.
38
+
39
+ ## 2026-09-17 — the doors, written down where a user will find them
40
+
41
+ - **`README.md` gains "After it installs — the doors"**: the two commands on PATH,
42
+ the twelve doors on `ymir`, the lawless name that answers once and points at the
43
+ lawful one, the patience words in full, and a section on **both shapes** — a
44
+ clone and a package, with one resolver and one rule (colour for the eye, TOON
45
+ for the pipe).
46
+ - **The skills that must know are taught.** Galdr's `assets/installation.md` gains
47
+ the shape table, the eighteen converted call sites, and the trap that named
48
+ itself (`printf -v` writes to the function's scope — scratch names are
49
+ function-prefixed for that reason). The `ymir-host` skill's `assets/install.md`
50
+ gains the doors and how to exercise the *other* shape without a second machine
51
+ (`YMIR_ROOT_DIR`). The `groa-update` skill gains the step that matters after
52
+ every update: **re-check where the apps live** — a shape assumption is a bug
53
+ that only shows itself on the other shape — and say the long hour in the house's
54
+ voice.
55
+
2
56
  ## 2026-09-17 — 0.1.9: both shapes reach the registry
3
57
 
4
58
  - **`ymir raise` works on a packaged install.** 0.1.8 shipped before the resolver,
package/README.md CHANGED
@@ -666,3 +666,57 @@ packages (Hlidskjalf, Óðrerir, Sessrúmnir, Smíðja) — one per app.
666
666
  Private data never touches npm — all user data lives at `$YMIR_HOME` (the hoard
667
667
  at `hodd/` and the realms at `svartalfaheim/`) and syncs via the user's
668
668
  **private** GitHub repo, never a public one.
669
+
670
+ ### After it installs — the doors
671
+
672
+ `npm install -g @zerwiz/ymir` puts **two** commands on your PATH: `ymir` and
673
+ `ymir-install`. Everything else is a door on `ymir`, named for the figure whose
674
+ work it does — because a great many doors either open or they do not, and a name
675
+ that says whose hand is on it is the difference between guessing and knowing.
676
+
677
+ ```
678
+ ymir first setup: the plan, then your word
679
+ ymir raise | lower lift the hall, or lay it down
680
+ ymir hlidskjalf the high seat's window
681
+ ymir smidja the smithy's board (:8437) — build · start · stop · status
682
+ ymir sessrumnir the seat-hall's window
683
+ ymir heimdall the way in — your credential (status · set · github)
684
+ ymir invite let someone else in — mint · list · revoke
685
+ ymir eir what stands, and mend what does not
686
+ ymir groa [migrate] take the latest, and mend this home forward
687
+ ymir mimir · sense · plan
688
+ ```
689
+
690
+ A name the law has not given a home still answers, once, and tells you the name
691
+ that has it: `ymir doctor` replies *the door is named `ymir eir` now*.
692
+
693
+ **A long hour says so, in its own voice.** The installer opens with the plan and,
694
+ when the work will take a while, tells you plainly:
695
+
696
+ ```
697
+ ◆ much moves the halls are being stood up for the first time
698
+ this hour is long, and nothing of yours is lost in it —
699
+ roots come home, shapes are re-cut, names are set true again.
700
+ Your patience is noted, and it is earned.
701
+ ```
702
+
703
+ ### Both shapes — a clone and a package are one tree
704
+
705
+ Ymir runs the same whether you **cloned** it or **installed** it from npm. The
706
+ apps are the difference, and only in where they live:
707
+
708
+ | | a clone | an npm install |
709
+ |---|---|---|
710
+ | the surfaces | `apps/hlidskjalf` · `odrerir` · `sessrumnir` · `smidja-factory` | `node_modules/@zerwiz/hlidskjalf` · `odrerir` · `sessrumnir` · `smidja-factory` |
711
+ | how they got there | the registry's `repo: apps/<path>` block clones each | declared as dependencies of the distro |
712
+
713
+ One resolver (`bin/app-lib.sh`) answers for both, so a script never has to care
714
+ which shape it woke in — and the same is true of the smithy (`bin/smidja-lib.sh`)
715
+ and of a desktop shell's runtime (`bin/electron-lib.sh`). The plan tells you which
716
+ surfaces are present, which are declared but not yet fetched, and which have no
717
+ package at all.
718
+
719
+ **Colour is for the eye, never for the pipe.** `bin/ymir-style.sh` renders in the
720
+ halls' own cloth — bone for words, bronze for what acts, steel for what stands,
721
+ blood for what is wrong — and only when a human is watching (a terminal, no
722
+ `NO_COLOR`). What a pipeline reads on stdout stays plain TOON.
@@ -51,12 +51,17 @@ CLASS_smidja="ymir-smidja"
51
51
  CLASS_sessrumnir="sessrumnir"
52
52
  CLASS_odrerir="ymir-odrerir"
53
53
 
54
- # ── launcher entries (Omarchy / Linux desktop only) ──────────────────────────
54
+ # ── launcher entries (ANY Linux desktop — GNOME, KDE, Hyprland) ──────────────
55
+ # Two halves live in this file, and they are not the same kind of thing:
56
+ # · the WINDOW RULES are Hyprland's (Omarchy) — numbered desktops, a Lua rule;
57
+ # · the LAUNCHER ENTRIES and the app icons are the freedesktop standard, which
58
+ # GNOME, KDE and Hyprland all read from the same place.
59
+ # A GNOME operator gets entries and icons and no window rules, which is the
60
+ # correct answer — and the reason a packaged install on GNOME had no icons at
61
+ # all, because both halves lived behind the Omarchy gate.
62
+ #
55
63
  # The .desktop files are templates (__YMIR_ROOT__, not an absolute path), because
56
- # where this checkout lives is a fact about the machine, not about Ymir. They are
57
- # rendered into the user's applications directory. This is DESKTOP INTEGRATION —
58
- # Omarchy/Hyprland is the supported environment; another OS needs its own launcher,
59
- # not a branch of this one.
64
+ # where this checkout lives is a fact about the machine, not about Ymir.
60
65
  install_entries() {
61
66
  local dst="$HOME/.local/share/applications" n=0
62
67
  local dirs=("$APP_HLIDSKJALF/electron" "$APP_SESSRUMNIR/resources" "$APP_ODRERIR/electron")
@@ -195,6 +200,16 @@ include_launchers_file() {
195
200
  }
196
201
 
197
202
  case "$ACTION" in
203
+ entries)
204
+ # The launcher half on its own: any Linux desktop, no Hyprland required.
205
+ install_entries
206
+ if command -v update-desktop-database >/dev/null 2>&1; then
207
+ update-desktop-database "$HOME/.local/share/applications" >/dev/null 2>&1 || true
208
+ fi
209
+ if command -v gtk-update-icon-cache >/dev/null 2>&1 && [ -d "$HOME/.local/share/icons/hicolor" ]; then
210
+ gtk-update-icon-cache -q -t -f "$HOME/.local/share/icons/hicolor" >/dev/null 2>&1 || true
211
+ fi
212
+ ;;
198
213
  plan)
199
214
  plan="$(plan_desktops)"
200
215
  printf 'desktop-place[%d]{app,desktop}:\n' "$(printf '%s\n' "$plan" | grep -c .)"
@@ -672,6 +672,13 @@ step_marks() {
672
672
  fi
673
673
  "$SCRIPT_DIR/design-icon.sh" mint --all >/dev/null 2>&1 || true
674
674
  n=$("$SCRIPT_DIR/design-icon.sh" install 2>/dev/null | grep -c '"ymir-') || n=0
675
+ # The launcher entries are the freedesktop half of desktop integration, and they
676
+ # belong to ANY Linux desktop — not only to Omarchy's window rules. A GNOME
677
+ # operator gets entries and icons and no window rules; before this, both halves
678
+ # sat behind the Omarchy gate and a packaged install placed nothing at all.
679
+ if [ -x "$SCRIPT_DIR/desktop-place.sh" ]; then
680
+ "$SCRIPT_DIR/desktop-place.sh" entries >/dev/null 2>&1 || true
681
+ fi
675
682
  [ -r "$HOME/.pi/agent/AGENTS.md" ] || [ -d "$HOME/.pi/agent" ] && {
676
683
  ln -sfn "$ROOT/AGENTS.md" "$HOME/.pi/agent/AGENTS.md" 2>/dev/null || true
677
684
  }
package/bin/ymir.js CHANGED
@@ -49,6 +49,25 @@ const faint = wrap('38;2;107;98;80');
49
49
  const blood = wrap('38;2;194;88;74');
50
50
  const bold = wrap('1');
51
51
 
52
+ // ── what a user went from, and to ────────────────────────────────────────────
53
+ // `npm install -g` prints "changed 266 packages" and no versions, so an operator
54
+ // cannot see what moved. We can: record the version we last ran, and say the
55
+ // transition once, on the first run after an update.
56
+ const STATE_DIR = process.env.YMIR_STATE_DIR || path.join(process.env.YMIR_HOME || path.join(require('node:os').homedir(), 'Documents', 'Ymir'), 'state');
57
+ function versionNotice() {
58
+ try {
59
+ const f = path.join(STATE_DIR, 'version');
60
+ const seen = fs.existsSync(f) ? fs.readFileSync(f, 'utf8').trim() : '';
61
+ if (seen === pkg.version) return;
62
+ fs.mkdirSync(STATE_DIR, { recursive: true });
63
+ fs.writeFileSync(f, pkg.version + '\n');
64
+ if (!seen) return; // a first install has nothing to compare
65
+ process.stderr.write(
66
+ bone(` the tree moved `) + faint(`${seen} → `) + bronze(`${pkg.version}`) + '\n' +
67
+ faint(` run \`ymir eir\` to see what stands, \`ymir raise\` to lift the hall\n\n`));
68
+ } catch { /* a version notice must never break a door */ }
69
+ }
70
+
52
71
  // ── the doors ────────────────────────────────────────────────────────────────
53
72
  // verb → { script, args } — args are prepended to whatever the operator passes,
54
73
  // so a verb can be a doorway to a sub-verb of a script that has several.
@@ -108,6 +127,7 @@ function run(script, args) {
108
127
  const argv = process.argv.slice(2);
109
128
  const first = argv[0];
110
129
 
130
+ versionNotice();
111
131
  if (!first) run(DOORS.install.script, []);
112
132
  if (first === '--version' || first === '-v' || first === '-V') {
113
133
  process.stdout.write(`${pkg.version}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerwiz/ymir",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Ymir \u2014 the single-tenant agent operating system. Omarchy-first, host-aware, self-healing.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/zerwiz/ymir",
package/scripts/start.sh CHANGED
@@ -31,6 +31,7 @@ if [ -z "${YMIR_APP_LIB_LOADED:-}" ]; then
31
31
  unset _ya _yac
32
32
  fi
33
33
  app_dir hlidskjalf APP_HLIDSKJALF || APP_HLIDSKJALF=""
34
+ app_dir odrerir HALL_DIR || HALL_DIR=""
34
35
 
35
36
  # Where the smithy's parts live: apps/smidja-factory in a clone, or the
36
37
  # @zerwiz/smidja-factory package in an npm install (bin/smidja-lib.sh).
@@ -196,7 +197,7 @@ fi
196
197
 
197
198
  # Óðrerir — the Live Hall. Its own Astro board on :4322, the carved planning
198
199
  # glass every hall door opens. Raised before the SPA so a window never waits.
199
- HALL_DIR="$ROOT/apps/odrerir"
200
+ app_dir odrerir HALL_DIR || HALL_DIR=""
200
201
  HALL_PORT="${ODRERIR_PORT:-4322}"
201
202
  HALL_PID_FILE="$RUN/odrerir.pid"
202
203
  HALL_LOG="$RUN/odrerir.log"
@@ -211,13 +212,14 @@ if [ -d "$HALL_DIR" ]; then
211
212
  echo "Óðrerir — Live Hall raised (pid $(cat "$HALL_PID_FILE")) → http://127.0.0.1:${HALL_PORT}/"
212
213
  fi
213
214
  else
214
- echo "Óðrerir — Live Hall skipped (missing apps/odrerir)." >&2
215
+ echo "Óðrerir — Live Hall skipped (the odrerir surface is not present)." >&2
215
216
  fi
216
217
 
217
218
  cd "$APP"
218
219
 
219
220
  if [[ "${1:-}" == "--foreground" ]]; then
220
- exec npm run dev
221
+ if [ -d "$APP/dist" ]; then exec npx --no-install vite preview --port "$PORT" --strictPort --host
222
+ else exec npm run dev -- --port "$PORT" --strictPort --host; fi
221
223
  fi
222
224
 
223
225
  # The SPA is already serving — nothing more for us to raise here.
@@ -227,11 +229,18 @@ if [[ "$SPA_UP" == "1" ]]; then
227
229
  fi
228
230
 
229
231
  # New session so we can signal the whole process group on stop.
230
- ymir_detach npm run dev >"$LOG" 2>&1
232
+ # The port is OUR decision, not Vite's: a bare `npm run dev` takes 5173 and the
233
+ # hall never answers on the port every other door expects. A packaged install
234
+ # ships ./dist, so it is served; a clone gets the dev server — both on $PORT.
235
+ if [ -d "$APP/dist" ]; then
236
+ ymir_detach bash -c "cd '$APP' && exec npx --no-install vite preview --port '$PORT' --strictPort --host" >"$LOG" 2>&1
237
+ else
238
+ ymir_detach bash -c "cd '$APP' && exec npm run dev -- --port '$PORT' --strictPort --host" >"$LOG" 2>&1
239
+ fi
231
240
  PID=$!
232
241
  echo "$PID" > "$PID_FILE"
233
242
 
234
- # Wait for the port to answer (max ~15s).
243
+ # Wait for the port to answer (max ~15s), then say what is true.
235
244
  for _ in $(seq 1 30); do
236
245
  if curl -s -o /dev/null "http://127.0.0.1:${PORT}/"; then
237
246
  echo "Hlidskjalf raised (pid $PID) → http://127.0.0.1:${PORT}/"
@@ -241,5 +250,6 @@ for _ in $(seq 1 30); do
241
250
  sleep 0.5
242
251
  done
243
252
 
244
- echo "Hlidskjalf started (pid $PID) but the port did not answer yet check $LOG" >&2
253
+ echo "Hlidskjalf started (pid $PID) but :${PORT} did not answer — see $LOG" >&2
254
+ tail -5 "$LOG" >&2 2>/dev/null || true
245
255
  exit 1