mercury-agent 0.24.1 → 0.24.2-beta.2

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 (167) hide show
  1. package/README.md +1 -1
  2. package/docs/auth/whatsapp.md +6 -4
  3. package/docs/configuration.md +5 -0
  4. package/docs/container-lifecycle.md +2 -2
  5. package/docs/extensions.md +1 -0
  6. package/docs/goals/conversation-review/decisions.md +352 -0
  7. package/docs/goals/conversation-review/goal.md +150 -0
  8. package/docs/goals/conversation-review/roadmap.md +94 -0
  9. package/docs/goals/rehearsal-bench/roadmap.md +1 -1
  10. package/docs/goals/space-links/decisions.md +443 -0
  11. package/docs/goals/space-links/goal.md +141 -0
  12. package/docs/goals/space-links/roadmap.md +129 -0
  13. package/docs/live-testing.md +34 -3
  14. package/docs/pending-verification.md +176 -1
  15. package/docs/plans/{flat-0916 → archive/2026-09-28-flat-0916}/plan.md +4 -4
  16. package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/plan.md +2 -2
  17. package/docs/plans/archive/2026-10-02-media-bugs-0928/briefs/S1.md +59 -0
  18. package/docs/plans/archive/2026-10-02-media-bugs-0928/briefs/S2.md +64 -0
  19. package/docs/plans/archive/2026-10-02-media-bugs-0928/briefs/S3.md +61 -0
  20. package/docs/plans/archive/2026-10-02-media-bugs-0928/decisions.md +6 -0
  21. package/docs/plans/archive/2026-10-02-media-bugs-0928/plan.md +111 -0
  22. package/docs/plans/archive/2026-10-02-media-bugs-0928/sessions/S1.md +34 -0
  23. package/docs/plans/archive/2026-10-02-media-bugs-0928/sessions/S2.md +43 -0
  24. package/docs/plans/archive/2026-10-02-media-bugs-0928/sessions/S3.md +35 -0
  25. package/docs/plans/{sensitive-bugs-0922 → archive/2026-10-02-sensitive-bugs-0922}/plan.md +2 -2
  26. package/docs/plans/archive/2026-10-02-sensitive-bugs-0922/sessions/S1.md +35 -0
  27. package/docs/plans/archive/2026-10-02-sensitive-bugs-0922/sessions/S2.md +30 -0
  28. package/docs/plans/archive/2026-10-02-sensitive-bugs-0922/sessions/S3.md +39 -0
  29. package/docs/plans/archive/2026-10-02-sensitive-bugs-0922/sessions/S4.md +38 -0
  30. package/docs/plans/archive/2026-10-02-sensitive-bugs-0922/sessions/S5.md +37 -0
  31. package/docs/plans/general-0928/briefs/S1.md +66 -0
  32. package/docs/plans/general-0928/briefs/S2.md +64 -0
  33. package/docs/plans/general-0928/briefs/S3.md +58 -0
  34. package/docs/plans/general-0928/briefs/S4.md +58 -0
  35. package/docs/plans/general-0928/briefs/S5.md +58 -0
  36. package/docs/plans/general-0928/decisions.md +8 -0
  37. package/docs/plans/general-0928/plan.md +107 -0
  38. package/docs/plans/search-bugs-0930/briefs/S1.md +73 -0
  39. package/docs/plans/search-bugs-0930/briefs/S2.md +75 -0
  40. package/docs/plans/search-bugs-0930/briefs/S3.md +77 -0
  41. package/docs/plans/search-bugs-0930/briefs/S4.md +75 -0
  42. package/docs/plans/search-bugs-0930/briefs/S5.md +74 -0
  43. package/docs/plans/search-bugs-0930/briefs/S6.md +77 -0
  44. package/docs/plans/search-bugs-0930/decisions.md +8 -0
  45. package/docs/plans/search-bugs-0930/plan.md +165 -0
  46. package/docs/plans/search-bugs-0930/sessions/S1.md +28 -0
  47. package/docs/plans/search-bugs-0930/sessions/S2.md +27 -0
  48. package/docs/plans/search-bugs-0930/sessions/S3.md +40 -0
  49. package/docs/plans/search-bugs-0930/sessions/S5.md +39 -0
  50. package/docs/plans/search-bugs-0930/sessions/S6.md +37 -0
  51. package/docs/plans/search-quality-0930/briefs/S12.md +69 -0
  52. package/docs/plans/search-quality-0930/briefs/S7.md +76 -0
  53. package/docs/plans/search-quality-0930/briefs/S9.md +68 -0
  54. package/docs/plans/search-quality-0930/decisions.md +11 -0
  55. package/docs/plans/search-quality-0930/plan.md +107 -0
  56. package/docs/plans/unattended-1003/briefs/S1.md +92 -0
  57. package/docs/plans/unattended-1003/briefs/S2.md +92 -0
  58. package/docs/plans/unattended-1003/briefs/S3.md +90 -0
  59. package/docs/plans/unattended-1003/briefs/S4.md +92 -0
  60. package/docs/plans/unattended-1003/briefs/S5.md +92 -0
  61. package/docs/plans/unattended-1003/briefs/S6.md +94 -0
  62. package/docs/plans/unattended-1003/briefs/S7.md +92 -0
  63. package/docs/plans/unattended-1003/briefs/S8.md +90 -0
  64. package/docs/plans/unattended-1003/briefs/S9.md +92 -0
  65. package/docs/plans/unattended-1003/decisions.md +7 -0
  66. package/docs/plans/unattended-1003/plan.md +153 -0
  67. package/docs/scheduler.md +5 -1
  68. package/docs/web-search.md +13 -7
  69. package/examples/extensions/napkin/distill-input.ts +27 -6
  70. package/examples/extensions/napkin/prompts/kb-distillation.md +3 -1
  71. package/examples/extensions/office-365-mail/README.md +3 -2
  72. package/examples/extensions/office-365-mail/index.ts +35 -1
  73. package/examples/extensions/office-365-mail/lib/graph.ts +158 -11
  74. package/examples/extensions/office-365-mail/lib/save.ts +140 -0
  75. package/examples/extensions/office-365-mail/skill/SKILL.md +15 -1
  76. package/examples/extensions/pinchtab/bin/pinchtab_ensure +129 -0
  77. package/examples/extensions/pinchtab/bin/pinchtab_nav +50 -0
  78. package/examples/extensions/pinchtab/bin/pinchtab_session_inject.cjs +184 -0
  79. package/examples/extensions/pinchtab/index.ts +41 -237
  80. package/examples/extensions/pinchtab/skill/SKILL.md +7 -110
  81. package/examples/extensions/voice-transcribe/index.ts +192 -50
  82. package/examples/extensions/voice-transcribe/skill/SKILL.md +2 -0
  83. package/examples/extensions/web-search/brave.ts +396 -18
  84. package/examples/extensions/web-search/index.ts +122 -22
  85. package/examples/extensions/web-search/ledger.ts +156 -0
  86. package/examples/extensions/web-search/skill/SKILL.md +30 -6
  87. package/examples/extensions/web-search/skill/references/zap.md +148 -0
  88. package/examples/profiles/football-reporter/config.yaml +11 -2
  89. package/examples/profiles/tel-aviv-flat-search/config.yaml +3 -2
  90. package/package.json +4 -4
  91. package/src/adapters/whatsapp-media.ts +262 -40
  92. package/src/adapters/whatsapp.ts +1 -1
  93. package/src/agent/container-entry.ts +348 -36
  94. package/src/agent/container-error.ts +39 -2
  95. package/src/agent/container-runner.ts +162 -23
  96. package/src/agent/pi-failure-class.ts +36 -6
  97. package/src/agent/pi-jsonl-parser.ts +151 -33
  98. package/src/agent/provider-auth-rejection.ts +279 -0
  99. package/src/agent/user-error-messages.ts +26 -0
  100. package/src/bridges/discord.ts +7 -0
  101. package/src/bridges/slack.ts +13 -1
  102. package/src/bridges/teams.ts +29 -16
  103. package/src/bridges/telegram.ts +13 -0
  104. package/src/bridges/whatsapp.ts +6 -0
  105. package/src/cli/mercury.ts +85 -0
  106. package/src/cli/mrctl-http.ts +67 -0
  107. package/src/cli/mrctl.ts +14 -12
  108. package/src/config.ts +5 -3
  109. package/src/core/api-types.ts +5 -0
  110. package/src/core/api.ts +9 -1
  111. package/src/core/auth.ts +1 -1
  112. package/src/core/caller-token.ts +32 -1
  113. package/src/core/handler.ts +11 -1
  114. package/src/core/media.ts +216 -16
  115. package/src/core/operator-alerts.ts +204 -4
  116. package/src/core/outbox.ts +125 -12
  117. package/src/core/routes/capability.ts +2 -2
  118. package/src/core/routes/config-builtin.ts +27 -0
  119. package/src/core/routes/console.ts +7 -0
  120. package/src/core/routes/dashboard.ts +10 -0
  121. package/src/core/routes/tasks.ts +11 -0
  122. package/src/core/runtime.ts +705 -113
  123. package/src/core/space-links.ts +273 -0
  124. package/src/core/system-messages.ts +45 -2
  125. package/src/core/task-scheduler.ts +115 -5
  126. package/src/env-dump/build-deps.ts +2 -55
  127. package/src/extensions/catalog.ts +5 -3
  128. package/src/extensions/context.ts +4 -0
  129. package/src/extensions/image-builder.ts +265 -65
  130. package/src/extensions/reserved.ts +6 -0
  131. package/src/extensions/types.ts +30 -0
  132. package/src/ops/shadow-diff.ts +1027 -0
  133. package/src/preflight/binary-version.ts +103 -0
  134. package/src/preflight/build-deps.ts +2 -0
  135. package/src/preflight/checks/credential.ts +16 -9
  136. package/src/preflight/checks/host-deps.ts +107 -10
  137. package/src/preflight/deps.ts +5 -0
  138. package/src/profile/space-profile.ts +21 -0
  139. package/src/server.ts +56 -1
  140. package/src/storage/db.ts +603 -0
  141. package/src/storage/pi-auth.ts +92 -8
  142. package/src/text/reporter-lint.ts +77 -30
  143. package/docs/plans/flat-0916/briefs/S1.md +0 -55
  144. package/docs/plans/flat-0916/briefs/S2.md +0 -55
  145. /package/docs/plans/{flat-0916 → archive/2026-09-28-flat-0916}/decisions.md +0 -0
  146. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/briefs/S1.md +0 -0
  147. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/briefs/S2.md +0 -0
  148. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/briefs/S3.md +0 -0
  149. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/briefs/S4.md +0 -0
  150. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/briefs/S5.md +0 -0
  151. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/briefs/S6.md +0 -0
  152. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/briefs/S7.md +0 -0
  153. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/briefs/S8.md +0 -0
  154. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/decisions.md +0 -0
  155. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/sessions/S1.md +0 -0
  156. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/sessions/S2.md +0 -0
  157. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/sessions/S3.md +0 -0
  158. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/sessions/S4.md +0 -0
  159. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/sessions/S5.md +0 -0
  160. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/sessions/S6.md +0 -0
  161. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/sessions/S7.md +0 -0
  162. /package/docs/plans/{sensitive-bugs-0922 → archive/2026-10-02-sensitive-bugs-0922}/briefs/S1.md +0 -0
  163. /package/docs/plans/{sensitive-bugs-0922 → archive/2026-10-02-sensitive-bugs-0922}/briefs/S2.md +0 -0
  164. /package/docs/plans/{sensitive-bugs-0922 → archive/2026-10-02-sensitive-bugs-0922}/briefs/S3.md +0 -0
  165. /package/docs/plans/{sensitive-bugs-0922 → archive/2026-10-02-sensitive-bugs-0922}/briefs/S4.md +0 -0
  166. /package/docs/plans/{sensitive-bugs-0922 → archive/2026-10-02-sensitive-bugs-0922}/briefs/S5.md +0 -0
  167. /package/docs/plans/{sensitive-bugs-0922 → archive/2026-10-02-sensitive-bugs-0922}/decisions.md +0 -0
@@ -0,0 +1,94 @@
1
+ # Roadmap: Conversation Review
2
+
3
+ **Goal**: [conversation-review](goal.md)
4
+ **Last updated**: 2026-09-28
5
+
6
+ ## Milestones
7
+
8
+ ### Milestone 1: Daily judge on the local bot
9
+ > On the local bot, a daily scan judges every exchange of the last 24 h and
10
+ > writes a redacted report on the box. The owner gets a DM digest.
11
+
12
+ | ID | Story | Slug | Depends on | Status |
13
+ |----|-------|------|------------|--------|
14
+ | M1.1 | Read a window of exchanges per space from `state.db`, read-only | review-exchange-reader | — | backlog |
15
+ | M1.2 | `redactIdentifiers` + fail-closed `assertNoIdentifiers` in core | redact-identifiers-with-gate | — | backlog |
16
+ | M1.3 | `conversation-review` extension: daily job, batched no-tools judge, validated findings | conversation-review-daily-judge | M1.1 | blocked |
17
+ | M1.4 | Report (`report.md` + `report.json`) on the box and the owner DM digest | conversation-review-report-and-digest | M1.2, M1.3 | blocked |
18
+
19
+ **Checkpoint:** on the local bot, after deploying and enabling:
20
+
21
+ 1. In a test space, the owner provokes one bad answer: a question the bot answers
22
+ wrongly or refuses without reason. They trigger a scan with the extension's
23
+ run-now hook (or wait for 04:00).
24
+ 2. `~/whatsapp-bot/.../reviews/<date>-daily/report.md` exists and flags that
25
+ exchange with a kind, a scope, a target layer and the right message id.
26
+ 3. `grep -E` for a phone number, `@s.whatsapp.net`, `@lid` and the owner's name
27
+ in the report returns nothing.
28
+ 4. A DM arrives with the finding count, the top three findings and the report
29
+ path.
30
+ 5. The report's footer lists spaces and exchanges judged, anything skipped with
31
+ the reason, spawns, wall clock and cost. The journal shows the scan never
32
+ stalled a live reply.
33
+
34
+ ### Milestone 2: Both bots, into the repo
35
+ > The Tagula bot runs the same scan on its release. `/f-review-intake` brings both
36
+ > bots' reports into `docs/reviews/` and triages each finding against what is
37
+ > already documented or landed before any of it becomes work (D-011).
38
+
39
+ | ID | Story | Slug | Depends on | Status |
40
+ |----|-------|------|------------|--------|
41
+ | M2.1 | `scripts/review-intake.ts` + `/f-review-intake`: pull, re-gate, then verify → relate → decide per finding, owner approves, ledger | review-intake-to-repo | M1.4 | blocked |
42
+ | M2.2 | Enable on Tagula: release, config, `include_clients` gate, tagula-agents runbook | conversation-review-on-tagula | M1.4 | blocked |
43
+
44
+ **Checkpoint:** a daily report from each bot lands in `docs/reviews/`. The
45
+ intake triage table then shows, each row citing the paths it matched:
46
+ - at least one finding re-judged as fine and proposed as `reject`;
47
+ - one proposed as `attach` to an existing open item;
48
+ - one checked against a landed item, as `fixed-not-deployed` or `revisit`;
49
+ - a finding raised again on a later day, under a different slug, reads
50
+ `duplicate` and follows the earlier decision.
51
+
52
+ Only the rows the owner approved were applied, and every row is in
53
+ `docs/reviews/ledger.json`.
54
+
55
+ ### Milestone 3: Weekly deep pass and a feedback loop
56
+ > A weekly strong-model synthesis clusters recurring issues across spaces and
57
+ > measures the daily judge's recall. The owner's verdicts on findings sharpen the
58
+ > judge.
59
+
60
+ | ID | Story | Slug | Depends on | Status |
61
+ |----|-------|------|------------|--------|
62
+ | M3.1 | Weekly job: cluster 7 days of findings, space → global promotion, recall spot check | conversation-review-weekly-synthesis | M1.4 | blocked |
63
+ | M3.2 | The M2.1 ledger feeds the judge: suppressed fingerprints and counter-examples from rejected findings go back to each bot; precision appears in the weekly report | review-verdict-feedback | M2.1, M3.1 | blocked |
64
+
65
+ **Checkpoint:** the first weekly report lists at least one cluster that spans two
66
+ or more spaces, and states the measured recall of the daily judge. After a week
67
+ of intake verdicts, it also states precision, and a rejected false positive is
68
+ not re-raised.
69
+
70
+ ### Future milestones (to be decomposed when M3 completes)
71
+ - **Trace-aware analysis:** link a reply to its run trace (message id in the
72
+ trace name, the system prompt recorded) so a finding can cite the tool call
73
+ that failed. This builds on `trace-records-system-prompt`.
74
+ - **Friction signals as extra evidence:** corrections, rephrasing, "you didn't
75
+ answer". Relates to `repeated-style-corrections-become-preferences`.
76
+ - **Cross-bot clustering** at intake, and a view of trends over time.
77
+ - **Proposal → rehearsal:** replay the flagged exchange against a proposed
78
+ profile change on the shadow bench (`rehearsal-bench`, `mercury smoke`) and
79
+ attach the before/after answer to the finding.
80
+ - **Drafted fixes:** a finding becomes a pre-filled bug or idea doc, or a draft
81
+ `AGENTS.md` diff, still approved by a human.
82
+
83
+ ## Dependency Graph
84
+
85
+ ```
86
+ M1.1 ─┐
87
+ ├→ M1.3 ─┐
88
+ M1.2 ─┼────────┴→ M1.4 ─┬→ M2.1 ─┐
89
+ │ ├→ M2.2 ├→ M3.2
90
+ │ └→ M3.1 ─┘
91
+ ```
92
+
93
+ M1.1 and M1.2 can run in parallel. M2.1, M2.2 and M3.1 can run in parallel once
94
+ M1.4 lands.
@@ -48,7 +48,7 @@ matrix in §2 or edits it; `ops-preflight` refuses the exact command of
48
48
  | M1.1 | `shadow: boolean` config (`MERCURY_SHADOW`), absent = live: no adapter constructed, `consoleUrl` unset, dashboard banner + `shadow=1` on every log line; negative tests for each branch | shadow-mode-flag | — | **done** 2026-09-02 — [spec](../../archive/rehearsal/2026-09-02-shadow-mode-flag.md), PR #68 |
49
49
  | M1.2 | `mercury shadow snapshot`: `VACUUM INTO` + copy of `spaces/ global/ extensions/ model-capabilities.yaml` into `~/mercury-shadow/<ts>/`, `SNAPSHOT.json` (source commit, live HEAD, time), `current` symlink, shadow `.env` derived by a script (copy + override lines, never printed) and a `mercury.yaml` with `runtime.data_dir` and `server.port` rewritten; the `.env` writer emits `MERCURY_SHADOW=1` (only `true`/`1` enable the mode) and the shadow **refuses to start** when `runtime.data_dir` is not under `~/mercury-shadow/` — the flag alone protects people, not the live DB (M1.1 review, 2026-09-02) | shadow-snapshot | M1.1 | **shipped** 2026-09-03 — [spec](../../archive/rehearsal/2026-09-03-shadow-snapshot.md), PR #71, deployed 2026-09-03. `mercury shadow snapshot\|prune`, `src/ops/shadow-root.ts` + `src/ops/shadow-snapshot.ts`; the data-dir refusal closes the M1.1 residual |
50
50
  | M1.3 | `mercury shadow up|down|status|logs`: transient user unit via `systemd-run --user --unit mercury-shadow`, `--src` defaults to `~/mercury-src`; `down` prints the run cost from the shadow's `token_usage`; refuses `up` when the live `mercury` unit is not the only thing on 8787 or when the snapshot is older than `--max-age`. **From M1.2's plan:** the unit must set `HOME` (the data-dir guard refuses on an empty home), and `up` must record its snapshot path so `shadow prune` skips the running one. **Added 2026-09-03 (D-011, D-012):** (a) *prerequisite, first commit of the story* — every container the runner creates (agent, probes) carries a `mercury.instance=<id>` label derived from the data dir, and `cleanupOrphans` **always** filters on it — no id, no sweep; negative test deletes the filter and watches a foreign container get listed; (b) `--src` also accepts an installed package (`mercury-agent` under npm, run as root with `XDG_RUNTIME_DIR=/run/user/0`), so the same command works on a fleet box; (c) `up` refuses below `--min-free-mem` / `--min-free-disk`, while `task_runs` has an unfinished row, or while a cron is due within the run window; never builds an image (missing derived image → refuse, name the tag); binds 8788 to loopback | shadow-service | M1.2 | **done** 2026-09-07 — [spec](../../archive/rehearsal/2026-09-07-shadow-service.md). **Three corrections found in the code while building it:** the roadmap's "refuse while `task_runs` has an unfinished row" could not be written as specified (`finished_at` is NOT NULL and rows are written at completion, so the table never shows a run in progress) — it is a *running container* carrying the live instance id instead; "binds 8788 to loopback" was **dropped**, because under `runc` agent containers reach the host API over TCP at `host.docker.internal:<port>` and a loopback bind would break agent runs in the shadow only (a bind-host option is B-2, live and shadow alike); and the label had to cover a fourth container site, `preflight/probe-container.ts` |
51
- | M1.4 | `mercury shadow diff [--space <id>]`: live snapshot vs shadow now — `space_config` / `space_preferences` / `tasks` / `space_roles` / `mutes` rows with `updated_by` before→after, `messages` added (count + first lines), workspace tree by hash (`MEMORY.md`, `knowledge/**`, `AGENTS.md`), `extension_state` keys. Output is the report a migration rehearsal is judged on | shadow-diff | M1.2 | backlog |
51
+ | M1.4 | `mercury shadow diff [--space <id>]`: live snapshot vs shadow now — `space_config` / `space_preferences` / `tasks` / `space_roles` / `mutes` rows with `updated_by` before→after, `messages` added (count + first lines), workspace tree by hash (`MEMORY.md`, `knowledge/**`, `AGENTS.md`), `extension_state` keys. Output is the report a migration rehearsal is judged on | shadow-diff | M1.2 | **done 2026-10-01** ([archive](../../archive/rehearsal/2026-10-01-shadow-diff.md)) — `diffShadow` in `src/ops/shadow-diff.ts` is the library call M3.2 / M4.1 reuse |
52
52
  | M1.5 | `longview` gains a `file` target and shadow mode forces it; the transcript shows the page path. `capability` route refuses non-`readOnly` actions in shadow mode | longview-file-target | M1.1 | backlog |
53
53
  | M1.3b | **Shadow admin list is chosen, never inherited** (added 2026-09-13, D-015, from bug `shadow-env-keys-does-not-override-mercury-admins`): `shadow snapshot --admins <id,…>` (default empty) is what `renderShadowEnv` writes as `MERCURY_ADMINS`; `SNAPSHOT.json.shadow.adminCount` records the count, `snapshot`/`up`/`status` print it (never the ids); `up` refuses a snapshot whose manifest records no count. Landed with the shadow-up hardening track (B1 of the 2026-09-12 bench-and-gate round), which also closed: the live project read from the unit's `WorkingDirectory` / `SNAPSHOT.json` (never cwd, disagreement refuses), the cron gate's third state (unknown denies, none allows), `mercury run` on `process.execPath` + the unit `PATH` composed from the live unit / `--path` (never the shell's), and the CLI tests' scoped user bus | shadow-admins | M1.3 | **done** 2026-09-13 — post-mortems under `docs/debug/{moderate,minor}/2026-09-1{2,3}-shadow-*` and `…-mercury-run-spawns-a-bare-bun-…` |
54
54
  | M1.3c | **Shadow-own model credential** (added 2026-09-15, from bug `shadow-snapshot-auth-json-goes-stale-when-the-live-bot-rotates-its-refresh-token`): the snapshot stops copying `global/auth.json`; `up` points `MERCURY_AUTH_PATH` at a shadow-only file the owner logs in to once (`mercury auth login` against the shadow root), so the shadow never holds the live refresh token — neither dies when the other rotates. D-016's principle ("a shadow answers to no live credential") extended from the API secrets to the model credential. The `Bun.serve` 10 s stream death on `/api/shadow/simulate` (bug `shadow-simulate-stream-dies-at-bun-serve-10s-idle-timeout`) — the other reason a bench run needs a person watching — is **not** this story's: it is fixed directly as plan `bugs-0915` S1 (corrected 2026-09-15), and the bug's shape (a) mitigation (`up` re-copies the live credential, `--dry-run` gates on it) ships as `bugs-0915` S4; this story owns shape (c) | shadow-own-credential | M1.3 | backlog |
@@ -0,0 +1,443 @@
1
+ # Decisions: Space Links
2
+
3
+ **Goal**: [space-links](goal.md)
4
+
5
+ > Revised 2026-09-26 at checkpoint 1. The owner rejected the first draft's
6
+ > closed capability catalogue: *"from a management space [we should be able] to
7
+ > change any setting, read any message in any other space, and in general to
8
+ > have any control we would have if we were an admin in the target space. The
9
+ > target space context must not be affected at all by this, but the source space
10
+ > context can fill with data from the target space when required (not always)."*
11
+ > D-005 is rewritten, D-011 to D-014 are new, and D-003, D-004, D-006, D-008 and
12
+ > D-009 are amended to match.
13
+ >
14
+ > Revised again 2026-09-26 (second round of checkpoint 1). The owner dropped
15
+ > transitivity: *"should we give up transitivity? maybe it just complicates
16
+ > things and can cause problems. any link should be easily visible and
17
+ > editable."* D-003 is rewritten. The owner also asked to summarize a client
18
+ > space's day, test the output it would give a client, and edit its profile,
19
+ > all from one management space, with no conversation linked to each client and
20
+ > no leak from management into the client. D-015 and D-016 are new.
21
+
22
+ ## D-001: No new stack; SQLite tables in `Db.migrate()`, Hono routes, `mrctl` subcommand
23
+ - **Category:** tech-stack
24
+ - **Decided:**
25
+ - Two new tables, `space_links` and `space_link_audit`, added to the
26
+ `CREATE TABLE IF NOT EXISTS` block in `src/storage/db.ts`.
27
+ - One route module, `src/core/routes/links.ts`, mounted at `/links`, plus one
28
+ change to the auth middleware in `src/core/api.ts` (D-011).
29
+ - One `mrctl links` subcommand group and one global `mrctl --space` flag.
30
+ - No new dependency.
31
+ - **Alternatives considered:**
32
+ - Links as `space_config` keys: no directed edge, no audit, and a
33
+ per-space key cannot express the target side.
34
+ - A links extension, the way `overview` is an extension: it would put the
35
+ authorization boundary in extension code that any space can disable or
36
+ shadow.
37
+ - **Reasoning:** Cross-space authorization is core. The `character` route
38
+ (b3c1841) and `mrctl model` (0bbb63a) are recent, working templates for exactly
39
+ this shape.
40
+ - **Revisit if:** links need to span two Mercury instances. That would need an
41
+ inter-host transport, which is out of scope. Owner, 2026-09-28: "for now we
42
+ will only handle links between spaces on the same bot. The management space
43
+ will be on the Tagula bot. Links between bots on separate servers is deferred
44
+ for later." Parked as `docs/ideas/cross-bot-space-links.md`.
45
+ - **Used by:** all stories
46
+
47
+ ## D-002: A link is one directed edge `holder → target`; "two-sided" is two edges
48
+ - **Category:** architecture
49
+ - **Decided:**
50
+ - Each `space_links` row is one directed edge. The **holder** side gains the
51
+ edge's access level over the **target** side.
52
+ - `mrctl links create A B --both` writes two rows, A→B and B→A, with the same
53
+ level, in one transaction.
54
+ - Asymmetric pairs are two separate `create` calls.
55
+ - The optional conversation filter (`target_conversation_id`) narrows the
56
+ **target** end only.
57
+ - **Alternatives considered:**
58
+ - One row with a `bidirectional` flag: cannot express the asymmetric case
59
+ (management administers a client space; the client sees nothing back).
60
+ - `none | a→b | b→a | both` per axis on one row, as the idea doc proposed:
61
+ every check would need to know which end is "a".
62
+ - **Reasoning:**
63
+ - Every check becomes the same question: is there an edge from the holder to
64
+ the target (or to `*`), and at what level?
65
+ - The direction words are fixed as **holder** and **target**, never "from/to"
66
+ or "a/b". Data flows target→holder; access points holder→target.
67
+ - **Revisit if:** a real case needs one revoke to take out both halves of a pair
68
+ atomically often enough to warrant a `pair_id`.
69
+ - **Used by:** all stories
70
+
71
+ ## D-003: Links are not transitive; only a direct edge (or `*`) grants access
72
+ - **Category:** architecture
73
+ - **Decided:**
74
+ - The holder reaches a target only through an active edge
75
+ `holder → target` or `holder → *`. B's own links give A nothing.
76
+ - When several direct edges match (a `*` edge and a specific one, or a
77
+ filtered and an unfiltered one), the **strongest** level wins.
78
+ - Computed on every call. Never materialized, never cached.
79
+ - **Alternatives considered:**
80
+ - Transitive, weakest edge on a path wins, best path wins (this decision's
81
+ first version, from the intake answer "links are transitive"). The owner
82
+ withdrew it on 2026-09-26: "maybe it just complicates things and can cause
83
+ problems. any link should be easily visible and editable."
84
+ - A per-edge `transitive` flag. It keeps every problem of transitivity for
85
+ the edges that set it.
86
+ - **Reasoning:**
87
+ - Every access that exists is one row in `mrctl links list`, and changing or
88
+ revoking it is one command on that row. With transitivity, an access could
89
+ exist on no row at all, and a link created for one purpose (X→mgmt) could
90
+ silently hand X the reach of mgmt's wildcard.
91
+ - The owner's case needs no chain: one management space with a `*` edge, or
92
+ one edge per client.
93
+ - The resolver becomes one SELECT with no loop, no hop cap and no path to
94
+ audit.
95
+ - **Revisit if:** a real case needs A to reach C through B, and writing the
96
+ A→C row is worse than what transitivity would cost. Nothing in the goal needs
97
+ it today.
98
+ - **Used by:** space-link-graph-and-grant-resolver and every story that checks a
99
+ grant
100
+
101
+ ## D-004: One resolver, literal arguments; the caller's own role in the holder still bounds them
102
+ - **Category:** pattern
103
+ - **Decided:**
104
+ - Every link-mediated request goes through
105
+ `resolveLinkGrant(db, holderSpaceId, targetSpaceId)` in
106
+ `src/core/space-links.ts`, called from exactly one place: the act-as step of
107
+ the auth middleware (D-011). No route calls it on its own.
108
+ - The holder is always `auth.spaceId` from the verified caller token. The
109
+ target is the explicit `x-mercury-act-as` value.
110
+ - The caller must hold `links.act` in the **holder** (default: admin). A caller
111
+ who is not an admin (or `system`) in the holder is capped at `read` in the
112
+ target, whatever the link says.
113
+ - **Alternatives considered:**
114
+ - A parameterised `checkPerm(c, perm, targetSpace)` sprinkled per route. Every
115
+ route would need its own cross-space branch, and they would drift.
116
+ - **Reasoning:**
117
+ - One implementation, one call site: read, control, send and files cannot
118
+ disagree about what a link allows.
119
+ - A link never turns a member of the management space into an admin of a
120
+ client space.
121
+ - **Revisit if:** never, without a security review.
122
+ - **Used by:** all stories
123
+
124
+ ## D-005: A link carries an access level: `read` or `admin`
125
+ - **Category:** scope
126
+ - **Decided:**
127
+ - Each edge carries one level. Absent, unknown or unparsable denies.
128
+
129
+ | Level | In the target, the holder may… |
130
+ |---|---|
131
+ | `read` | Everything an admin can **see** and nothing they can change: search messages, list conversations and tasks, get config, prefs, roles, permissions, mutes and the model chain, and (M2) list and read workspace files, including `MEMORY.md` and the knowledge vault. Enforced per route: each `ACT_AS_ROUTES` entry is marked `read` or `admin`, and a `read` grant reaches only the `read` entries (D-011). |
132
+ | `admin` | Exactly what the target's `admin` role may do (`getRolePermissions(db, target, "admin")`): prefs, config, tasks, roles and permissions, mutes, model switch, rename, stop, compact, clear, send, and (M2) write workspace files and run extension actions. |
133
+
134
+ - What stays out, and why:
135
+ - **The Bot Character.** It is not per-space. A global admin already edits it
136
+ from any space with `mrctl character`, so there is nothing for a link to
137
+ grant.
138
+ - **Creating or changing links.** Global admin only (D-006). An `admin`
139
+ link into B does not let the holder link B to anything.
140
+ - **Anything global-admin-only** (`/character`, `/broadcast`, the `?all`
141
+ views). These check `callerId`, not the space, so a link cannot reach them.
142
+ - **Alternatives considered:**
143
+ - The first draft's closed nine-capability catalogue (`read.messages`,
144
+ `control.prefs`, …). Rejected by the owner on 2026-09-26: it left out roles,
145
+ memory and knowledge, and made "act as an admin there" into nine separate
146
+ grants plus a per-route implementation of each.
147
+ - A link naming a target role (`--as member`). Role names and permissions are
148
+ per-space and editable, so the minimum rule of D-003 would have to compare
149
+ permission sets across spaces. Two fixed levels keep the rule total.
150
+ - **Reasoning:** The owner's requirement is literally "whatever an admin of the
151
+ target could do". Reusing the target's own admin permission set means there is
152
+ no second list to keep in step with the permission system. `read` exists
153
+ because peer spaces (the 2026-09-24 TV-shortlist case) need to look without
154
+ being able to touch.
155
+ - **Revisit if:** a real case needs a middle level (for example "may change
156
+ prefs but not roles"). A third level slots between the two; it never widens
157
+ either.
158
+ - **Used by:** all stories
159
+
160
+ ## D-006: Only a global admin creates, updates or revokes; target-side admins can always see inbound reach
161
+ - **Category:** architecture
162
+ - **Decided:**
163
+ - `create`, `update` and `revoke` are gated on `isGlobalAdmin(callerId)`, the
164
+ same as `routes/character.ts`.
165
+ - `list`, `inspect` and `audit` need the new `links.list` permission (default:
166
+ admin) in the caller's space. They show:
167
+ - edges where the caller's space is holder or target;
168
+ - the **inbound** reach (every space with a direct or `*` edge to this one,
169
+ and at what level) and the outbound reach.
170
+ - Acting across a link needs `links.act` (default: admin) in the holder (D-004).
171
+ - **Alternatives considered:**
172
+ - A space admin on both ends consents. Rejected by the owner on 2026-09-26.
173
+ - **Reasoning:**
174
+ - Creating a link grants access to someone else's conversations.
175
+ - Asymmetric visibility is acceptable; unaccountable visibility is not.
176
+ - **Revisit if:** customer-operated deployments need a space admin to create
177
+ links inside their own tenancy.
178
+ - **Used by:** mrctl-links-admin-commands, act-as-linked-space
179
+
180
+ ## D-007: Disclosure is recorded, not enforced
181
+ - **Category:** scope
182
+ - **Decided:**
183
+ - `space_links.disclosure` is a free-text field set at create or update with
184
+ `--disclosure "<how and when the target side was told>"`.
185
+ - It is shown by `list` and `inspect`. Nothing is posted to the target side
186
+ automatically (which D-012 would forbid anyway).
187
+ - **Alternatives considered:**
188
+ - A required automatic notice.
189
+ - Leaving disclosure out entirely.
190
+ - **Reasoning:** Owner decision, 2026-09-26. Recording it keeps the question
191
+ visible without deciding it in code.
192
+ - **Revisit if:** a customer deployment or a legal review requires notice. The
193
+ field is then the natural gate.
194
+ - **Used by:** space-link-graph-and-grant-resolver, mrctl-links-admin-commands
195
+
196
+ ## D-008: Every link-mediated request is audited in its own table
197
+ - **Category:** architecture
198
+ - **Decided:**
199
+ - `space_link_audit` gets one row per:
200
+ - `create`, `update`, `revoke` of a link;
201
+ - act-as request (D-011), successful or refused after the grant check, with
202
+ method, route, a redacted body summary, response status, and the id of
203
+ the edge that granted it.
204
+ - Rows survive `revoke` and `deleteSpace`.
205
+ - The audit table is host bookkeeping. It is never part of any space's prompt
206
+ (D-012).
207
+ - **Alternatives considered:**
208
+ - Logger lines only. The journal rotates and cannot be queried by the target's
209
+ admin.
210
+ - Per-route before/after capture. Twelve route modules would each need it; the
211
+ request body plus status is uniform and answers "who did what". Before/after
212
+ is added only where the route already returns it.
213
+ - **Reasoning:** The target's admin must be able to answer "who read or changed
214
+ what in my space". Today there is no config-change audit anywhere.
215
+ - **Revisit if:** audit volume from read requests becomes material. In that case,
216
+ aggregate reads per run.
217
+ - **Used by:** all stories
218
+
219
+ ## D-009: `overview` stays as it is for now; it becomes a wildcard `read` link later
220
+ - **Category:** scope
221
+ - **Decided:**
222
+ - The `overview` extension keeps its management-space gate during this goal.
223
+ - With D-013, "management holds `read` over `*`" is exactly what `overview`
224
+ does. Porting it is a follow-up after M1 is live, not part of this goal.
225
+ - **Reasoning:** Migrating a working, live-verified capability inside the same
226
+ goal is risk without payoff.
227
+ - **Revisit if:** M1 ships. File the port as an idea then.
228
+ - **Used by:** —
229
+
230
+ ## D-010: Naming: "space link", table `space_links`, verb group `links`
231
+ - **Category:** pattern
232
+ - **Decided:**
233
+ - In prose the new concept is always a **space link**.
234
+ - Code uses `SpaceLink` / `space_links` / `mrctl links` / `mrctl --space`.
235
+ - The existing "conversation linked to a space" (`conversations.space_id`,
236
+ `/spaces link`) is unchanged.
237
+ - **Alternatives considered:**
238
+ - "Bridge" is taken by `src/bridges/`. "Relation" is a word nobody uses when
239
+ they talk about the feature.
240
+ - **Reasoning:** The owner and every idea doc already say "link". The collision
241
+ is managed by always qualifying the word.
242
+ - **Revisit if:** users confuse `/spaces link` with `mrctl links` in practice.
243
+ - **Used by:** all stories
244
+
245
+ ## D-011: Acting in a linked space is one middleware step, not a route per feature
246
+ - **Category:** architecture
247
+ - **Decided:**
248
+ - `mrctl --space <target> <any command>` sends `x-mercury-act-as: <target>`.
249
+ - The auth middleware in `src/core/api.ts`, after it has verified the caller
250
+ in the holder, runs the act-as step once:
251
+ 1. `links.act` in the holder, or 403;
252
+ 2. the target exists (`getSpace`, never `ensureSpace`), else the same 403;
253
+ 3. the method and route are on the `ACT_AS_ROUTES` allowlist, each entry
254
+ marked `read` or `admin`, else 400;
255
+ 4. `resolveLinkGrant(holder, target)` grants a level, else 403; a `read`
256
+ grant on an entry marked `admin` is also 403;
257
+ 5. the auth context becomes `{ callerId, spaceId: target, role, via }`,
258
+ where `role` is always `admin` and `via` carries the holder, the level
259
+ and the granting edge id;
260
+ 6. one audit row is written after the handler returns.
261
+ - Every existing route (`/messages`, `/prefs`, `/config`, `/tasks`, `/roles`,
262
+ `/permissions`, `/mutes`, `/model`, `/conversations`, `/spaces/current`,
263
+ `/control`, `/send`) then works against the target **unchanged**, because
264
+ they already read `auth.spaceId` and `checkPerm`.
265
+ - **Alternatives considered:**
266
+ - A `links read`, `links prefs`, … verb per capability (the first draft). Each
267
+ one re-implements an existing route and they drift.
268
+ - **Reasoning:** "Anything an admin of the target could do" is the existing API
269
+ surface evaluated in the target. Swapping the space once, at the boundary,
270
+ gets all of it with one check.
271
+ - **Revisit if:** a route needs to know it is being called across a link beyond
272
+ what `auth.via` tells it.
273
+ - **Used by:** act-as-linked-space, link-workspace-files, link-extension-actions
274
+
275
+ ## D-012: The target's context is never touched by a link; the holder's fills only on demand
276
+ - **Category:** architecture
277
+ - **Decided:**
278
+ - **Target side.** A link, and any read through it, leaves the target
279
+ byte-identical: no prompt block, no notice, no row in `messages` or `members`,
280
+ no container spawned. The act-as step never calls `resolveRole` on the
281
+ target, because that upserts a member row.
282
+ - What *does* change the target is only an explicit admin action the holder
283
+ asked for (a preference set, a task created, a message sent, a session
284
+ cleared). That is the point of `admin`, and each is audited.
285
+ - **Holder side.** Nothing about linked spaces is injected into the holder's
286
+ prompt automatically: no per-space list, no digest. One static paragraph in
287
+ the platform block, gated on `links.act`, says that other spaces may be
288
+ reachable, that `mrctl links list` shows which, and that `mrctl --space` is
289
+ used only when the user asks about another space. Content from a target
290
+ enters the holder's context only as the output of a command the agent chose
291
+ to run.
292
+ - **Alternatives considered:**
293
+ - The first draft's `<linked_spaces>` block and `inject.digest`. Dropped: a
294
+ management space linked to thirty spaces would carry thirty spaces of context
295
+ on every turn.
296
+ - **Reasoning:** Owner requirement, 2026-09-26.
297
+ - **Revisit if:** a small, opt-in per-link digest is wanted for one specific pair.
298
+ It would be a new level or flag, never a default.
299
+ - **Used by:** act-as-linked-space, link-workspace-files
300
+
301
+ ## D-013: A link target may be `*`, meaning every other space, present and future
302
+ - **Category:** scope
303
+ - **Decided:**
304
+ - `mrctl links create <holder> '*' --level admin` makes one edge that reaches
305
+ every space except the holder, including spaces created later.
306
+ - A wildcard edge takes no conversation filter.
307
+ - Every space's `links list` shows wildcard holders in its inbound reach.
308
+ - **Alternatives considered:**
309
+ - One edge per space. A new space would silently be outside management's reach
310
+ until someone remembered to link it.
311
+ - **Reasoning:** The owner's case is "from a management space … any other
312
+ space". The wildcard says that in one row, and it is visible from every
313
+ target.
314
+ - **Revisit if:** a deployment hosts spaces that must never be reachable even by
315
+ management. That would need a per-space `unlinkable` flag, which a wildcard
316
+ respects.
317
+ - **Used by:** space-link-graph-and-grant-resolver, mrctl-links-admin-commands
318
+
319
+ ## D-014: Content from a target is untrusted, and the holder acts only on its own user's request
320
+ - **Category:** pattern
321
+ - **Decided:**
322
+ - Act-as responses that carry third-party text (messages, file contents) are
323
+ passed through `stripInvisibleUnicode` and tagged with the source space.
324
+ - The platform paragraph (D-012) tells the agent that text read from another
325
+ space is data, never instructions, and that `admin` actions through a link
326
+ are taken only when the holder's own user asked for them.
327
+ - **Reasoning:** With `admin` over many spaces, a prompt injection planted in
328
+ one client's chat could try to steer the management agent into changing
329
+ another client's settings. Prose is not a guard, so this is also a residual:
330
+ the audit is how such an action would be found.
331
+ - **Revisit if:** the audit ever shows an act-as write that no human asked for.
332
+ The next step is refusing act-as writes from a run whose triggering message is
333
+ not the holder's own user.
334
+ - **Used by:** act-as-linked-space, link-workspace-files
335
+
336
+ ## D-015: Reading a period of a space's history is a read route, and summarizing is the holder's job
337
+ - **Category:** architecture
338
+ - **Decided:**
339
+ - A new route `GET /messages/recent?since=&until=&limit=&cursor=` lists a
340
+ space's messages in a time range, oldest first, with a cursor for the next
341
+ page. `mrctl messages recent --since 24h` calls it, and so does
342
+ `mrctl --space X messages recent --since 24h` through the act-as step
343
+ (marked `read` in `ACT_AS_ROUTES`).
344
+ - It serves the owner's "summarize everything from the past day in client X".
345
+ The management agent pages through the output and writes the summary. For a
346
+ long day it can hand the paging to a subagent, so the raw messages stay out
347
+ of the management conversation and only the summary comes back.
348
+ - The route is useful inside a space too ("what did we talk about
349
+ yesterday?"), so it is gated on its own `messages.read` permission
350
+ (default: admin), not on `compact`.
351
+ - **Alternatives considered:**
352
+ - Summarize on the host and return only the summary. That is a host-side
353
+ model call, which needs its own system prompt and model leg (the pi-spawn
354
+ rule in `CLAUDE.md`) and its own budget, for what the agent already does.
355
+ - Reuse `/messages/search` with an empty query. Search is newest-first,
356
+ capped at 200 and has no time range, so a day is not addressable with it.
357
+ - **Reasoning:** One read primitive the agent can page through covers "the past
358
+ day", "last week" and "since the last summary", and it adds nothing to the
359
+ holder's context until it is called (D-012).
360
+ - **Revisit if:** summaries of very busy spaces keep exhausting the holder's
361
+ context even with a subagent. A host-side digest per space per day would then
362
+ be the next step.
363
+ - **Used by:** messages-recent-range, act-as-linked-space
364
+
365
+ ## D-016: Asking as another space is one turn of that space's agent on a throwaway copy of its state
366
+ - **Category:** architecture
367
+ - **Decided:**
368
+ - `mrctl --space X ask "<message>" [--as member|admin]` runs one agent
369
+ turn **as space X would run it**: X's `AGENTS.md`, preferences, memory,
370
+ character, model, history and tools. The reply comes back to the holder as
371
+ command output. No conversation of X is involved and nothing is sent to
372
+ anyone.
373
+ - It serves two requests with one mechanism:
374
+ - **testing**: "how would X answer 'what are your opening hours?'";
375
+ - **delegating**: "act as if you're in X and search for leads for a story
376
+ about aliens". The turn really runs X's search with X's instructions and
377
+ tools, so the answer is the one X's bot would give, not the management
378
+ agent's imitation of it.
379
+ - The management agent passes the reply on as it came back, labelled with the
380
+ space. It does not rewrite it in its own voice (D-017).
381
+ - Read-only tools (web search, fetch, reading X's files) work in the turn.
382
+ Only writes and sends are held back, below.
383
+ - The turn runs on a **throwaway copy**: a temporary copy of X's workspace
384
+ and session, deleted after the turn. X's `messages`, session, `MEMORY.md`,
385
+ tasks and preferences are unchanged afterwards, whatever the turn tried.
386
+ - Inside the turn every `mrctl` write and every outbound send is
387
+ refused, and the refusals are listed in the output ("would have
388
+ sent", "would have set"). Extensions that act on an outside account (gws,
389
+ yahoo-mail, tradestation) get no credentials in the turn.
390
+ - The only input from the management space is the message itself. The
391
+ turn never sees the management conversation, and the management
392
+ agent sees only the reply and the refusal list.
393
+ - Nothing is kept in X by default: leads found by a delegated search are not
394
+ added to X's memory or files. Whether a `--keep` option should let the
395
+ turn's results persist in X is open (it would put the request into X's
396
+ record, which D-012 otherwise forbids).
397
+ - `ask` is marked `admin` in `ACT_AS_ROUTES`. It changes nothing, but it
398
+ runs X's agent and spends tokens on X's model leg.
399
+ - **Alternatives considered:**
400
+ - A test conversation linked to each client space. That is what the owner
401
+ asked to avoid, and a turn there does write to X's history and session.
402
+ - The rehearsal-bench shadow (`docs/goals/rehearsal-bench/`). It has full
403
+ fidelity (real ingress, triggers, a copy of the whole data dir), but it is
404
+ a second service started by an operator from a shell. It stays the tool for
405
+ testing a deploy or a trigger. `ask` is the everyday tool from chat.
406
+ - The management agent imitating X after reading X's `AGENTS.md`. It would
407
+ run on management's model, prompt and tools, so the answer would not be
408
+ X's, which is what the owner asked for ("genuinely give a response as if
409
+ the bot in that space was asked it").
410
+ - **Reasoning:** "Test the output it would give a client" and "act as if you're
411
+ in X" both need X's real context and tools and must leave X untouched. A copy
412
+ that is thrown away gives both, and the refusals make the "would have done"
413
+ side visible without doing it.
414
+ - **Revisit if:** an `ask` reply differs from the live reply to the same
415
+ message in a way that matters. The likely cause is ingress (triggers,
416
+ mentions), and the answer is the shadow, not a more faithful `ask`.
417
+ - **Used by:** link-ask-as-space
418
+
419
+ ## D-017: The owner speaks plain language; the agent picks the command
420
+ - **Category:** pattern
421
+ - **Decided:**
422
+ - `mrctl --space`, `messages recent` and `ask` are the agent's tools, not the
423
+ owner's interface. The owner says "what leads did you find in Phishkin's
424
+ space in the last 48 hours?" or "act as if you're in Phishkin's space and
425
+ …", and the agent chooses the command.
426
+ - The routing rules live in the one platform paragraph gated on `links.act`
427
+ (D-012): resolve a named space with `mrctl links list` (tolerating
428
+ misspellings, asking when two match); answer "what happened in X" from X's
429
+ record, stating the space and period covered; answer "act as X" with
430
+ `ask` and pass the reply on unchanged.
431
+ - Every milestone checkpoint is run with plain-language messages only, and
432
+ the trace is read to confirm which command the agent chose.
433
+ - **Alternatives considered:**
434
+ - A chat slash command per operation. The owner asked for plain language.
435
+ - Injecting the list of reachable spaces into the prompt so names resolve
436
+ without a call. Rejected by D-012: it grows with every linked space.
437
+ - **Reasoning:** An accurate answer to "what did X find" depends on reading X's
438
+ record, not on the management agent's recollection; the paragraph tells it
439
+ where each kind of record lives.
440
+ - **Revisit if:** live checks show the agent answering about another space
441
+ without running a command (guessing), or picking the wrong space. The next
442
+ step is a skill with worked examples.
443
+ - **Used by:** act-as-linked-space, messages-recent-range, link-ask-as-space