mercury-agent 0.16.3 → 0.18.0-beta.0

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 (120) hide show
  1. package/README.md +20 -1
  2. package/container/Dockerfile +1 -0
  3. package/container/Dockerfile.base +1 -0
  4. package/docs/autopilot/config.yaml +47 -0
  5. package/docs/behavior-layers.md +3 -0
  6. package/docs/configuration.md +52 -0
  7. package/docs/container-lifecycle.md +18 -2
  8. package/docs/extensions.md +43 -0
  9. package/docs/goals/football-reporter-profile/decisions.md +140 -1
  10. package/docs/goals/football-reporter-profile/harness-2026-09-01.json +303 -0
  11. package/docs/goals/football-reporter-profile/harness-2026-09-01.md +75 -0
  12. package/docs/goals/football-reporter-profile/roadmap.md +73 -12
  13. package/docs/goals/rehearsal-bench/README.md +253 -0
  14. package/docs/goals/rehearsal-bench/decisions.md +398 -0
  15. package/docs/goals/rehearsal-bench/goal.md +198 -0
  16. package/docs/goals/rehearsal-bench/roadmap.md +173 -0
  17. package/docs/goals/release-gate/README.md +58 -0
  18. package/docs/goals/release-gate/decisions.md +112 -0
  19. package/docs/goals/release-gate/goal.md +146 -0
  20. package/docs/goals/release-gate/roadmap.md +115 -0
  21. package/docs/goals/whatsapp-bot-hardening/open-threads.md +21 -10
  22. package/docs/live-testing.md +368 -0
  23. package/docs/pending-verification.md +822 -0
  24. package/docs/permissions.md +15 -0
  25. package/docs/profile-guide.md +306 -19
  26. package/docs/skills-guide.md +24 -0
  27. package/examples/extensions/feed-watch/config.ts +36 -0
  28. package/examples/extensions/feed-watch/digest.ts +230 -32
  29. package/examples/extensions/feed-watch/index.ts +8 -0
  30. package/examples/extensions/feed-watch/items.ts +48 -4
  31. package/examples/extensions/feed-watch/watch.ts +63 -3
  32. package/examples/extensions/longview/hook.ts +12 -6
  33. package/examples/extensions/longview/index.ts +47 -11
  34. package/examples/extensions/longview/render/images.ts +214 -0
  35. package/examples/extensions/longview/render/telegraph-nodes.ts +61 -1
  36. package/examples/extensions/longview/summarize.ts +46 -7
  37. package/examples/extensions/longview/targets/telegraph.ts +55 -2
  38. package/examples/extensions/napkin/distill-input.ts +127 -0
  39. package/examples/extensions/napkin/distill-safety.ts +495 -0
  40. package/examples/extensions/napkin/index.ts +54 -73
  41. package/examples/extensions/napkin/pi-spawn.ts +8 -28
  42. package/examples/extensions/overview/actions.ts +264 -0
  43. package/examples/extensions/overview/gate.ts +82 -0
  44. package/examples/extensions/overview/index.ts +123 -0
  45. package/examples/extensions/overview/skill/SKILL.md +80 -0
  46. package/examples/extensions/tradestation/index.ts +9 -4
  47. package/examples/extensions/yahoo-mail/index.ts +42 -10
  48. package/examples/extensions/yahoo-mail/lib/ymail.ts +592 -0
  49. package/examples/extensions/yahoo-mail/{cli/package.json → package.json} +1 -4
  50. package/examples/extensions/yahoo-mail/skill/SKILL.md +36 -21
  51. package/examples/profiles/_template/config.yaml +34 -0
  52. package/examples/profiles/football-reporter/AGENTS.md +29 -6
  53. package/examples/profiles/football-reporter/README.md +126 -1
  54. package/examples/profiles/football-reporter/config.yaml +49 -15
  55. package/examples/profiles/football-reporter/standard.json +137 -0
  56. package/examples/profiles/football-reporter/tasks/daily-article.md +76 -47
  57. package/package.json +14 -3
  58. package/resources/templates/mercury.example.yaml +9 -0
  59. package/src/adapters/discord-native.ts +7 -0
  60. package/src/adapters/setup.ts +33 -0
  61. package/src/adapters/whatsapp-identity.ts +77 -0
  62. package/src/adapters/whatsapp-ingress.ts +325 -0
  63. package/src/adapters/whatsapp-media.ts +80 -3
  64. package/src/adapters/whatsapp.ts +119 -182
  65. package/src/agent/container-entry.ts +102 -15
  66. package/src/agent/container-env.ts +11 -0
  67. package/src/agent/container-error.ts +24 -1
  68. package/src/agent/container-runner.ts +789 -141
  69. package/src/agent/pi-jsonl-parser.ts +166 -5
  70. package/src/audit/ledger-queries.ts +345 -0
  71. package/src/bridges/discord.ts +8 -1
  72. package/src/bridges/slack.ts +5 -1
  73. package/src/bridges/teams.ts +5 -1
  74. package/src/bridges/telegram.ts +34 -2
  75. package/src/bridges/whatsapp.ts +3 -5
  76. package/src/cli/mercury.ts +586 -9
  77. package/src/cli/mrctl-http.ts +18 -0
  78. package/src/cli/mrctl.ts +11 -4
  79. package/src/config-file.ts +14 -0
  80. package/src/config.ts +121 -2
  81. package/src/core/attachment-notes.ts +4 -0
  82. package/src/core/confirmation.ts +3 -1
  83. package/src/core/debounce.ts +10 -5
  84. package/src/core/exec.ts +75 -0
  85. package/src/core/handler.ts +32 -14
  86. package/src/core/media.ts +104 -2
  87. package/src/core/model-leg.ts +41 -0
  88. package/src/core/outbox.ts +23 -3
  89. package/src/core/process-tree.ts +24 -18
  90. package/src/core/reply-context.ts +66 -0
  91. package/src/core/routes/chat.ts +83 -4
  92. package/src/core/routes/control.ts +2 -2
  93. package/src/core/routes/dashboard.ts +82 -7
  94. package/src/core/runtime.ts +324 -57
  95. package/src/core/storage-cleanup.ts +59 -0
  96. package/src/core/task-output.ts +36 -18
  97. package/src/core/task-scheduler.ts +18 -1
  98. package/src/core/trigger.ts +10 -4
  99. package/src/extensions/catalog.ts +1 -0
  100. package/src/extensions/hooks.ts +8 -0
  101. package/src/extensions/image-builder.ts +33 -51
  102. package/src/extensions/loader.ts +68 -7
  103. package/src/extensions/types.ts +35 -2
  104. package/src/logger.ts +27 -2
  105. package/src/main.ts +58 -4
  106. package/src/ops/preflight.ts +227 -0
  107. package/src/ops/shadow-root.ts +234 -0
  108. package/src/ops/shadow-snapshot.ts +1076 -0
  109. package/src/profile/space-profile.ts +48 -5
  110. package/src/server.ts +95 -33
  111. package/src/storage/backup.ts +403 -0
  112. package/src/storage/db.ts +261 -15
  113. package/src/storage/integrity.ts +94 -0
  114. package/src/text/markdown.ts +72 -1
  115. package/src/text/reporter-lint.ts +1607 -0
  116. package/src/text/sanitize-text.ts +118 -0
  117. package/src/text/write-claim.ts +68 -0
  118. package/src/types.ts +105 -3
  119. package/examples/extensions/yahoo-mail/cli/bun.lock +0 -66
  120. package/examples/extensions/yahoo-mail/cli/ymail.mjs +0 -353
package/README.md CHANGED
@@ -267,6 +267,12 @@ mercury conversations
267
267
  mercury conversations --unlinked
268
268
  mercury link <conversation-id> <space-id>
269
269
 
270
+ # backups (state.db snapshots — also taken automatically by the running service)
271
+ mercury backup create # verified VACUUM INTO snapshot in <dataDir>/backups/
272
+ mercury backup list
273
+ mercury backup verify [snapshot] # quick_check a snapshot (default: newest)
274
+ mercury restore <snapshot> # verify + restore to a fresh path (never overwrites)
275
+
270
276
  # extensions
271
277
  mercury add ./path/to/extension
272
278
  mercury add npm:<package>
@@ -417,11 +423,24 @@ Supported OAuth providers: Anthropic, GitHub Copilot, Google Gemini CLI, Antigra
417
423
  | `MERCURY_CONTAINER_TIMEOUT_MS` | `300000` | Container timeout (5 min) |
418
424
  | `MERCURY_CONTAINER_RUNTIME` | `runc` | `runc` (default) or `runsc` ([gVisor](https://gvisor.dev)) |
419
425
  | `MERCURY_CONTAINER_BWRAP_DOCKER_COMPAT` | `false` | Set `true` on Linux Docker Engine (see note below) |
426
+ | `MERCURY_CONTAINER_ALLOW_UNCONFINED` | `false` | Set `true` to accept the Docker Desktop sandbox relaxation (see note below) |
420
427
  | `MERCURY_AGENT_ID` | — | Unique ID for this assistant (required when running multiple assistants on the same Docker daemon) |
421
428
 
422
429
  > **Multiple assistants on the same machine:** Mercury builds a derived Docker image (`mercury-agent-ext:<hash>`) from the base image plus your extensions. When a new image is built, older tags in the same repo are pruned. If two assistants share a Docker daemon without distinct `MERCURY_AGENT_ID` values, they share the same image repo and **one will silently prune the other's image**, causing container launch failures. Set `MERCURY_AGENT_ID` to a unique value per project (e.g. in `.env`). Cloud console deployments set this automatically.
423
430
 
424
- > **Linux Docker Engine:** Mercury uses [bubblewrap](https://github.com/containers/bubblewrap) for in-container sandboxing. On Linux Docker Engine (not Docker Desktop), bwrap cannot mount `/proc` without extra privileges. Either set `container_bwrap_docker_compat: true` in `mercury.yaml` (adds `--privileged` to `docker run`), or install [gVisor](https://gvisor.dev/docs/user_guide/install/) and set `MERCURY_CONTAINER_RUNTIME=runsc` to skip bwrap entirely.
431
+ > **Docker Desktop (macOS/Windows):** Mercury detects Docker Desktop and would
432
+ > otherwise relax the sandbox automatically (`--security-opt seccomp=unconfined
433
+ > --cap-add SYS_ADMIN`) so `bwrap` can nest. It now **refuses to do that
434
+ > silently**: containers fail to start with a message naming the remedy.
435
+ > Set `MERCURY_CONTAINER_ALLOW_UNCONFINED=true` (or `agent.allow_unconfined:
436
+ > true` in `mercury.yaml`) to accept the trade knowingly — it is then logged at
437
+ > `warn` on every spawn. **Upgrading an existing Docker Desktop install requires
438
+ > this**, or every message gets a generic container failure. Linux hosts are
439
+ > unaffected; `MERCURY_CONTAINER_RUNTIME=runsc` (or `agent.container_runtime:
440
+ > runsc` in `mercury.yaml`; gVisor) avoids the relaxation entirely, and the
441
+ > flag is ignored when `container_bwrap_docker_compat` is set.
442
+
443
+ > **Linux Docker Engine:** Mercury uses [bubblewrap](https://github.com/containers/bubblewrap) for in-container sandboxing. On Linux Docker Engine (not Docker Desktop), bwrap cannot mount `/proc` without extra privileges. Either set `container_bwrap_docker_compat: true` in `mercury.yaml` (adds `--privileged` to `docker run`), or install [gVisor](https://gvisor.dev/docs/user_guide/install/) and set `MERCURY_CONTAINER_RUNTIME=runsc` (or `agent.container_runtime: runsc` in `mercury.yaml`) to skip bwrap entirely.
425
444
 
426
445
  **KB Distillation:**
427
446
 
@@ -111,6 +111,7 @@ COPY src/cli/mrctl.ts /app/src/cli/mrctl.ts
111
111
  COPY src/cli/mrctl-http.ts /app/src/cli/mrctl-http.ts
112
112
  COPY src/extensions/reserved.ts /app/src/extensions/reserved.ts
113
113
  COPY src/extensions/permission-guard.ts /app/src/extensions/permission-guard.ts
114
+ COPY src/text/sanitize-text.ts /app/src/text/sanitize-text.ts
114
115
  COPY src/types.ts /app/src/types.ts
115
116
  COPY resources/ /app/resources/
116
117
  COPY examples/extensions/ /tmp/examples-extensions/
@@ -80,6 +80,7 @@ COPY src/cli/mrctl.ts /app/src/cli/mrctl.ts
80
80
  COPY src/cli/mrctl-http.ts /app/src/cli/mrctl-http.ts
81
81
  COPY src/extensions/reserved.ts /app/src/extensions/reserved.ts
82
82
  COPY src/extensions/permission-guard.ts /app/src/extensions/permission-guard.ts
83
+ COPY src/text/sanitize-text.ts /app/src/text/sanitize-text.ts
83
84
  COPY src/types.ts /app/src/types.ts
84
85
  COPY resources/ /app/resources/
85
86
  COPY examples/extensions/ /tmp/examples-extensions/
@@ -0,0 +1,47 @@
1
+ # Autopilot Configuration
2
+ # Read by the orchestrator each iteration. See docs/templates/TEMPLATE-AUTOPILOT-CONFIG.yaml.
3
+
4
+ gates:
5
+ max_iterations: 10
6
+ max_features_without_review: 3
7
+ max_retries_same_task: 3
8
+ iteration_timeout_minutes: 30
9
+
10
+ risk:
11
+ sensitive_paths: # changes to these = immediate stop for review
12
+ - "**/auth/**"
13
+ - "**/migration*"
14
+ - "**/.env*"
15
+ - "**/schema*"
16
+ # Container spawn shape: sandbox flags, mounts, network, runtime selection.
17
+ # A wrong change here passes `bun run check` and is only observable in the
18
+ # running container — the check gate cannot verify it. Human-only.
19
+ - "src/agent/container-runner.ts"
20
+ - "src/agent/container-entry.ts"
21
+ auto_stop_risk_threshold: 8
22
+
23
+ scope:
24
+ allowed_actions:
25
+ - implement_feature
26
+ - continue_feature
27
+ - fix_bug
28
+ - execute_refactor
29
+ - sync_docs
30
+ skip_slugs: # never touch autonomously
31
+ # Both are design decisions, not mechanical fixes, and neither is verifiable
32
+ # by the check suite. See docs/bugs/ for the filed reports.
33
+ - docker-desktop-sandbox-relaxation-silent # fix = a product call: warn, or refuse and break Docker Desktop users
34
+ # container-egress-unrestricted moved to docs/ideas/ on 2026-09-01 -- the
35
+ # two-container split it proposed was ruled out by investigation. Its one
36
+ # remaining code-shaped piece is env-passthrough-defaults-to-all, below.
37
+ - env-passthrough-defaults-to-all # breaking default change: needs the owner'''s go-ahead and a week of startup-log evidence first
38
+ - linux-default-sandbox-path-broken-on-modern-docker # product call on the relaxation shape; unverifiable by the check suite (container spawn flags)
39
+ # attachment-ingest-type-gating — unblocked 2026-08-27 (warn-only decision
40
+ # recorded in the spec). Eligible once it is listed under ROADMAP Now/Next.
41
+
42
+ planning:
43
+ allow_autonomous_planning: false
44
+
45
+ notifications:
46
+ on_stop: console
47
+ decision_log: docs/autopilot/decision-log.md
@@ -29,6 +29,9 @@ the repo, after a pi upgrade.
29
29
  ## Run budget (tools-capable models only)
30
30
  ## Destructive Operations — Confirmation Required
31
31
  ## Presenting tool results ("simple lists", "never show JSON/commands")
32
+ ## Delivery ("[reply] … [/reply] — only the span inside is sent";
33
+ pi-jsonl-parser.ts extractReplyEnvelope honours it,
34
+ a reply with no markers is sent whole)
32
35
  ## Character (mrctl character flow + "standing instruction → set a preference"
33
36
  — only for a caller holding prefs.set)
34
37
  reply-anchor sentence (swipe-replies only)
@@ -85,6 +85,58 @@ Per-space: `mrctl config set ambient.enabled false` disables capture entirely (t
85
85
 
86
86
  You may also set a top-level **`model_chain`** array as an alias for `model.chain`.
87
87
 
88
+ ## State database backups
89
+
90
+ `state.db` is snapshotted by the storage sweep via `VACUUM INTO
91
+ <dataDir>/backups/state-<timestamp>.db`. Every snapshot is verified
92
+ (`PRAGMA quick_check`) before it counts; a crash mid-snapshot leaves only a
93
+ `.partial` file that is never mistaken for a backup and is swept later.
94
+
95
+ - **`MERCURY_BACKUP_INTERVAL_HOURS`** (default `12`) — a new snapshot is taken
96
+ once the newest one is at least this old.
97
+ - **`MERCURY_BACKUP_RETENTION_COUNT`** (default `7`) — snapshots kept; older
98
+ ones are pruned by the same sweep.
99
+
100
+ `mercury backup create | list | verify` manages snapshots by hand;
101
+ `mercury restore <snapshot>` verifies a snapshot and writes it to a **fresh
102
+ path only** (default `<dataDir>/state.restored-<timestamp>.db`) — it never
103
+ overwrites the live database, and prints the manual swap-in steps.
104
+
105
+ `mercury doctor` **fails** when no snapshot at most 24 h old exists (a missing
106
+ backups directory included), and warns when the data dir sits on a
107
+ 9p/virtiofs/CIFS filesystem (a `.mercury/` under `/mnt/c` in WSL2, or a VM
108
+ shared folder) — SQLite WAL locking is unreliable there.
109
+
110
+ The database itself refuses to open when `quick_check` fails — loudly, with a
111
+ typed error, never auto-repaired. The fix is `mercury restore`.
112
+
113
+ ## Shadow mode (`MERCURY_SHADOW`)
114
+
115
+ **`MERCURY_SHADOW`** (default off; accepts only `1` or `true`,
116
+ case-insensitively — every other spelling, `yes` and `on` included, means
117
+ live) turns a process into a *rehearsal shadow*: it constructs no platform
118
+ adapter, makes no console billing call, delivers no task output, marks every
119
+ log line and the dashboard, and reports `shadow: true` on `/health`.
120
+
121
+ **A shadow refuses to start unless its data dir is inside
122
+ `<home>/mercury-shadow/`.** The flag keeps a shadow away from people; this
123
+ keeps it away from live *state*. Without it, `MERCURY_SHADOW=1` started in
124
+ the live project directory would construct no adapter and touch no session
125
+ while running a **second scheduler against the live database** — every due
126
+ task fires twice and SQLite gets a second writer.
127
+
128
+ The check runs in `loadConfig()`, so every entry point inherits it, and it
129
+ denies on absence at each input: no home directory (a systemd unit with no
130
+ `HOME`), a relative data dir, a `..` segment, the shadow root itself, a path
131
+ outside the root, or an existing directory whose `realpath` escapes the root.
132
+ It compares path *segments*, never string prefixes, so `mercury-shadowy` is
133
+ not inside `mercury-shadow`. A live process (`MERCURY_SHADOW` unset) is never
134
+ checked and runs exactly the code it ran before.
135
+
136
+ Snapshot directories are built by `mercury shadow snapshot`, which generates
137
+ the shadow's `.env` and `mercury.yaml` — nothing here needs to be set by
138
+ hand. See [live-testing.md](live-testing.md) §2 and §7.
139
+
88
140
  ## Container env passthrough (`agent.env_passthrough`)
89
141
 
90
142
  Controls which host `MERCURY_*` variables reach agent containers:
@@ -191,13 +191,29 @@ The pi agent runs inside `bwrap`, which creates a minimal mount namespace with o
191
191
 
192
192
  | Env Var | Purpose |
193
193
  |---------|---------|
194
- | `MERCURY_CONTAINER_BWRAP_DOCKER_COMPAT=1` | **Host only.** Adds `docker run --security-opt seccomp=unconfined --cap-add SYS_ADMIN` so `bwrap` can nest inside the agent container (e.g. Docker Desktop). Keeps bubblewrap on. |
194
+ | `MERCURY_CONTAINER_BWRAP_DOCKER_COMPAT=1` | **Host only.** Runs the agent container `--privileged` Docker's widest grant (every capability, no seccomp, no AppArmor, all devices) — so `bwrap` can nest inside it. Keeps bubblewrap on, but the outer isolation layer is gone. Prefer `MERCURY_CONTAINER_RUNTIME=runsc`. |
195
+ | `MERCURY_CONTAINER_ALLOW_UNCONFINED=true` | **Host only.** Accepts the *auto-detected* Docker Desktop relaxation (`seccomp=unconfined`, `apparmor=unconfined`, `--cap-add SYS_ADMIN` — narrower than `--privileged`). Defaults to `false`, so on Docker Desktop containers refuse to start until it is set — the relaxation is never applied without the operator seeing it. Logged at `warn` on every spawn once enabled. Ignored under `runsc` and when `BWRAP_DOCKER_COMPAT` is set. |
195
196
  | `MERCURY_DISABLE_BUBBLEWRAP=1` | Disable bubblewrap; run pi directly (last resort / debugging) |
196
197
 
197
- If you see `bwrap: Creating new namespace failed: Operation not permitted`, try **`MERCURY_CONTAINER_BWRAP_DOCKER_COMPAT=1`** first so you keep defense-in-depth. Only use `MERCURY_DISABLE_BUBBLEWRAP=1` if compat mode is not enough.
198
+ **On a stock Linux host the default shape cannot run bwrap.** Docker's default seccomp profile blocks the user-namespace clone and Ubuntu's `docker-default` AppArmor profile independently blocks bwrap's mount step, so every turn fails with `bwrap: No permissions to create new namespace` (or `Creating new namespace failed: Operation not permitted`). On a stock modern host the kernel sysctls that message points at (`unprivileged_userns_clone`, `max_user_namespaces`) are not the blocker both gates are Docker's; check them only if they are off their defaults. Mercury probes for this once at boot (`docker run --entrypoint bwrap …` with the real bwrap shape) and, if blocked, logs one `error` line naming the fix and refuses every spawn with the same diagnosis instead of a per-turn bwrap error. Escapes, narrowest first: `MERCURY_CONTAINER_RUNTIME=runsc` (gVisor — strongest sandbox, no relaxation needed), then `MERCURY_CONTAINER_BWRAP_DOCKER_COMPAT=1` (`--privileged`), then `MERCURY_DISABLE_BUBBLEWRAP=1` (no inner sandbox at all).
198
199
 
199
200
  Custom images must install `bubblewrap` for sandboxing to work.
200
201
 
202
+ **runsc (gVisor) mode requires `--host-uds=open` on the runtime registration.** In runsc mode the agent container reaches the host API over a bind-mounted unix socket — the socket is mrctl's *only* transport there (`API_URL` is a deliberate dummy). gVisor refuses `connect()` to host unix sockets unless the daemon registers runsc with the flag:
203
+
204
+ ```json
205
+ {
206
+ "runtimes": {
207
+ "runsc": {
208
+ "path": "/usr/bin/runsc",
209
+ "runtimeArgs": ["--host-uds=open"]
210
+ }
211
+ }
212
+ }
213
+ ```
214
+
215
+ Without it, Mercury starts clean and containers run, but **every mrctl verb fails** (config, tasks, mutes, stop, capabilities — the whole host API surface) with a bare connect error inside the conversation. Mercury probes for this once at boot (a runsc container connect-tests the socket with the real spawn's mount shape) and, if blocked, logs one `error` line naming this fix. Applying it is zero-downtime: `runtimes` is a SIGHUP-reloadable daemon.json key, so edit the file and `kill -SIGHUP $(pidof dockerd)` — no daemon restart, no container kills.
216
+
201
217
  ## Agent Image Preset
202
218
 
203
219
  Mercury publishes an image preset to GitHub Container Registry:
@@ -397,6 +397,49 @@ mercury extensions list # or: mercury ext list
397
397
 
398
398
  Shows all installed extensions (user + built-in) with features and descriptions.
399
399
 
400
+ ## Cross-Space Overview (`overview`)
401
+
402
+ `examples/extensions/overview/` gives **one** designated space a read-only
403
+ window onto the others — the only cross-space read an agent has. It is not a
404
+ builtin: install it deliberately.
405
+
406
+ ```bash
407
+ mercury add ./examples/extensions/overview
408
+ MERCURY_MANAGEMENT_SPACE_ID=<space id> # host-only; unset = every read denied
409
+ ```
410
+
411
+ The agent calls it with `mrctl capability overview <action> '<json>'`:
412
+
413
+ | Action | Body | Returns |
414
+ |--------|------|---------|
415
+ | `list` | — | every space with `messageCount`, `conversationCount`, `platforms`, `lastActivity` |
416
+ | `recent` | `{spaceId, limit?}` | that space's latest messages, newest first |
417
+ | `search` | `{q, spaceId?, limit?}` | substring matches; every space when `spaceId` is omitted |
418
+ | `tasks` | `{spaceId?, limit?}` | scheduled tasks, one space or all |
419
+ | `roles` | `{spaceId}` | role assignments |
420
+
421
+ `limit` defaults to 20 and is capped at 100; message bodies — and a task's
422
+ `prompt` and `lastError` — are truncated at 500 characters with
423
+ `truncated: true`. Every action is a plain `SELECT` — there is no
424
+ cross-space write, and `listMutes` is deliberately not exposed because it deletes
425
+ expired rows.
426
+
427
+ Three gates run before any action, in the handler, and they are the whole
428
+ boundary:
429
+
430
+ 1. `MERCURY_MANAGEMENT_SPACE_ID` is set — **absence denies**.
431
+ 2. The caller's token-bound space id equals it, by exact string equality.
432
+ 3. The caller is a global admin (`config.admins` / `dmAutoSpaceAdminIds`).
433
+
434
+ The `overview` permission checked by the capability route is **not** a scope:
435
+ `admin` is granted every registered permission in its own space, so every space
436
+ admin in the deployment passes it. Only the three gates above stop them.
437
+
438
+ The skill is installed into the management space's own workspace on
439
+ `workspace_init`, not via `mercury.skill()` — that copies into the global skills
440
+ dir mounted into *every* container, which would advertise the capability to
441
+ spaces that cannot use it.
442
+
400
443
  ## Examples
401
444
 
402
445
  See [`examples/extensions/`](../examples/extensions/) for complete, working extensions ranging from minimal (charts — CLI + skill) to full-featured (napkin — hooks, jobs, config, widgets, KB distillation).
@@ -475,5 +475,144 @@
475
475
  implementing: `## Current State (as of …)` is not matched by the injector, so
476
476
  every note napkin had written for this space was reaching the model as a
477
477
  summary line with no facts —
478
- `docs/bugs/episode-current-state-heading-suffix-drops-body.md`.
478
+ `docs/debug/major/08-2026/2026-08-31-episode-current-state-heading-suffix-drops-body.md`.
479
479
  - **Used by:** reporter-notebook (M2.3, re-cut), football-reporter-fixture-harness (provenance source = `episodes/`), napkin (distill addendum)
480
+
481
+ ## D-020: Live tuning is adopted into the repo, not reverted — and scans carry a fixed club emoji (decided 2026-08-31)
482
+ - **Category:** layer placement / process (refines [D-003](#d-003-layer-placement-for-football), [D-013](#d-013-tone-is-editorial-and-scan-length-is-per-item-decided-2026-08-20))
483
+ - **Decided:** (1) When the owner edits the live `AGENTS.md` on the box while
484
+ the bot is running, the repo **adopts** the live text (copy live → repo,
485
+ read the whole file for the contradiction the edit introduced, fix that in
486
+ the repo, `apply`) — it never reverts it with `apply`. The live file is the
487
+ owner's most recent editorial judgement and the repo's job is to record it;
488
+ `check` failing is the signal to reconcile, not to overwrite. (2) The
489
+ 2026-08-27 edits are now the standard: every scan item opens with one fixed
490
+ club emoji (😈 United, 🔵 Barcelona, ⚪ Real, 🔴 Hapoel TA, 💛 Beitar,
491
+ 🟢 Maccabi Haifa; none for an item outside those six, none invented), items
492
+ are grouped under one club heading when several concern one club, an item
493
+ is ≤ 200 characters including its `(source, dd/mm)`, items are separated by
494
+ a blank line, the displayed date is `dd/mm` while the year is still verified
495
+ in the source, and the article stays at zero emoji.
496
+ - **Alternatives considered:**
497
+ - *Revert the box to the repo with `apply`.* Rejected: the edits are the
498
+ owner's, dated, and were running for four days; the repo was the stale
499
+ copy.
500
+ - *Keep "0 emoji in scheduled output" as decided at the M1 checkpoint.*
501
+ Rejected by the owner's edit: a fixed per-club marker is a format element
502
+ (it tells a phone reader which club before the sentence), not decoration.
503
+ M3.1's emoji lint becomes an **allowlist** (exactly one of the six, at line
504
+ start, scan only) rather than a zero count.
505
+ - **Reasoning:** `check` on 2026-08-31 reported one difference — `AGENTS.md`
506
+ — whose diff was purely additive owner tuning from 2026-08-27 15:39. The
507
+ same file's `## סגנון` still said "scan and article: zero", so the adoption
508
+ also fixed the in-file contradiction the guide's §4.5 warns about. The
509
+ fourth bullet the owner added ("no English working sentence as a first
510
+ line") is a prose rule over a host defect and did not hold (four leaks after
511
+ it); it stays in the file as documentation of intent but the fix is
512
+ `docs/bugs/narration-in-same-text-block-leaks-into-reply.md`.
513
+ - **Revisit if:** live edits become frequent — then the right tool is a
514
+ profile PR flow from the box (edit → `dump` → commit), not a rule.
515
+ - **Used by:** football-reporter profile (`AGENTS.md`), football-reporter-fixture-harness (M3.1 lint spec)
516
+
517
+ ## D-021: The daily article is an article, not a page of bins (decided 2026-09-01)
518
+ - **Category:** article contract / design (extends [D-005](#d-005-keep-telegraph-make-the-summary-fuller-and-redesign-the-article-itself-decided-2026-08-20), [D-010](#d-010-the-chat-summary-is-author-supplied-by-the-same-run-not-a-second-model-call); refines [D-013](#d-013-tone-is-editorial-and-scan-length-is-per-item-decided-2026-08-20))
519
+ - **Decided:** The owner opened the 2026-09-01 page on a phone (RTL fine) and
520
+ ruled the design wrong: *"it still looks too much like an itinerary and not
521
+ an article."* Eight prototype pages were published and compared on the phone
522
+ the same evening; prototype **C5** is the target shape. Five rulings:
523
+ 1. **Images are in**, but only when the image is good and relevant, and it
524
+ must not take over the page. C2 (one wide banner) beat C1 (none); C4/C5
525
+ (a press photo) beat both.
526
+ 2. **Side-by-side is dropped** — a platform limit, not a preference.
527
+ Telegraph strips every attribute except `src` and unwraps a `div`, so an
528
+ image beside text is impossible; footprint is controlled by aspect ratio
529
+ only (a 4.5:1 banner is ~85 px tall on a phone, a portrait ~430 px).
530
+ 3. **Press photos are allowed**, hotlinked from the cited source and credited
531
+ in the caption. `telegra.ph/upload` answers 400, so hotlinking is the only
532
+ path anyway.
533
+ 4. **The long source list goes.** Short bracketed links inline —
534
+ `([וואלה](url))` — plus **one** italic footer line naming every outlet
535
+ read. The footer line stays.
536
+ 5. **Stay on Telegraph.** A richer target (real CSS, images beside text,
537
+ cards) is possible through `longview`'s `targets/` seam but needs a host
538
+ and is a separate project.
539
+
540
+ And four image rules, learned by publishing a wrong one (C4's hero showed
541
+ Anthony Gordon under a caption, written from assumption, naming Álvarez):
542
+ the image URL is the cited article's `og:image`; the caption is the source's
543
+ own caption, translated, never re-described — it lives in the article HTML,
544
+ not the RSS; no caption at the source means no image; the photo sits in the
545
+ section whose story it depicts, not automatically at the top (C5 is C4 with
546
+ only that changed). Wikimedia Commons is not a substitute: wide and relevant
547
+ are mutually exclusive there.
548
+
549
+ Two consequences the owner confirmed on 2026-09-01 when the story was
550
+ planned: the per-article "first three blocks get similar length" measure no
551
+ longer applies — the lead story leads, and the coverage balance in
552
+ `AGENTS.md` § תפקיד is a rule about coverage over time; and the
553
+ `DD.MM.YYYY · N מקורות` dateline goes, replaced by Telegraph's own
554
+ author-and-date block under the headline (`author_name`, empty on every page
555
+ published so far, is free article furniture).
556
+ - **Alternatives considered:**
557
+ - *Typography only, words unchanged (prototype A).* Better, and most of it
558
+ needs no code — `>>`, `---` and numbered lists are already in the
559
+ renderer. Not enough on its own: the bins are the tell, not the type.
560
+ - *Keep the eight blocks and drop only the empty ones.* Rejected: the same
561
+ headings in the same order every day is what reads as generated, whether
562
+ or not a bin is empty.
563
+ - *Images beside text via `h3`/`h4` level or `div` wrappers.* Measured
564
+ impossible (rule 2). Heading level is not a lever either: `h3` is 28 px,
565
+ `h4` 24 px, both sans-bold.
566
+ - *Commons images.* Every wide result is a stadium panorama, every usable
567
+ player photo a portrait, nothing of *yesterday's* match.
568
+ - **Reasoning:** the contract shipped by M1.2 is followed correctly — every
569
+ element renders — and the page still reads as a log because the contract
570
+ itself is a list of bins. The fix is the contract, and since M3.1 the
571
+ contract is countable, so the lint, its data file and its fixtures move in
572
+ the same change or the check goes green for the wrong reason.
573
+ - **Revisit if:** the model gets captions wrong in practice (then `longview`
574
+ fetches `og:image` and the caption host-side instead of the run emitting
575
+ them), or a private target with real CSS gets a host (then ruling 5).
576
+ - **Used by:** daily-article-design-follow-up (M1.3), football-reporter
577
+ profile (`tasks/daily-article.md`, `AGENTS.md` § שתיקה, `config.yaml`
578
+ `longview.byline`), football-reporter-fixture-harness (M3.1 lint
579
+ `article-structure` / `article-image`)
580
+
581
+ ## D-022: M2.2 is closed as accepted by construction, with two revisit triggers (decided 2026-09-01)
582
+ - **Category:** scope / sequencing (closes [D-015](#d-015-m22-is-deferred-until-after-m31-not-dropped-decided-2026-08-21); keeps [D-006](#d-006-scheduled-research-runs-post-from-the-primary-model-leg-or-not-at-all) as the standing rule)
583
+ - **Decided:** `scheduled-task-model-leg-policy` (M2.2) is **closed**, not
584
+ built. The property it was written for — a scheduled research run posts from
585
+ the primary leg or not at all — has held by construction since the
586
+ 2026-08-20 pin: every space carries a `model.active` row, the runtime turns
587
+ it into a one-element chain, a leg failure is a task error that retries and
588
+ reports to the admin and posts nothing, and the M3.2 ledger records the
589
+ model of every run. The backlog doc moves to `docs/ideas/` with its design
590
+ intact, re-labelled as what the same column is really worth: **per-task
591
+ model choice** (the article on one leg, verify runs on another — the Opus
592
+ A/B that was never started), with the safety property as a side effect.
593
+ - **Revisit triggers (either reopens it, as a prerequisite, not an option):**
594
+ 1. anyone clears a `model.active` pin, or a `/model reset` verb is built —
595
+ the fix the `model-switch-disables-fallback` bug asks for. That verb must
596
+ not ship before a scheduled-run guard exists; the bug doc now says so.
597
+ 2. the ledger records a scheduled run on a non-primary leg.
598
+ - **Alternatives considered:**
599
+ - *Build it as designed* (nullable `tasks.model_policy`, `primary_only`, a
600
+ `skipped` status, one truncation site). Sound, about a session of work,
601
+ and it buys nothing while the pin policy stands. Rejected for now, not on
602
+ merit.
603
+ - *Keep it deferred a third time.* Rejected: D-015's own revisit condition
604
+ (M3.1 in hand) has been met and the doc's open question — is this worth
605
+ its cost while every space is pinned — had gone unanswered since
606
+ 2026-08-20. A story that cannot be scheduled or dropped is noise on the
607
+ roadmap.
608
+ - *Re-cut it as per-task model choice now.* Not rejected — parked as the
609
+ idea, to be planned on its own merits if the article A/B is wanted.
610
+ - **Reasoning:** the pin was a deliberate choice ("Opus-only, no fallback,
611
+ all legs switchable", 2026-08-20), not an accident to be guarded against.
612
+ The cost argument for re-opening died with the digest-replay fix
613
+ (M2 checkpoint, 2026-08-31). What remained was a guarantee against a future
614
+ the owner has not asked for, and that future has a clear trigger. 300-plus
615
+ scheduled runs since the pin, zero on a non-primary leg.
616
+ - **Used by:** football-reporter-profile roadmap (M2.2 row),
617
+ `docs/bugs/model-switch-disables-fallback.md` (the reset-verb constraint),
618
+ `docs/ideas/scheduled-task-model-leg-policy.md`