mercury-agent 0.23.0-beta.2 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/goals/rehearsal-bench/roadmap.md +78 -3
- package/docs/goals/release-gate/roadmap.md +2 -2
- package/docs/pending-verification.md +190 -114
- package/docs/plans/bench-patterns-0915/briefs/S4.md +55 -0
- package/docs/plans/bench-patterns-0915/briefs/S5.md +54 -0
- package/docs/plans/bench-patterns-0915/briefs/S6.md +54 -0
- package/docs/plans/bench-patterns-0915/briefs/S7.md +54 -0
- package/docs/plans/bench-patterns-0915/briefs/S8.md +54 -0
- package/docs/plans/bench-patterns-0915/decisions.md +13 -0
- package/docs/plans/bench-patterns-0915/plan.md +107 -0
- package/docs/plans/bugs-0915/briefs/S1.md +61 -0
- package/docs/plans/bugs-0915/briefs/S2.md +63 -0
- package/docs/plans/bugs-0915/briefs/S3.md +67 -0
- package/docs/plans/bugs-0915/briefs/S4.md +64 -0
- package/docs/plans/bugs-0915/decisions.md +10 -0
- package/docs/plans/bugs-0915/plan.md +121 -0
- package/examples/profiles/tel-aviv-flat-search/AGENTS.md +319 -0
- package/examples/profiles/tel-aviv-flat-search/README.md +166 -0
- package/examples/profiles/tel-aviv-flat-search/config.yaml +84 -0
- package/examples/profiles/tel-aviv-flat-search/seed/MEMORY.md +41 -0
- package/examples/profiles/tel-aviv-flat-search/seed/napkin-distill.md +27 -0
- package/examples/profiles/tel-aviv-flat-search/seed/viewing-checklist.md +34 -0
- package/examples/profiles/tel-aviv-flat-search/tasks/morning-status.md +37 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Roadmap: Rehearsal Bench
|
|
2
2
|
|
|
3
3
|
**Goal**: [rehearsal-bench](goal.md)
|
|
4
|
-
**Last updated**: 2026-09-09
|
|
4
|
+
**Last updated**: 2026-09-15 (§ *What the pending list says the bench still lacks* added after the beta.2 sweep — the 55 open manual checks sorted by what would take each one over, and two stories cut from it: M1.7 `shadow-feed-fixtures` and M1.3c `shadow-own-credential`. Earlier: 2026-09-09 M2.2 **split** into M2.2 `simulate-ingress`, the function half, and M2.2b `shadow-simulate-route`, the HTTP face — after an adversarial review of the M2.2 plan found three things the row asserted but the code does not support, and a route auth gap: the shadow inherits the **live** API secret, so M2.2b owns a new `MERCURY_SHADOW_API_SECRET`. M2.3 now depends on M2.2b; `release-gate` R3 still waits on M2.2 alone. Earlier: 2026-09-06 (M4.2 `test-bot-instance` added as an owner's-decision story after the "why not a test bot on a snapshot?" review — D-007 extended with the identity/egress/remap analysis; nothing else moved). Earlier: 2026-09-03 (re-scope after the VPS cutover review, [note](../../notes/2026-09-03-release-gate-and-test-environment.md):
|
|
5
5
|
M1.3 gains the instance-label prerequisite and the fleet-box shape; M1.6
|
|
6
6
|
`snapshot-across-hosts` added; M3.2 extended to tagula's space-sync path;
|
|
7
7
|
M3.3 `sync-rehearse` **moved** to [`release-gate`](../release-gate/roadmap.md)
|
|
@@ -51,7 +51,9 @@ matrix in §2 or edits it; `ops-preflight` refuses the exact command of
|
|
|
51
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 |
|
|
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
|
+
| 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 |
|
|
54
55
|
| M1.6 | **Snapshot across hosts** (added 2026-09-03, D-013): `mercury shadow snapshot --out <file>` packs a snapshot as one verified archive (the `VACUUM INTO` db, `spaces/ global/ extensions/`, `SNAPSHOT.json`; never `whatsapp-auth/`, never `.env` values); `mercury shadow restore <file>` unpacks it under `~/mercury-shadow/<ts>/` on another host with the shadow `.env` + `mercury.yaml` generated there, ownership normalised to the running user (the Windows uid 197609 trap), and `MERCURY_SHADOW=1` written — a restored copy **cannot** start live without `mercury shadow promote`, which refuses while any other host is known to hold the WhatsApp session. Replaces tagula's `pack.sh`/`pull.sh` (torn WAL, exit 0, 2026-09-01) | snapshot-across-hosts | M1.2, M1.3 | backlog |
|
|
56
|
+
| M1.7 | **Synthetic feed input** (added 2026-09-15): in shadow mode feed-watch's `fetchImpl` (`examples/extensions/feed-watch/index.ts:183`) serves `~/mercury-shadow/current/fixtures/feeds/<source-key>` when the file exists and falls through to the network otherwise — no `http://` sources, so `config.ts:252`'s https-only rule stays; and `shadow snapshot --reset-feed-state` drops feed-watch's `seen:`/`buffer:`/`pending:` rows for a space so the first poll treats the fixture as new (the copied `seen` state is why a shadow at debug level found 0 exclude-term drops on 2026-09-15). A fixture is a plain RSS/JSON/HTML file; a `dead` marker makes the source fail, for the failure-routing lines | shadow-feed-fixtures | M1.2 | backlog |
|
|
55
57
|
|
|
56
58
|
**Checkpoint:** from a fresh snapshot, `space-profile apply --project
|
|
57
59
|
~/mercury-shadow/current --profile examples/profiles/football-reporter`
|
|
@@ -70,7 +72,7 @@ unchanged across the whole checkpoint.
|
|
|
70
72
|
|----|-------|------|------------|--------|
|
|
71
73
|
| M2.1 | Extract the adapter's pre-handler derivations into `src/adapters/whatsapp-ingress.ts` as pure functions (`buildReplyContext`, `isSelfJid` over `{id, lid}`, mention detection, caller canonicalisation); the adapter calls them; tests pin the F5 adapter half that `docs/bugs/reply-to-member-triggers-bot.md` says is untestable | whatsapp-ingress-extraction | — | **done** 2026-09-02 — [spec](../../archive/ingress-hardening/2026-09-02-whatsapp-ingress-extraction.md), PR #69 |
|
|
72
74
|
| M2.2 | **Function half** (split from the route on 2026-09-09, see M2.2b): `simulateIngress(event)` in `src/core/` — synthetic WhatsApp event → chat `Message` → the real `createMessageHandler` closure via a shadow bridge whose sends append to `shadow/outbox.jsonl` **as both the raw text the handler produced and the wire form `applyRtlDirection(normalizeChatMarkdown(text))` the adapter would have sent** — the shadow constructs no adapter (B1), so an outbox recording only the raw text would show what the model wrote, not what the group would have seen; response streams the outbox events for that thread until the run settles (heartbeat, edits, deletes, reply, files). Attachments arrive as supplied files and reach the *received* branch of `gateIncomingMedia`, not its `count === 0` "could not be downloaded" branch. **Three corrections found by the plan review of 2026-09-09, before any code:** (a) the handler's promise resolves *before the run starts* on the default WhatsApp path — `debouncer.submit` is the closure's last statement (`handler.ts:473`) and returns `void`, so "resolves when the run settles" is a thing this story **builds**, not a definition it inherits, and it must also quiesce the heartbeat's fire-and-forget `discardStatusMessage` continuation and bound the `SpaceQueue` wait; (b) `isBotMentioned` / `isReplyToBot` are **derived, never caller-supplied** — accepting them as flags is the `/chat` shortcut the goal forbids and makes the Milestone 2 checkpoint vacuous; (c) the wire form lives at **four** sites, two of them in the bridge (`bridges/whatsapp.ts:178` `editMessage`, `:237` `sendFiles` caption), so it is extracted once and shared rather than copied a fifth time | simulate-ingress | M1.3, M2.1 | backlog |
|
|
73
|
-
| M2.2b | **Route half** (split from M2.2 on 2026-09-09): `POST /api/shadow/simulate`, registered only under `MERCURY_SHADOW=1`, as a thin HTTP face over M2.2's function — multipart upload for attachments, and a response that streams that thread's outbox events until the run settles (heartbeat, edits, deletes, reply, files); `streamSSE` already exists (`routes/dashboard.ts`). **Owns a new secret**: `MERCURY_SHADOW_API_SECRET`, generated per snapshot and written by `renderShadowEnv`, absent ⇒ the route refuses. Today `SHADOW_ENV_KEYS` (`ops/shadow-snapshot.ts:100-108`) does **not** override `MERCURY_API_SECRET`, so a shadow authenticates with the **live** bot's secret, and M1.3 deliberately dropped the loopback bind — without its own secret the route lets anyone holding the live secret spend model credits on the shadow at the same auth cost as reading `/api/spaces` | shadow-simulate-route | M2.2 | **built** 2026-09-13 — [spec](../../archive/2026-09-13-shadow-simulate-route.md), B2b of the bench-and-gate round; the route, `MERCURY_SHADOW_API_SECRET` (D-016), `/health.simulateRoute`, `shadow status`; deploy-gated live checks in `pending-verification.md` |
|
|
75
|
+
| M2.2b | **Route half** (split from M2.2 on 2026-09-09): `POST /api/shadow/simulate`, registered only under `MERCURY_SHADOW=1`, as a thin HTTP face over M2.2's function — multipart upload for attachments, and a response that streams that thread's outbox events until the run settles (heartbeat, edits, deletes, reply, files); `streamSSE` already exists (`routes/dashboard.ts`). **Owns a new secret**: `MERCURY_SHADOW_API_SECRET`, generated per snapshot and written by `renderShadowEnv`, absent ⇒ the route refuses. Today `SHADOW_ENV_KEYS` (`ops/shadow-snapshot.ts:100-108`) does **not** override `MERCURY_API_SECRET`, so a shadow authenticates with the **live** bot's secret, and M1.3 deliberately dropped the loopback bind — without its own secret the route lets anyone holding the live secret spend model credits on the shadow at the same auth cost as reading `/api/spaces` | shadow-simulate-route | M2.2 | **built** 2026-09-13 — [spec](../../archive/rehearsal/2026-09-13-shadow-simulate-route.md), B2b of the bench-and-gate round; the route, `MERCURY_SHADOW_API_SECRET` (D-016), `/health.simulateRoute`, `shadow status`; deploy-gated live checks in `pending-verification.md` |
|
|
74
76
|
| M2.3 | `mercury shadow say --space <id> --as <jid\|name\|new> [--reply-to last-bot\|last-user\|<id>] [--mention] [--file …] "<text>"` and `mercury shadow transcript --space <id> [--since]` rendering both sides like a chat; `--as new` registers a fresh member (the "second phone" without a phone) | shadow-cli | M2.2b | backlog |
|
|
75
77
|
| M2.4 | `scripts/football-harness.ts --shadow`: live mode targets the shadow through `simulate` instead of `/chat`, in `football-friends` itself, so the group-trigger fixtures and the member-role fixtures the suite could never run become runnable | harness-on-shadow | M2.3 | backlog |
|
|
76
78
|
|
|
@@ -108,6 +110,77 @@ in `pending-verification.md` for that fix is replaced by the transcript.
|
|
|
108
110
|
lab space and every item is either ticked with the date or moved into M2 as
|
|
109
111
|
a shadow-testable item.
|
|
110
112
|
|
|
113
|
+
### What the pending list says the bench still lacks (2026-09-15)
|
|
114
|
+
> Written after the v0.23.0-beta.2 sweep, in which 63 manual checks were
|
|
115
|
+
> cleared from the box and the bench and seven from the owner's phone, and 55
|
|
116
|
+
> stayed open. The earlier version of this analysis was a session report and
|
|
117
|
+
> did not survive; this one is the durable copy. Line numbers are
|
|
118
|
+
> `docs/pending-verification.md` at `3c73933`; the per-line notes there
|
|
119
|
+
> are the evidence.
|
|
120
|
+
|
|
121
|
+
**The state of the seam.** `simulateIngress` + `/api/shadow/simulate`
|
|
122
|
+
(M2.2, M2.2b) already carry any group or DM text from any member, with
|
|
123
|
+
`@digits` mentions resolved to names on the way in and back to PN+LID jids
|
|
124
|
+
on the way out, swipe-replies (`quoted`), files, and slash commands; the
|
|
125
|
+
shadow DB is editable before a turn; a scheduled task runs on demand by
|
|
126
|
+
moving `next_run_at`. This session drove it by hand for tag-by-name (F5
|
|
127
|
+
shape), `/model reset` in a task-less space, and the truncation marker —
|
|
128
|
+
each one an event JSON, a fire-and-poll on `outbox.jsonl`, a trace read,
|
|
129
|
+
and a DB read. **Nothing runs unattended**: every check is a person writing
|
|
130
|
+
the event and the assertion, and two filed bugs make even that fragile
|
|
131
|
+
(the copied `auth.json` dies at the live bot's next token rotation — hours;
|
|
132
|
+
the simulate stream dies at 10 s).
|
|
133
|
+
|
|
134
|
+
**Where the 55 open lines sit, by what would take each one over:**
|
|
135
|
+
|
|
136
|
+
| Cluster | Lines | Bench status |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| Needs the world to produce a feed item — maqaf women's headline (L1598), undated Maccabi item (L1621), the eight exclude terms (L1623), first highlights / WhoScored item (L1574, L1575), a correction (L1640), a re-post (L1641, L1718), an empty roundup window (L1535), a feed-watch failure routed to the DM (L1691), a partial delivery (L1698) | 11 | **One story away: M1.7** |
|
|
139
|
+
| Bench-doable today with a hand-built event and a shadow-DB edit — history cap set-and-observe (L54), two-group isolation via an inserted `conversations` row (L1689, L1690), glossary read on an on-demand article (L1639), consent replay with a file once the guard is armed in the shadow copy (L1791, L1792) | 6 | Doable now, ~$2 of runs, by a person |
|
|
140
|
+
| Earlier FAILs that owe a fix, not a check — notes over budget (L429), a clickbait word (L566), page shape (L811, L1667), the stream timeout (L1861) | 5 | n/a |
|
|
141
|
+
| Calendar — 7-day and 14-day reads (L1697, L1716, L1741, L1752), tonight's napkin (L1734), DST on 26 Oct (L503), the first failure of task 24/621 (L1692) | 7 | Reads, not phone work; the bench cannot fast-forward |
|
|
142
|
+
| Owner decisions — poster install (L1271), archive enable (L1087), GHCR link/visibility/duplicate (L1025, L1028, L1029), release-gate strike (L1758), L1451 rewrite, L1852 after its bug | 8 | n/a |
|
|
143
|
+
| VPS / fleet boxes — bwrap (L924, L925), runsc pair (L1139, L1378), reply-in-its-chat (L931), member role on the VPS (L944), refresh path (L1767), `unprobed` (L1785), prerelease `:latest` (L1027), second listener (L1081) | 10 | Needs a bench **on those boxes** — M1.6 |
|
|
144
|
+
| Socket-only, forever — a never-seen sender's LID lookup (L874, first half), swipe-reply to an unseen member (L897), "nobody complains" (L1279), the phone half of any notify check | 4 | No bench answers these; one proof per pattern (the 2026-09-15 tag → highlighted + buzz), then wire checks |
|
|
145
|
+
| An unattended shadow across a due task (L1134) | 1 | Blocked on M1.3c |
|
|
146
|
+
|
|
147
|
+
**Three pieces of work, by leverage:**
|
|
148
|
+
|
|
149
|
+
1. **M1.3c `shadow-own-credential`** — the shadow stops sharing the live
|
|
150
|
+
refresh token, and the simulate stream survives a run. Nothing unattended
|
|
151
|
+
is safe before this: a shadow that refreshes first kills the live bot's
|
|
152
|
+
token, which is the hazard the bug names and the reason L1134 was not run.
|
|
153
|
+
2. **M1.7 `shadow-feed-fixtures`** — the single biggest unlock: the eleven
|
|
154
|
+
feed-shaped lines now, and every future feed-watch check is born as a
|
|
155
|
+
fixture file instead of a "wait for the first …" line. "Wait for the
|
|
156
|
+
first maqaf headline" becomes: drop an RSS fixture carrying
|
|
157
|
+
`כדורגל־נשים`, tick, read the drop line.
|
|
158
|
+
3. **M2.4 `harness-on-shadow`** (the release-gate R3 smoke runner is the
|
|
159
|
+
same shape) — a fixture is an event, optional DB edits, and predicates
|
|
160
|
+
over the trace, the outbox and the shadow DB (`prompt has 0 raw digits`,
|
|
161
|
+
`outbox mentions.length == 4`, `no model.active row for main`).
|
|
162
|
+
Everything driven by hand this session is already that shape in a
|
|
163
|
+
scratch `sim.sh`; it is not a file format with a pass/fail. This is the
|
|
164
|
+
piece that makes "without a person" true rather than "with a person
|
|
165
|
+
typing faster".
|
|
166
|
+
|
|
167
|
+
After the three: about 25 of the 55 (the feed cluster, the six bench-doable
|
|
168
|
+
lines, L1134, and the feed-shaped half of the calendar reads) run from a
|
|
169
|
+
script, and a new check of those kinds is a fixture, not a phone line. What
|
|
170
|
+
stays human: the four socket-only lines, the decisions, and the fleet boxes
|
|
171
|
+
until M1.6. **Rendering is not a bench problem at all** — blank lines
|
|
172
|
+
collapsing, the club emoji at the head of a block, a nine-line first
|
|
173
|
+
paragraph, webp on an older phone — the first three are a reporter-lint
|
|
174
|
+
rule away (the cheaper fix for L1667's failure); the fourth is one page
|
|
175
|
+
with a Walla photo, read once.
|
|
176
|
+
|
|
177
|
+
**Order.** M1.3c → M1.7 → M2.4, ahead of M2.3 (`shadow say` is
|
|
178
|
+
convenience over the same seam) and ahead of M1.6 (the fleet lines are
|
|
179
|
+
fewer and older). The execution-order line at the top of this file is
|
|
180
|
+
**not** rewritten here; it is the owner's call whether these three cut in
|
|
181
|
+
front of M1.4 and M1.5.
|
|
182
|
+
|
|
183
|
+
|
|
111
184
|
### Interface with `space-probe-harness` (not a milestone of this goal)
|
|
112
185
|
> Probing, assertion and alerting are the owner's own work
|
|
113
186
|
> (`docs/ideas/space-probe-harness.md`, scope decided 2026-09-02: incident
|
|
@@ -151,7 +224,9 @@ call it, or what dwell time a version needs before a customer box takes it.
|
|
|
151
224
|
```
|
|
152
225
|
M0.1 ─ M0.2
|
|
153
226
|
M1.1 ─ M1.2 ─ M1.3 ─┬─ M3.1
|
|
154
|
-
│
|
|
227
|
+
│ ├──── M1.6
|
|
228
|
+
│ └──── M1.3c ─ (unattended shadow)
|
|
229
|
+
└── M1.7 ─────────────────────► M2.4 (feed-shaped fixtures)
|
|
155
230
|
└── M1.4 ───┬─ M3.2 ◄─ release-gate R3 (smoke)
|
|
156
231
|
M1.1 ─ M1.5 │
|
|
157
232
|
M2.1 ──────┬─ M2.2 ─┴─ M2.3 ─ M2.4
|
|
@@ -97,8 +97,8 @@ evidence, after a grep proves no secret value is in them.
|
|
|
97
97
|
|
|
98
98
|
| ID | Story | Slug | Depends on | Status |
|
|
99
99
|
|----|-------|------|------------|--------|
|
|
100
|
-
| R3.1 | **Fixture format**: one file per space (`smoke.yaml` under the space's profile or workspace), each fixture = sender role (`member` / `admin` / `owner`), message (text, optional quoted message, optional file), timeout, and assertions on *shape* only: replied-within, not-a-refusal (a small refusal lexicon per language, absence denies), produced-file (kind + non-empty), lint passes (`reporter-lint` where the space has a standard), optional regex on structure. Documented with three worked examples (a DM answer, a group mention, a proposal PDF) | smoke-fixture-format | — | **planned 2026-09-09**, not started — Goal and Architecture in [`docs/backlog/smoke-fixture-format.md`](../../backlog/smoke-fixture-format.md), written by the round's S2 session after R2.2 landed. Four decisions past the row: D-11 YAML validated by a `.strict()` zod schema, so a misspelled assertion key is a **refusal** rather than a fixture that asserts nothing; D-12 resolution is a fixed order (`--fixtures` → the space's workspace → its profile) and the parse result names the file that won, which is what keeps customer fixtures out of this repo (G-006); D-13 the refusal lexicon is per language and **absence denies** — an unknown language fails `not-a-refusal` instead of passing it, and `isRefusal` returns a third case so a caller cannot ignore it; D-14 `lint` delegates to `reporter-lint`'s existing `evaluateReplyCase` shape rather than re-implementing a comparison. Two open questions are the owner's: the lexicon's contents (a false positive there fails a healthy deploy) and whether `produced-file` may name a filename pattern. **Built 2026-09-12 (B3, bench-and-gate round), gate-green on its branch, unlanded at the time of writing** — `src/smoke/fixture.ts` (strict zod, an unknown key names its file and line), `src/smoke/refusal.ts` (`isRefusal` with a third `unknownLanguage` case), three worked examples as real files under `examples/smoke/`, `docs/smoke-fixtures.md`; three deviations the code forced are recorded there (`mentionBot`, `lint.standard` as a space-relative path, examples outside the football profile). Spec: [`docs/archive/2026-09-13-smoke-fixture-format.md`](../../archive/2026-09-13-smoke-fixture-format.md) |
|
|
101
|
-
| R3.2 | `mercury smoke --space <id> [--fixtures <file>] [--json]`: runs each fixture through `simulateIngress` (bench M2.2, function half) in the current process — a shadow, or a designated test space on live when the caller says so — as the fixture's role, records `sent[]`, evaluates the assertions, writes a report with per-fixture verdict, transcript, elapsed and cost from `token_usage`. Refuses on live unless the target space is named in config as a test space (absence denies) | smoke-runner | R3.1, rehearsal-bench M2.2 | **built 2026-09-12/13 (B3), gate-green on its branch, unlanded at the time of writing** — `mercury smoke --space <id> [--fixtures] [--profile] [--json] [--report]`, its own process booting a headless shadow (`src/smoke/boot.ts`, a documented twin of `main.ts` — idea `shared-runtime-boot`), one pure evaluation (`src/smoke/evaluate.ts`), the report beside preflight's with cost since the run started, exit 0/1/2. **The live leg and the test-space key are dropped: G-010.** The role is a chosen sender and the run's own read-back is the verdict (owner = first configured admin; absent denies). Live check is shadow-side, deploy 3. Spec: [`docs/archive/2026-09-13-smoke-runner.md`](../../archive/2026-09-13-smoke-runner.md) |
|
|
100
|
+
| R3.1 | **Fixture format**: one file per space (`smoke.yaml` under the space's profile or workspace), each fixture = sender role (`member` / `admin` / `owner`), message (text, optional quoted message, optional file), timeout, and assertions on *shape* only: replied-within, not-a-refusal (a small refusal lexicon per language, absence denies), produced-file (kind + non-empty), lint passes (`reporter-lint` where the space has a standard), optional regex on structure. Documented with three worked examples (a DM answer, a group mention, a proposal PDF) | smoke-fixture-format | — | **planned 2026-09-09**, not started — Goal and Architecture in [`docs/backlog/smoke-fixture-format.md`](../../backlog/smoke-fixture-format.md), written by the round's S2 session after R2.2 landed. Four decisions past the row: D-11 YAML validated by a `.strict()` zod schema, so a misspelled assertion key is a **refusal** rather than a fixture that asserts nothing; D-12 resolution is a fixed order (`--fixtures` → the space's workspace → its profile) and the parse result names the file that won, which is what keeps customer fixtures out of this repo (G-006); D-13 the refusal lexicon is per language and **absence denies** — an unknown language fails `not-a-refusal` instead of passing it, and `isRefusal` returns a third case so a caller cannot ignore it; D-14 `lint` delegates to `reporter-lint`'s existing `evaluateReplyCase` shape rather than re-implementing a comparison. Two open questions are the owner's: the lexicon's contents (a false positive there fails a healthy deploy) and whether `produced-file` may name a filename pattern. **Built 2026-09-12 (B3, bench-and-gate round), gate-green on its branch, unlanded at the time of writing** — `src/smoke/fixture.ts` (strict zod, an unknown key names its file and line), `src/smoke/refusal.ts` (`isRefusal` with a third `unknownLanguage` case), three worked examples as real files under `examples/smoke/`, `docs/smoke-fixtures.md`; three deviations the code forced are recorded there (`mentionBot`, `lint.standard` as a space-relative path, examples outside the football profile). Spec: [`docs/archive/2026-09-13-smoke-fixture-format.md`](../../archive/rehearsal/2026-09-13-smoke-fixture-format.md) |
|
|
101
|
+
| R3.2 | `mercury smoke --space <id> [--fixtures <file>] [--json]`: runs each fixture through `simulateIngress` (bench M2.2, function half) in the current process — a shadow, or a designated test space on live when the caller says so — as the fixture's role, records `sent[]`, evaluates the assertions, writes a report with per-fixture verdict, transcript, elapsed and cost from `token_usage`. Refuses on live unless the target space is named in config as a test space (absence denies) | smoke-runner | R3.1, rehearsal-bench M2.2 | **built 2026-09-12/13 (B3), gate-green on its branch, unlanded at the time of writing** — `mercury smoke --space <id> [--fixtures] [--profile] [--json] [--report]`, its own process booting a headless shadow (`src/smoke/boot.ts`, a documented twin of `main.ts` — idea `shared-runtime-boot`), one pure evaluation (`src/smoke/evaluate.ts`), the report beside preflight's with cost since the run started, exit 0/1/2. **The live leg and the test-space key are dropped: G-010.** The role is a chosen sender and the run's own read-back is the verdict (owner = first configured admin; absent denies). Live check is shadow-side, deploy 3. Spec: [`docs/archive/2026-09-13-smoke-runner.md`](../../archive/rehearsal/2026-09-13-smoke-runner.md) |
|
|
102
102
|
| R3.3 | Regression map: `tests/release-gate-map.test.ts` asserts every post-mortem under `docs/debug/**` since 2026-09-01 names a check in `preflight`, `smoke` or `env diff` (by a `Gate check:` line) — the football regression-map pattern applied to the gate | gate-regression-map | R1.1, R3.2 | **built 2026-09-13 (B3), gate-green on its branch** — `tests/release-gate-map.test.ts` + `tests/fixtures/release-gate/regression-map.json` (97 allowlist entries on day one, 14 of them `gate-candidate` naming the check that should own the failure); an allowlisted file that gains a line is refused; G-011. Every post-mortem archived from here on must carry a `Gate check:` line or an entry. Spec: [`docs/archive/2026-09-13-gate-regression-map.md`](../../archive/2026-09-13-gate-regression-map.md) |
|
|
103
103
|
|
|
104
104
|
**Checkpoint:** a fixture file for `football-friends` and one for a copy of
|