@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
package/bin/ymir.js CHANGED
@@ -1,19 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * ymir — the CLI the npm package publishes.
3
+ * ymir — the CLI the npm package publishes, and the operator's front door.
4
4
  *
5
5
  * It is a thin, honest wrapper: the real work lives in the shell scripts under
6
- * bin/, which are the same scripts a git clone runs. This file exists because
7
- * npm needs a Node entry point (`bin: ymir`) and because a package user should
8
- * not have to know where the checkout landed.
6
+ * bin/ and scripts/ the same scripts a git clone runs. This file exists
7
+ * because npm needs a Node entry point (`bin: ymir`) and because an operator
8
+ * should never have to know where the package landed.
9
9
  *
10
- * ymir # the same as `ymir install`
11
- * ymir install [--yes] # first setup (idempotent, self-healing)
12
- * ymir sense # what THIS machine is
13
- * ymir validate # prove what stands
14
- * ymir migrate # heal an older home forward
10
+ * The doors are named for the figure whose work they do (the naming law):
11
+ * Eir heals, Gróa renews, Heimdall keeps the way in, Smíðja is the smithy and
12
+ * its board, Hlidskjalf the high seat, Sessrúmnir the seat-hall, Mímir the well.
13
+ *
14
+ * ymir first setup (the same as `ymir install`)
15
+ * ymir install [...] first setup, idempotent and self-healing
16
+ * ymir raise / lower lift the hall, or lay it down
17
+ * ymir eir what stands, and mend what does not
18
+ * ymir groa take the latest, and mend this home forward
19
+ * ymir heimdall the way in: your credential, and invites
20
+ * ymir smidja the smithy's board (:8437) — build · start · stop · status
21
+ * ymir hlidskjalf the high seat's window
22
+ * ymir sessrumnir the seat-hall's window
23
+ * ymir mimir the memory well
24
+ * ymir sense what THIS machine is
15
25
  * ymir --version
16
26
  *
27
+ * Colour appears only where a human is watching (a TTY, no NO_COLOR); the data
28
+ * on stdout is always plain TOON.
29
+ *
17
30
  * Exit codes follow the scripts: 0 ok, 1 error, 2 usage.
18
31
  */
19
32
  'use strict';
@@ -27,32 +40,65 @@ const pkg = (() => {
27
40
  try { return require(path.join(ROOT, 'package.json')); } catch { return { version: '0.0.0' }; }
28
41
  })();
29
42
 
30
- const TASKS = {
31
- install: 'ymir-install.sh',
32
- validate: 'ymir-validate.sh',
33
- migrate: 'ymir-migrate.sh',
34
- sense: 'host-sense.sh',
43
+ // ── the cloth (the same palette as bin/ymir-style.sh, cut from the tokens) ───
44
+ const colour = process.stderr.isTTY && !process.env.NO_COLOR && process.env.TERM !== 'dumb';
45
+ const wrap = (code) => (s) => (colour ? `\u001b[${code}m${s}\u001b[0m` : `${s}`);
46
+ const bone = wrap('38;2;207;195;169');
47
+ const bronze = wrap('38;2;201;151;79');
48
+ const faint = wrap('38;2;107;98;80');
49
+ const blood = wrap('38;2;194;88;74');
50
+ const bold = wrap('1');
51
+
52
+ // ── the doors ────────────────────────────────────────────────────────────────
53
+ // verb → { script, args } — args are prepended to whatever the operator passes,
54
+ // so a verb can be a doorway to a sub-verb of a script that has several.
55
+ const DOORS = {
56
+ install: { script: 'bin/ymir-install.sh', about: 'first setup (idempotent, self-healing)' },
57
+ raise: { script: 'scripts/start.sh', about: 'lift the hall — SPA, gate API, Nornir, bridges, the board' },
58
+ lower: { script: 'scripts/stop.sh', about: 'lay the hall down' },
59
+ eir: { script: 'bin/eir-doctor.sh', about: 'diagnose every surface; mend what is broken' },
60
+ groa: { script: 'bin/groa-update.sh', about: 'take the latest, then mend this home forward' },
61
+ heimdall: { script: 'bin/ymir-setup-auth.sh', about: 'the way in — your credential (status · set · github)' },
62
+ invite: { script: 'bin/ymir-invite.sh', about: 'let someone else in (mint · list · revoke)' },
63
+ smidja: { script: 'bin/smidja-board.sh', about: "the smithy's board on :8437 (build · start · stop · status)" },
64
+ hlidskjalf: { script: 'scripts/electron.sh', about: "the high seat's window", args: ['start', '--view', 'hlidskjalf'] },
65
+ sessrumnir: { script: 'scripts/electron.sh', about: "the seat-hall's window", args: ['start', '--view', 'sessrumnir'] },
66
+ mimir: { script: 'bin/mimir.sh', about: 'the memory well' },
67
+ sense: { script: 'bin/host-sense.sh', about: 'what THIS machine is' },
68
+ plan: { script: 'bin/ymir-plan.sh', about: 'what an install would do here — writes nothing' },
69
+ migrate: { script: 'bin/ymir-migrate.sh', about: "heal this home's structure forward (Gr\u00f3a's mend)" },
70
+ validate: { script: 'bin/ymir-validate.sh', about: 'alias of `ymir eir`' },
35
71
  };
36
72
 
73
+ // Names the law has not given a home are kept for a while, so a muscle memory
74
+ // built yesterday still works — and says what to type instead.
75
+ const RENAMED = { validate: 'eir', auth: 'heimdall', desktop: 'hlidskjalf', doctor: 'eir', update: 'groa' };
76
+
37
77
  function usage() {
38
- process.stdout.write(
39
- `ymir ${pkg.version} — the single-tenant agent operating system\n\n` +
40
- 'Usage:\n' +
41
- ' ymir first setup (same as `ymir install`)\n' +
42
- ' ymir install [--yes] first setup, idempotent and self-healing\n' +
43
- ' ymir sense report THIS machine (distro, session, desktop, capabilities)\n' +
44
- ' ymir validate verify the running system\n' +
45
- ' ymir migrate heal an older home forward\n' +
46
- ' ymir --version\n\n' +
47
- 'Output is TOON. The scripts are the source of truth; this CLI only points at them.\n'
48
- );
78
+ const lines = [];
79
+ lines.push('');
80
+ lines.push(` ${bronze('\u16c9')} ${bold('Ymir')} ${faint(pkg.version)}`);
81
+ lines.push(` ${faint('the single-tenant agent operating system')}`);
82
+ lines.push('');
83
+ lines.push(bold(' The doors'));
84
+ for (const [verb, d] of Object.entries(DOORS)) {
85
+ lines.push(` ${bronze(verb.padEnd(11))} ${faint(d.about)}`);
86
+ }
87
+ lines.push('');
88
+ lines.push(bold(' Composed by hand'));
89
+ lines.push(` ${bronze('ymir groa migrate'.padEnd(11))} ${faint("heal this home's structure (run by groa / install)")}`);
90
+ lines.push(` ${bronze('ymir heimdall set'.padEnd(11))} ${faint('set the operator password (or `github`)')}`);
91
+ lines.push('');
92
+ lines.push(faint(' A bare `ymir` runs the first setup. Output is TOON; colour only on a TTY.'));
93
+ lines.push('');
94
+ process.stdout.write(lines.join('\n') + '\n');
49
95
  }
50
96
 
51
97
  function run(script, args) {
52
- const file = path.join(ROOT, 'bin', script);
98
+ const file = path.join(ROOT, script);
53
99
  if (!fs.existsSync(file)) {
54
- process.stderr.write(`error: ${path.relative(ROOT, file)} is missing from this install\n`);
55
- process.stderr.write('help: reinstall — npm i -g @zerwiz/ymir, or git clone the distro\n');
100
+ process.stderr.write(`${blood('error:')} ${script} is missing from this install\n`);
101
+ process.stderr.write(`${faint('help: reinstall — npm i -g @zerwiz/ymir, or git clone the distro')}\n`);
56
102
  process.exit(1);
57
103
  }
58
104
  const r = spawnSync('bash', [file, ...args], { stdio: 'inherit' });
@@ -62,17 +108,19 @@ function run(script, args) {
62
108
  const argv = process.argv.slice(2);
63
109
  const first = argv[0];
64
110
 
65
- if (!first) run(TASKS.install, []);
111
+ if (!first) run(DOORS.install.script, []);
66
112
  if (first === '--version' || first === '-v' || first === '-V') {
67
113
  process.stdout.write(`${pkg.version}\n`);
68
114
  process.exit(0);
69
115
  }
70
116
  if (first === '--help' || first === '-h' || first === 'help') { usage(); process.exit(0); }
71
117
 
72
- const script = TASKS[first];
73
- if (!script) {
74
- process.stderr.write(`error: unknown command ${first}\n`);
75
- process.stderr.write('help: ymir [install|sense|validate|migrate|--version]\n');
118
+ let door = DOORS[first];
119
+ if (!door) {
120
+ const replacement = RENAMED[first];
121
+ process.stderr.write(`${blood('error:')} unknown door ${bold(first)}\n`);
122
+ if (replacement) process.stderr.write(`${faint(`help: the door is named \`ymir ${replacement}\` now (the naming law — a figure does the work)`)}\n`);
123
+ process.stderr.write(`${faint('help: `ymir --help` lists every door')}\n`);
76
124
  process.exit(2);
77
125
  }
78
- run(script, argv.slice(1));
126
+ run(door.script, [...(door.args || []), ...argv.slice(1)]);
package/docs/design.md CHANGED
@@ -226,7 +226,7 @@ everywhere; never put bronze text on bronze.
226
226
  - **Runecoded, not emoji.** A minimal, hand-drawn-style rune set replaces emoji
227
227
  in all UI: 16×16 and 24×24 grid, 2px stroke, chisel bevel on primary.
228
228
  - System subsystems each carry one rune-glyph (reuse Algiz-family geometry:
229
- Bifrost gate, Ratatoskr squirrel, the Well). Map is in `midgard/design-system/icons.md` (TBD) created from this doc.
229
+ Bifrost gate, Ratatoskr squirrel, the Well). Map is in `midgard/design-system/icons.md` and the library in `midgard/design-system/runes.md`.
230
230
  - Status dot: filled rune ring (ok) / half-ring (warn) / broken ring (danger), never color-only.
231
231
 
232
232
  ### 4.5 Surface & texture
@@ -345,7 +345,7 @@ All components read tokens from `tokens.css`; color-only states are forbidden
345
345
 
346
346
  - `docs/design.md` — this document (master).
347
347
  - `midgard/design-system/tokens.css` — the consumable design tokens.
348
- - `midgard/design-system/icons.md` — rune-glyph map (to create).
348
+ - `midgard/design-system/icons.md` — rune-glyph map (21 glyphs, drawn in `icons/*.svg`).
349
349
  - `docs/ymir-rut.md` Part 3 — the original Rut design-system specification this
350
350
  document implements.
351
351
  - `docs/lore.md` §VI — house lineage behind the house accents.
package/docs/lore.md CHANGED
@@ -818,9 +818,13 @@ what belongs to all houses, nothing to one.
818
818
  | Path | What it holds |
819
819
  |------|---------------|
820
820
  | `midgard/design-system/` | Theme tokens, icons, rune glyphs, component rules |
821
- | `midgard/company_wiki/` | Cross-tenant company knowledge |
822
821
  | `midgard/github_org_repos/` | GitHub organization repo inventory |
823
822
  | `midgard/shared-packages/` | Shared npm/Python packages |
823
+
824
+ Midgard holds **public-by-design** cross-tenant assets only. Company knowledge —
825
+ policies, vision, specs, client names — is **tenant data** and lives at
826
+ `$YMIR_HOME/hodd/identity/companies/`. A company wiki here would publish that
827
+ company's internals in a public repo.
824
828
  | `midgard/infrastructure/` | DB schemas, Caddyfile, cloudflared, oauth2-proxy |
825
829
 
826
830
  **Rule:** Midgard is shared. No realm may mutate another realm's Midgard assets.
@@ -6,7 +6,7 @@ symlinked into each harness.
6
6
  ## Where agents live
7
7
 
8
8
  - **Canonical:** `.agents/agents/<name>-<craft>.md` (edit these only).
9
- - **Bindings:** `.opencode/agent/*.md` and `.pi/agents/*.md` are symlinks.
9
+ - **Bindings:** `.opencode/agents/*.md` and `.pi/agents/*.md` are symlinks.
10
10
 
11
11
  Rebind after any change:
12
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerwiz/ymir",
3
- "version": "0.1.5",
3
+ "version": "0.1.8",
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",
@@ -29,7 +29,6 @@
29
29
  "bin/",
30
30
  ".agents/",
31
31
  "scripts/",
32
- "config/",
33
32
  "RULES/",
34
33
  "docs/",
35
34
  "packaging/",
@@ -38,7 +37,12 @@
38
37
  "CHANGELOG.md",
39
38
  "LICENSE",
40
39
  "NOTICE",
41
- "!**/node_modules/**"
40
+ "!**/node_modules/**",
41
+ "!.agents/memory/kaia.engram*",
42
+ "!.agents/memory/well/*.jsonl",
43
+ "!.agents/memory/*.db",
44
+ "!**/__pycache__/",
45
+ "!**/*.pyc"
42
46
  ],
43
47
  "engines": {
44
48
  "node": ">=20"
@@ -53,5 +57,11 @@
53
57
  "scripts": {
54
58
  "test": "bash .agents/skills/galdr-ymirsystem/scripts/compliance-check.sh",
55
59
  "compliance": "bash .agents/skills/galdr-ymirsystem/scripts/compliance-check.sh"
60
+ },
61
+ "optionalDependencies": {
62
+ "@zerwiz/hlidskjalf": "^0.1.0",
63
+ "@zerwiz/odrerir": "^0.1.0",
64
+ "@zerwiz/sessrumnir": "^0.1.8-alpha",
65
+ "@zerwiz/smidja-factory": "^0.1.0"
56
66
  }
57
67
  }
@@ -26,6 +26,19 @@ fi
26
26
  VERSION="1.1.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
  APP="$ROOT/apps/hlidskjalf"
30
43
  Odrerir_app="$ROOT/apps/odrerir"
31
44
  NO_INSTALL=0
@@ -85,6 +98,23 @@ pid_of() {
85
98
  printf '%s' "$(printf '%s\n' "$pids" | head -1)"
86
99
  }
87
100
 
101
+ stop_view() { # stop ONE view's pids - and only that view's
102
+ local v="$1" pids
103
+ pids="$(view_pids "$v")"
104
+ if [ -z "$pids" ]; then
105
+ printf 'electron: %s already stopped\n' "$v"
106
+ rm -f "$(pid_file "$v")"
107
+ return 0
108
+ fi
109
+ local list; list="$(printf '%s' "$pids" | tr '\n' ',')"
110
+ printf '%s\n' "$pids" | xargs -r kill 2>/dev/null || true
111
+ sleep 0.5
112
+ local left; left="$(view_pids "$v")"
113
+ [ -z "$left" ] || printf '%s\n' "$left" | xargs -r kill -KILL 2>/dev/null || true
114
+ rm -f "$(pid_file "$v")"
115
+ printf 'electron: stopped %s pid(s)=%s\n' "$v" "${list%,}"
116
+ }
117
+
88
118
  case "$ACTION" in
89
119
  status)
90
120
  printf 'electron[%s]{view,state,pid}:\n' "${#VIEWS[@]}"
@@ -93,15 +123,13 @@ case "$ACTION" in
93
123
  done
94
124
  exit 0 ;;
95
125
  stop)
96
- for v in "${VIEWS[@]}"; do
97
- local pids; pids="$(view_pids "$v")"
98
- if [ -n "$pids" ]; then
99
- # Kill every live pid for this view (the shim era could leave several).
100
- printf '%s\n' "$pids" | while IFS= read -r p; do [ -n "$p" ] && kill "$p" 2>/dev/null || true; done
101
- printf 'electron: stopped %s pid(s)=%s\n' "$v" "$(printf '%s' "$pids" | tr '\n' ',' | sed 's/,$//')"
102
- fi
103
- rm -f "$(pid_file "$v")"
104
- done
126
+ # ONLY the view asked for. Walking every view here is why `stop --view odrerir`
127
+ # used to take the smithy's window down with it.
128
+ if [ "$VIEW" = both ]; then
129
+ for v in "${VIEWS[@]}"; do stop_view "$v"; done
130
+ else
131
+ stop_view "$VIEW"
132
+ fi
105
133
  printf 'electron: stopped\n'
106
134
  exit 0 ;;
107
135
  start|hlidskjalf|smidja|odrerir) [ "$ACTION" = smidja ] && VIEW=smidja; [ "$ACTION" = odrerir ] && VIEW=odrerir ;;
@@ -109,7 +137,7 @@ case "$ACTION" in
109
137
  esac
110
138
 
111
139
  command -v npm >/dev/null 2>&1 || { printf 'error: npm not found\nhelp: install node/npm to run the desktop shell\n' >&2; exit 1; }
112
- mkdir -p "$ROOT/state"
140
+ mkdir -p "$YMIR_STATE_DIR"
113
141
 
114
142
  if [ ! -x "$APP/node_modules/.bin/electron" ]; then
115
143
  if [ "$NO_INSTALL" = 1 ]; then
@@ -151,14 +179,39 @@ fi
151
179
  app_dir() { case "$1" in odrerir) printf '%s' "$Odrerir_app" ;; *) printf '%s' "$APP" ;; esac; }
152
180
  view_host() { case "$1" in smidja) printf '%s' 'http://127.0.0.1:8437/' ;; odrerir) printf '%s' 'http://127.0.0.1:4322/' ;; *) printf '%s' 'http://127.0.0.1:3888/' ;; esac; }
153
181
 
182
+ service_up() { curl -s -o /dev/null --max-time 3 "$1" 2>/dev/null; }
183
+ wait_for_service() {
184
+ local u="$1" i
185
+ for i in $(seq 1 120); do service_up "$u" && return 0; sleep 0.5; done
186
+ return 1
187
+ }
188
+ # Does the compositor hold a window of this class at all? A process with no window
189
+ # cannot be focused, and focusing nothing is exactly the silence a click must not get.
190
+ window_present() {
191
+ command -v hyprctl >/dev/null 2>&1 || return 0 # no compositor to ask: assume yes
192
+ hyprctl clients 2>/dev/null | grep -qi "$1"
193
+ }
194
+
154
195
  start_one() {
155
196
  local v="$1" f l
156
197
  f="$(pid_file "$v")"; l="$(log_file "$v")"
198
+ # THE SYSTEM FIRST. A shell whose backing service is down draws an empty window,
199
+ # and a window that never mapped cannot be focused. Clicking an icon must raise
200
+ # THE SYSTEM, so the service is converged before the window is judged.
201
+ local host; host="$(view_host "$v")"
202
+ if ! service_up "$host"; then
203
+ printf 'electron: %s has no service at %s — raising the system\n' "$v" "$host" >&2
204
+ ( nohup bash "$ROOT/scripts/start.sh" >/dev/null 2>&1 </dev/null & )
205
+ if ! wait_for_service "$host"; then
206
+ printf 'electron: %s still does not answer at %s — see $YMIR_STATE_DIR/\n' "$v" "$host" >&2
207
+ fi
208
+ # A process that lived through the outage is windowless; it must be reborn.
209
+ if is_running "$v"; then stop_view "$v"; fi
210
+ fi
157
211
  if is_running "$v"; then
158
- # A speed-start must RAISE the app, not report that it is already up. The
212
+ # A bound key must RAISE the app, never report that it is already up. The
159
213
  # window lives on its own numbered desktop by design, so "nothing happens"
160
- # is exactly what a bound key must not do. focuswindow also switches to that
161
- # desktop when the window is elsewhere.
214
+ # is exactly what a click must not do. focuswindow also switches desktops.
162
215
  local cls state=already-up
163
216
  cls="$(view_mark "$v")" # the window class, from the one place that defines it
164
217
  if command -v hyprctl >/dev/null 2>&1; then
@@ -167,8 +220,14 @@ start_one() {
167
220
  # string form is a syntax error there. This is the working idiom.
168
221
  hyprctl dispatch "hl.dsp.focus({window=\"class:^${cls}\$\"})" >/dev/null 2>&1 && state=raised
169
222
  fi
170
- printf 'electron[1]{view,state,pid}:\n "%s","%s",%s\n' "$v" "$state" "$(pid_of "$v")"
171
- return 0
223
+ # A process whose window is gone cannot be focused — reborn, not silent.
224
+ if [ "$state" = already-up ] && ! window_present "$cls"; then
225
+ printf 'electron: %s is running without a window — restarting it\n' "$v" >&2
226
+ stop_view "$v"
227
+ else
228
+ printf 'electron[1]{view,state,pid}:\n "%s","%s",%s\n' "$v" "$state" "$(pid_of "$v")"
229
+ return 0
230
+ fi
172
231
  fi
173
232
  # Launch the REAL Electron binary, not the .bin node shim, so the recorded pid
174
233
  # is the app itself (the shim spawns and would leave a stale/incorrect pid).
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+ # lower.sh — the whole house in one word: the windows first, then the web.
3
+ #
4
+ # The shells are lowered BEFORE the services, so no window is left watching a
5
+ # port that has just gone away. That order is the whole point of owning both
6
+ # lifecycles in one place.
7
+ #
8
+ # scripts/lower.sh # every shell + the services
9
+ # scripts/lower.sh --desktop # the shells only
10
+ # scripts/lower.sh --web # the services only
11
+ set -u
12
+
13
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
15
+
16
+ WHAT="all"
17
+ case "${1-}" in
18
+ --web) WHAT=web ;;
19
+ --desktop) WHAT=desktop ;;
20
+ -h|--help|"") sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
21
+ esac
22
+
23
+ if [ "$WHAT" != "web" ]; then
24
+ printf '\n— the windows —\n'
25
+ if [ -x "$ROOT/bin/sessrumnir.sh" ]; then
26
+ "$ROOT/bin/sessrumnir.sh" stop || printf 'lower: sessrumnir did not stop cleanly\n' >&2
27
+ fi
28
+ "$ROOT/scripts/electron.sh" stop || printf 'lower: electron.sh stop reported errors\n' >&2
29
+ fi
30
+
31
+ if [ "$WHAT" != "desktop" ]; then
32
+ printf '\n— the web —\n'
33
+ "$ROOT/scripts/stop.sh" || printf 'lower: stop.sh reported errors\n' >&2
34
+ fi
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env bash
2
+ # raise.sh — the whole house in one word: the web AND the windows.
3
+ #
4
+ # scripts/start.sh raises the services; scripts/electron.sh raises the desktop
5
+ # shells. Two lifecycles that never met — which is why "the electrons are not
6
+ # running" was true while 28 processes stood, and why a web restart left the
7
+ # shells pointing at a dead port. This is the pair of hands.
8
+ #
9
+ # scripts/raise.sh # services + every desktop shell
10
+ # scripts/raise.sh --web # the services only
11
+ # scripts/raise.sh --desktop # the shells only
12
+ set -u
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
+ ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
16
+
17
+ WHAT="all"
18
+ case "${1-}" in
19
+ --web) WHAT=web ;;
20
+ --desktop) WHAT=desktop ;;
21
+ -h|--help|"") sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
22
+ esac
23
+
24
+ if [ "$WHAT" != "desktop" ]; then
25
+ printf '\n— the web —\n'
26
+ "$ROOT/scripts/start.sh" || printf 'raise: start.sh reported errors\n' >&2
27
+ fi
28
+
29
+ if [ "$WHAT" != "web" ]; then
30
+ printf '\n— the windows —\n'
31
+ # Every shell, by name: hlidskjalf is the default view; smidja and odrerir need
32
+ # their own start; sessrumnir is its own app.
33
+ "$ROOT/scripts/electron.sh" start || printf 'raise: hlidskjalf failed\n' >&2
34
+ for v in smidja odrerir; do
35
+ "$ROOT/scripts/electron.sh" start --view "$v" || printf 'raise: %s failed\n' "$v" >&2
36
+ done
37
+ if [ -x "$ROOT/bin/sessrumnir.sh" ]; then
38
+ "$ROOT/bin/sessrumnir.sh" start || printf 'raise: sessrumnir failed\n' >&2
39
+ fi
40
+ printf '\n'
41
+ "$ROOT/scripts/electron.sh" status
42
+ fi
package/scripts/start.sh CHANGED
@@ -17,10 +17,57 @@ if [ -z "${YMIR_PLATFORM_LOADED:-}" ]; then
17
17
  fi
18
18
 
19
19
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
20
+ # Where the smithy's parts live: apps/smidja-factory in a clone, or the
21
+ # @zerwiz/smidja-factory package in an npm install (bin/smidja-lib.sh).
22
+ if [ -z "${YMIR_SMIDJA_LIB_LOADED:-}" ]; then
23
+ _ys="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24
+ for _yc in "$_ys/smidja-lib.sh" "$(dirname "$_ys")/bin/smidja-lib.sh"; do
25
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_SMIDJA_LIB_LOADED=1; break; }
26
+ done
27
+ unset _ys _yc
28
+ fi
29
+ smidja_visualizer_dir SMIDJA_VIZ
30
+ smidja_factory_dir SMIDJA_FACTORY
31
+
32
+
33
+ # The operator's settings and secrets live in the home they chose, never in the
34
+ # code tree — a packaged install replaces its tree on upgrade, and a credential
35
+ # must never sit in a tree that ships (Rule 04).
36
+ if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
37
+ _yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
38
+ for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
39
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
40
+ done
41
+ unset _yr _yc
42
+ fi
43
+ hoard_settings_dir YMIR_SETTINGS_DIR
44
+ hoard_local_env YMIR_ENV_FILE
45
+ hoard_state_dir YMIR_STATE_DIR
46
+ hoard_data_dir YMIR_DATA_DIR
47
+
48
+ # The roots that live OUTSIDE the code tree: this machine's records and the
49
+ # runtime state belong to the home the operator chose at installation, never in
50
+ # the tree — a packaged install replaces its tree on upgrade (Rule 04).
20
51
  APP="$ROOT/apps/hlidskjalf"
21
52
  RUN="$ROOT/.run"
22
53
  PID_FILE="$RUN/hlidskjalf.pid"
23
54
  LOG="$RUN/hlidskjalf.log"
55
+
56
+ # Load the platform env once (Rule 04): the gate, the Bifrost bridge and Mimir
57
+ # all read their credentials from .env.local — 0600, gitignored, never printed.
58
+ # The bun gate reads process.env, and nothing loaded the file for it, so a
59
+ # credential set by bin/ymir-setup-auth.sh never reached it — and the gate treats
60
+ # an empty GATE_AUTH as "open". Loaded before the ports below so those may be
61
+ # overridden from it too.
62
+ if [ -r "$YMIR_ENV_FILE" ]; then
63
+ set +eu
64
+ set -a
65
+ # shellcheck disable=SC1090,SC1091
66
+ . "$YMIR_ENV_FILE" || true
67
+ set +a
68
+ set -eu
69
+ fi
70
+
24
71
  PORT="${HLIDSKJALF_PORT:-3888}"
25
72
 
26
73
  mkdir -p "$RUN"
@@ -43,6 +90,11 @@ fi
43
90
  # Raise the gate API (real runtime data) when bun is available. Demo mode works
44
91
  # without it; live data needs it.
45
92
  API_PORT="${HLIDSKJALF_API_PORT:-3889}"
93
+ # The public hostnames the gate fronts (written by bin/gjallarhorn-expose.sh):
94
+ # without them the gate cannot route a host to its app, and the login page for
95
+ # that app would never be reached.
96
+ [ -r "$YMIR_STATE_DIR/gjallarhorn-hosts.env" ] && . "$YMIR_STATE_DIR/gjallarhorn-hosts.env" && \
97
+ export SMIDJA_HOST ODRERIR_HOST YMIR_PRIMARY_HOST
46
98
  API_PID_FILE="$RUN/hlidskjalf-api.pid"
47
99
  API_LOG="$RUN/hlidskjalf-api.log"
48
100
  if command -v bun >/dev/null 2>&1; then
@@ -66,7 +118,7 @@ if [ -x "$ROOT/bin/nornir-cron-start.sh" ]; then
66
118
  if "$ROOT/bin/nornir-cron-start.sh" >/dev/null 2>&1; then echo "Nornir cron: started"; else echo "Nornir cron: start failed" >&2; fi
67
119
  fi
68
120
  if [ -x "$ROOT/bin/bifrost-bridge.sh" ]; then
69
- if BROKK_ENV_FILE="$ROOT/.env.local" "$ROOT/bin/bifrost-bridge.sh" --start >/dev/null 2>&1; then echo "Bifrost bridge: up"; else echo "Bifrost bridge: not up (needs OPENCODE_GO_API_KEY)" >&2; fi
121
+ if BROKK_ENV_FILE="$YMIR_ENV_FILE" "$ROOT/bin/bifrost-bridge.sh" --start >/dev/null 2>&1; then echo "Bifrost bridge: up"; else echo "Bifrost bridge: not up (needs OPENCODE_GO_API_KEY)" >&2; fi
70
122
  fi
71
123
  # The well — Mimirsbrunn (engram) on :4602; the gate API and mimir.sh drink here.
72
124
  if [ -x "$ROOT/bin/mimir-bridge.sh" ]; then
@@ -76,12 +128,22 @@ fi
76
128
  # Smíðja's eye — the Vue trace visualizer (API :8437, UI :8438). Reads the repo's
77
129
  # own smidja.db and exposes the Sessions/Trace/Decisions/Stats views behind the
78
130
  # Hlidskjalf Sessions gate's "Open visualizer" button.
79
- VIZ_DIR="$ROOT/.agents/skills/smidja-factory/apps/visualizer"
131
+ VIZ_DIR="${SMIDJA_VIZ:-}"
80
132
  VIZ_API_PORT="${SMIDJA_VIZ_API_PORT:-8437}"
81
133
  VIZ_UI_PORT="${SMIDJA_VIZ_UI_PORT:-8438}"
82
134
  VIZ_API_PID_FILE="$RUN/smidja-viz-api.pid"
83
135
  VIZ_UI_PID_FILE="$RUN/smidja-viz-ui.pid"
84
- SMIDJA_DB_PATH="${SMIDJA_DB:-$ROOT/apps/smidja/smidja_data/smidja.db}"
136
+ SMIDJA_DB_PATH="${SMIDJA_DB:-}"
137
+ # The smithy DB follows the HOME: 0003-private-data-separation moved it to
138
+ # $YMIR_HOME/smidja/smidja.db. Prefer an existing DB there, then an in-repo copy
139
+ # for a checkout that keeps its own; SMIDJA_DB overrides either.
140
+ if [ -z "$SMIDJA_DB_PATH" ]; then
141
+ for _db in "${YMIR_HOME:-$HOME/Documents/Ymir}/smidja/smidja.db" \
142
+ "$ROOT/apps/smidja/smidja_data/smidja.db"; do
143
+ [ -f "$_db" ] && { SMIDJA_DB_PATH="$_db"; break; }
144
+ done
145
+ SMIDJA_DB_PATH="${SMIDJA_DB_PATH:-${YMIR_HOME:-$HOME/Documents/Ymir}/smidja/smidja.db}"
146
+ fi
85
147
  if command -v bun >/dev/null 2>&1 && [ -d "$VIZ_DIR" ]; then
86
148
  [ -d "$VIZ_DIR/node_modules" ] || (cd "$VIZ_DIR" && bun install >/dev/null 2>&1 || true)
87
149
  # The API serves the UI from ./dist. Without a build it answers the API but
@@ -1,50 +0,0 @@
1
- # config/agents.yaml — the Allfather's own combination of smiths and models.
2
- #
3
- # THIS IS YOUR FILE. Edit it, then run: bin/agents-config.sh apply
4
- # No other Ymir file hardcodes a model for you; this YAML is the one place.
5
- # Any agent without its own `model:` inherits `default_model`.
6
-
7
- version: 1
8
-
9
- # The harness is chosen by where the model lives — local through pi, online
10
- # through opencode. An agent may still name `harness:` to override the rule.
11
- harness:
12
- local: pi # a model on a local provider runs through pi
13
- online: opencode # a hosted model runs through opencode
14
- local_providers: [llama.cpp, llama-cpp, lmstudio]
15
- # The same local server is named differently per harness; translate on the way.
16
- providers:
17
- llama.cpp: { pi: llama-cpp }
18
-
19
- # The fallback smith for any agent that does not name a model.
20
- default_model: llama.cpp/qwen3.6-35b-a3b@q2_k_xl
21
-
22
- # Local, keyless model servers. `apply` writes these into the project harness
23
- # config (opencode.json) so a model listed here is immediately usable. A bare
24
- # alias (no @quant) is auto-resolved against the server's /v1/models.
25
- providers:
26
- llama.cpp:
27
- npm: "@ai-sdk/openai-compatible"
28
- name: "llama-server (local)"
29
- base_url: "http://127.0.0.1:8080/v1"
30
- models:
31
- # A local llama.cpp router needs the EXACT served id (the @quant suffix);
32
- # `apply` resolves the bare alias below to the id the server reports.
33
- - frontend-design-expert-8b
34
-
35
- # agent name -> { model, harness?, primary? }. Omit model to inherit
36
- # default_model. Omit harness to follow the local/online rule above.
37
- # `primary: true` marks the agent runnable directly (mode: all), not only
38
- # dispatchable by another agent.
39
- agents:
40
- brokk: { model: llama.cpp/qwen3.6-35b-a3b@q2_k_xl }
41
- # Hnoss the Shaper runs local (no cloud key) -> pi, and is directly runnable.
42
- hnoss: { model: "llama.cpp/qwen3.6-35b-a3b@q2_k_xl", primary: true }
43
- sindri: { model: llama.cpp/qwen3.6-35b-a3b@q2_k_xl }
44
- bragi: { model: llama.cpp/qwen3.6-35b-a3b@q2_k_xl }
45
- huginn: { model: llama.cpp/qwen3.6-35b-a3b@q2_k_xl }
46
- kvasir: { model: llama.cpp/qwen3.6-35b-a3b@q2_k_xl }
47
- mimir: { model: llama.cpp/qwen3.6-35b-a3b@q2_k_xl }
48
- snotra: { model: llama.cpp/qwen3.6-35b-a3b@q2_k_xl }
49
- forseti: { model: llama.cpp/qwen3.6-35b-a3b@q2_k_xl }
50
- galdr: { model: llama.cpp/qwen3.6-35b-a3b@q2_k_xl }
@@ -1,27 +0,0 @@
1
- # config/tailscale-sync.example.yaml — TEMPLATE for the Allfather's machine mesh.
2
- #
3
- # Copies to the private, gitignored config/tailscale-sync.yaml on install, or
4
- # run: bin/tailscale-sync.sh init
5
- # Override the path with YMIR_TAILSCALE_SYNC.
6
- #
7
- # Sync is peer-to-peer over Tailscale (rsync over ssh, addressed by MagicDNS).
8
- # There is no central server and no relay unless the Allfather adds one.
9
-
10
- machine: zerwiz
11
-
12
- # The allowlist: only these tailnet hosts may be synced with. Use the MagicDNS
13
- # name (e.g. `ymir-desktop`) or the tailnet IP.
14
- peers:
15
- - host: omarchy-1
16
- - host: zerwizserver
17
-
18
- # What may move. Paths are on both ends; ~ expands on each machine.
19
- paths:
20
- - ~/.pi/agent/models.json
21
- - ~/.pi/agent/models-store.json
22
- - ~/.pi/agent/settings.json
23
- - ~/.pi/agent/trust.json
24
-
25
- # auth.json holds live credentials. Leave false unless the Allfather trusts
26
- # every listed peer to hold them; Tailscale encrypts the wire, not the trust.
27
- include_auth: false
Binary file
Binary file
Binary file