mercury-agent 0.24.1 → 0.24.2-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 (36) hide show
  1. package/docs/extensions.md +1 -0
  2. package/docs/goals/conversation-review/decisions.md +352 -0
  3. package/docs/goals/conversation-review/goal.md +150 -0
  4. package/docs/goals/conversation-review/roadmap.md +94 -0
  5. package/docs/goals/space-links/decisions.md +443 -0
  6. package/docs/goals/space-links/goal.md +141 -0
  7. package/docs/goals/space-links/roadmap.md +129 -0
  8. package/docs/pending-verification.md +14 -1
  9. package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/plan.md +2 -2
  10. package/examples/extensions/office-365-mail/README.md +3 -2
  11. package/examples/extensions/office-365-mail/index.ts +35 -1
  12. package/examples/extensions/office-365-mail/lib/graph.ts +158 -11
  13. package/examples/extensions/office-365-mail/lib/save.ts +140 -0
  14. package/examples/extensions/office-365-mail/skill/SKILL.md +15 -1
  15. package/examples/extensions/voice-transcribe/index.ts +192 -50
  16. package/examples/extensions/voice-transcribe/skill/SKILL.md +2 -0
  17. package/package.json +1 -1
  18. package/src/core/runtime.ts +260 -53
  19. package/src/extensions/types.ts +14 -0
  20. package/src/storage/db.ts +50 -0
  21. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/briefs/S1.md +0 -0
  22. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/briefs/S2.md +0 -0
  23. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/briefs/S3.md +0 -0
  24. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/briefs/S4.md +0 -0
  25. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/briefs/S5.md +0 -0
  26. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/briefs/S6.md +0 -0
  27. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/briefs/S7.md +0 -0
  28. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/briefs/S8.md +0 -0
  29. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/decisions.md +0 -0
  30. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/sessions/S1.md +0 -0
  31. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/sessions/S2.md +0 -0
  32. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/sessions/S3.md +0 -0
  33. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/sessions/S4.md +0 -0
  34. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/sessions/S5.md +0 -0
  35. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/sessions/S6.md +0 -0
  36. /package/docs/plans/{p0-plan-0922 → archive/2026-09-28-p0-plan-0922}/sessions/S7.md +0 -0
@@ -174,6 +174,7 @@ Both `workspace_init` and `before_container` events include:
174
174
 
175
175
  `before_container` also includes:
176
176
  - `attachments` — incoming `MessageAttachment[]` (e.g. voice, images), if any
177
+ - `ambientAttachments` — files overheard in this conversation since the bot last ran in it (a group voice note sent without a mention), each with `authorId` / `authorName`. Only authors whose role holds `media.receive`, only files still on disk, at most 10 rows, never on scheduled runs. Handed over once: the run saves its user turn, and the next run looks only past it — including when the handler failed, so a failed transcription is not retried.
177
178
 
178
179
  #### `before_container` mutations
179
180
 
@@ -0,0 +1,352 @@
1
+ # Decisions: Conversation Review
2
+
3
+ **Goal**: [conversation-review](goal.md)
4
+
5
+ ## D-001: The scan is a host-side Mercury extension, `conversation-review`, running on each bot
6
+ - **Category:** architecture
7
+ - **Decided:**
8
+ - New directory `examples/extensions/conversation-review/`, host-side only. It
9
+ has no CLI, no skill, no permission and adds nothing to the agent image.
10
+ - It borrows two existing patterns:
11
+ - from **napkin**: `mercury.job` scheduling, a throttle kept in
12
+ `mercury.store`, and a read-only `bun:sqlite` open of `state.db`;
13
+ - from **longview**: a `pi` spawn with no tools.
14
+ - Both bots run the same code. The local bot gets it from the installed
15
+ snapshot. Tagula gets it through the release.
16
+ - **Alternatives considered:**
17
+ - **A laptop-side scanner that pulls `state.db` from both boxes.** Raw client
18
+ conversations would leave Hetzner, and the Tagula runbooks already exclude
19
+ `state.db` from backups for exactly that reason.
20
+ - **A scheduled agent task** (a `tasks` row with a review prompt). The agent
21
+ would then read other spaces' messages from inside a container, which needs
22
+ cross-space read authority that does not exist until `space-links`. Its
23
+ output also becomes a chat message, not a file.
24
+ - **Core code in `src/`.** Every deployment would pay for it. An extension can
25
+ be off by default.
26
+ - **Reasoning:** Data stays where it lives, one code path serves both bots, and
27
+ napkin already proves the shape: an hourly job, a read-only DB and a pi spawn
28
+ on the owner's subscription.
29
+ - **Revisit if:** `space-links` ships a management space with cross-space read on
30
+ Tagula. The weekly deep pass could then run as a management-space task that
31
+ can take follow-up questions.
32
+ - **Used by:** all stories.
33
+
34
+ ## D-002: A shared identifier redactor with a fail-closed gate, in core
35
+ - **Category:** pattern
36
+ - **Decided:**
37
+ - `src/core/redact-identifiers.ts` exports `redactIdentifiers(text, known)` and
38
+ `assertNoIdentifiers(text)`.
39
+ - It replaces:
40
+ - phone numbers (international and Israeli local forms)
41
+ - WhatsApp JIDs and LIDs (`…@s.whatsapp.net`, `…@lid`, `…@g.us`)
42
+ - e-mail addresses
43
+ - every `author_name` and `display_name` the scan saw in the window, and
44
+ `space_roles.display_name`
45
+ - Each replacement is a stable per-report tag (`[member-3]`, `[phone-1]`), so
46
+ one person reads consistently within a report. Authors become their role.
47
+ - The gate runs on the final assembled report string and throws if any
48
+ identifier survives. The report is then not written, and the DM says why.
49
+ - **Alternatives considered:**
50
+ - **Redact per field.** CLAUDE.md warns against exactly this: each field fixed
51
+ in turn, and the next one missed.
52
+ - **Paraphrase only.** Rejected by the owner, 2026-09-28. Evidence is what
53
+ makes a finding actionable.
54
+ - **Reuse `redactTokens`.** It covers secrets, not people.
55
+ - **Reasoning:** "Prefer one pass at the join." The gate on the final string
56
+ cannot miss a site. Phone numbers also appear inside message bodies, not just
57
+ in columns (Tagula INBOX thread, 2026-08-28), so a column-level redactor is not
58
+ enough. It lives in core because `secret-redaction-at-ingress` and
59
+ `mercury-triage` will want the same function.
60
+ - **Revisit if:** a report needs a real identifier to be actionable (for example
61
+ "this member's LID never resolves"). The fix is a local lookup table beside the
62
+ report on the box, never an identifier in the committed file.
63
+ - **Used by:** redact-identifiers-with-gate, conversation-review-report-and-digest,
64
+ review-intake-to-repo.
65
+
66
+ ## D-003: The daily judge runs on a cheap model on the chain's provider; the weekly pass runs on the strong leg
67
+ - **Category:** trade-off
68
+ - **Decided:**
69
+ - **Daily.** The daily judge spawns pi on the provider of the space's
70
+ resolved leg (`resolveSpaceModelLeg`), the same one it would use anyway,
71
+ with an explicit model from `conversation-review.judge_model`. The default is
72
+ the cheap tier (`claude-haiku-4-5`). Credentials come from the leg's provider,
73
+ exactly as longview gets them.
74
+ - **Weekly.** The weekly synthesis runs on the space's resolved leg unchanged,
75
+ which is the strong tier.
76
+ - Both spawns get a row in `tests/inventories/pi-spawns.ts`. The row records
77
+ the explicit model override and its reason.
78
+ - **Alternatives considered:**
79
+ - **The strong leg for every daily exchange.** At the owner's volume that is
80
+ Opus on every exchange every day, which is the cost shape
81
+ `feed-watch-triaged-roundup` exists to remove.
82
+ - **A heuristic pre-filter.** Rejected by the owner: the judge reads every
83
+ exchange.
84
+ - **Reasoning:** The 2026-09-07 owner decision for the `explore` subagent set the
85
+ precedent: a cheap tier as an explicit `provider` + `model` pair on the same
86
+ subscription. The house rule forbids pi's default preamble and a foreign
87
+ credential. It does not forbid a cheaper model on the same provider.
88
+ - **Revisit if:** the weekly spot check (D-006) finds that the cheap judge misses
89
+ real problems at a rate the owner won't accept. Then raise
90
+ `judge_model`, which is a config change.
91
+ - **Used by:** conversation-review-daily-judge,
92
+ conversation-review-weekly-synthesis.
93
+
94
+ ## D-004: The unit of judgement is the exchange: user turns, then the bot's reply, plus context before them
95
+ - **Category:** architecture
96
+ - **Decided:**
97
+ - An **exchange** is one or more consecutive `role='user'` rows in one
98
+ `conversation`, followed by the `role='assistant'` rows up to the next user
99
+ turn.
100
+ - The judge also sees up to N earlier rows as context. The default is 6,
101
+ capped by characters.
102
+ - Rows with `source='scheduler'` (task output) are judged as **task
103
+ exchanges**, where the prompt is the task, not a user.
104
+ - `ambient` rows are context only, never judged.
105
+ - `tool` rows are dropped.
106
+ - Exchanges are **batched per conversation** into one judge spawn, up to a
107
+ character budget, so a space with 40 exchanges costs a few spawns rather than
108
+ 40.
109
+ - **Alternatives considered:**
110
+ - **Whole-day conversation per spawn.** It overflows the argv cap
111
+ (`MAX_ARG_STRLEN`) and the judge loses precision.
112
+ - **One spawn per exchange.** Cost and wall clock scale with volume.
113
+ - **Reasoning:** The 48 h audit cited findings as message `#ids` inside
114
+ exchanges. Batching keeps cost linear in text size, not in message count.
115
+ - **Revisit if:** trace linking lands (`trace-records-system-prompt`, M4). The
116
+ unit could then include the run's tool calls.
117
+ - **Used by:** review-exchange-reader, conversation-review-daily-judge.
118
+
119
+ ## D-005: The finding is a fixed JSON schema; the report is Markdown rendered from it
120
+ - **Category:** pattern
121
+ - **Decided:**
122
+ - The judge returns JSON: `{findings: Finding[]}`. The host validates it and
123
+ drops anything invalid, counting and reporting the drops.
124
+ - `Finding` fields:
125
+ - `kind`: `bug | feature | profile | config`
126
+ - `scope`: `global | space`
127
+ - `target_layer`: one of the named layers in `docs/behavior-layers.md` §3, a
128
+ closed enum copied into the prompt
129
+ - `severity`: `high | moderate | minor`
130
+ - `confidence`: 0–1
131
+ - `message_ids`: must exist in the batch; the host checks this
132
+ - `what_went_wrong`
133
+ - `proposed_improvement`
134
+ - `fingerprint`: normalized kind + target_layer + a short slug, used to find
135
+ duplicates across days
136
+ - The host renders `report.md` and writes `report.json` beside it. Intake reads
137
+ the JSON.
138
+ - **Alternatives considered:**
139
+ - **Free-text reports.** Nothing can be deduplicated or promoted in one step.
140
+ - **Reasoning:** The finding already had this shape in the 72 h review. The
141
+ closed layer enum is what makes "global or space-specific" something the host
142
+ checks, not prose.
143
+ - **Revisit if:** the layer set in `behavior-layers.md` §3 changes. The enum is
144
+ pinned by a test against that file (one contract, one statement).
145
+ - **Used by:** conversation-review-daily-judge,
146
+ conversation-review-report-and-digest, review-intake-to-repo,
147
+ conversation-review-weekly-synthesis.
148
+
149
+ ## D-006: The weekly pass synthesizes the daily findings and spot-checks unflagged exchanges
150
+ - **Category:** scope
151
+ - **Decided:**
152
+ - The weekly job reads the last 7 daily `report.json` files, not the raw week.
153
+ - It clusters findings by fingerprint and meaning across spaces, and promotes
154
+ recurring space-level findings to global candidates.
155
+ - It re-judges a random sample of exchanges the daily judge passed, on the
156
+ strong leg. The default sample is 20 per bot.
157
+ - It reports the miss rate as the daily judge's measured recall.
158
+ - **Alternatives considered:**
159
+ - **A strong-model re-read of the whole week.** It duplicates the daily cost
160
+ at Opus prices.
161
+ - **Reasoning:** Synthesis is where "global vs space" becomes visible. The same
162
+ failure seen in three spaces is a code or global-profile finding, not three
163
+ `AGENTS.md` lines. The spot check turns "the cheap judge is good enough" into a
164
+ number that is re-measured every week.
165
+ - **Revisit if:** recall stays near 100% for a month. Shrink the sample.
166
+ - **Used by:** conversation-review-weekly-synthesis.
167
+
168
+ ## D-007: Reports reach the repo by a pull on this machine, run by the owner (`/f-review-intake`)
169
+ - **Category:** architecture
170
+ - **Decided:**
171
+ - Each bot writes `<dataDir>/reviews/<YYYY-MM-DD>-{daily|weekly}/report.{md,json}`
172
+ and keeps 60 days.
173
+ - `scripts/review-intake.ts` on this machine:
174
+ 1. reads the local bot's directory directly;
175
+ 2. copies Tagula's over `ssh root@<box>` (`scp` of the reviews directory
176
+ only);
177
+ 3. re-runs `assertNoIdentifiers` on every file;
178
+ 4. writes `docs/reviews/<date>-<bot>-<kind>.md`;
179
+ 5. emits an index of candidate docs and the ledger for triage (D-011).
180
+ - The `/f-review-intake` command wraps the script and triages every finding
181
+ against what is already documented or landed, then proposes an action per
182
+ finding for the owner to approve ([D-011](#d-011-intake-relates-every-finding-to-existing-work-before-deciding-what-to-do-with-it)).
183
+ - Nothing is committed or pushed automatically.
184
+ - **Alternatives considered:**
185
+ - **The bot commits to the repo.** It would need a write-capable credential on
186
+ a client box, and it would race live sessions in the shared checkout
187
+ (CLAUDE.local.md: "staging in the shared checkout is a race").
188
+ - **Tagula's `/api/console/*` control plane.** It is authenticated with the box
189
+ secret and has no reviews route. Add a route only if SSH access goes away.
190
+ - **Reasoning:** Human in the loop is the stated scope. The DM is the trigger,
191
+ and intake is where review happens. SSH to the box is already how operators
192
+ work there (tagula-agents runbooks).
193
+ - **Revisit if:** detection is trusted enough to automate filing (a later goal).
194
+ - **Used by:** review-intake-to-repo, conversation-review-on-tagula.
195
+
196
+ ## D-008: Scheduling is a cron with an explicit time zone, off by default, one `conversation-review.enabled` switch per bot
197
+ - **Category:** pattern
198
+ - **Decided:**
199
+ - The jobs are hourly `interval` ticks that check a due-time computed in
200
+ `conversation-review.timezone` (default `Asia/Jerusalem`). The defaults are
201
+ daily at 04:00 and weekly on Sunday at 05:00.
202
+ - The throttle is kept in `mercury.store`. Missed runs catch up once, never
203
+ several times.
204
+ - The extension is **off** until `enabled=true`. Spaces can be excluded with
205
+ `conversation-review.exclude` (per space).
206
+ - The owner DM target is `conversation-review.digest_to`, a caller id resolved
207
+ by `ctx.send`.
208
+ - **Alternatives considered:**
209
+ - **`mercury.job({cron})`.** It is parsed with no time zone (`jobs.ts:114`),
210
+ so it runs in the host's local time, and the Hetzner box is probably UTC.
211
+ - **Reasoning:** Napkin's interval + throttle shape already survives restarts and
212
+ missed ticks. A fixed hour in a named time zone reads the same on both boxes.
213
+ - **Revisit if:** `mercury.job` gains a time zone.
214
+ - **Used by:** conversation-review-daily-judge,
215
+ conversation-review-weekly-synthesis, conversation-review-on-tagula.
216
+
217
+ ## D-009: Client spaces on Tagula are opt-in per bot, and client consent is an open owner question
218
+ - **Category:** scope
219
+ - **Decided:**
220
+ - On Tagula, `enabled=true` covers the owner's own spaces.
221
+ - Client spaces are scanned only when `conversation-review.include_clients=true`,
222
+ set by the owner after the consent question is answered.
223
+ - The local bot has no client spaces.
224
+ - **Alternatives considered:**
225
+ - **Scan everything from day one.** Nothing on disk records client consent to
226
+ AI review (tagula-agents survey, 2026-09-28).
227
+ - **Reasoning:** Excerpts of client conversations are the owner's decision to
228
+ expose, even redacted. That decision should be made once, explicitly, not by
229
+ deploying a release.
230
+ - **Open question (owner):** do the Tagula terms, or each client, need to allow
231
+ automated quality review of conversations before `include_clients` is turned
232
+ on?
233
+ - **Used by:** conversation-review-on-tagula.
234
+
235
+ ## D-010: The daily judge is a screener; a finding is a hypothesis until intake verifies it
236
+ - **Category:** trade-off
237
+ - **Decided:** (owner question, 2026-09-28: "what if the weak model makes a
238
+ mistake?")
239
+ - Nothing the cheap judge says is acted on directly. Its finding is a
240
+ *hypothesis with evidence*; the verdict belongs to intake (D-011), which runs
241
+ on a strong model with the repo in hand, and to the owner.
242
+ - Every kind of judge mistake has a named catcher:
243
+
244
+ | Mistake | Caught by | When |
245
+ |---|---|---|
246
+ | Output that isn't valid JSON, or a missing or out-of-range field | The host validator drops it and counts the drop | Daily |
247
+ | Invented message ids | Checked against the batch; dropped as `badIds` | Daily |
248
+ | Misquoted or invented evidence | The excerpt is read from `state.db` by message id, never taken from the judge's text | Daily |
249
+ | Flags an answer that was actually fine | Intake re-judges the excerpt first. A rejection goes into the ledger, the same fingerprint is suppressed afterwards, and it becomes a counter-example (M3.2) | Intake |
250
+ | Wrong kind, layer, scope or proposed fix | Treated as a hint only. Intake re-derives the layer by reading the real layer: `behavior-layers.md`, the space's profile, the code | Intake |
251
+ | Misses a bad answer | The weekly strong-model spot check measures recall (D-006). If recall is low, raise `judge_model` (D-003) | Weekly |
252
+ | Obeys an instruction written inside a conversation | The judge has no tools. The conversation is framed in the prompt as quoted data. Output is schema-validated and length-capped, and intake treats finding text as untrusted | Daily and intake |
253
+ | Wrong ranking of the digest's top three | The digest labels findings *unverified*. The DM is a pointer, not a verdict | Daily |
254
+
255
+ - The judge prompt leans toward **recall**. A false positive costs one line of
256
+ triage, but a miss is invisible until the weekly spot check. Noise is bounded
257
+ by a per-batch findings cap instead.
258
+ - **Alternatives considered:**
259
+ - **A strong-model second opinion on every finding before the report is
260
+ written.** This duplicates intake's check without intake's repo context:
261
+ it can't see whether the behaviour is a documented decision, already fixed,
262
+ or a known bug. It also adds strong-leg spend on the box every day.
263
+ - **A confidence threshold that drops low-confidence findings.** The cheap
264
+ model's confidence is not calibrated, so a threshold would hide misses
265
+ rather than remove noise. Confidence is shown in the report and used only
266
+ for ordering.
267
+ - **Reasoning:** Evidence that comes from the DB can't be wrong the way a claim
268
+ can. Classification is cheap to re-derive when you have the repo. Recall is
269
+ the only error that no reader downstream can see, so recall is the one that
270
+ gets a scheduled measurement.
271
+ - **Revisit if:** precision measured at intake (M3.2) stays below about 50% for
272
+ two weeks. The owner's triage time is then the cost to cut, so tighten the
273
+ prompt or add counter-examples before anything else.
274
+ - **Used by:** conversation-review-daily-judge,
275
+ conversation-review-report-and-digest, review-intake-to-repo,
276
+ conversation-review-weekly-synthesis, review-verdict-feedback.
277
+
278
+ ## D-011: Intake relates every finding to existing work before deciding what to do with it
279
+ - **Category:** architecture
280
+ - **Decided:** (owner, 2026-09-28)
281
+ - `/f-review-intake` never files a finding as new work straight from the
282
+ report. Each finding goes through three steps, in this order:
283
+ 1. **Verify.** Re-read the redacted excerpt and decide whether the answer
284
+ really was poor. If it wasn't, the action is `reject` and steps 2 and 3
285
+ are skipped.
286
+ 2. **Relate.** Search everything already documented or landed, and cite each
287
+ related item by path:
288
+ - open work: `docs/ideas/`, `docs/backlog/`, `docs/in-progress/`,
289
+ `docs/bugs/`, goal roadmaps' stories, `docs/refactor/`;
290
+ - landed work: `docs/archive/` and `docs/debug/**` post-mortems, including
291
+ their Residuals sections, plus `git log` for the area;
292
+ - decisions: goal `decisions.md` files, owner decisions in `CLAUDE.md` and
293
+ `CLAUDE.local.md`, and documented out-of-scope or residual lines;
294
+ - earlier reviews: the ledger `docs/reviews/ledger.json`;
295
+ - pending checks: `docs/pending-verification.md`.
296
+ 3. **Decide.** Assess whether the finding is worth working on, then pick
297
+ exactly one action from a closed set:
298
+
299
+ | Action | When | What intake does after the owner approves |
300
+ |---|---|---|
301
+ | `reject` | The answer was fine (a judge error), or the problem is real but not worth the cost | Ledger entry with the reason. The fingerprint is suppressed |
302
+ | `known-limitation` | It matches a documented decision, out-of-scope line or accepted residual | Ledger entry linking that line. If the new evidence changes that decision's cost, the action becomes `needs-owner` instead |
303
+ | `duplicate` | The ledger already holds the same finding | Increment the seen count, then follow the earlier decision. A finding rejected earlier stays suppressed unless the evidence is new |
304
+ | `attach` | An open item already covers it: an idea, backlog or in-progress item, a bug, or a goal story | Append an "Evidence from conversation review" entry to that doc. The recurrence is priority signal |
305
+ | `fixed-not-deployed` | A landed change covers it, but the bot that produced the report runs a version that doesn't contain it | Ledger entry. Add or confirm a MANUAL-VERIFY line to re-check after the next deploy of that bot |
306
+ | `revisit` | A landed change covers it, **and** the bot's running version contains it | A new bug that links the closed item: "the fix for X does not cover this case", or a regression. If it's unclear whether the fix is even active, add a MANUAL-VERIFY re-check instead |
307
+ | `new` | Nothing related exists | Prefill `/f-bug-report` or `/f-feature-idea`, or propose a profile or config edit plus a MANUAL-VERIFY line |
308
+ | `needs-owner` | It conflicts with an owner decision, touches consent or scope, or the value call is close | Nothing. It is listed for the owner |
309
+
310
+ - **Worth working on** is judged on these points, and each one appears in the
311
+ triage row:
312
+ - severity;
313
+ - recurrence: days, spaces, and both bots;
314
+ - who was affected (a paying client over an owner test space);
315
+ - breadth of impact on Mercury over a single personal space (owner priority
316
+ of 2026-09-22);
317
+ - the cost of the target layer (an `AGENTS.md` line versus a code change);
318
+ - fit with `docs/ROADMAP.md`.
319
+ - The skill proposes a triage table with one row per finding: verdict,
320
+ related items with paths, action, value note. The owner approves, edits or
321
+ overrides each row, and only then does the skill apply the approved actions.
322
+ Every decision, approved or overridden, lands in the ledger.
323
+ - The split of work:
324
+ - `scripts/review-intake.ts` is deterministic. It pulls, re-gates, writes
325
+ `docs/reviews/`, and emits an index of candidate documents (path, title,
326
+ stage, status) plus the ledger.
327
+ - Matching and judgement are done by the skill, because similarity by
328
+ meaning is not a grep.
329
+ - The ledger also records the aliases of each canonical fingerprint, because
330
+ the cheap judge will slug the same problem differently on different days.
331
+ - `revisit` needs the bot's running version: every report records it (M1.4).
332
+ - **Alternatives considered:**
333
+ - **Exact-fingerprint dedupe only** (the original D-007 step 5). It misses the
334
+ same problem under a different slug, and it cannot tell that a finding is
335
+ evidence for an open idea, a known limitation, or a regression of a fix.
336
+ - **Letting the script auto-file `new` findings.** That breaks the
337
+ human-in-the-loop scope, and it is exactly where a judge's false positive
338
+ would turn into backlog noise.
339
+ - **Reasoning:** The value of a finding depends on what already exists. The same
340
+ excerpt can be:
341
+ - noise, when it restates an accepted limitation;
342
+ - priority signal, as the third occurrence of an open idea;
343
+ - the most valuable kind of finding, when it shows that a landed fix doesn't
344
+ work.
345
+
346
+ Only a pass that has read the corpus can tell these apart, and the planning
347
+ docs are the corpus.
348
+ - **Revisit if:** the ledger shows the owner accepting the proposed action over
349
+ about 90% of the time for a month. Automating `duplicate` and
350
+ `known-limitation` is then the next step, in a later goal.
351
+ - **Used by:** review-intake-to-repo, review-verdict-feedback,
352
+ conversation-review-report-and-digest.
@@ -0,0 +1,150 @@
1
+ # Conversation Review
2
+
3
+ **Status**: Active
4
+ **Slug**: conversation-review
5
+ **Created**: 2026-09-28
6
+ **Last updated**: 2026-09-28
7
+
8
+ ---
9
+
10
+ ## Goal Statement
11
+
12
+ Scan the conversation logs of every space on both of the owner's bots on a
13
+ schedule. The two bots are the local live bot (`~/whatsapp-bot`, the owner's
14
+ personal bot) and the Tagula bot on the Hetzner server (client spaces; see the
15
+ `tagula-agents` repo). Each scan produces a report of **poor bot answers that
16
+ could be significantly improved**, with a proposed kind of fix for each one:
17
+
18
+ - a **bug** in Mercury code
19
+ - a **feature** Mercury does not have yet
20
+ - a **profile/character change**, such as an `AGENTS.md` line, a preference, a
21
+ task prompt or a skill
22
+
23
+ Each finding also says whether the change is **global** or **specific to one
24
+ space**.
25
+
26
+ The end state is a system that keeps improving Mercury on its own. This goal
27
+ automates only the **detection** step. A human still reviews every finding and
28
+ still does every implementation. Later milestones add more sophisticated scan
29
+ features.
30
+
31
+ Owner's words (2026-09-28): *"the scan should detect poor answers by the bot
32
+ that can be significantly improved either by some fix or by a new feature for
33
+ mercury. can be either code modifications or profile/character modifications,
34
+ global or space specific. the end goal is to have an automated system that keeps
35
+ improving mercury. as mentioned, at this point in time I first want to automate
36
+ the detection of such improvements - review and implementation will still
37
+ require a human in the loop."*
38
+
39
+ This automates the manual reviews already done by hand:
40
+
41
+ - `docs/notes/football-group-48h-audit.md`: 128 rows read one by one, producing
42
+ findings F1–F8.
43
+ - The 72 h log review of 2026-09-16. It left about ten `docs/ideas/*` files,
44
+ four `AGENTS.md` sentences applied live, and MANUAL-VERIFY lines, but no
45
+ single note of its own.
46
+
47
+ The triage those reviews used becomes the report's schema:
48
+
49
+ | Kind of finding | Where it goes |
50
+ |---|---|
51
+ | Bug | `docs/bugs/` |
52
+ | Feature | `docs/ideas/` |
53
+ | Profile change | a layer named from `docs/behavior-layers.md` §3 |
54
+
55
+ ## Constraints
56
+
57
+ - **Detection only.** The scan never changes a profile or a config, never files
58
+ a doc and never opens a PR. It writes a report and sends a DM, nothing else.
59
+ - **Raw conversations never leave their box.** The scan runs on each bot, next
60
+ to that bot's `state.db`. Only the redacted report crosses to this machine.
61
+ - **Redaction is enforced, not just intended.** Before a report file is written:
62
+ - phone numbers, JIDs, LIDs, e-mail addresses and member names are replaced;
63
+ - authors appear only as `owner` / `admin` / `member` / `bot`;
64
+ - excerpts are capped in length.
65
+
66
+ A gate aborts the write if any identifier survives. This follows the tagula-agents
67
+ `scripts/export-space-messages.ts` precedent.
68
+ - **Client spaces:** a report may quote short excerpts with identifiers redacted
69
+ (owner, 2026-09-28). The client-consent question is open, see Open Questions
70
+ in [decisions.md](decisions.md) D-009.
71
+ - **Subscription OAuth only.** No Anthropic API key is used
72
+ (`no-api-key-model-billing`).
73
+ - **Every scan is bounded, and the bound can be read.** There is a cap on the
74
+ exchanges judged and on the wall clock per scan and per spawn. Anything skipped
75
+ is stated in the report, never dropped silently (CLAUDE.md: "a limit the actor
76
+ cannot read is a trap").
77
+ - **Every pi spawn follows the house rule.** It uses `--system-prompt`, runs with
78
+ no tools, and has a row in `tests/inventories/pi-spawns.ts`.
79
+ - **The bot is never disturbed.** The scan reads `state.db` read-only and never
80
+ through `Db`, because `Db` runs migrations. It never blocks the host thread.
81
+ - **Tagula runs released Mercury.** The Hetzner box installs the `mercury-agent`
82
+ npm package and image and updates nightly to `:latest`. Code reaches it only
83
+ through a release, never through `~/sync-mercury.sh`.
84
+
85
+ ## Preferences
86
+
87
+ - Findings land in two places (owner, 2026-09-28):
88
+ - a report file in the mercury repo, under `docs/reviews/`, shaped so that
89
+ `/f-bug-report`, `/f-feature-idea` or a profile edit can promote any finding
90
+ in one step (required);
91
+ - a WhatsApp DM digest to the owner, a short summary pointing at the report
92
+ (required).
93
+ - An LLM judge reads **every** exchange. Heuristics are not used as a pre-filter
94
+ (owner, 2026-09-28) (required).
95
+ - There are two scans (owner, 2026-09-28) (required):
96
+ - a **light daily** pass: a cheap-tier judge over the last 24 h;
97
+ - a **deep weekly** pass: strong-tier synthesis over the week's findings, plus
98
+ a spot check of exchanges the daily judge did not flag.
99
+ - Intake checks each finding against what is already documented or landed, and
100
+ decides from that and from whether the finding is worth the work. The options
101
+ are:
102
+ - merge it into an open item;
103
+ - file it as new;
104
+ - revisit a closed item to check that it works;
105
+ - drop it.
106
+
107
+ (owner, 2026-09-28) (required → [D-011](decisions.md))
108
+ - It is built as a Mercury extension, in the style of napkin and longview, so both
109
+ bots run the same code (you decide → [D-001](decisions.md)).
110
+
111
+ ## Target Users
112
+
113
+ - **The owner, as reviewer.** They get a DM each day and read a report that lists
114
+ the improvements worth making, each with evidence and a suggested target layer.
115
+ They then promote, reject or ignore each finding. (inferred)
116
+ - **Future Claude sessions, as implementers.** They read a promoted finding the
117
+ same way they read `/f-bug-report` or `/f-feature-idea` output. (inferred)
118
+
119
+ ## Success Criteria
120
+
121
+ 1. On each bot, a daily scan runs unattended. It writes a report covering every
122
+ space with user exchanges in the window, and sends the owner a DM digest.
123
+ 2. Every finding carries the following, and a report missing any field fails
124
+ validation:
125
+ - kind (bug / feature / profile / config)
126
+ - scope (global / space)
127
+ - the target layer from `behavior-layers.md` §3
128
+ - severity and confidence
129
+ - message ids
130
+ - a redacted excerpt
131
+ - what went wrong and the proposed improvement
132
+ 3. A committed report contains no phone number, JID, LID, e-mail address or member
133
+ name. This is proven by the redaction gate and by a test that plants each form.
134
+ 4. A seeded bad answer is flagged by the next daily scan in a test space on the
135
+ local bot. The weekly report lists recurring issues across spaces with their
136
+ frequency.
137
+ 5. One command (`/f-review-intake`) brings both bots' reports into
138
+ `docs/reviews/` and triages every finding before any of it becomes work
139
+ ([D-011](decisions.md)):
140
+ - it verifies the finding;
141
+ - it relates it to documented and landed items, citing paths;
142
+ - it proposes an action: reject, known limitation, duplicate, attach to an
143
+ open item, fixed but not deployed, revisit a landed item, new, or needs
144
+ owner;
145
+ - it applies only the actions the owner approves.
146
+ 6. A judge mistake never becomes work unreviewed ([D-010](decisions.md)):
147
+ - evidence is read from the DB, never from the judge's text;
148
+ - classification is re-derived at intake;
149
+ - rejected false positives are not re-raised;
150
+ - recall is measured weekly.
@@ -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.