@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.
- package/.agents/agents/huginn-researcher.md +1 -1
- package/.agents/agents/kvasir-scout.md +1 -1
- package/.agents/agents/sindri-developer.md +1 -1
- package/.agents/backend/README.md +74 -0
- package/.agents/backend/model-bridge.py +74 -4
- package/.agents/config/cron.yaml +2 -0
- package/.agents/config/eindri-dispatch.json +6 -6
- package/.agents/migrations/0003-private-data-separation.sh +11 -1
- package/.agents/migrations/0005-roots-out-of-tree.sh +158 -0
- package/.agents/skills/README.md +1 -1
- package/.agents/skills/galdr-ymirsystem/SKILL.md +2 -1
- package/.agents/skills/galdr-ymirsystem/assets/harness-integration/README.md +73 -10
- package/.agents/skills/galdr-ymirsystem/assets/hlidskjalf-ui.md +44 -1
- package/.agents/skills/galdr-ymirsystem/assets/installation.md +290 -24
- package/.agents/skills/galdr-ymirsystem/assets/memory-well.md +3 -1
- package/.agents/skills/galdr-ymirsystem/assets/nornir-jobs.md +39 -1
- package/.agents/skills/galdr-ymirsystem/assets/odrerir-hall.md +4 -0
- package/.agents/skills/galdr-ymirsystem/assets/pi-boot/pi-profile.yml +7 -2
- package/.agents/skills/galdr-ymirsystem/assets/registry.md +1 -1
- package/.agents/skills/galdr-ymirsystem/assets/runtime-compliance.md +2 -2
- package/.agents/skills/galdr-ymirsystem/assets/smidja.md +16 -11
- package/.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh +147 -4
- package/.agents/skills/groa-update/SKILL.md +20 -0
- package/.agents/skills/hamr-adapters/SKILL.md +2 -2
- package/.agents/skills/lifecycle/smoke_test.sh +7 -2
- package/.agents/skills/ymir-host/SKILL.md +2 -1
- package/.agents/skills/ymir-host/assets/install.md +69 -0
- package/AGENTS.md +78 -34
- package/CHANGELOG.md +1362 -1
- package/README.md +1 -1
- package/RULES/02-agents.md +2 -2
- package/RULES/04-hoard.md +32 -0
- package/RULES/06-append-only.md +50 -0
- package/RULES/07-config.md +56 -0
- package/RULES/09-electron.md +58 -0
- package/RULES/README.md +2 -0
- package/bin/a2a-mcp.sh +42 -7
- package/bin/agents-config.sh +30 -7
- package/bin/apodex-smoke-test.sh +14 -1
- package/bin/bifrost-bridge.sh +25 -6
- package/bin/bootstrap-macos.sh +6 -1
- package/bin/brokk-send.sh +14 -1
- package/bin/changelog-assemble.sh +154 -0
- package/bin/changelog-guard.sh +39 -7
- package/bin/crash-sense.sh +14 -1
- package/bin/design-check.sh +80 -0
- package/bin/design-icon.sh +156 -0
- package/bin/docs-guard.sh +2 -2
- package/bin/editor-place.sh +33 -15
- package/bin/eindri-acclaim.sh +14 -1
- package/bin/eindri-seat.sh +83 -0
- package/bin/eindri-seen.sh +14 -1
- package/bin/eindri-start.sh +14 -1
- package/bin/einherjar-spawn.sh +9 -3
- package/bin/eir-doctor.sh +89 -1
- package/bin/electron-lib.sh +34 -0
- package/bin/feature-inventory.sh +73 -0
- package/bin/fleet-apply.sh +15 -2
- package/bin/github-deploy.sh +14 -1
- package/bin/gjallarhorn-expose.sh +43 -9
- package/bin/gjallarhorn-notify.sh +14 -1
- package/bin/gjallarhorn-purge.sh +14 -1
- package/bin/gjallarhorn-tunnel.sh +16 -3
- package/bin/groa-update.sh +9 -1
- package/bin/hall-snapshot.sh +21 -2
- package/bin/herdr-run.sh +23 -2
- package/bin/hlidskjalf-agents.sh +172 -0
- package/bin/hlidskjalf-usage.sh +196 -0
- package/bin/hoard-guard.sh +135 -0
- package/bin/hoard-lib.sh +91 -11
- package/bin/hodd.sh +28 -4
- package/bin/huginn-research-worker.sh +14 -1
- package/bin/local-model-lock.sh +22 -3
- package/bin/mimir-bridge.py +33 -0
- package/bin/mimir-bridge.sh +14 -1
- package/bin/mimir-reflect.sh +27 -0
- package/bin/mjollnir-webhook.sh +14 -1
- package/bin/mjollnir.sh +22 -3
- package/bin/model-hardware.sh +17 -3
- package/bin/model-resolve.sh +14 -1
- package/bin/nornir-job-hall-snapshot.sh +28 -0
- package/bin/nornir-job-nsr-compliance.sh +54 -0
- package/bin/npm-publish.sh +136 -0
- package/bin/omarchy-install.sh +18 -5
- package/bin/omarchy-plugins.sh +15 -2
- package/bin/omarchy-sense.sh +14 -1
- package/bin/pi-model-agnostic.sh +71 -0
- package/bin/private-guard.sh +20 -0
- package/bin/project-git.sh +18 -4
- package/bin/public-guard.sh +3 -2
- package/bin/sessrumnir-ensure.sh +1 -1
- package/bin/sessrumnir.sh +16 -3
- package/bin/smidja-board.sh +136 -0
- package/bin/smidja-bootstrap.sh +5 -1
- package/bin/smidja-lib.sh +49 -0
- package/bin/syn-asset-pretool-check.sh +14 -1
- package/bin/syn-spawn-pretool-check.sh +58 -0
- package/bin/tailscale-sync.sh +15 -2
- package/bin/telegram-bot.sh +14 -1
- package/bin/ui-truth-check.sh +75 -0
- package/bin/valknut-load.sh +42 -4
- package/bin/wedge-notify.sh +14 -1
- package/bin/workspace-rag.sh +14 -1
- package/bin/ymir-install.sh +257 -57
- package/bin/ymir-plan.sh +390 -0
- package/bin/ymir-say.sh +14 -1
- package/bin/ymir-setup-auth.sh +12 -1
- package/bin/ymir-style.sh +108 -0
- package/bin/ymir-validate.sh +64 -11
- package/bin/ymir.js +82 -34
- package/docs/design.md +2 -2
- package/docs/lore.md +5 -1
- package/docs/runbooks/agents.md +1 -1
- package/package.json +13 -3
- package/scripts/electron.sh +74 -15
- package/scripts/lower.sh +34 -0
- package/scripts/raise.sh +42 -0
- package/scripts/start.sh +65 -3
- package/.agents/config/agents.yaml +0 -50
- package/.agents/config/tailscale-sync.yaml +0 -27
- package/.agents/memory/kaia.engram +0 -0
- package/.agents/memory/kaia.engram-shm +0 -0
- package/.agents/memory/kaia.engram-wal +0 -0
- package/.agents/memory/well/episodes.jsonl +0 -367
- package/.agents/memory/well/workspace.jsonl +0 -27
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# `.agents/backend/` — the backend layer
|
|
2
|
+
|
|
3
|
+
**What this folder is.** Two things that share one job — *make work happen and
|
|
4
|
+
report it truthfully*:
|
|
5
|
+
|
|
6
|
+
1. **The vendored firstmate runtime** (`fm-*`) — the validated upstream we ported
|
|
7
|
+
Ymir's dispatch from (`~/firstmate`, the source of the Einherjar/fm-spawn
|
|
8
|
+
lifecycle). Vendored, not rewritten: it is the reference implementation of
|
|
9
|
+
spawn → send → peek → watch → teardown, and it stays byte-comparable to
|
|
10
|
+
upstream so a port can be checked against it.
|
|
11
|
+
2. **The model bridges** (`model-bridge.py`, `opencode-go-bridge.py`) — Ymir's
|
|
12
|
+
own, the only files here we wrote. They are the reason this README exists: the
|
|
13
|
+
bridge was the cause of the Cloudflare 1010 night (see the plan's Amendment I).
|
|
14
|
+
|
|
15
|
+
**Every file's own header is the owner of its contract.** They all say so
|
|
16
|
+
explicitly ("this header is the one owner of…"), and that is deliberate: this
|
|
17
|
+
README must not become a second, drifting description. Read a file's first twenty
|
|
18
|
+
lines for its exact rule; read here for the map.
|
|
19
|
+
|
|
20
|
+
## The owners, by family
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
families[12]{family,owns,subsystem,ui_part}:
|
|
24
|
+
"spawn / lifecycle","fm-spawn.sh fm-send.sh fm-peek.sh fm-control.sh fm-teardown.sh fm-promote.sh fm-merge-local.sh","Einherjar (the door)","Hlidskjalf · Fleet — create, steer, land a task"
|
|
25
|
+
"state","state/<id>.meta, crew/current state: fm-crew-state.sh fm-inactive-reconcile.sh fm-project-mode.sh","Hlidskjalf · Fleet","the task cards: kind, posture, branch, worktree"
|
|
26
|
+
"watch / wake","the watcher and its wake queue: fm-watch.sh fm-watch-arm.sh fm-watch-checkpoint.sh fm-wake-*.sh fm-classify-lib.sh fm-transition-lib.sh","Sýn (supervision)","Hlidskjalf · Status — needs-decision, blocked, the lock state"
|
|
27
|
+
"lease / lock","who may act on a task: fm-lease.sh fm-lease-lib.sh fm-lock.sh fm-session-lock-lib.sh","Gleipnir (the lock)","Hlidskjalf · Status — the session-lock indicator"
|
|
28
|
+
"busy / composer","is the agent working, is the composer ready: fm-busy-lib.sh fm-busy-event.sh fm-composer-lib.sh fm-tmux-lib.sh","Hlidskjalf · Fleet","the per-task state chip (working / paused / waiting)"
|
|
29
|
+
"process → events","long-polling children turned into wakes: fm-procevent.sh fm-procevent-lib.sh fm-procevent-{quota,when,lavish,remote-reply}.sh","Nornir (fate)","Hlidskjalf · Status — scheduled events and quota warnings"
|
|
30
|
+
"remote / secondmate","other homes over SSH: fm-on.sh fm-remote-*.sh fm-home-seed.sh fm-remote-home-*.sh","Yggdrasil (the trunk)","Hlidskjalf · Homes — the secondmate list and its liveness"
|
|
31
|
+
"pr / merge","the delivery gate's other half: fm-pr-check.sh fm-pr-lib.sh fm-pr-merge.sh fm-pr-poll.sh fm-merge-outcome-lib.sh","Mjollnir (the gate)","Hlidskjalf · Reviews (Glitnir) — open, merged, refused"
|
|
32
|
+
"briefs / contracts","what a worker is told, and what done means: fm-brief.sh fm-dod-lib.sh fm-branch-prompt.sh","Smiðja · Erindi","Smiðja · run detail — the brief and the definition of done"
|
|
33
|
+
"voice","the spoken interface: fm-voice-client.py fm-voice-relay.py fm_voice_*.py","Óðrerir (the hall)","Óðrerir · the hall — spoken status and queued work"
|
|
34
|
+
"CI installs","pinned, checksum-verified tool builds: fm-install-{herdr,shellcheck,actionlint,treehouse}.sh","Ymir's CI","(no UI — the pipeline only)"
|
|
35
|
+
"model bridge","the provider door: model-bridge.py + the opencode-go-bridge.py shim","Bifrost (the crossing)","Hlidskjalf · Models — which provider answers, and whether it is local"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## What the UI must be connected to
|
|
39
|
+
|
|
40
|
+
The rule for every row above: **the UI reads the record, never the prose.** A
|
|
41
|
+
panel that parses a log for state is a panel that will lie to the operator.
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
ui_links[6]{surface,reads,shows}:
|
|
45
|
+
"Hlidskjalf · Fleet","state/<id>.meta via fm-crew-state.sh","each task: kind, harness, model, branch, worktree, posture"
|
|
46
|
+
"Hlidskjalf · Status","fm-busy-lib.sh's semantic busy contract (never a pane scrape)","working / paused / needs-decision / done / failed, and the lock"
|
|
47
|
+
"Hlidskjalf · Reviews (Glitnir)","fm-pr-*.sh records (provider, url, number, head)","what is open, merged or refused — the delivery gate"
|
|
48
|
+
"Hlidskjalf · Homes","fm-fleet-snapshot.sh --json and the secondmate registry","the other homes, their liveness and their scope"
|
|
49
|
+
"Óðrerir · the hall","fm-fleet-snapshot.sh --json (the ONE structured contract)","the live board: sessions, phases, spend, ledger"
|
|
50
|
+
"Smiðja · trace","the run's own SQLite trace","phases, envelopes, retries — what the work actually did"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**The rule this folder exists to enforce, and it is the lesson of this session:**
|
|
54
|
+
a seat is not a spawn; a spawn is not a delivery; a delivery is not a record. The
|
|
55
|
+
lifecycle above is the only path that turns an intention into a *record* the UI can
|
|
56
|
+
honestly show — which is why `bin/einherjar-spawn.sh` is the door and typing into a
|
|
57
|
+
pane is forbidden (rule 09 / the spawn seatbelt).
|
|
58
|
+
|
|
59
|
+
## The bridges, and the law they broke
|
|
60
|
+
|
|
61
|
+
`model-bridge.py` exposes a provider to pi as a local OpenAI-compatible endpoint,
|
|
62
|
+
reading the credential from the env. It sat at `127.0.0.1:4603` **while forwarding
|
|
63
|
+
to a cloud gateway** — a localhost address that left the machine. Its `urllib`
|
|
64
|
+
client was refused by Cloudflare (1010, browser_signature_banned) while `curl`
|
|
65
|
+
passed on the same request, and the fix is a session header plus a transport with
|
|
66
|
+
a fingerprint the edge admits. The law it violated is now Rule 09's sibling:
|
|
67
|
+
*local is not a port number; it is a promise.*
|
|
68
|
+
|
|
69
|
+
## Maintaining this
|
|
70
|
+
|
|
71
|
+
- **Owner:** Brokk. **Upstream:** `~/firstmate` — a change here that upstream does
|
|
72
|
+
not have is drift, and should be a documented port, not a quiet edit.
|
|
73
|
+
- **Never rename an `fm-*` file** to a Norse name while the upstream reference is
|
|
74
|
+
needed: names are the join between the two trees.
|
|
@@ -79,6 +79,12 @@ KEY = ""
|
|
|
79
79
|
BASE_URL = ""
|
|
80
80
|
PROVIDER = "opencode-go"
|
|
81
81
|
|
|
82
|
+
# The gateway sits behind Cloudflare, which BANS the signature of a scripted
|
|
83
|
+
# client (Python-urllib/3.x) with error 1010: "banned your access based on your
|
|
84
|
+
# browser's signature". That was the whole 403 - our own bridge was the client.
|
|
85
|
+
USER_AGENT = os.environ.get("YMIR_MODEL_UA", "curl/8.5.0")
|
|
86
|
+
SESSION_ID = os.environ.get("YMIR_MODEL_SESSION") or str(__import__("uuid").uuid4())
|
|
87
|
+
|
|
82
88
|
|
|
83
89
|
def load_env(path: str) -> dict:
|
|
84
90
|
env = {}
|
|
@@ -149,8 +155,10 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
149
155
|
# Ask the provider what it serves; fall back to the static catalogue.
|
|
150
156
|
try:
|
|
151
157
|
req = urllib.request.Request(_upstream("models"),
|
|
152
|
-
headers=_auth_headers()
|
|
153
|
-
|
|
158
|
+
headers={**_auth_headers(), "User-Agent": USER_AGENT,
|
|
159
|
+
# The Zen gateway requires a session id; without it: 400 MissingSessionID.
|
|
160
|
+
"x-opencode-session": SESSION_ID})
|
|
161
|
+
with _curl_open(req, timeout=10) as r:
|
|
154
162
|
data = json.loads(r.read())
|
|
155
163
|
return data.get("data", [])
|
|
156
164
|
except Exception:
|
|
@@ -192,7 +200,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
192
200
|
method="POST",
|
|
193
201
|
)
|
|
194
202
|
try:
|
|
195
|
-
resp =
|
|
203
|
+
resp = _curl_open(req, timeout=300)
|
|
196
204
|
except urllib.error.HTTPError as e:
|
|
197
205
|
# Relay the provider's error verbatim so the operator sees the cause.
|
|
198
206
|
payload = e.read()
|
|
@@ -240,12 +248,74 @@ def _upstream(suffix: str) -> str:
|
|
|
240
248
|
|
|
241
249
|
def _auth_headers(stream: bool = False) -> dict:
|
|
242
250
|
h = {"Content-Type": "application/json",
|
|
243
|
-
"Accept": "text/event-stream" if stream else "application/json"
|
|
251
|
+
"Accept": "text/event-stream" if stream else "application/json",
|
|
252
|
+
# Cloudflare at the gateway bans the signature of a scripted client
|
|
253
|
+
# (Python-urllib/3.x) with error 1010. Our own bridge WAS that client,
|
|
254
|
+
# which is why every worker's first turn came back 403.
|
|
255
|
+
"User-Agent": USER_AGENT,
|
|
256
|
+
# The Zen gateway requires a session id; without it: 400 MissingSessionID.
|
|
257
|
+
"x-opencode-session": SESSION_ID}
|
|
244
258
|
if KEY:
|
|
245
259
|
h["Authorization"] = f"Bearer {KEY}"
|
|
246
260
|
return h
|
|
247
261
|
|
|
248
262
|
|
|
263
|
+
|
|
264
|
+
class _CurlResponse:
|
|
265
|
+
"""What urlopen's callers actually use: .status, .read(), iteration.
|
|
266
|
+
|
|
267
|
+
My first curl transport returned a bare BytesIO, so the proxy died on
|
|
268
|
+
'.status' - a bridge that answers 500 looks like a broken model when it is a
|
|
269
|
+
broken wrapper. This presents the shape the callers expect.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
def __init__(self, body: bytes, status: int):
|
|
273
|
+
self._body = body
|
|
274
|
+
self.status = status
|
|
275
|
+
self.headers = {"content-type": "application/json"}
|
|
276
|
+
|
|
277
|
+
def read(self, *_a) -> bytes:
|
|
278
|
+
return self._body
|
|
279
|
+
|
|
280
|
+
def __iter__(self):
|
|
281
|
+
return iter(self._body.splitlines(True))
|
|
282
|
+
|
|
283
|
+
def __enter__(self):
|
|
284
|
+
return self
|
|
285
|
+
|
|
286
|
+
def __exit__(self, *_a):
|
|
287
|
+
return False
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _curl_open(req, timeout: int = 60):
|
|
291
|
+
"""Perform an upstream request with curl's fingerprint.
|
|
292
|
+
|
|
293
|
+
Cloudflare at the gateway bans a scripted client's TLS shape (error 1010,
|
|
294
|
+
browser_signature_banned) whatever User-Agent it claims - measured: urllib 403,
|
|
295
|
+
curl 200 on the same request. So the bridge speaks through curl and hands the
|
|
296
|
+
caller a response-shaped object.
|
|
297
|
+
"""
|
|
298
|
+
import json as _json, subprocess
|
|
299
|
+
argv = ["curl", "-sS", "--max-time", str(timeout), "-w", "\n%{http_code}",
|
|
300
|
+
"-X", (req.get_method() or "GET")]
|
|
301
|
+
for k, v in (req.header_items() or []):
|
|
302
|
+
argv += ["-H", f"{k}: {v}"]
|
|
303
|
+
data = req.data
|
|
304
|
+
if data:
|
|
305
|
+
argv += ["--data-binary", data.decode() if isinstance(data, bytes) else data]
|
|
306
|
+
argv.append(req.full_url)
|
|
307
|
+
out = subprocess.run(argv, capture_output=True)
|
|
308
|
+
raw = out.stdout
|
|
309
|
+
body, _, tail = raw.rpartition(b"\n")
|
|
310
|
+
try:
|
|
311
|
+
status = int(tail.strip() or b"0")
|
|
312
|
+
except ValueError:
|
|
313
|
+
body, status = raw, 0
|
|
314
|
+
if status == 0 and out.returncode != 0:
|
|
315
|
+
raise OSError((out.stderr or b"curl failed").decode()[:300])
|
|
316
|
+
return _CurlResponse(body, status or 200)
|
|
317
|
+
|
|
318
|
+
|
|
249
319
|
def main() -> int:
|
|
250
320
|
ap = argparse.ArgumentParser(description=__doc__)
|
|
251
321
|
ap.add_argument("--port", type=int,
|
package/.agents/config/cron.yaml
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
07:00 bin/nornir-job-daily-briefing.sh
|
|
6
6
|
06:00 bin/nornir-job-observer.sh
|
|
7
|
+
08:00 bin/nornir-job-hall-snapshot.sh
|
|
8
|
+
02:30 bin/nornir-job-nsr-compliance.sh
|
|
7
9
|
00:30 bin/nornir-job-skillopt-sleep.sh
|
|
8
10
|
00:15 bin/nornir-job-memory-housekeeping.sh
|
|
9
11
|
00:00 bin/nornir-job-git-sync.sh
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
3
|
"platform": "ymir",
|
|
4
|
-
"notes": "Brokk reads these rules before dispatching an Eindri and passes only concrete --harness/--model/--effort flags to bin/einherjar-spawn.sh.
|
|
4
|
+
"notes": "TEMPLATE \u2014 copy to $YMIR_HOME/hodd/config/eindri-dispatch.json and fill in YOUR models. Brokk reads these rules before dispatching an Eindri and passes only concrete --harness/--model/--effort flags to bin/einherjar-spawn.sh. Model tokens must match a model your harness can run on your host, or be omitted to use the harness default. effort values: low|medium|high|xhigh|max.",
|
|
5
5
|
"rules": [
|
|
6
6
|
{
|
|
7
7
|
"when": "General implementation, refactoring, or bug fixes in a codebase",
|
|
8
8
|
"use": [
|
|
9
9
|
{
|
|
10
10
|
"harness": "opencode",
|
|
11
|
-
"model": "
|
|
11
|
+
"model": "<your-model-id>",
|
|
12
12
|
"effort": "medium"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"use": [
|
|
20
20
|
{
|
|
21
21
|
"harness": "opencode",
|
|
22
|
-
"model": "
|
|
22
|
+
"model": "<your-model-id>",
|
|
23
23
|
"effort": "xhigh"
|
|
24
24
|
}
|
|
25
25
|
],
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"use": [
|
|
31
31
|
{
|
|
32
32
|
"harness": "opencode",
|
|
33
|
-
"model": "
|
|
33
|
+
"model": "<your-model-id>",
|
|
34
34
|
"effort": "low"
|
|
35
35
|
}
|
|
36
36
|
],
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
"default": [
|
|
61
61
|
{
|
|
62
62
|
"harness": "opencode",
|
|
63
|
-
"model": "
|
|
63
|
+
"model": "<your-model-id>",
|
|
64
64
|
"effort": "medium"
|
|
65
65
|
}
|
|
66
66
|
]
|
|
67
|
-
}
|
|
67
|
+
}
|
|
@@ -15,10 +15,20 @@ echo " target: $YMIR_HOME"
|
|
|
15
15
|
mkdir -p "$YMIR_HOME"/{config,secrets,identity,workspaces/{work,personal},memory/{daily,well},smidja,state,data}
|
|
16
16
|
|
|
17
17
|
# 2. Migrate hodd/ (Hoard) → YMIR_HOME/
|
|
18
|
+
# Non-destructive: never overwrites an existing file, and MERGES a directory
|
|
19
|
+
# into its target. The target dirs are pre-created above, so a naive
|
|
20
|
+
# `[ -e "$2" ] && return` skipped every directory source — `data/` among them,
|
|
21
|
+
# which silently dropped the realm declaration and made 0004 fall back to a
|
|
22
|
+
# neutral realm.
|
|
18
23
|
copy() { # <src> <dest>
|
|
19
24
|
[ -e "$1" ] || return 0
|
|
20
|
-
|
|
25
|
+
if [ -d "$1" ]; then
|
|
26
|
+
mkdir -p "$2" || return 0
|
|
27
|
+
cp -rn "$1"/. "$2"/ 2>/dev/null && printf ' copied %s\n' "${1#$ROOT/}"
|
|
28
|
+
return 0
|
|
29
|
+
fi
|
|
21
30
|
if [ -e "$2" ]; then return 0; fi
|
|
31
|
+
mkdir -p "$(dirname "$2")" || return 0
|
|
22
32
|
cp -rn "$1" "$2" 2>/dev/null && printf ' copied %s\n' "${1#$ROOT/}"
|
|
23
33
|
}
|
|
24
34
|
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 0005-roots-out-of-tree — the operator's things leave the code tree.
|
|
3
|
+
#
|
|
4
|
+
# The law this migration serves: the package is the CODE THAT RUNS THE PROGRAMS;
|
|
5
|
+
# everything the operator owns lives in the home they chose. A packaged install
|
|
6
|
+
# (npm) treats its tree as read-only, and the next upgrade replaces it — so
|
|
7
|
+
# anything of theirs kept in the tree is kept at its peril. Every *writer* was
|
|
8
|
+
# converted to resolve the home through bin/hoard-lib.sh; this carries what is
|
|
9
|
+
# already sitting in the tree to where those writers now look.
|
|
10
|
+
#
|
|
11
|
+
# what moves from the tree to the home
|
|
12
|
+
# ------------------ --------------------- ----------------------------
|
|
13
|
+
# this machine's data/ <hoard>/data
|
|
14
|
+
# records
|
|
15
|
+
# runtime state state/ <home>/state
|
|
16
|
+
# (pids · logs · locks · the applied-marker)
|
|
17
|
+
# the operator's the local env file <home>/.env.local
|
|
18
|
+
# credentials (if one is in the tree)
|
|
19
|
+
# the operator's .agents/config/* <home>/config
|
|
20
|
+
# settings that git does not track
|
|
21
|
+
#
|
|
22
|
+
# Idempotent: every entry is checked before it moves, a second run finds nothing
|
|
23
|
+
# to do, and a move that would land on an existing file is REFUSED and reported
|
|
24
|
+
# rather than merged blindly — the home always wins, nothing is ever overwritten.
|
|
25
|
+
#
|
|
26
|
+
# What is deliberately NOT moved: `*.example` templates and `.gitkeep` (they are
|
|
27
|
+
# code), and any settings file git tracks (that is the distro's shipped default,
|
|
28
|
+
# not the operator's own). Moving a tracked file out of the tree would be a code
|
|
29
|
+
# change wearing a migration's clothes.
|
|
30
|
+
#
|
|
31
|
+
# NOTE: run this while the runtime is STOPPED if you want no stale pids; running
|
|
32
|
+
# services keep their open file handles across the move (an `mv` preserves the
|
|
33
|
+
# inode), so nothing is lost either way, but the pid files they wrote will be
|
|
34
|
+
# found in their new place after the next restart.
|
|
35
|
+
set -u
|
|
36
|
+
|
|
37
|
+
R="${YMIR_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
|
38
|
+
# The resolver lives in the tree that ships this migration. When YMIR_ROOT points
|
|
39
|
+
# somewhere else (a sandbox, a test fixture), fall back to the lib beside this
|
|
40
|
+
# script rather than failing to resolve the home at all.
|
|
41
|
+
LIB="$R/bin/hoard-lib.sh"
|
|
42
|
+
[ -r "$LIB" ] || LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/bin/hoard-lib.sh"
|
|
43
|
+
# shellcheck source=bin/hoard-lib.sh
|
|
44
|
+
. "$LIB"
|
|
45
|
+
ymir_home_root H
|
|
46
|
+
hoard_root HOARD
|
|
47
|
+
hoard_data_dir RECORDS
|
|
48
|
+
hoard_state_dir RUNTIME
|
|
49
|
+
hoard_settings_dir SETTINGS
|
|
50
|
+
hoard_local_env ENVFILE
|
|
51
|
+
|
|
52
|
+
moved=0; kept=0; refused=0; merged=0; rows=0
|
|
53
|
+
# The row buffers are written as they happen and printed at the end, so the TOON
|
|
54
|
+
# header can carry the true count. `rows` is kept beside them: under `set -u`,
|
|
55
|
+
# bash older than 4.4 reads an empty `declare -a` array as unbound.
|
|
56
|
+
declare -a ROW_ACT=() ROW_FROM=() ROW_TO=()
|
|
57
|
+
row() { ROW_ACT+=("$1"); ROW_FROM+=("$2"); ROW_TO+=("$3"); rows=$((rows+1)); }
|
|
58
|
+
|
|
59
|
+
# tracked_by_git <path> — 1 when git knows the file (the distro ships it)
|
|
60
|
+
tracked_by_git() {
|
|
61
|
+
command -v git >/dev/null 2>&1 || return 1
|
|
62
|
+
git -C "$R" ls-files --error-unmatch "${1#"$R"/}" >/dev/null 2>&1
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# carry <src> <dst-dir> — move one entry home, unless the home already holds it
|
|
66
|
+
carry() {
|
|
67
|
+
local src="$1" dst="$2" base stale
|
|
68
|
+
base="$(basename "$src")"
|
|
69
|
+
mkdir -p "$dst" 2>/dev/null || { refused=$((refused+1)); row refuse "$src" "cannot write $dst"; return 0; }
|
|
70
|
+
if [ ! -e "$dst/$base" ]; then
|
|
71
|
+
if mv "$src" "$dst/$base" 2>/dev/null; then
|
|
72
|
+
moved=$((moved+1)); row moved "$src" "$dst/$base"
|
|
73
|
+
else
|
|
74
|
+
refused=$((refused+1)); row refuse "$src" "could not move to $dst/$base"
|
|
75
|
+
fi
|
|
76
|
+
return 0
|
|
77
|
+
fi
|
|
78
|
+
# The home already holds this name. Two files, one name — decide by evidence,
|
|
79
|
+
# never by assumption: identical content means the tree's copy can go (nothing
|
|
80
|
+
# is lost, and the content is at home); different content means BOTH are real,
|
|
81
|
+
# so the home's wins the name and the tree's is carried beside it as `.stale-*`
|
|
82
|
+
# rather than merged or discarded.
|
|
83
|
+
if [ -f "$src" ] && [ -f "$dst/$base" ] && cmp -s "$src" "$dst/$base"; then
|
|
84
|
+
rm -f "$src" && { merged=$((merged+1)); row merged "$src" "identical to $dst/$base — the tree's copy removed, nothing lost"; }
|
|
85
|
+
elif [ -f "$src" ]; then
|
|
86
|
+
stale="$dst/$base.stale-$(date -u +%Y%m%dT%H%M%SZ)"
|
|
87
|
+
if mv "$src" "$stale" 2>/dev/null; then
|
|
88
|
+
moved=$((moved+1)); row kept-as "$src" "$stale — differs from the home's copy, so both are kept"
|
|
89
|
+
else
|
|
90
|
+
refused=$((refused+1)); row refuse "$src" "differs from the home's copy and could not be carried"
|
|
91
|
+
fi
|
|
92
|
+
else
|
|
93
|
+
kept=$((kept+1)); row kept "$src" "home already holds $base — left in place, nothing overwritten"
|
|
94
|
+
fi
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# ── the machine's records ────────────────────────────────────────────────────
|
|
98
|
+
if [ -d "$R/data" ]; then
|
|
99
|
+
for f in "$R"/data/* "$R"/data/.[!.]*; do
|
|
100
|
+
[ -e "$f" ] || continue
|
|
101
|
+
case "$(basename "$f")" in .gitkeep|*.example|*.example.*) kept=$((kept+1)); continue ;; esac
|
|
102
|
+
carry "$f" "$RECORDS"
|
|
103
|
+
done
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# ── the runtime state ────────────────────────────────────────────────────────
|
|
107
|
+
if [ -d "$R/state" ]; then
|
|
108
|
+
for f in "$R"/state/* "$R"/state/.[!.]*; do
|
|
109
|
+
[ -e "$f" ] || continue
|
|
110
|
+
case "$(basename "$f")" in .gitkeep) kept=$((kept+1)); continue ;; esac
|
|
111
|
+
carry "$f" "$RUNTIME"
|
|
112
|
+
done
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# ── the operator's credentials ───────────────────────────────────────────────
|
|
116
|
+
# Never printed, never parsed — only carried.
|
|
117
|
+
for cand in "$R"/.env.loc[a]l; do
|
|
118
|
+
[ -f "$cand" ] || continue
|
|
119
|
+
if [ -f "$ENVFILE" ]; then
|
|
120
|
+
kept=$((kept+1)); row kept "${cand#"$R"/}" "the home already holds its own — left in place, never merged"
|
|
121
|
+
else
|
|
122
|
+
mkdir -p "$(dirname "$ENVFILE")" 2>/dev/null
|
|
123
|
+
if mv "$cand" "$ENVFILE" 2>/dev/null; then
|
|
124
|
+
chmod 600 "$ENVFILE" 2>/dev/null || true
|
|
125
|
+
moved=$((moved+1)); row moved "${cand#"$R"/}" "${ENVFILE#"$H"/} (0600)"
|
|
126
|
+
else
|
|
127
|
+
refused=$((refused+1)); row refuse "${cand#"$R"/}" "could not move to the home"
|
|
128
|
+
fi
|
|
129
|
+
fi
|
|
130
|
+
done
|
|
131
|
+
|
|
132
|
+
# ── the operator's own settings ──────────────────────────────────────────────
|
|
133
|
+
# Only what git does not track: a tracked settings file is the distro's shipped
|
|
134
|
+
# default and stays where the code ships it. `config` may be a symlink into
|
|
135
|
+
# .agents/config, so resolve the real directory before walking it.
|
|
136
|
+
cfgdir="$R/.agents/config"
|
|
137
|
+
[ -d "$R/config" ] && [ ! -L "$R/config" ] && cfgdir="$R/config"
|
|
138
|
+
if [ -d "$cfgdir" ]; then
|
|
139
|
+
for f in "$cfgdir"/* "$cfgdir"/.[!.]*; do
|
|
140
|
+
[ -e "$f" ] || continue
|
|
141
|
+
base="$(basename "$f")"
|
|
142
|
+
case "$base" in .gitkeep|*.example|*.example.*) kept=$((kept+1)); continue ;; esac
|
|
143
|
+
if tracked_by_git "$f"; then
|
|
144
|
+
kept=$((kept+1)); row kept "config/$base" "the distro ships this one — it is a default, not the operator's own"
|
|
145
|
+
continue
|
|
146
|
+
fi
|
|
147
|
+
carry "$f" "$SETTINGS"
|
|
148
|
+
done
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
if [ "$rows" -gt 0 ]; then
|
|
152
|
+
printf 'ymir-migrate-0005[%d]{action,from,to}:\n' "$rows"
|
|
153
|
+
for i in "${!ROW_ACT[@]}"; do
|
|
154
|
+
printf ' "%s","%s","%s"\n' "${ROW_ACT[$i]}" "${ROW_FROM[$i]}" "${ROW_TO[$i]}"
|
|
155
|
+
done
|
|
156
|
+
fi
|
|
157
|
+
printf 'ymir-migrate-0005[1]{moved,merged,kept,refused}:\n "%s",%s,%s,%s\n' "$moved" "$merged" "$kept" "$refused"
|
|
158
|
+
exit 0
|
package/.agents/skills/README.md
CHANGED
|
@@ -16,7 +16,7 @@ skills[26]{skill,does,owner}:
|
|
|
16
16
|
"tyr-check","the judge — galdr principles + runtime gates","tyr"
|
|
17
17
|
"rules-check-drift","rules-file drift — keeps AGENTS.md true after code changes","tyr"
|
|
18
18
|
"no-mistakes","the clean-PR gate — vendored engine skill: validate, push, PR, CI","brokk"
|
|
19
|
-
"smidja-factory","the smithy — agent factory: roster, phases, envelopes, visualizer","volundr"
|
|
19
|
+
"smidja-factory","the smithy — agent factory: roster, phases, envelopes, visualizer (app-provided: cloned by step_apps from its own repo, absent until then)","volundr"
|
|
20
20
|
"hvild-afk","away-mode — supervision of routine wakes and batched escalations","brokk"
|
|
21
21
|
"saga-bearings","bearings — fleet status digest (/bearings) + recap (/ahoy)","saga"
|
|
22
22
|
"muninn-stow","memory — session-knowledge curation, routing, persistence","muninn"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: galdr
|
|
3
3
|
description: Galdr — agent-CLI ergonomics and the master builder/maintainer of the Ymir (Brokk distro) runtime. Use when building, modifying, or reviewing any agent-facing CLI, or when building/maintaining any Ymir subsystem across harnesses (OpenCode, Pi, Claude Code, Cursor, Codex). Load its assets[] row for the task before editing a governed path.
|
|
4
|
+
domain: brokkforge
|
|
4
5
|
allowed-tools: read,write,bash,glob,grep
|
|
5
6
|
---
|
|
6
7
|
|
|
@@ -46,7 +47,7 @@ assets[24]{path,load_when}:
|
|
|
46
47
|
"assets/local-models.md","Vog — local models: engines (llama.cpp, LM Studio, Ollama), detection, harness wiring, honest measurement"
|
|
47
48
|
"assets/runtime-compliance.md","runtime acceptance gates + runnable checklist"
|
|
48
49
|
"assets/memory-well.md","Mimirsbrunn/engram: the well, bridge, MCP, harness wiring, laws"
|
|
49
|
-
"assets/installation.md","first setup / install: ymir-
|
|
50
|
+
"assets/installation.md","first setup / install: the computed plan (bin/ymir-plan.sh), the home the operator chooses, the roots law (code in the package, the operator's things in the home), engines, hermes"
|
|
50
51
|
"assets/harness-integration/README.md","choosing a harness; adding one"
|
|
51
52
|
"assets/harness-integration/opencode.md","OpenCode adapter"
|
|
52
53
|
"assets/harness-integration/pi.md","Pi adapter"
|
|
@@ -20,13 +20,19 @@ The governing plan is [`docs/plans/29-brokk-distro-runtime.md`](../../../../../d
|
|
|
20
20
|
|
|
21
21
|
## 1. What a harness adapter is
|
|
22
22
|
|
|
23
|
-
> **MCP scope (
|
|
23
|
+
> **MCP scope (well · mesh · Teams · Anchor).** `bin/a2a-mcp.sh install` wires the
|
|
24
24
|
> `engram` (memory well) and `a2abridge` (A2A mesh) MCP servers into OpenCode's
|
|
25
|
-
> repo `opencode.json` **and** Pi.
|
|
25
|
+
> repo `opencode.json` **and** Pi. The **Teams** plane (`wayofteams`) and
|
|
26
|
+
> **Anchor** memory are **remote** MCP servers named by URL: set
|
|
27
|
+
> `WAYOFTEAMS_MCP_URL` / `ANCHOR_MCP_URL` in the private platform env, then run
|
|
28
|
+
> `bin/a2a-mcp.sh install`. OpenCode speaks `type: remote` natively; Pi has no
|
|
29
|
+
> remote transport, so it reaches them through the `mcp-remote` stdio bridge. The
|
|
30
|
+
> URLs never enter the tracked tree. By default the Pi side writes the **global**
|
|
26
31
|
> `~/.pi/agent/mcp.json`; `bin/a2a-mcp.sh install --project` writes the repo's
|
|
27
32
|
> `.pi/mcp.json` instead, leaving a Pi used elsewhere untouched (launch with
|
|
28
33
|
> `pi --mcp-config .pi/mcp.json`). Prefer `--project` when integrating Ymir into
|
|
29
|
-
> an existing workflow.
|
|
34
|
+
> an existing workflow. `bin/a2a-mcp.sh show` reports what is **actually** wired —
|
|
35
|
+
> every key present, not a fixed list.
|
|
30
36
|
|
|
31
37
|
|
|
32
38
|
The Ymir runtime is a **distro**: a directory of instructions, skills, tooling and conventions that turns a general-purpose agent into a specialized one. Launching a supported harness inside `BROKK_HOME` is supposed to instantiate **Brokk** and address the operator as the **Allfather** *before the model's first turn*.
|
|
@@ -220,6 +226,7 @@ Legend: ✅ implemented · ⚠️ partial/inert-by-design · ❌ not implemented
|
|
|
220
226
|
| `.pi/shared/extensions/open-editor.ts` | `/edit [path]` and `ctrl+shift+e` — opens files from cwd in the Allfather's editor; strictly user-facing, no LLM tool. **Resolution:** `$VISUAL` → `$EDITOR` → the first editor that exists (`code cursor zed subl nvim vim hx helix nano micro emacs vi`), so a host that is not Omarchy — where Omarchy's own launcher or a bare `vi` may be absent — still gets a working editor instead of an ENOENT | user |
|
|
221
227
|
| `.pi/shared/extensions/herdr-agent-state.ts` | reports pane agent lifecycle state to herdr | 2 |
|
|
222
228
|
| `.pi/shared/extensions/todo.ts` | the todo surface | user |
|
|
229
|
+
| `.pi/shared/extensions/ymir-subagents.ts` | **the Eindri roster as a Pi tool** — reads the canonical `.agents/agents/*.md` tree and exposes every figure through a `subagent` tool (and a `/subagents` command). See "Pi has no agent loader" below | user |
|
|
223
230
|
| `.pi/shared/extensions/lib/rodd-operational-input.ts` | `encodeRoddOperationalInput`, `classifyRoddOperationalText`, `classifyRoddCurrentOperationalText` | shared wire |
|
|
224
231
|
| `.pi/extensions/lib/vordr-sessionstart-supervisor.mjs` | detached child supervisor (Vörðr) | 1 (transport) |
|
|
225
232
|
| `.claude/settings.json` | `hooks.SessionStart[]`, `hooks.Stop[]` | 1, 3, 5 |
|
|
@@ -495,7 +502,7 @@ harness config like `mode`/`model`/`permission` in their frontmatter). The
|
|
|
495
502
|
harness directories **bind** them by symlink — they are never hand-written
|
|
496
503
|
duplicates:
|
|
497
504
|
|
|
498
|
-
- OpenCode: `.opencode/
|
|
505
|
+
- OpenCode: `.opencode/agents/<name>.md` → `../../.agents/agents/<profile>.md`
|
|
499
506
|
- Pi: `.pi/agents/<profile>.md` → the same canonical files
|
|
500
507
|
|
|
501
508
|
`bin/valknut-load.sh` creates the symlinks (`--opencode`, `--pi`, `--global`,
|
|
@@ -505,7 +512,7 @@ hand-made subset. Naming differs by harness and must be respected:
|
|
|
505
512
|
|
|
506
513
|
```
|
|
507
514
|
agent_binding[5]{harness,dir,name_rule}:
|
|
508
|
-
"opencode",".opencode/
|
|
515
|
+
"opencode",".opencode/agents/","the frontmatter `name:` — bragi.md -> bragi-marketer.md"
|
|
509
516
|
"pi",".pi/agents/","the profile file name — bragi-marketer.md"
|
|
510
517
|
"claude",".claude/agents/","the profile file name"
|
|
511
518
|
"codex",".codex/agents/","the profile file name"
|
|
@@ -544,7 +551,10 @@ writers[2]{writer,owns}:
|
|
|
544
551
|
|
|
545
552
|
**Both merge; neither overwrites.** The loader's `config_out` adds missing keys (
|
|
546
553
|
deep, `setdefault`-style), ensures `skills.paths`, and re-asserts nothing else;
|
|
547
|
-
`agents-config` does the same for providers and models
|
|
554
|
+
`agents-config` does the same for providers and models — and it writes **only
|
|
555
|
+
OpenCode agents** into `opencode.json`. A `pi` (or `hermes`) agent's model is that
|
|
556
|
+
harness's own id (e.g. `llamacpp/qwen3.5-9b`), which OpenCode cannot resolve, so
|
|
557
|
+
those agents are deliberately left out rather than handed to OpenCode broken. This is not tidiness —
|
|
548
558
|
the loader used to re-render from the example with `sed` + `mv`, and because the
|
|
549
559
|
example carried only `llama.cpp`, **every loader run deleted the Apodex
|
|
550
560
|
provider**, which lived only in the live file. A blind render of a file two
|
|
@@ -581,10 +591,63 @@ einherjar[20]{figure,craft,domain,engine}:
|
|
|
581
591
|
"Völundr","master smith — Smíðja's orchestrator","brokkforge","—"
|
|
582
592
|
```
|
|
583
593
|
|
|
584
|
-
Bind with `bin/valknut-load.sh --all` (OpenCode: `.opencode/
|
|
594
|
+
Bind with `bin/valknut-load.sh --all` (OpenCode: `.opencode/agents/<name>.md`; Pi:
|
|
585
595
|
`.pi/agents/<profile>.md`) — never edit the harness dirs.
|
|
586
596
|
|
|
587
|
-
## Pi
|
|
597
|
+
## Pi has no agent loader (2026-09-17)
|
|
598
|
+
|
|
599
|
+
**Pi core does not load `.pi/agents/`.** Agent loading in Pi is a *package*
|
|
600
|
+
(`pi-agents`, `pi-agent-mode`, `pi-simple-agents`), not a core feature, and Ymir
|
|
601
|
+
installs none of them. So `.pi/agents/` held twenty correct profile links that
|
|
602
|
+
**nothing in Pi ever read** — the same failure as OpenCode's singular
|
|
603
|
+
`.opencode/agent/`, arrived at from the other side.
|
|
604
|
+
|
|
605
|
+
The fix lives in the repo, not in a root-pi package: **`.pi/shared/extensions/ymir-subagents.ts`**
|
|
606
|
+
discovers the canonical `.agents/agents/*.md` tree itself and registers a
|
|
607
|
+
`subagent` tool. A call runs the chosen figure as a nested model call in the
|
|
608
|
+
current session — the figure's markdown body is its system prompt, its
|
|
609
|
+
frontmatter `model:` picks the model where the machine serves it.
|
|
610
|
+
|
|
611
|
+
```
|
|
612
|
+
subagent({ agent: "kvasir", task: "find every AGENTS.md" }) # dispatch
|
|
613
|
+
subagent({}) # list the roster
|
|
614
|
+
/subagents # list the roster
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
Rules this extension follows, learned the hard way:
|
|
618
|
+
|
|
619
|
+
- **No imports.** `@earendil-works/pi-coding-agent` is **not installed as a
|
|
620
|
+
package**, so an extension that imports its types cannot load at all. Every
|
|
621
|
+
working extension in this tree takes `pi` as `any` and declares tool
|
|
622
|
+
`parameters` as a plain JSON schema object. `typebox` *is* installed, but the
|
|
623
|
+
plain object needs nothing.
|
|
624
|
+
- **One home.** It lives in `.pi/shared/extensions/` (deployed), never in
|
|
625
|
+
`.pi/extensions/` — a copy in both makes pi refuse the duplicate tool.
|
|
626
|
+
- **The canonical tree is the source.** It reads `.agents/agents/`; it never
|
|
627
|
+
copies from it (Rule 02).
|
|
628
|
+
|
|
629
|
+
### A rename that left a reader behind (same day)
|
|
630
|
+
|
|
631
|
+
`skuld-branch-supervision.ts` imported `calmTranscriptClassIsVisible` and
|
|
632
|
+
`CalmPresentationState` from `./lib/ro-visibility.ts`, but that module exports
|
|
633
|
+
`roTranscriptClassIsVisible` and `RoPresentationState` — the names were renamed
|
|
634
|
+
and the importer was not. Pi refuses the **whole extension** at load with
|
|
635
|
+
`does not provide an export named …`, so Skuld's supervision branch was dead in
|
|
636
|
+
every session while nothing reported it. Fixed in the same change.
|
|
637
|
+
|
|
638
|
+
**Check every extension actually loads**, not just that its file exists:
|
|
639
|
+
|
|
640
|
+
```bash
|
|
641
|
+
cd ~/.pi/agent/extensions
|
|
642
|
+
for f in *.ts; do
|
|
643
|
+
node --input-type=module --eval "import('file://$PWD/$f').then(()=>console.log('$f LOADS')).catch(e=>console.log('$f FAIL: '+e.message.split('\n')[0]))"
|
|
644
|
+
done
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
Run it after every deploy. A module that cannot resolve is invisible from the
|
|
648
|
+
file listing, and pi reports it only as a startup line that scrolls away.
|
|
649
|
+
|
|
650
|
+
|
|
588
651
|
|
|
589
652
|
Pi loads **both** the project `.pi/extensions/` and the global
|
|
590
653
|
`~/.pi/agent/extensions/` directories; because it does not de-duplicate by
|
|
@@ -632,14 +695,14 @@ under **`ymir_tools:`**; permission is expressed by the `permission:` block.
|
|
|
632
695
|
|
|
633
696
|
```
|
|
634
697
|
canonical[3]{kind,canonical,load_path}:
|
|
635
|
-
"agent profiles",".agents/agents/<profile>.md",".opencode/
|
|
698
|
+
"agent profiles",".agents/agents/<profile>.md",".opencode/agents/<name>.md · .pi/agents/<name>.md (symlinks)"
|
|
636
699
|
"OpenCode plugins",".agents/harness/opencode/plugins/",".opencode/plugins (symlink)"
|
|
637
700
|
"skills",".agents/skills/","loaded via opencode.json skills.paths"
|
|
638
701
|
```
|
|
639
702
|
|
|
640
703
|
Rules:
|
|
641
704
|
- **Edit only `.agents/`.** Never edit a file under `.opencode/` or `.pi/` — those are
|
|
642
|
-
symlinks to `.agents/` (e.g. `.opencode/
|
|
705
|
+
symlinks to `.agents/` (e.g. `.opencode/agents/bragi.md` links to
|
|
643
706
|
`.agents/agents/bragi-marketer.md`).
|
|
644
707
|
- After any change: `bin/valknut-load.sh --all` to rebind.
|
|
645
708
|
- **OpenCode requires REAL `.opencode/{node_modules,package.json,.gitignore}`** — the
|