@zerwiz/ymir 0.1.5 → 0.1.7

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 (118) 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 +215 -23
  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 +1033 -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 +153 -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/eir-doctor.sh +47 -1
  55. package/bin/feature-inventory.sh +73 -0
  56. package/bin/fleet-apply.sh +15 -2
  57. package/bin/github-deploy.sh +14 -1
  58. package/bin/gjallarhorn-expose.sh +43 -9
  59. package/bin/gjallarhorn-notify.sh +14 -1
  60. package/bin/gjallarhorn-purge.sh +14 -1
  61. package/bin/gjallarhorn-tunnel.sh +16 -3
  62. package/bin/groa-update.sh +9 -1
  63. package/bin/hall-snapshot.sh +21 -2
  64. package/bin/herdr-run.sh +23 -2
  65. package/bin/hlidskjalf-agents.sh +172 -0
  66. package/bin/hlidskjalf-usage.sh +196 -0
  67. package/bin/hoard-guard.sh +135 -0
  68. package/bin/hoard-lib.sh +91 -11
  69. package/bin/hodd.sh +28 -4
  70. package/bin/huginn-research-worker.sh +14 -1
  71. package/bin/local-model-lock.sh +22 -3
  72. package/bin/mimir-bridge.py +33 -0
  73. package/bin/mimir-bridge.sh +14 -1
  74. package/bin/mimir-reflect.sh +27 -0
  75. package/bin/mjollnir-webhook.sh +14 -1
  76. package/bin/mjollnir.sh +22 -3
  77. package/bin/model-hardware.sh +17 -3
  78. package/bin/model-resolve.sh +14 -1
  79. package/bin/nornir-job-hall-snapshot.sh +28 -0
  80. package/bin/nornir-job-nsr-compliance.sh +54 -0
  81. package/bin/npm-publish.sh +136 -0
  82. package/bin/omarchy-install.sh +18 -5
  83. package/bin/omarchy-plugins.sh +15 -2
  84. package/bin/omarchy-sense.sh +14 -1
  85. package/bin/pi-model-agnostic.sh +71 -0
  86. package/bin/private-guard.sh +20 -0
  87. package/bin/project-git.sh +18 -4
  88. package/bin/public-guard.sh +3 -2
  89. package/bin/sessrumnir-ensure.sh +1 -1
  90. package/bin/sessrumnir.sh +16 -3
  91. package/bin/smidja-bootstrap.sh +5 -1
  92. package/bin/syn-asset-pretool-check.sh +14 -1
  93. package/bin/syn-spawn-pretool-check.sh +58 -0
  94. package/bin/tailscale-sync.sh +15 -2
  95. package/bin/telegram-bot.sh +14 -1
  96. package/bin/ui-truth-check.sh +75 -0
  97. package/bin/valknut-load.sh +42 -4
  98. package/bin/wedge-notify.sh +14 -1
  99. package/bin/workspace-rag.sh +14 -1
  100. package/bin/ymir-install.sh +219 -54
  101. package/bin/ymir-plan.sh +356 -0
  102. package/bin/ymir-say.sh +14 -1
  103. package/bin/ymir-setup-auth.sh +12 -1
  104. package/bin/ymir-validate.sh +28 -8
  105. package/docs/lore.md +5 -1
  106. package/docs/runbooks/agents.md +1 -1
  107. package/package.json +13 -3
  108. package/scripts/electron.sh +74 -15
  109. package/scripts/lower.sh +34 -0
  110. package/scripts/raise.sh +42 -0
  111. package/scripts/start.sh +52 -2
  112. package/.agents/config/agents.yaml +0 -50
  113. package/.agents/config/tailscale-sync.yaml +0 -27
  114. package/.agents/memory/kaia.engram +0 -0
  115. package/.agents/memory/kaia.engram-shm +0 -0
  116. package/.agents/memory/kaia.engram-wal +0 -0
  117. package/.agents/memory/well/episodes.jsonl +0 -367
  118. package/.agents/memory/well/workspace.jsonl +0 -27
package/README.md CHANGED
@@ -243,7 +243,7 @@ Utgard sandbox on a Yggdrasil worktree.
243
243
  | **Kvasir** | the knowing | scout | reconnaissance — changes nothing |
244
244
 
245
245
  Profiles live in [`.agents/agents/`](.agents/agents/) and are bound to each tool by
246
- `bin/valknut-load.sh` (OpenCode reads `.opencode/agent/`; Pi links resolve under
246
+ `bin/valknut-load.sh` (OpenCode reads `.opencode/agents/`; Pi links resolve under
247
247
  `.pi/agents/`).
248
248
 
249
249
  ---
@@ -8,9 +8,9 @@ Agent profiles live **only** in **`.agents/agents/*.md`**. That directory is the
8
8
  source of truth; everything else binds to it.
9
9
 
10
10
  - Harness directories are **symlinks**, never copies:
11
- - OpenCode: `.opencode/agent/<name>.md` → `../../.agents/agents/<profile>.md`
11
+ - OpenCode: `.opencode/agents/<name>.md` → `../../.agents/agents/<profile>.md`
12
12
  - Pi: `.pi/agents/<profile>.md` → the same canonical files
13
- - **Never edit** `.opencode/agent` or `.pi/agents` — they are links. Edit
13
+ - **Never edit** `.opencode/agents` or `.pi/agents` — they are links. Edit
14
14
  `.agents/agents/<profile>.md` and re-run `bin/valknut-load.sh --all`.
15
15
 
16
16
  ## 2. The two kinds of agent
package/RULES/04-hoard.md CHANGED
@@ -63,3 +63,35 @@ read. Operator-private documents — plans, strategy, roadmaps, the masterplan,
63
63
  `bin/docs-guard.sh` blocks a commit that stages such a document under `docs/`
64
64
  (wired into the pre-commit hook beside `secret-guard.sh`). When it fires, move
65
65
  the file to `$YMIR_HOME/docs/` in the private repo.
66
+
67
+ ---
68
+
69
+ ## Correction — 2026-09-17: hodd/ *is* the YMIR_HOME data path
70
+
71
+ *Appended, citing the sections above; nothing above is rewritten.*
72
+
73
+ The mapping table above ("What lives at YMIR_HOME") implies a **flat** layout —
74
+ `hodd/identity/` → `$YMIR_HOME/identity/`, `data/` → `$YMIR_HOME/data/` — i.e.
75
+ that the repo-side `hodd/` and the data path are two different locations. On a
76
+ real home that reading produced **two parallel stores**: a flat
77
+ `$YMIR_HOME/identity/` and `$YMIR_HOME/data/` alongside `$YMIR_HOME/hodd/identity/`
78
+ and `$YMIR_HOME/hodd/data/`. They drifted, and the flat pair went stale.
79
+
80
+ The truth on a live home, and the intent of this rule:
81
+
82
+ - **`$YMIR_HOME/hodd/` *is* the private data path.** There is no second, flat
83
+ copy. `identity/`, `data/`, `docs/`, `secrets/`, `tenants/`, and the memory
84
+ well live **under `hodd/`**.
85
+ - **`bin/hoard-lib.sh` is the single source of truth** for that path: it
86
+ resolves `${YMIR_HOARD:-${YMIR_HOME:-$HOME/Documents/Ymir}/hodd}`. A script
87
+ that needs the hoard calls `hoard_root`, never a hardcoded path.
88
+ - **`.ymir-layout.yaml` records the real layout** and must name only paths that
89
+ exist. A layout entry pointing at a nonexistent directory is stale, and a
90
+ stale map is what let private work land outside the hoard.
91
+ - The three-way reading in the section above (`hodd/x/` → `$YMIR_HOME/x/`) is
92
+ **superseded**: read every row as `$YMIR_HOME/hodd/x/`.
93
+ - The "docs/ is public" section's closing line ("move the file to
94
+ `$YMIR_HOME/docs/`") is likewise read as **`$YMIR_HOME/hodd/docs/`**.
95
+
96
+ Confirmed by the Allfather 2026-09-17. Drift of this kind is caught by
97
+ `bin/eir-doctor.sh`'s hoard placement check.
@@ -56,3 +56,53 @@ are always named.
56
56
 
57
57
  **Related:** `RULES/04-hoard.md` (where private records live), `AGENTS.md`,
58
58
  `.agents/skills/galdr-ymirsystem/assets/installation.md` (the move procedure).
59
+
60
+ ## Amendment — the changelog writes through fragments (2026-09-17)
61
+
62
+ Appended, not rewritten; the clause above stands.
63
+
64
+ **The problem this amendment answers.** `CHANGELOG.md` is one file, and every
65
+ branch appends at the same position — the top. Git sees two branches inserting
66
+ different lines at the same spot and calls it a conflict, so **every merge
67
+ re-conflicts every open branch**. With N branches that is O(N²) conflicts, all of
68
+ them meaningless: both entries belong, and neither is wrong. Sixteen open PRs
69
+ were each resolved by hand for this reason alone.
70
+
71
+ **The amendment.** A change tells its story through a **fragment**:
72
+
73
+ ```
74
+ CHANGELOG.d/<YYYY-MM-DD>-<slug>.md the entry, beginning with its `## YYYY-MM-DD — title` heading
75
+ ```
76
+
77
+ - **A fragment is a new file with a unique name.** Two branches never touch the
78
+ same one, so the collision cannot occur. This is the whole point.
79
+ - **`bin/changelog-assemble.sh` folds fragments into `CHANGELOG.md`** — newest
80
+ first, above everything already recorded. Folding is an **append**: the ledger's
81
+ existing entries are copied verbatim, never reordered, never rewritten.
82
+ - **The pre-push hook runs the assembler first**, so the ledger is never behind
83
+ what the fragments already tell. A push that would leave unfolded fragments
84
+ behind is refused with the instruction to commit the fold and push again.
85
+ - **The guard accepts either form.** A push satisfies the duty by appending to
86
+ `CHANGELOG.md` **or** by adding a fragment. Appending directly still works and
87
+ is still valid; the fragment is simply the form that cannot conflict.
88
+
89
+ **Why this obeys the clause above, not contradicts it.** Append, never rewrite —
90
+ a fragment is a *new file*, and the ledger only ever grows. Never truncate —
91
+ fragments are folded in, never dropped. Never lose one in a move — **`CHANGELOG.d/`
92
+ joins the append-only set**, so a move must carry it exactly as it carries
93
+ `CHANGELOG.md`. A correction is a new entry citing the old — a fragment may cite;
94
+ nothing is edited.
95
+
96
+ **The append-only set is now:**
97
+
98
+ ```
99
+ append_only[7]{artifact,why}:
100
+ "$YMIR_HOME/memory/runes_audit.md","the Runes ledger — checksum-chained"
101
+ "$YMIR_HOME/docs/append-only-log.md","the decision log"
102
+ "CHANGELOG.md","the assembled chronological ledger; folded from fragments, never hand-rewritten"
103
+ "CHANGELOG.d/","pending entries — one file per change; folded in, never dropped"
104
+ "RULES/*.md","house law"
105
+ "$YMIR_HOME/**","the private record"
106
+ "*.jsonl / branch outcomes / session logs","event streams"
107
+ ```
108
+
@@ -43,3 +43,59 @@ Hardcoding is how a substrate written on one machine breaks on another, or
43
43
  collides with a neighbour's port on a shared server — the exact failure a
44
44
  host-agnostic core exists to prevent. This rule is the companion of Rule 05: Rule
45
45
  05 keeps *behaviour* portable; Rule 07 keeps *configuration* portable.
46
+
47
+ ## Amendment — models, providers, and paths are the user's, in the hoard (2026-09-17)
48
+
49
+ Appended, not rewritten; the clauses above stand.
50
+
51
+ ### A. Models and providers live in the hoard, per user, read from YAML
52
+
53
+ **Every user's models and providers differ.** One operator runs local llama.cpp
54
+ on a 16 GB card; another runs hosted models; a third runs both. There is therefore
55
+ **no such thing as a global model or provider** — a shipped default that names a
56
+ model is a claim about someone else's machine.
57
+
58
+ - **The one source.** A user's models and providers live in **`$YMIR_HOME/hodd/`**
59
+ — the user's own workspace, their private hoard — and are read from a **YAML
60
+ file there** (`hodd/config/agents.yaml`). Nothing else owns them.
61
+ - **No global.** A model id or provider name must not appear in shipped runtime
62
+ code, in a tracked config, or in any repo file as a *value*. The repo ships a
63
+ **template** (`config/agents.yaml.example`) with placeholders; the live file is
64
+ the user's, and it is private (Rule 04).
65
+ - **Resolve, never restate.** Code reads a model or provider through the resolver
66
+ (`bin/model-resolve.sh`, `bin/agents-config.sh`) or from env. It never embeds
67
+ the id.
68
+ - **A comment may name an example; code may not.** A usage line in a header
69
+ (`# bin/pi-seat.sh -m <model>`) is documentation. A literal assigned to a
70
+ variable, written into a config, or passed as a default is a violation.
71
+
72
+ ### B. No hardcoded absolute file paths
73
+
74
+ **A path that names a user, or a machine's layout, is not portable.**
75
+
76
+ - **User-agnostic.** No `/home/<user>/…`, no `/Users/<user>/…`, no `C:\Users\…`,
77
+ no baked-in username anywhere in shipped code, config, or docs.
78
+ - **Relative or resolved.** A path is relative to a resolved root
79
+ (`$YMIR_HOME`, `$YMIR_HOARD`, `$ROOT`, `$SCRIPT_DIR`), or comes from env/config
80
+ with one documented default. Never a literal absolute path.
81
+ - **The home is resolved, not assumed.** `$YMIR_HOME` resolves through
82
+ `bin/hoard-lib.sh` (`hoard_root`) — never a hardcoded `~/Documents/Ymir`, never
83
+ another user's home.
84
+ - **A test may use a temp dir; it may not use a real home.** `mktemp -d` and
85
+ `$TMPDIR` are correct. A fixture that names a real user's home is a violation
86
+ that will fail on every other machine.
87
+
88
+ ### Enforcement
89
+
90
+ `compliance-check.sh` gains the **`config`** gate, which fails on:
91
+
92
+ ```
93
+ config_gate[3]{class,what_it_catches}:
94
+ "model","a model id or provider name used as a VALUE in shipped code or a tracked config"
95
+ "path","an absolute path naming a user (/home/<user>, /Users/<user>, C:\\Users\\<user>)"
96
+ "hoard","a model/provider config outside $YMIR_HOME/hodd/"
97
+ ```
98
+
99
+ Comments, templates (`*.example`), `CHANGELOG*`, and `assets/reference/`
100
+ (provenance) are exempt by intent — they document, they do not configure.
101
+
@@ -0,0 +1,58 @@
1
+ # Rule 09 — Electron is a Local Seat
2
+
3
+ A desktop window is the machine talking to itself. Every rule below follows from
4
+ that one sentence.
5
+
6
+ ## 09.1 Local connections only
7
+
8
+ **An Electron shell loads loopback and nothing else.** A URL that is not
9
+ `127.0.0.1`, `localhost` or `::1` is refused, with a warning, and replaced by the
10
+ local default. If a shell is handed a public hostname — a tunnel, a `*.zerwiz.org`
11
+ name — it must **not** honour it.
12
+
13
+ Why the law, not a preference: a desktop app that dials out is a desktop app whose
14
+ failures belong to somebody else's edge. A bridge that dressed a cloud endpoint as
15
+ `127.0.0.1` cost a night of "the agents never start"; a shell that reaches for a
16
+ public hall when its own machine is quiet is the same mistake wearing different
17
+ clothes.
18
+
19
+ **Cloudflare has nothing to do with Electron.** Tunnels exist for the *web* door,
20
+ so a person elsewhere can reach the apps. The desktop is already where the apps
21
+ live.
22
+
23
+ ## 09.2 No login in the desktop seat
24
+
25
+ The web door has exactly one login; the desktop seat does not have one at all. The
26
+ shell is a local, trusted seat — its preload marks its requests, and the gate
27
+ honours that marker **only from loopback**. A login prompt inside an Electron
28
+ window means the surface has been mistaken for the web.
29
+
30
+ ## 09.3 A service that is down says so
31
+
32
+ When a local service is not answering, the shell reports it — it does not fall
33
+ back to a remote copy, a cached one, or a public hostname. A wrong answer from
34
+ the wrong server is worse than no answer.
35
+
36
+ ## 09.4 The shell carries the house mark
37
+
38
+ Every app ships its own **rune** icon (see `midgard/design-system/runes.md`), and
39
+ installation writes both the icon into the user's icon theme and the `.desktop`
40
+ entry into their applications directory — with this machine's root rendered in, so
41
+ the entry works and the operator can pin it. An app nobody can pin is an app half
42
+ installed.
43
+
44
+ ## 09.5 One lifecycle for the web and the windows
45
+
46
+ `scripts/raise.sh` and `scripts/lower.sh` own both. Lowering takes the **windows
47
+ down first**, so no shell is left watching a port that has just vanished.
48
+
49
+ ## 09.6 References
50
+
51
+ - `apps/hlidskjalf/electron/`, `apps/odrerir/electron/`, `apps/sessrumnir/src/main/`
52
+ - `scripts/electron.sh` — the shell lifecycle; `scripts/raise.sh` / `scripts/lower.sh`
53
+ - `bin/design-icon.sh` — the rune and the entry, installed
54
+ - Rule 05 (platforms), Rule 07 (config resolves from env, never a literal)
55
+
56
+ ---
57
+
58
+ *Append-only. A correction is a new entry citing the old one.*
package/RULES/README.md CHANGED
@@ -18,3 +18,5 @@ rules_index[9]{file,subject}:
18
18
  ```
19
19
 
20
20
  Keep each rule short and unambiguous; reference it from code where it bites.
21
+
22
+ | 09 | `09-electron.md` | Electron is a local seat: local connections only, no login, the rune icon and entry installed, one lifecycle with the web. |
package/bin/a2a-mcp.sh CHANGED
@@ -39,9 +39,26 @@ ENGRAM_BIN="${ENGRAM_BIN:-$HOME/.local/bin/engram-mcp}"
39
39
  YMIR_HOME="${YMIR_HOME:-$HOME/Documents/Ymir}"
40
40
  ENGRAM_DB="${ENGRAM_DB:-$YMIR_HOME/memory/kaia.engram}"
41
41
 
42
- python3 - "$ROOT" "$PI_MCP" "$OC" "$A2AB" "$DIR" "$ADVERT" "$WOTES" "$ACTION" "$ENGRAM_BIN" "$ENGRAM_DB" <<'PY'
42
+ # The Teams plane (WayOfTeams) and Anchor memory are REMOTE MCP servers, named by
43
+ # URL. The URL is credential-ish, so it comes from the environment — never the
44
+ # tracked tree. Pi has no remote transport, so it reaches them through the
45
+ # `mcp-remote` stdio bridge; OpenCode speaks `type: remote` natively.
46
+ # The environment wins; else the private platform env, so no caller must export.
47
+ platform_env_val() { # <KEY>
48
+ local k=$1 f v
49
+ for f in "${YMIR_HOARD:-$YMIR_HOME/hodd}/secrets/platform.env" "$YMIR_HOME/hodd/secrets/platform.env" "$YMIR_HOME/secrets/platform.env"; do
50
+ [ -r "$f" ] || continue
51
+ v="$(grep -E "^$k=" "$f" 2>/dev/null | tail -1 | cut -d= -f2-)"
52
+ [ -n "$v" ] && { printf '%s' "$v"; return 0; }
53
+ done
54
+ }
55
+ TEAMS_URL="${WAYOFTEAMS_MCP_URL:-}"; [ -n "$TEAMS_URL" ] || TEAMS_URL="$(platform_env_val WAYOFTEAMS_MCP_URL)"
56
+ ANCHOR_URL="${ANCHOR_MCP_URL:-}"; [ -n "$ANCHOR_URL" ] || ANCHOR_URL="$(platform_env_val ANCHOR_MCP_URL)"
57
+
58
+ python3 - "$ROOT" "$PI_MCP" "$OC" "$A2AB" "$DIR" "$ADVERT" "$WOTES" "$ACTION" "$ENGRAM_BIN" "$ENGRAM_DB" "$TEAMS_URL" "$ANCHOR_URL" <<'PY'
43
59
  import sys, os, json
44
- root, pi_path, oc_path, a2ab, durl, advert, wotes, action, engram_bin, engram_db = sys.argv[1:11]
60
+ (root, pi_path, oc_path, a2ab, durl, advert, wotes, action, engram_bin, engram_db,
61
+ teams_url, anchor_url) = sys.argv[1:13]
45
62
 
46
63
  def load(p, default):
47
64
  try: return json.load(open(p))
@@ -59,20 +76,38 @@ ones = {
59
76
  "oc": {"type": "local", "command": [engram_bin, "--db", engram_db], "environment": {}},
60
77
  },
61
78
  }
62
- if wotes:
79
+ # The Teams plane: a URL (remote) wins over a local `wayofteams-mcp` binary.
80
+ if teams_url:
81
+ ones["wayofteams"] = {
82
+ "pi": {"command": "npx", "args": ["-y", "mcp-remote", teams_url]},
83
+ "oc": {"type": "remote", "url": teams_url},
84
+ }
85
+ elif wotes:
63
86
  ones["wayofteams"] = {
64
87
  "pi": {"command": wotes, "args": ["--stdio"], "env": {"WOTEAMS_URL": os.environ.get("WOTEAMS_URL","")}},
65
88
  "oc": {"type": "local", "command": [wotes, "--stdio"], "environment": {"WOTEAMS_URL": os.environ.get("WOTEAMS_URL","")}},
66
89
  }
67
90
 
91
+ # Anchor — the memory plane, remote by URL.
92
+ if anchor_url:
93
+ ones["way-of-anchor-sse"] = {
94
+ "pi": {"command": "npx", "args": ["-y", "mcp-remote", anchor_url]},
95
+ "oc": {"type": "remote", "url": anchor_url},
96
+ }
97
+
68
98
  if action == "show":
69
99
  pi = load(pi_path, {}).get("mcpServers", {})
70
100
  oc = load(oc_path, {}).get("mcp", {})
71
- print(f"a2a-mcp[{len(ones)}]{{server,in_pi,in_opencode}}:")
72
- for name in ones:
101
+ # Report what is ACTUALLY wired, not a fixed list: a key may come from an
102
+ # earlier install, a hand edit, or a server this script does not itself add.
103
+ names = sorted(set(ones) | set(pi) | set(oc))
104
+ print(f"a2a-mcp[{len(names)}]{{server,in_pi,in_opencode}}:")
105
+ for name in names:
73
106
  print(f' "{name}","{"yes" if name in pi else "no"}","{"yes" if name in oc else "no"}"')
74
- if not wotes:
75
- print('help: wayofteams-mcp not on PATH — install it to enable the Teams plane')
107
+ if not wotes and not teams_url:
108
+ print('help: set WAYOFTEAMS_MCP_URL (or install wayofteams-mcp) to enable the Teams plane')
109
+ if not anchor_url:
110
+ print('help: set ANCHOR_MCP_URL to enable the Anchor memory plane')
76
111
  sys.exit(0)
77
112
 
78
113
  if action != "install":
@@ -18,9 +18,21 @@ set -u
18
18
  VERSION="1.0.0"
19
19
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
20
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
21
- YMIR_HOME="${YMIR_HOME:-$HOME/Documents/Ymir}"
22
- CFG="${YMIR_AGENTS_YAML:-$YMIR_HOME/config/agents.yaml}"
23
- RESOLVED="${YMIR_STATE_DIR:-$YMIR_HOME/state}/agents-resolved.json"
21
+
22
+ # The operator's settings and secrets live in the home they chose, never in the
23
+ # code tree — a packaged install replaces its tree on upgrade, and a credential
24
+ # must never sit in a tree that ships (Rule 04).
25
+ if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
26
+ _yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
27
+ for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
28
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
29
+ done
30
+ unset _yr _yc
31
+ fi
32
+ hoard_settings_dir YMIR_SETTINGS_DIR
33
+ hoard_local_env YMIR_ENV_FILE
34
+ CFG="${YMIR_AGENTS_YAML:-$YMIR_SETTINGS_DIR/agents.yaml}"
35
+ RESOLVED="${YMIR_STATE_DIR}/agents-resolved.json"
24
36
 
25
37
  case "${1-}" in
26
38
  -v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;;
@@ -35,6 +47,7 @@ if [ "$ACTION" = init ]; then
35
47
  exit 0
36
48
  fi
37
49
  TPL="$ROOT/config/agents.yaml.example"
50
+ [ -r "$TPL" ] || TPL="$ROOT/.agents/config/agents.yaml.example"
38
51
  [ -r "$TPL" ] || { printf 'error: template missing: %s\n' "$TPL" >&2; exit 1; }
39
52
  mkdir -p "$(dirname "$CFG")" 2>/dev/null || true
40
53
  cp "$TPL" "$CFG" || { printf 'error: could not write %s\n' "$CFG" >&2; exit 1; }
@@ -211,12 +224,22 @@ if os.path.exists(oc):
211
224
  changed.append(f"provider:{name}")
212
225
  ablock = d.setdefault("agent", {})
213
226
  for a in agents:
227
+ # opencode.json is OpenCode's file: only OpenCode agents belong in it. A
228
+ # pi (or hermes) agent's model is that harness's id — e.g.
229
+ # `llamacpp/qwen3.5-9b` — which OpenCode cannot resolve, so writing it
230
+ # here would hand OpenCode an agent it cannot run. Skip them.
231
+ if harness_of(a) != "opencode":
232
+ continue
214
233
  m = resolve_model(model_of(a))
215
- if a in ablock:
216
- if ablock[a].get("model") != m:
217
- ablock[a]["model"] = m; changed.append(f"oc:{a}")
234
+ # The roster DECLARES every figure it names. Before this, `apply` only
235
+ # touched an agent already present in opencode.json (`if a in ablock`),
236
+ # so a figure the roster knew but the config had never seen stayed
237
+ # undeclared — and OpenCode never loaded it. The roster is the source of
238
+ # truth; a name in it must reach the harness config.
239
+ entry = ablock.setdefault(a, {})
240
+ if entry.get("model") != m:
241
+ entry["model"] = m; changed.append(f"oc:{a}")
218
242
  if spec_of(a).get("primary"):
219
- entry = ablock.setdefault(a, {})
220
243
  entry["mode"] = "primary"
221
244
  entry["model"] = m
222
245
  entry.setdefault("description", f"{a} — primary (from config/agents.yaml)")
@@ -15,8 +15,21 @@ set -u
15
15
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
16
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
17
17
 
18
+ # The operator's settings and secrets live in the home they chose, never in the
19
+ # code tree — a packaged install replaces its tree on upgrade, and a credential
20
+ # must never sit in a tree that ships (Rule 04).
21
+ if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
22
+ _yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23
+ for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
24
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
25
+ done
26
+ unset _yr _yc
27
+ fi
28
+ hoard_settings_dir YMIR_SETTINGS_DIR
29
+ hoard_local_env YMIR_ENV_FILE
30
+
18
31
  # Source env if available (project .env.local first, then the private hoard).
19
- for f in "$ROOT/.env.local" "${YMIR_HOARD:-$HOME/Documents/Ymir}/.env.local"; do
32
+ for f in "$YMIR_ENV_FILE" "${YMIR_HOARD:-$HOME/Documents/Ymir}/.env.local"; do
20
33
  [ -f "$f" ] && . "$f" 2>/dev/null || true
21
34
  done
22
35
 
@@ -25,11 +25,30 @@ fi
25
25
  VERSION="2.0.0"
26
26
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
27
27
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
28
+
29
+ # The operator's settings and secrets live in the home they chose, never in the
30
+ # code tree — a packaged install replaces its tree on upgrade, and a credential
31
+ # must never sit in a tree that ships (Rule 04).
32
+ if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
33
+ _yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
34
+ for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
35
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
36
+ done
37
+ unset _yr _yc
38
+ fi
39
+ hoard_settings_dir YMIR_SETTINGS_DIR
40
+ hoard_local_env YMIR_ENV_FILE
41
+ hoard_state_dir YMIR_STATE_DIR
42
+ hoard_data_dir YMIR_DATA_DIR
43
+
44
+ # The roots that live OUTSIDE the code tree: this machine's records and the
45
+ # runtime state belong to the home the operator chose at installation, never in
46
+ # the tree — a packaged install replaces its tree on upgrade (Rule 04).
28
47
  BRIDGE="$ROOT/.agents/backend/model-bridge.py"
29
- ENV_FILE="${BROKK_ENV_FILE:-$ROOT/.env.local}"
48
+ ENV_FILE="${BROKK_ENV_FILE:-$YMIR_ENV_FILE}"
30
49
  PORT="${OPENCODE_GO_BRIDGE_PORT:-4603}"
31
- PID_FILE="$ROOT/state/model-bridge.pid"
32
- LOG_FILE="$ROOT/state/model-bridge.log"
50
+ PID_FILE="$YMIR_STATE_DIR/model-bridge.pid"
51
+ LOG_FILE="$YMIR_STATE_DIR/model-bridge.log"
33
52
  PROVIDER="${YMIR_MODEL_PROVIDER:-}"
34
53
 
35
54
  case "${1-}" in
@@ -40,8 +59,8 @@ ACTION="start"
40
59
  while [ $# -gt 0 ]; do
41
60
  case "$1" in
42
61
  --start|"") ACTION=start; shift ;;
43
- --stop) ACTION=stop; shift ;;
44
- --status) ACTION=status; shift ;;
62
+ --stop|stop) ACTION=stop; shift ;;
63
+ --status|status) ACTION=status; shift ;;
45
64
  --port) PORT=${2-}; shift 2 ;;
46
65
  --provider) PROVIDER=${2-}; shift 2 ;;
47
66
  *) printf 'error: unknown flag %s\nhelp: bin/bifrost-bridge.sh [--start|--stop|--status|--port|--provider]\n' "$1" >&2; exit 2 ;;
@@ -95,7 +114,7 @@ if [ -z "$PROVIDER" ]; then
95
114
  fi
96
115
 
97
116
  # A missing env file is fine for keyless providers; the bridge reads what it can.
98
- mkdir -p "$ROOT/state"
117
+ mkdir -p "$YMIR_STATE_DIR"
99
118
  nohup python3 "$BRIDGE" --port "$PORT" --env "$ENV_FILE" --provider "$PROVIDER" >"$LOG_FILE" 2>&1 &
100
119
  echo $! >"$PID_FILE"
101
120
  sleep 2
@@ -25,7 +25,12 @@ VERSION="1.0.0"
25
25
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
26
26
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
27
27
  VM="ymir"; CHECK=0; REPO=""
28
- VM_HOME="${YMIR_HOME:-/home/ubuntu/Documents/Ymir}"
28
+ # The VM's home is resolved, never assumed (Rule 07B). Inside the Lima VM the
29
+ # operator is `ubuntu`, but that is the VM's fact, not this script's — it comes
30
+ # from env with one documented default, and the guest user is overridable so a
31
+ # host that provisions a different user still works.
32
+ VM_USER="${YMIR_VM_USER:-ubuntu}"
33
+ VM_HOME="${YMIR_HOME:-/home/${VM_USER}/Documents/Ymir}"
29
34
 
30
35
  while [ $# -gt 0 ]; do
31
36
  case "$1" in
package/bin/brokk-send.sh CHANGED
@@ -10,7 +10,20 @@ set -u
10
10
  VERSION="1.0.0"
11
11
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
12
  ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
13
- REG="${BROKK_EINDRI_HOMES:-$ROOT/data/eindri-homes.md}"
13
+
14
+ # The roots that live OUTSIDE the code tree: this machine's records and the
15
+ # runtime state belong to the home the operator chose at installation, never in
16
+ # the tree — a packaged install replaces its tree on upgrade (Rule 04).
17
+ if [ -z "${YMIR_HOARD_LIB_LOADED:-}" ]; then
18
+ _yr="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ for _yc in "$_yr/hoard-lib.sh" "$(dirname "$_yr")/bin/hoard-lib.sh"; do
20
+ [ -r "$_yc" ] && { . "$_yc"; YMIR_HOARD_LIB_LOADED=1; break; }
21
+ done
22
+ unset _yr _yc
23
+ fi
24
+ hoard_state_dir YMIR_STATE_DIR
25
+ hoard_data_dir YMIR_DATA_DIR
26
+ REG="${BROKK_EINDRI_HOMES:-$YMIR_DATA_DIR/eindri-homes.md}"
14
27
 
15
28
  case "${1-}" in -v|-V|--version) printf '%s\n' "$VERSION"; exit 0 ;; -h|--help|"") sed -n '2,9p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; esac
16
29
  ID="${1-}"; shift || true
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env bash
2
+ # changelog-assemble.sh — fold CHANGELOG.d/ fragments into CHANGELOG.md.
3
+ #
4
+ # WHY THIS EXISTS
5
+ # CHANGELOG.md is one file, and every branch appends at the same position — the
6
+ # top. Git sees two branches inserting different lines at the same spot and
7
+ # calls it a conflict, so every merge re-conflicts every open branch. With N
8
+ # branches that is O(N^2) conflicts, all of them meaningless.
9
+ #
10
+ # A fragment directory removes the collision at the root: each change adds ONE
11
+ # file with a UNIQUE name, so two branches never touch the same file and can
12
+ # never conflict. This script folds the fragments into the ledger.
13
+ #
14
+ # THE LAW (RULES/06-append-only.md)
15
+ # Append, never rewrite. Never truncate. Never lose one in a move.
16
+ # A fragment is a new file; the ledger only ever grows. Folding is an append.
17
+ # CHANGELOG.d/ joins the append-only set, so a move must carry it.
18
+ #
19
+ # FRAGMENT FORMAT
20
+ # CHANGELOG.d/<YYYY-MM-DD>-<slug>.md — the entry exactly as it should appear in
21
+ # CHANGELOG.md, starting with its `## YYYY-MM-DD — title` heading.
22
+ #
23
+ # USAGE
24
+ # bin/changelog-assemble.sh # fold every fragment, newest first
25
+ # bin/changelog-assemble.sh --dry-run # report what would fold
26
+ # bin/changelog-assemble.sh --check # exit 1 if fragments are unfolded
27
+ #
28
+ # Exit: 0 ok, 1 error (or --check with fragments present), 2 usage.
29
+ set -euo pipefail
30
+
31
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
32
+ ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
33
+ FRAG_DIR="$ROOT/CHANGELOG.d"
34
+ LEDGER="$ROOT/CHANGELOG.md"
35
+
36
+ DRY=0; CHECK=0
37
+ for a in "$@"; do
38
+ case "$a" in
39
+ --dry-run) DRY=1 ;;
40
+ --check) CHECK=1 ;;
41
+ -h|--help) sed -n '2,26p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
42
+ *) printf 'error: unknown flag %s\nhelp: bin/changelog-assemble.sh [--dry-run|--check]\n' "$a" >&2; exit 2 ;;
43
+ esac
44
+ done
45
+
46
+ [ -f "$LEDGER" ] || { printf 'error: no ledger at %s\n' "$LEDGER" >&2; exit 1; }
47
+
48
+ # One python block owns the whole fold: regex, ordering, and the ledger's
49
+ # structure are all easier to get right in one language than split across awk,
50
+ # sed and shell — and an awk interval regex silently matched nothing, which
51
+ # duplicated the entire ledger.
52
+ ROOT="$ROOT" FRAG_DIR="$FRAG_DIR" LEDGER="$LEDGER" DRY="$DRY" CHECK="$CHECK" python3 - <<'PY'
53
+ import os, re, sys, glob
54
+
55
+ root = os.environ["ROOT"]
56
+ frag_dir = os.environ["FRAG_DIR"]
57
+ ledger = os.environ["LEDGER"]
58
+ dry = os.environ["DRY"] == "1"
59
+ check = os.environ["CHECK"] == "1"
60
+
61
+ HEADING = re.compile(r"^## (\d{4}-\d{2}-\d{2}) ")
62
+
63
+ # --- fragments -------------------------------------------------------------
64
+ frags = []
65
+ if os.path.isdir(frag_dir):
66
+ for p in sorted(glob.glob(os.path.join(frag_dir, "*.md"))):
67
+ if os.path.basename(p) == "README.md":
68
+ continue
69
+ frags.append(p)
70
+
71
+ if not frags:
72
+ if check:
73
+ sys.exit(0)
74
+ print('changelog-assemble[1]{fragments,folded,state}:')
75
+ print(' "0","0","none pending"')
76
+ sys.exit(0)
77
+
78
+ # A fragment must begin with its dated heading, or folding it would corrupt the
79
+ # ledger's structure.
80
+ bad = []
81
+ for p in frags:
82
+ first = open(p, encoding="utf-8").readline()
83
+ if not HEADING.match(first):
84
+ bad.append(os.path.relpath(p, root))
85
+ if bad:
86
+ for b in bad:
87
+ print(f'error: fragment has no dated heading: {b}', file=sys.stderr)
88
+ print('help: first line must be "## YYYY-MM-DD — title"', file=sys.stderr)
89
+ sys.exit(1)
90
+
91
+ def frag_date(p):
92
+ m = re.match(r"^(\d{4}-\d{2}-\d{2})", os.path.basename(p))
93
+ return m.group(1) if m else "0000-00-00"
94
+
95
+ # newest first; ties broken by name for determinism
96
+ frags.sort(key=lambda p: (frag_date(p), os.path.basename(p)), reverse=True)
97
+
98
+ if check:
99
+ print(f'changelog-assemble[{len(frags)}]{{fragment,state}}:')
100
+ for p in frags:
101
+ print(f' "{os.path.relpath(p, root)}","unfolded"')
102
+ print(f'error: {len(frags)} changelog fragment(s) not folded into CHANGELOG.md', file=sys.stderr)
103
+ print('help: run bin/changelog-assemble.sh', file=sys.stderr)
104
+ sys.exit(1)
105
+
106
+ if dry:
107
+ print(f'changelog-assemble[{len(frags)}]{{fragment,date}}:')
108
+ for p in frags:
109
+ print(f' "{os.path.relpath(p, root)}","{frag_date(p)}"')
110
+ print('changelog-assemble: dry run — nothing written')
111
+ sys.exit(0)
112
+
113
+ # --- the ledger ------------------------------------------------------------
114
+ text = open(ledger, encoding="utf-8").read()
115
+ lines = text.split("\n")
116
+
117
+ # preamble = everything before the first dated entry (the `# CHANGELOG` head)
118
+ split = None
119
+ for i, ln in enumerate(lines):
120
+ if HEADING.match(ln):
121
+ split = i
122
+ break
123
+ if split is None:
124
+ preamble = lines[:] # no dated entry yet: the whole file is preamble
125
+ existing = []
126
+ else:
127
+ preamble = lines[:split]
128
+ existing = lines[split:]
129
+
130
+ # trim trailing blanks from the preamble, then one blank separator
131
+ while preamble and preamble[-1].strip() == "":
132
+ preamble.pop()
133
+
134
+ # --- assemble: fragments first (newest at top), then the existing ledger ----
135
+ out = list(preamble)
136
+ out.append("")
137
+ for p in frags:
138
+ body = open(p, encoding="utf-8").read().rstrip("\n").split("\n")
139
+ out.extend(body)
140
+ out.append("")
141
+ out.append("")
142
+ out.extend(existing)
143
+
144
+ # collapse runs of 3+ blank lines to two (entry separation only — never content)
145
+ t = "\n".join(out)
146
+ t = re.sub(r"\n{3,}", "\n\n", t).rstrip("\n") + "\n"
147
+
148
+ open(ledger, "w", encoding="utf-8").write(t)
149
+ for p in frags:
150
+ os.remove(p)
151
+
152
+ print('changelog-assemble[1]{fragments,folded,ledger}:')
153
+ print(f' "{len(frags)}","{len(frags)}","CHANGELOG.md"')
154
+ PY