@zerwiz/ymir 0.1.13 → 0.1.15

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 (78) hide show
  1. package/.agents/agents/brokk.md +10 -9
  2. package/.agents/agents/forseti-reviewer.md +2 -2
  3. package/.agents/agents/mimir-planner.md +2 -2
  4. package/.agents/assets/agents/registry.md +22 -0
  5. package/.agents/config/agents.yaml +53 -0
  6. package/.agents/config/{cron.yaml → cron.yaml.example} +5 -3
  7. package/.agents/config/tailscale-sync.yaml +9 -0
  8. package/.agents/config/wedge-alarm +3 -0
  9. package/.agents/migrations/0003-private-data-separation.sh +4 -4
  10. package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +6 -1
  11. package/.agents/skills/galdr-ymirsystem/assets/installation.md +5 -4
  12. package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +5 -5
  13. package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +59 -4
  14. package/.agents/skills/galdr-ymirsystem/assets/registry.md +74 -338
  15. package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +1 -1
  16. package/.agents/skills/galdr-ymirsystem/assets/smidja.md +5 -3
  17. package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +1 -1
  18. package/.agents/skills/ratatoskr-a2a/SKILL.md +1 -1
  19. package/.agents/skills/tyr-check/SKILL.md +5 -5
  20. package/.agents/skills/ymir-host/SKILL.md +1 -1
  21. package/.agents/state/.wake-queue +0 -0
  22. package/.agents/state/.wake-queue.seq +1 -0
  23. package/.agents/state/.watcher-down +1 -0
  24. package/.agents/state/procevent-inbox/when-hall-button.1.adapter +1 -0
  25. package/.agents/state/procevent-inbox/when-hall-button.1.handled +0 -0
  26. package/.agents/state/procevent-inbox/when-hall-button.1.result +8 -0
  27. package/.agents/state/procevent-inbox/when-hall-button.2.adapter +1 -0
  28. package/.agents/state/procevent-inbox/when-hall-button.2.handled +0 -0
  29. package/.agents/state/procevent-inbox/when-hall-button.2.result +5 -0
  30. package/.agents/state/procevent-inbox/when-odrerir.1.adapter +1 -0
  31. package/.agents/state/procevent-inbox/when-odrerir.1.handled +0 -0
  32. package/.agents/state/procevent-inbox/when-odrerir.1.result +8 -0
  33. package/.agents/state/procevent-inbox/when-odrerir.2.adapter +1 -0
  34. package/.agents/state/procevent-inbox/when-odrerir.2.handled +0 -0
  35. package/.agents/state/procevent-inbox/when-odrerir.2.result +5 -0
  36. package/.agents/state/procevent-inbox/when-odrerir.3.adapter +1 -0
  37. package/.agents/state/procevent-inbox/when-odrerir.3.handled +0 -0
  38. package/.agents/state/procevent-inbox/when-odrerir.3.result +5 -0
  39. package/.agents/state/procevent-inbox/when-odrerir.4.adapter +1 -0
  40. package/.agents/state/procevent-inbox/when-odrerir.4.handled +0 -0
  41. package/.agents/state/procevent-inbox/when-odrerir.4.result +8 -0
  42. package/.agents/state/procevent-inbox/when-odrerir.5.adapter +1 -0
  43. package/.agents/state/procevent-inbox/when-odrerir.5.handled +0 -0
  44. package/.agents/state/procevent-inbox/when-odrerir.5.result +8 -0
  45. package/.agents/state/procevent-inbox/when-sessrumnir-cloth.1.adapter +1 -0
  46. package/.agents/state/procevent-inbox/when-sessrumnir-cloth.1.handled +0 -0
  47. package/.agents/state/procevent-inbox/when-sessrumnir-cloth.1.result +8 -0
  48. package/AGENTS.md +17 -10
  49. package/CHANGELOG.md +129 -0
  50. package/bin/a2a-mcp.sh +9 -2
  51. package/bin/app-lib.sh +8 -1
  52. package/bin/docs-guard.sh +1 -1
  53. package/bin/erindi-brief.sh +1 -1
  54. package/bin/hamr-harness.sh +1 -1
  55. package/bin/hoard-lib.sh +10 -0
  56. package/bin/mimir-bridge.py +8 -1
  57. package/bin/mimir-bridge.sh +7 -1
  58. package/bin/mimir-reflect.sh +8 -1
  59. package/bin/nornir-cron-start.sh +21 -1
  60. package/bin/nornir-job-bragi-scrape.sh +116 -0
  61. package/bin/nornir-job-daily-briefing.sh +7 -7
  62. package/bin/nornir-job-forgejo-git.sh +87 -0
  63. package/bin/nornir-job-observer.sh +5 -4
  64. package/bin/rodd-operational-input.sh +1 -1
  65. package/bin/saga-session-start.sh +4 -3
  66. package/bin/sessrumnir.sh +11 -0
  67. package/bin/syn-arm-pretool-check.sh +1 -1
  68. package/bin/syn-cd-pretool-check.sh +1 -1
  69. package/bin/syn-watch-arm.sh +1 -1
  70. package/bin/vor-crew-state.sh +1 -1
  71. package/bin/ymir-install.sh +47 -2
  72. package/bin/ymir-marketing-stack.sh +323 -0
  73. package/docs/runbooks/models.md +1 -1
  74. package/docs/workspaces.md +4 -0
  75. package/package.json +10 -4
  76. package/scripts/electron.sh +2 -2
  77. package/.agents/bus/messages.json +0 -1
  78. package/.agents/config/agents.machine.example.yaml +0 -16
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env bash
2
+ # nornir-job-forgejo-git.sh — the local git round: issues and PRs on the forge.
3
+ #
4
+ # The Forgejo door (forgejo.zerwiz.org → the server's :3030, or a locally
5
+ # provisioned forge via bin/ymir-marketing-stack.sh --with-forgejo) holds the
6
+ # Allfather's issue-to-PR loop. Each morning this job reads the forge's open
7
+ # issues, lands a public-safe digest in the hoard, and carves a Rune with the
8
+ # tally — so a broken door or an accumulating queue is seen at sunrise, never
9
+ # found by accident.
10
+ #
11
+ # The forge address comes from the USER'S home, never the tree (Rule 04/07):
12
+ # $YMIR_HOME/config/forge.env : FORGEJO_URL=... FORGEJO_TOKEN=... (optional)
13
+ # Stateless: read the door → list issues → write one digest → carve one Rune.
14
+ set -u
15
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+ ROOT="${BROKK_ROOT_OVERRIDE:-$(cd "$SCRIPT_DIR/.." && pwd)}"
17
+
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
+ ymir_home_root _HOME
26
+
27
+ FORGE_ENV="$_HOME/config/forge.env"
28
+ [ -r "$FORGE_ENV" ] && . "$FORGE_ENV"
29
+ FORGEJO_URL="${FORGEJO_URL:-}"
30
+ FORGEJO_TOKEN="${FORGEJO_TOKEN:-}"
31
+
32
+ # No address configured — nothing scheduled, exit clean (the same honest shape
33
+ # as a scrape round with no sources).
34
+ if [ -z "$FORGEJO_URL" ]; then
35
+ printf 'forgejo-git: no FORGEJO_URL in %s — issues loop not armed (exit 0)\n' "$FORGE_ENV"
36
+ exit 0
37
+ fi
38
+
39
+ # The API base: https://host/api/v1 (Forgejo/Gitea compatible).
40
+ API="${FORGEJO_URL%/}/api/v1"
41
+ AUTH=()
42
+ [ -n "$FORGEJO_TOKEN" ] && AUTH=(-H "Authorization: token $FORGEJO_TOKEN")
43
+
44
+ # The door must actually answer — a dead tunnel is reported, not guessed.
45
+ if ! curl -fsS --max-time 15 "${AUTH[@]}" "$API/version" >/dev/null 2>&1; then
46
+ printf 'forgejo-git: door closed at %s (tunnel down?) — issues not listed\n' "$FORGEJO_URL"
47
+ # shellcheck source=bin/runes-append.sh
48
+ . "$ROOT/bin/runes-append.sh"
49
+ runes_append "forgejo" "git.door-down" --realm "${BROKK_REALM:-}" \
50
+ --message "Forgejo door closed at $FORGEJO_URL — tunnel or service down" >/dev/null 2>&1 || true
51
+ exit 1
52
+ fi
53
+
54
+ # All repos' open issues (the forge API lists them per repo; we query the
55
+ # user's token scope: /repos/issues/search?type=issues&state=open).
56
+ ISSUES_JSON="$(curl -fsS --max-time 25 "${AUTH[@]}" \
57
+ "$API/repos/issues/search?type=issues&state=open&limit=50" 2>/dev/null)" || ISSUES_JSON="[]"
58
+
59
+ COUNT=$(printf '%s' "$ISSUES_JSON" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0)
60
+ ISSUES="$(printf '%s' "$ISSUES_JSON" | python3 -c "
61
+ import json,sys
62
+ try:
63
+ d=json.load(sys.stdin)
64
+ for i in d[:15]:
65
+ repo=(i.get('repository') or {}).get('full_name','?')
66
+ print('- [%s] %s (@%s)' % (repo, i.get('title','?')[:90], i.get('user',{}).get('login','?')))
67
+ except Exception:
68
+ pass
69
+ " 2>/dev/null)"
70
+
71
+ TODAY="$(date -u +%Y-%m-%d)"
72
+ DIGEST="$_HOME/memory/daily/forgejo-$TODAY.md"
73
+ mkdir -p "$(dirname "$DIGEST")"
74
+ {
75
+ printf '# Forgejo issues — %s\n\n' "$TODAY"
76
+ printf '%s open (limit 50 scanned; first 15 shown)\n\n' "$COUNT"
77
+ printf '%s\n' "$ISSUES"
78
+ } >"$DIGEST"
79
+
80
+ printf 'forgejo-git: issues=%s digest=%s\n' "$COUNT" "$DIGEST"
81
+
82
+ # shellcheck source=bin/runes-append.sh
83
+ . "$ROOT/bin/runes-append.sh"
84
+ runes_append "forgejo" "git.issues" --realm "${BROKK_REALM:-}" \
85
+ --message "forgejo issues round: $COUNT open -> $DIGEST" >/dev/null \
86
+ || printf 'forgejo-git: rune append failed (non-fatal)\n'
87
+ exit 0
@@ -4,7 +4,7 @@
4
4
  # Read-only observation of the Ymir runtime into Runes. This job touches nothing
5
5
  # outside Ymir; every source below lives inside the repo (or the external worktree
6
6
  # root, which is read-only):
7
- # docs/masterplan.md open forge orders
7
+ # hodd/docs/masterplan.md open forge orders
8
8
  # .agents/agents/*.md the agent roster
9
9
  # .agents/memory/well/ the well (episodes)
10
10
  # workspace/memory/runes_audit.md the ledger
@@ -32,10 +32,11 @@ WORKTREE_ROOT="${BROKK_YGGDRASIL_ROOT:-$HOME/.treehouse}"
32
32
  YMIR_HOME="${YMIR_HOME:-$HOME/Documents/Ymir}"
33
33
  SMIDJA_DB="${YMIR_HOME:+$YMIR_HOME/smidja/smidja.db}"
34
34
  [ -z "${SMIDJA_DB:-}" ] || [ ! -f "$SMIDJA_DB" ] && SMIDJA_DB="$ROOT/apps/smidja/smidja_data/smidja.db"
35
- MASTERPLAN="$ROOT/docs/masterplan.md"
36
- WELL_DIR="$ROOT/.agents/memory/well"
35
+ MASTERPLAN="${BROKK_MASTERPLAN:-${YMIR_HOME:-$HOME/Documents/Ymir}/hodd/docs/masterplan.md}"
36
+ WELL_DIR="${BROKK_WELL_DIR:-${YMIR_HOME:+$YMIR_HOME/hodd/memory/well}}"
37
+ [ -n "$WELL_DIR" ] && [ -d "$WELL_DIR" ] || WELL_DIR="$ROOT/.agents/memory/well"
37
38
  AGENTS_DIR="$ROOT/.agents/agents"
38
- RUNES_LEDGER="${YMIR_HOME:+$YMIR_HOME/memory/runes_audit.md}"
39
+ RUNES_LEDGER="${YMIR_HOME:+$YMIR_HOME/hodd/memory/runes_audit.md}"
39
40
  [ -z "${RUNES_LEDGER:-}" ] || [ ! -r "$RUNES_LEDGER" ] && RUNES_LEDGER="$ROOT/workspace/memory/runes_audit.md"
40
41
  OBS_LOG="$STATE/observer.log"
41
42
  OBS_LAST="$STATE/observer.last"
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # Rödd ("voice") is the structured message wire between the Brokk primary and
5
5
  # Ymir's workers. Ported from the upstream agent-distro reference for the Brokk
6
- # distro runtime (docs/plans/29-brokk-distro-runtime.md). This file is both a
6
+ # distro runtime (memory/plans/core/29-brokk-distro-runtime.md). This file is both a
7
7
  # source-safe shell library and the cross-language CLI used by the .pi / .opencode
8
8
  # integrations.
9
9
  #
@@ -4,7 +4,7 @@
4
4
  # Sága ("the seeress", who sees all that happens) collapses the Brokk runtime's
5
5
  # session start into ONE ordered digest, so a harness opens in one or two turns
6
6
  # instead of many reads. Ported from the upstream agent-distro reference for plan 29
7
- # (docs/plans/29-brokk-distro-runtime.md).
7
+ # (memory/plans/core/29-brokk-distro-runtime.md).
8
8
  #
9
9
  # Stages, in order:
10
10
  # 1. lock acquire the per-home session lock first
@@ -109,8 +109,9 @@ if [ -d "$STATE" ]; then
109
109
  else
110
110
  printf 'task metadata records: 0\n'
111
111
  fi
112
- if [ -r "$ROOT/docs/masterplan.md" ]; then
113
- open=$(grep -c '^- Status: ADDED' "$ROOT/docs/masterplan.md" 2>/dev/null || echo 0)
112
+ MP="${BROKK_MASTERPLAN:-${YMIR_HOME:-$HOME/Documents/Ymir}/hodd/docs/masterplan.md}"
113
+ if [ -r "$MP" ]; then
114
+ open=$(grep -c '^- Status: ADDED' "$MP" 2>/dev/null || echo 0)
114
115
  printf 'open forge orders: %s\n' "${open:-0}"
115
116
  fi
116
117
 
package/bin/sessrumnir.sh CHANGED
@@ -42,6 +42,17 @@ APP="$APP_SESSRUMNIR"
42
42
  PID_FILE="$YMIR_STATE_DIR/sessrumnir.pid"
43
43
  LOG_FILE="$YMIR_STATE_DIR/sessrumnir.log"
44
44
 
45
+ # npm gates install scripts, so the Electron runtime a seat needs is often absent
46
+ # after a global install — the window then cannot open and says nothing useful.
47
+ # Mend it before refusing (the same lesson as scripts/electron.sh).
48
+ repair_runtime() {
49
+ local dir="$1"
50
+ [ -f "$dir/node_modules/electron/path.txt" ] && return 0
51
+ command -v npm >/dev/null 2>&1 || return 1
52
+ echo "Sessrúmnir — its Electron runtime is absent; fetching it (this is the gated postinstall)…" >&2
53
+ ( cd "$dir" && npm rebuild electron >/dev/null 2>&1 ) && [ -f "$dir/node_modules/electron/path.txt" ]
54
+ }
55
+
45
56
  case "${1-}" in -v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;; -h|--help|"") sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; esac
46
57
  ACTION="${1:-start}"; shift || true
47
58
  WORKSPACE="${1-}"
@@ -8,7 +8,7 @@
8
8
  # the extension. Reading it, or merely checking its syntax, is fine.
9
9
  #
10
10
  # Exit: 0 = allow, 2 = block (stderr carries the reason).
11
- # Owner: plan 29 (docs/plans/29-brokk-distro-runtime.md).
11
+ # Owner: plan 29 (memory/plans/core/29-brokk-distro-runtime.md).
12
12
  set -u
13
13
 
14
14
  command_text=""
@@ -4,7 +4,7 @@
4
4
  # Sýn guards the working directory. v0 is inert (always allow); the contract
5
5
  # exists so the Sýn Pi extension can deny a bash invocation that would strand
6
6
  # the primary outside its checkout. Owner: plan 29
7
- # (docs/plans/29-brokk-distro-runtime.md).
7
+ # (memory/plans/core/29-brokk-distro-runtime.md).
8
8
  #
9
9
  # Usage: syn-cd-pretool-check.sh --command <bash command>
10
10
  # Exit: 0 = allow, 2 = block (stderr carries the reason)
@@ -5,7 +5,7 @@
5
5
  # actionable `signal:`/`stale:`/`check:`/`heartbeat:` line when something needs
6
6
  # the primary. The Pi extension (gna-pi-watch.ts) owns continuity and re-arms.
7
7
  # Ported/trimmed from the upstream agent-distro reference for plan 29
8
- # (docs/plans/29-brokk-distro-runtime.md).
8
+ # (memory/plans/core/29-brokk-distro-runtime.md).
9
9
  #
10
10
  # Usage: syn-watch-arm.sh --restart
11
11
  # syn-watch-arm.sh --handling-delivered <generation> --watcher-pid <pid>
@@ -14,7 +14,7 @@
14
14
  # from a tail alone, and is read-only.
15
15
  #
16
16
  # Ported from the upstream distro crew-state reconciliation for plan 29
17
- # (docs/plans/29-brokk-distro-runtime.md).
17
+ # (memory/plans/core/29-brokk-distro-runtime.md).
18
18
  #
19
19
  # Usage: vor-crew-state.sh <task-id>
20
20
  # Exits 0 on any successful read regardless of state; exit 2 on a usage error.
@@ -131,7 +131,9 @@ Each row is a phase, a step, its state, and why.
131
131
  Nothing is deleted. Every step is idempotent.
132
132
  PLAN
133
133
  printf '\n'
134
- bash "$SCRIPT_DIR/ymir-plan.sh" --colour >/dev/null 2>&1 || true
134
+ # The rendering lives on stderr (the cloth); the TOON on stdout is data and is
135
+ # dropped here. Sending BOTH to /dev/null is how the plan became invisible.
136
+ bash "$SCRIPT_DIR/ymir-plan.sh" --colour >/dev/null || true
135
137
  printf '\nProceed with the install? [y/N] '
136
138
  read -r reply || reply=""
137
139
  case "$reply" in
@@ -291,6 +293,15 @@ step_home() {
291
293
  # engine (apps/smidja) is stamped from the cloned factory's templates, exactly
292
294
  # as install.py does for a target repo.
293
295
  step_apps() {
296
+ # A packaged install has no apps/ of its own on purpose: the surfaces arrive as
297
+ # dependencies. Cloning the repos into node_modules/@zerwiz/ymir/apps/ is how a
298
+ # hollow directory shadowed a real package, so a package tree clones nothing.
299
+ # A CLONE still clones — that is the shape this step exists for.
300
+ case "$ROOT" in
301
+ */node_modules/*)
302
+ add apps SKIP "a package install — the surfaces are dependencies (npm i -g @zerwiz/ymir)"
303
+ return 0 ;;
304
+ esac
294
305
  local reg="$HOARD/identity/projects.yaml"
295
306
  if [ ! -r "$reg" ]; then
296
307
  add apps SKIP "no registry — no app repos to pull ($reg)"
@@ -533,7 +544,14 @@ step_sandbox() {
533
544
 
534
545
  # ── 5. memory (well + harness MCP) ───────────────────────────────────────────
535
546
  step_memory() {
536
- local db="$YMIR_HOME/memory/kaia.engram" mcp=0
547
+ # The well is ONE memory and it lives in the hoard — always.
548
+ if [ -n "${ENGRAM_DB:-}" ]; then
549
+ local db="$ENGRAM_DB"
550
+ else
551
+ . "$SCRIPT_DIR/hoard-lib.sh" 2>/dev/null || true
552
+ hoard_memory_store db
553
+ fi
554
+ local mcp=0
537
555
  [ "$CHECK" = 0 ] && "$SCRIPT_DIR/mimir-bridge.sh" --start >/dev/null 2>&1 || true
538
556
  for f in "$ROOT/opencode.json" "$HOME/.config/opencode/opencode.json" "$HOME/.pi/agent/settings.json" "$HOME/.claude.json" "$HOME/.cursor/mcp.json"; do
539
557
  [ -f "$f" ] && grep -q '"engram"' "$f" 2>/dev/null && mcp=$((mcp+1))
@@ -851,6 +869,33 @@ step_auth; step_invite; step_register
851
869
 
852
870
  printf 'install[%d]{step,status,detail}:\n' "${#IDS[@]}"
853
871
  for i in "${!IDS[@]}"; do printf ' "%s","%s","%s"\n' "${IDS[$i]}" "${STATUS[$i]}" "${DETAIL[$i]}"; done
872
+ # The same rows, rendered for the eye (stderr) — the TOON above is the data a
873
+ # script reads, this is what a person gets. An install that ends in a raw table
874
+ # has no design, and design here is not decoration: it is whether a reader can
875
+ # see, at a glance, what stands, what warned, and what was skipped and why.
876
+ if [ "${STYLE_ON:-0}" = 1 ]; then
877
+ ok=0; warn=0; skip=0; fail=0
878
+ for s_ in "${STATUS[@]}"; do
879
+ case "$s_" in OK) ok=$((ok+1)) ;; WARN) warn=$((warn+1)) ;; SKIP) skip=$((skip+1)) ;; FAIL) fail=$((fail+1)) ;; esac
880
+ done
881
+ style_rule 56
882
+ if [ "$fail" -gt 0 ]; then
883
+ style_line FAIL "not usable" "$fail step(s) failed — nothing below it can be trusted"
884
+ elif [ "$warn" -gt 0 ]; then
885
+ style_line OK "it stands" "$ok steps done · $warn warned · $skip skipped"
886
+ else
887
+ style_line OK "it stands" "$ok steps done · $skip skipped — nothing warned"
888
+ fi
889
+ # the warnings carry the why; they are the rows a person must read
890
+ for i in "${!IDS[@]}"; do
891
+ [ "${STATUS[$i]}" = WARN ] && style_line WARN "${IDS[$i]}" "${DETAIL[$i]}"
892
+ [ "${STATUS[$i]}" = FAIL ] && style_line FAIL "${IDS[$i]}" "${DETAIL[$i]}"
893
+ done
894
+ # and the skips as one line, with their reasons, since they are decisions not faults
895
+ skips=""
896
+ for i in "${!IDS[@]}"; do [ "${STATUS[$i]}" = SKIP ] && skips="$skips ${IDS[$i]}"; done
897
+ [ -n "$skips" ] && style_hint "skipped:$skips — each with its reason in the table above"
898
+ fi
854
899
  printf '\nnext: gh auth login · register your projects in hoard/identity/projects.yaml · open http://127.0.0.1:3888/\n'
855
900
  [ -n "$INVITE_CODE" ] && printf 'invite: %s — share it to let someone register (bin/ymir-invite.sh list shows what is spent)\n' "$INVITE_CODE"
856
901
 
@@ -0,0 +1,323 @@
1
+ #!/usr/bin/env bash
2
+ # ymir-marketing-stack.sh — the marketing stack, provisionable on ANY computer.
3
+ #
4
+ # Stands up the Allfather's marketing techstack as a user-space Docker Compose
5
+ # set: Mautic (email automation), Postiz (social publishing), Activepieces
6
+ # (workflow), and — with --with-forgejo — Forgejo (local git issues & PRs).
7
+ # Every Eindri (Bragi for marketing, Sindri for git) can run this for another
8
+ # user; nothing here assumes the server. Ports are env-virtualized so two
9
+ # stacks never collide on one host.
10
+ #
11
+ # bin/ymir-marketing-stack.sh up [--with-forgejo] [--dir DIR]
12
+ # [--name NAME] [--host-port P]
13
+ # bin/ymir-marketing-stack.sh status [--dir DIR]
14
+ # bin/ymir-marketing-stack.sh down [--dir DIR]
15
+ # bin/ymir-marketing-stack.sh doors [--dir DIR] # where each service lives
16
+ #
17
+ # Config: env vars (see below) or $YMIR_HOME/config/marketing-stack.env.
18
+ # OSS engines only — Mautic 7 (Apache), Postiz, Activepieces, Forgejo.
19
+ set -u
20
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
+ ROOT="${BROKK_ROOT_OVERRIDE:-$(cd "$SCRIPT_DIR/.." && pwd)}"
22
+
23
+ # The home the operator chose — one answer, never drift (Rule 04/07).
24
+ if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
25
+ _yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
26
+ for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
27
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
28
+ done
29
+ unset _yr _yc
30
+ fi
31
+ hoard_root _HOME
32
+ hoard_state_dir _STATE
33
+
34
+ # ---- config resolution: env first, then the home's file ---------------------
35
+ CONF_ENV="$_HOME/config/marketing-stack.env"
36
+ [ -r "$CONF_ENV" ] && . "$CONF_ENV"
37
+
38
+ ACTION="${1:-up}"; shift || true
39
+ WITH_FORGEJO=0; DIR="${YMIR_MARKETING_STACK_DIR:-$_STATE/marketing-stack}"
40
+ NAME="${MARKETING_STACK_NAME:-marketing}"
41
+ HOST_PORT="${MARKETING_STACK_HOST_PORT:-8001}"
42
+ while [ $# -gt 0 ]; do
43
+ case "$1" in
44
+ --with-forgejo) WITH_FORGEJO=1; shift ;;
45
+ --dir) DIR=${2-}; [ $# -ge 2 ] && shift 2 || shift ;;
46
+ --name) NAME=${2-}; [ $# -ge 2 ] && shift 2 || shift ;;
47
+ --host-port) HOST_PORT=${2-}; [ $# -ge 2 ] && shift 2 || shift ;;
48
+ *) printf 'error: unknown flag %s\nhelp: ymir-marketing-stack.sh up|status|down|doors [--with-forgejo] [--dir DIR] [--host-port P]\n' "$1" >&2; exit 2 ;;
49
+ esac
50
+ done
51
+
52
+ # ---- secrets: generated once, kept in the home, never inline -----------------
53
+ SECRETS="$DIR/.secrets.env"
54
+ gen_secret() { head -c 24 /dev/urandom | od -An -tx1 | tr -d ' \n' | head -c 40; }
55
+ if [ ! -f "$SECRETS" ]; then
56
+ mkdir -p "$DIR"
57
+ {
58
+ echo "MAUTIC_DB_PASSWORD=$(gen_secret)"
59
+ echo "POSTIZ_DB_PASSWORD=$(gen_secret)"
60
+ echo "POSTIZ_JWT_SECRET=$(gen_secret)"
61
+ echo "AP_POSTGRES_PASSWORD=$(gen_secret)"
62
+ echo "FORGEJO_DB_PASSWORD=$(gen_secret)"
63
+ echo "FORGEJO_SECRET_KEY=$(gen_secret)"
64
+ } >"$SECRETS"
65
+ chmod 600 "$SECRETS"
66
+ fi
67
+ . "$SECRETS"
68
+
69
+ MAUTIC_PORT="${MAUTIC_PORT:-$HOST_PORT}"
70
+ POSTIZ_PORT="${POSTIZ_PORT:-$((HOST_PORT + 2))}"
71
+ AP_PORT="${AP_PORT:-$((HOST_PORT + 4))}"
72
+ FORGEJO_PORT="${FORGEJO_PORT:-$((HOST_PORT + 6))}"
73
+
74
+ STRATEGY="compose"
75
+
76
+ # ---- the compose file, rendered fresh each run -------------------------------
77
+ render_compose() {
78
+ local forgejo_services="" forgejo_volumes=""
79
+ if [ "$WITH_FORGEJO" = 1 ]; then
80
+ forgejo_services='
81
+ forgejo:
82
+ image: codeberg.org/forgejo/forgejo:16
83
+ container_name: "'"$NAME"'-forgejo"
84
+ restart: unless-stopped
85
+ environment:
86
+ FORGEJO__server__ROOT_URL: "http://127.0.0.1:'"$FORGEJO_PORT"'/"
87
+ FORGEJO__server__HTTP_PORT: "3000"
88
+ FORGEJO__database__DB_TYPE: postgres
89
+ FORGEJO__database__HOST: forgejo-db
90
+ FORGEJO__database__NAME: forgejo
91
+ FORGEJO__database__USER: forgejo
92
+ FORGEJO__database__PASSWD: '"$FORGEJO_DB_PASSWORD"'
93
+ ports:
94
+ - "127.0.0.1:'"$FORGEJO_PORT"':3000"
95
+ volumes:
96
+ - forgejo-data:/data
97
+ depends_on:
98
+ forgejo-db:
99
+ condition: service_started
100
+
101
+ forgejo-db:
102
+ image: postgres:16-alpine
103
+ container_name: "'"$NAME"'-forgejo-db"
104
+ restart: unless-stopped
105
+ environment:
106
+ POSTGRES_DB: forgejo
107
+ POSTGRES_USER: forgejo
108
+ POSTGRES_PASSWORD: '"$FORGEJO_DB_PASSWORD"'
109
+ volumes:
110
+ - forgejo-db:/var/lib/postgresql/data
111
+ '
112
+ forgejo_volumes=' forgejo-data:
113
+ forgejo-db:'
114
+ fi
115
+ cat >"$DIR/docker-compose.yml" <<EOF
116
+ # generated by ymir-marketing-stack.sh ($(date -u +%Y-%m-%dT%H:%M:%SZ))
117
+ # OSS engines: mautic/mautic:7 · ghcr.io/gitroomhq/postiz-app · ghcr.io/activepieces/activepieces · codeberg.org/forgejo/forgejo
118
+ services:
119
+ ${NAME}-mautic-web:
120
+ image: mautic/mautic:7.0.1-apache
121
+ container_name: "${NAME}-mautic-web"
122
+ restart: unless-stopped
123
+ ports:
124
+ - "127.0.0.1:${MAUTIC_PORT}:80"
125
+ environment:
126
+ MAUTIC_DB_HOST: ${NAME}-mautic-db
127
+ MAUTIC_DB_PORT: "3306"
128
+ MAUTIC_DB_DATABASE: mautic
129
+ MAUTIC_DB_USER: mautic
130
+ MAUTIC_DB_PASSWORD: ${MAUTIC_DB_PASSWORD}
131
+ DOCKER_MAUTIC_ROLE: mautic_web
132
+ DOCKER_MAUTIC_RUN_MIGRATIONS: "true"
133
+ volumes:
134
+ - mautic-config:/var/www/html/config
135
+ - mautic-logs:/var/www/html/var/logs
136
+ - mautic-media-files:/var/www/html/docroot/media/files
137
+ - mautic-media-images:/var/www/html/docroot/media/images
138
+ depends_on:
139
+ ${NAME}-mautic-db:
140
+ condition: service_healthy
141
+
142
+ ${NAME}-mautic-worker:
143
+ image: mautic/mautic:7.0.1-apache
144
+ container_name: "${NAME}-mautic-worker"
145
+ restart: unless-stopped
146
+ environment:
147
+ MAUTIC_DB_HOST: ${NAME}-mautic-db
148
+ MAUTIC_DB_PORT: "3306"
149
+ MAUTIC_DB_DATABASE: mautic
150
+ MAUTIC_DB_USER: mautic
151
+ MAUTIC_DB_PASSWORD: ${MAUTIC_DB_PASSWORD}
152
+ DOCKER_MAUTIC_ROLE: mautic_worker
153
+ DOCKER_MAUTIC_WORKERS_CONSUME_EMAIL: "2"
154
+ DOCKER_MAUTIC_WORKERS_CONSUME_HIT: "2"
155
+ DOCKER_MAUTIC_WORKERS_CONSUME_FAILED: "2"
156
+ volumes:
157
+ - mautic-config:/var/www/html/config
158
+ - mautic-logs:/var/www/html/var/logs
159
+ - mautic-media-files:/var/www/html/docroot/media/files
160
+ - mautic-media-images:/var/www/html/docroot/media/images
161
+ depends_on:
162
+ ${NAME}-mautic-db:
163
+ condition: service_healthy
164
+
165
+ ${NAME}-mautic-db:
166
+ image: mariadb:10.11
167
+ container_name: "${NAME}-mautic-db"
168
+ restart: unless-stopped
169
+ command: >
170
+ --character-set-server=utf8mb4
171
+ --collation-server=utf8mb4_unicode_ci
172
+ environment:
173
+ MYSQL_DATABASE: mautic
174
+ MYSQL_USER: mautic
175
+ MYSQL_PASSWORD: ${MAUTIC_DB_PASSWORD}
176
+ MYSQL_ROOT_PASSWORD: ${MAUTIC_DB_PASSWORD}
177
+ volumes:
178
+ - mautic-db:/var/lib/mysql
179
+ healthcheck:
180
+ test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
181
+ interval: 10s
182
+ timeout: 5s
183
+ retries: 6
184
+
185
+ ${NAME}-postiz:
186
+ image: ghcr.io/gitroomhq/postiz-app:latest
187
+ container_name: "${NAME}-postiz"
188
+ restart: always
189
+ environment:
190
+ MAIN_URL: "http://127.0.0.1:${POSTIZ_PORT}"
191
+ FRONTEND_URL: "http://127.0.0.1:${POSTIZ_PORT}"
192
+ NEXT_PUBLIC_BACKEND_URL: "http://127.0.0.1:${POSTIZ_PORT}/api"
193
+ JWT_SECRET: ${POSTIZ_JWT_SECRET}
194
+ DATABASE_URL: "postgresql://postiz-user:${POSTIZ_DB_PASSWORD}@${NAME}-postiz-db:5432/postiz-db-local"
195
+ REDIS_URL: "redis://${NAME}-postiz-redis:6379"
196
+ BACKEND_INTERNAL_URL: "http://localhost:3000"
197
+ IS_GENERAL: "true"
198
+ DISABLE_REGISTRATION: "false"
199
+ RUN_CRON: "true"
200
+ STORAGE_PROVIDER: "local"
201
+ UPLOAD_DIRECTORY: "/uploads"
202
+ NEXT_PUBLIC_UPLOAD_DIRECTORY: "/uploads"
203
+ ports:
204
+ - "127.0.0.1:${POSTIZ_PORT}:5000"
205
+ depends_on:
206
+ - ${NAME}-postiz-db
207
+ - ${NAME}-postiz-redis
208
+
209
+ ${NAME}-postiz-db:
210
+ image: postgres:17-alpine
211
+ container_name: "${NAME}-postiz-db"
212
+ restart: always
213
+ environment:
214
+ POSTGRES_DB: postiz-db-local
215
+ POSTGRES_USER: postiz-user
216
+ POSTGRES_PASSWORD: ${POSTIZ_DB_PASSWORD}
217
+ volumes:
218
+ - postiz-db:/var/lib/postgresql/data
219
+
220
+ ${NAME}-postiz-redis:
221
+ image: redis:7-alpine
222
+ container_name: "${NAME}-postiz-redis"
223
+ restart: always
224
+ volumes:
225
+ - postiz-redis:/data
226
+
227
+ ${NAME}-activepieces:
228
+ image: ghcr.io/activepieces/activepieces:latest
229
+ container_name: "${NAME}-activepieces"
230
+ restart: unless-stopped
231
+ ports:
232
+ - "127.0.0.1:${AP_PORT}:80"
233
+ environment:
234
+ AP_FRONTEND_URL: "http://127.0.0.1:${AP_PORT}"
235
+ AP_ENGINE_EXECUTABLE_PATH: "dist/packages/engine/main.js"
236
+ AP_DB_TYPE: POSTGRES
237
+ AP_DB_USERNAME: ap_user
238
+ AP_DB_PASSWORD: ${AP_POSTGRES_PASSWORD}
239
+ AP_DB_DATABASE: activepieces
240
+ AP_DB_HOST: ${NAME}-activepieces-db
241
+ AP_DB_PORT: "5432"
242
+ AP_REDIS_HOST: ${NAME}-activepieces-redis
243
+ AP_REDIS_PORT: "6379"
244
+ AP_QUEUE_MODE: REDIS
245
+ volumes:
246
+ - activepieces-cache:/usr/src/app/cache
247
+ depends_on:
248
+ ${NAME}-activepieces-db:
249
+ condition: service_healthy
250
+ ${NAME}-activepieces-redis:
251
+ condition: service_started
252
+
253
+ ${NAME}-activepieces-db:
254
+ image: postgres:16-alpine
255
+ container_name: "${NAME}-activepieces-db"
256
+ restart: unless-stopped
257
+ environment:
258
+ POSTGRES_DB: activepieces
259
+ POSTGRES_USER: ap_user
260
+ POSTGRES_PASSWORD: ${AP_POSTGRES_PASSWORD}
261
+ volumes:
262
+ - activepieces-db:/var/lib/postgresql/data
263
+ healthcheck:
264
+ test: ["CMD-SHELL", "pg_isready -U ap_user"]
265
+ interval: 10s
266
+ start_period: 20s
267
+
268
+ ${NAME}-activepieces-redis:
269
+ image: redis:7-alpine
270
+ container_name: "${NAME}-activepieces-redis"
271
+ restart: unless-stopped
272
+ volumes:
273
+ - activepieces-redis:/data
274
+ ${forgejo_services}
275
+ volumes:
276
+ mautic-config:
277
+ mautic-logs:
278
+ mautic-media-files:
279
+ mautic-media-images:
280
+ mautic-db:
281
+ postiz-db:
282
+ postiz-redis:
283
+ activepieces-cache:
284
+ activepieces-db:
285
+ activepieces-redis:
286
+ ${forgejo_volumes}
287
+ EOF
288
+ }
289
+
290
+ engine_up() {
291
+ render_compose
292
+ (cd "$DIR" && docker compose up -d 2>&1 | tail -4)
293
+ }
294
+
295
+ status() {
296
+ if [ ! -f "$DIR/docker-compose.yml" ]; then
297
+ printf 'marketing-stack[1]{state,dir}:\n "not-provisioned","%s"\n' "$DIR"
298
+ return 0
299
+ fi
300
+ (cd "$DIR" && docker compose ps --format 'table {{.Name}}\t{{.Status}}\t{{.Ports}}' 2>/dev/null | head -14)
301
+ }
302
+
303
+ doors() {
304
+ printf 'marketing-stack[1]{service,url}:\n'
305
+ printf ' "Mautic (email automation)",http://127.0.0.1:%s\n' "$MAUTIC_PORT"
306
+ printf ' "Postiz (social publishing)",http://127.0.0.1:%s\n' "$POSTIZ_PORT"
307
+ printf ' "Activepieces (workflow)",http://127.0.0.1:%s\n' "$AP_PORT"
308
+ [ "$WITH_FORGEJO" = 1 ] && printf ' "Forgejo (git · issues · PRs)",http://127.0.0.1:%s\n' "$FORGEJO_PORT"
309
+ }
310
+
311
+ case "$ACTION" in
312
+ up) engine_up ;;
313
+ status) status ;;
314
+ down) [ -f "$DIR/docker-compose.yml" ] && (cd "$DIR" && docker compose down 2>&1 | tail -2) || printf 'marketing-stack: nothing provisioned at %s\n' "$DIR" ;;
315
+ doors) doors ;;
316
+ *) printf 'error: unknown action %s\nhelp: ymir-marketing-stack.sh up|status|down|doors\n' "$ACTION" >&2; exit 2 ;;
317
+ esac
318
+
319
+ # ---- carve the Rune (append-only ledger, in the hoard) -----------------------
320
+ # shellcheck source=bin/runes-append.sh
321
+ . "$ROOT/bin/runes-append.sh" 2>/dev/null || true
322
+ runes_append "marketing" "stack.$ACTION" --realm "${BROKK_REALM:-}" \
323
+ --message "marketing stack $ACTION (Mautic:$MAUTIC_PORT Postiz:$POSTIZ_PORT Activepieces:$AP_PORT${WITH_FORGEJO:+ Forgejo:$FORGEJO_PORT}) at $DIR" >/dev/null 2>&1 || true
@@ -56,7 +56,7 @@ bin/agent-run.sh hnoss "design a hero section"
56
56
 
57
57
  Keep a per-machine overlay `config/agents.<hostname>.yaml`; it deep-merges over
58
58
  the base. Example: `config/agents.machine.example.yaml`. Override the host with
59
- `YMIR_AGENTS_MACHINE=omarchy-1`.
59
+ `YMIR_AGENTS_MACHINE=omarchy`.
60
60
 
61
61
  ## Hosted models
62
62
 
@@ -6,6 +6,10 @@
6
6
  > question — where a *project* lives — had no single answer. Two rules decide it:
7
7
  > private data is never inside the checkout (Rule 04), and a domain is not a
8
8
  > house (Rule 01/03).
9
+ >
10
+ > **2026-09-17:** the roots settled — workspaces now live under
11
+ > `$YMIR_HOME/hodd/workspaces/` (marketing/ · personal/ · work/); this page
12
+ > stays the map, the hoard is authoritative.
9
13
 
10
14
  ## The one idea
11
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerwiz/ymir",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
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",
@@ -60,9 +60,15 @@
60
60
  "compliance": "bash .agents/skills/galdr-ymirsystem/scripts/compliance-check.sh"
61
61
  },
62
62
  "optionalDependencies": {
63
- "@zerwiz/hlidskjalf": "^0.1.0",
64
- "@zerwiz/odrerir": "^0.1.0",
63
+ "@zerwiz/hlidskjalf": "0.1.1",
64
+ "@zerwiz/odrerir": "0.1.1",
65
65
  "@zerwiz/sessrumnir": "0.1.9",
66
66
  "@zerwiz/smidja-factory": "^0.1.0"
67
+ },
68
+ "dependencies": {
69
+ "@zerwiz/hlidskjalf": "^0.1.1",
70
+ "@zerwiz/odrerir": "^0.1.1",
71
+ "@zerwiz/sessrumnir": "^0.1.10",
72
+ "@zerwiz/smidja-factory": "^0.1.2"
67
73
  }
68
- }
74
+ }
@@ -289,10 +289,10 @@ if [ "$VIEW" = both ]; then
289
289
  start_one odrerir
290
290
  sleep 2
291
291
  for v in "${VIEWS[@]}"; do
292
- if is_running "$v"; then printf 'electron[1]{view,state,pid,url}:\n "%s","up",%s,"%s"\n' "$v" "$(pid_of "$v")" "$(view_host "$v")"; else printf 'error: electron %s failed to start; see %s\n' "$v" "${log_file "$v"#"$ROOT"/}" >&2; exit 1; fi
292
+ if is_running "$v"; then printf 'electron[1]{view,state,pid,url}:\n "%s","up",%s,"%s"\n' "$v" "$(pid_of "$v")" "$(view_host "$v")"; else printf 'error: electron %s failed to start; see %s\n' "$v" "$(log_file "$v")" >&2; exit 1; fi
293
293
  done
294
294
  else
295
295
  start_one "$VIEW"
296
296
  sleep 2
297
- if is_running "$VIEW"; then printf 'electron[1]{view,state,pid,url}:\n "%s","up",%s,"%s"\n' "$VIEW" "$(pid_of "$VIEW")" "$(view_host "$VIEW")"; else printf 'error: electron failed to start; see %s\n' "${log_file "$VIEW"#"$ROOT"/}" >&2; exit 1; fi
297
+ if is_running "$VIEW"; then printf 'electron[1]{view,state,pid,url}:\n "%s","up",%s,"%s"\n' "$VIEW" "$(pid_of "$VIEW")" "$(view_host "$VIEW")"; else printf 'error: electron failed to start; see %s\n' "$(log_file "$VIEW")" >&2; exit 1; fi
298
298
  fi