mercury-agent 0.17.0 → 0.18.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -3
- package/docs/autopilot/config.yaml +5 -1
- package/docs/configuration.md +52 -0
- package/docs/container-lifecycle.md +18 -3
- package/docs/goals/football-reporter-profile/decisions.md +104 -1
- package/docs/goals/football-reporter-profile/harness-2026-09-01.json +303 -0
- package/docs/goals/football-reporter-profile/harness-2026-09-01.md +75 -0
- package/docs/goals/football-reporter-profile/roadmap.md +12 -9
- package/docs/goals/rehearsal-bench/README.md +253 -0
- package/docs/goals/rehearsal-bench/decisions.md +398 -0
- package/docs/goals/rehearsal-bench/goal.md +198 -0
- package/docs/goals/rehearsal-bench/roadmap.md +173 -0
- package/docs/goals/release-gate/README.md +58 -0
- package/docs/goals/release-gate/decisions.md +112 -0
- package/docs/goals/release-gate/goal.md +146 -0
- package/docs/goals/release-gate/roadmap.md +115 -0
- package/docs/goals/whatsapp-bot-hardening/open-threads.md +21 -10
- package/docs/live-testing.md +368 -0
- package/docs/pending-verification.md +822 -0
- package/docs/profile-guide.md +3 -2
- package/examples/extensions/feed-watch/watch.ts +63 -3
- package/examples/extensions/longview/hook.ts +12 -6
- package/examples/extensions/longview/index.ts +47 -11
- package/examples/extensions/longview/render/images.ts +214 -0
- package/examples/extensions/longview/render/telegraph-nodes.ts +61 -1
- package/examples/extensions/longview/summarize.ts +46 -7
- package/examples/extensions/longview/targets/telegraph.ts +55 -2
- package/examples/extensions/napkin/distill-input.ts +127 -0
- package/examples/extensions/napkin/distill-safety.ts +495 -0
- package/examples/extensions/napkin/index.ts +54 -73
- package/examples/extensions/napkin/pi-spawn.ts +8 -28
- package/examples/extensions/tradestation/index.ts +9 -4
- package/examples/extensions/yahoo-mail/index.ts +42 -10
- package/examples/extensions/yahoo-mail/lib/ymail.ts +592 -0
- package/examples/extensions/yahoo-mail/{cli/package.json → package.json} +1 -4
- package/examples/extensions/yahoo-mail/skill/SKILL.md +36 -21
- package/examples/profiles/football-reporter/AGENTS.md +8 -3
- package/examples/profiles/football-reporter/README.md +118 -1
- package/examples/profiles/football-reporter/config.yaml +28 -13
- package/examples/profiles/football-reporter/standard.json +137 -0
- package/examples/profiles/football-reporter/tasks/daily-article.md +74 -47
- package/package.json +10 -3
- package/resources/templates/mercury.example.yaml +4 -0
- package/src/adapters/discord-native.ts +7 -0
- package/src/adapters/setup.ts +33 -0
- package/src/adapters/whatsapp-identity.ts +77 -0
- package/src/adapters/whatsapp-ingress.ts +325 -0
- package/src/adapters/whatsapp.ts +119 -182
- package/src/agent/container-entry.ts +2 -0
- package/src/agent/container-error.ts +13 -1
- package/src/agent/container-runner.ts +742 -146
- package/src/audit/ledger-queries.ts +345 -0
- package/src/bridges/discord.ts +4 -1
- package/src/bridges/slack.ts +1 -1
- package/src/bridges/teams.ts +1 -1
- package/src/bridges/telegram.ts +1 -1
- package/src/bridges/whatsapp.ts +3 -5
- package/src/cli/mercury.ts +585 -9
- package/src/cli/mrctl-http.ts +18 -0
- package/src/cli/mrctl.ts +11 -4
- package/src/config-file.ts +8 -0
- package/src/config.ts +98 -2
- package/src/core/debounce.ts +10 -5
- package/src/core/exec.ts +75 -0
- package/src/core/handler.ts +30 -13
- package/src/core/model-leg.ts +41 -0
- package/src/core/process-tree.ts +24 -18
- package/src/core/reply-context.ts +5 -0
- package/src/core/routes/chat.ts +13 -1
- package/src/core/routes/control.ts +2 -2
- package/src/core/routes/dashboard.ts +2 -2
- package/src/core/runtime.ts +185 -46
- package/src/core/storage-cleanup.ts +59 -0
- package/src/core/trigger.ts +10 -4
- package/src/extensions/image-builder.ts +33 -51
- package/src/logger.ts +27 -2
- package/src/main.ts +40 -5
- package/src/ops/preflight.ts +227 -0
- package/src/ops/shadow-root.ts +234 -0
- package/src/ops/shadow-snapshot.ts +1076 -0
- package/src/profile/space-profile.ts +48 -5
- package/src/server.ts +80 -25
- package/src/storage/backup.ts +403 -0
- package/src/storage/db.ts +30 -3
- package/src/storage/integrity.ts +94 -0
- package/src/text/markdown.ts +72 -1
- package/src/text/reporter-lint.ts +1607 -0
- package/src/text/write-claim.ts +68 -0
- package/src/types.ts +32 -2
- package/examples/extensions/yahoo-mail/cli/bun.lock +0 -66
- package/examples/extensions/yahoo-mail/cli/ymail.mjs +0 -353
package/README.md
CHANGED
|
@@ -267,6 +267,12 @@ mercury conversations
|
|
|
267
267
|
mercury conversations --unlinked
|
|
268
268
|
mercury link <conversation-id> <space-id>
|
|
269
269
|
|
|
270
|
+
# backups (state.db snapshots — also taken automatically by the running service)
|
|
271
|
+
mercury backup create # verified VACUUM INTO snapshot in <dataDir>/backups/
|
|
272
|
+
mercury backup list
|
|
273
|
+
mercury backup verify [snapshot] # quick_check a snapshot (default: newest)
|
|
274
|
+
mercury restore <snapshot> # verify + restore to a fresh path (never overwrites)
|
|
275
|
+
|
|
270
276
|
# extensions
|
|
271
277
|
mercury add ./path/to/extension
|
|
272
278
|
mercury add npm:<package>
|
|
@@ -430,10 +436,11 @@ Supported OAuth providers: Anthropic, GitHub Copilot, Google Gemini CLI, Antigra
|
|
|
430
436
|
> true` in `mercury.yaml`) to accept the trade knowingly — it is then logged at
|
|
431
437
|
> `warn` on every spawn. **Upgrading an existing Docker Desktop install requires
|
|
432
438
|
> this**, or every message gets a generic container failure. Linux hosts are
|
|
433
|
-
> unaffected; `MERCURY_CONTAINER_RUNTIME=runsc` (
|
|
434
|
-
>
|
|
439
|
+
> unaffected; `MERCURY_CONTAINER_RUNTIME=runsc` (or `agent.container_runtime:
|
|
440
|
+
> runsc` in `mercury.yaml`; gVisor) avoids the relaxation entirely, and the
|
|
441
|
+
> flag is ignored when `container_bwrap_docker_compat` is set.
|
|
435
442
|
|
|
436
|
-
> **Linux Docker Engine:** Mercury uses [bubblewrap](https://github.com/containers/bubblewrap) for in-container sandboxing. On Linux Docker Engine (not Docker Desktop), bwrap cannot mount `/proc` without extra privileges. Either set `container_bwrap_docker_compat: true` in `mercury.yaml` (adds `--privileged` to `docker run`), or install [gVisor](https://gvisor.dev/docs/user_guide/install/) and set `MERCURY_CONTAINER_RUNTIME=runsc` to skip bwrap entirely.
|
|
443
|
+
> **Linux Docker Engine:** Mercury uses [bubblewrap](https://github.com/containers/bubblewrap) for in-container sandboxing. On Linux Docker Engine (not Docker Desktop), bwrap cannot mount `/proc` without extra privileges. Either set `container_bwrap_docker_compat: true` in `mercury.yaml` (adds `--privileged` to `docker run`), or install [gVisor](https://gvisor.dev/docs/user_guide/install/) and set `MERCURY_CONTAINER_RUNTIME=runsc` (or `agent.container_runtime: runsc` in `mercury.yaml`) to skip bwrap entirely.
|
|
437
444
|
|
|
438
445
|
**KB Distillation:**
|
|
439
446
|
|
|
@@ -31,7 +31,11 @@ scope:
|
|
|
31
31
|
# Both are design decisions, not mechanical fixes, and neither is verifiable
|
|
32
32
|
# by the check suite. See docs/bugs/ for the filed reports.
|
|
33
33
|
- docker-desktop-sandbox-relaxation-silent # fix = a product call: warn, or refuse and break Docker Desktop users
|
|
34
|
-
|
|
34
|
+
# container-egress-unrestricted moved to docs/ideas/ on 2026-09-01 -- the
|
|
35
|
+
# two-container split it proposed was ruled out by investigation. Its one
|
|
36
|
+
# remaining code-shaped piece is env-passthrough-defaults-to-all, below.
|
|
37
|
+
- env-passthrough-defaults-to-all # breaking default change: needs the owner'''s go-ahead and a week of startup-log evidence first
|
|
38
|
+
- linux-default-sandbox-path-broken-on-modern-docker # product call on the relaxation shape; unverifiable by the check suite (container spawn flags)
|
|
35
39
|
# attachment-ingest-type-gating — unblocked 2026-08-27 (warn-only decision
|
|
36
40
|
# recorded in the spec). Eligible once it is listed under ROADMAP Now/Next.
|
|
37
41
|
|
package/docs/configuration.md
CHANGED
|
@@ -85,6 +85,58 @@ Per-space: `mrctl config set ambient.enabled false` disables capture entirely (t
|
|
|
85
85
|
|
|
86
86
|
You may also set a top-level **`model_chain`** array as an alias for `model.chain`.
|
|
87
87
|
|
|
88
|
+
## State database backups
|
|
89
|
+
|
|
90
|
+
`state.db` is snapshotted by the storage sweep via `VACUUM INTO
|
|
91
|
+
<dataDir>/backups/state-<timestamp>.db`. Every snapshot is verified
|
|
92
|
+
(`PRAGMA quick_check`) before it counts; a crash mid-snapshot leaves only a
|
|
93
|
+
`.partial` file that is never mistaken for a backup and is swept later.
|
|
94
|
+
|
|
95
|
+
- **`MERCURY_BACKUP_INTERVAL_HOURS`** (default `12`) — a new snapshot is taken
|
|
96
|
+
once the newest one is at least this old.
|
|
97
|
+
- **`MERCURY_BACKUP_RETENTION_COUNT`** (default `7`) — snapshots kept; older
|
|
98
|
+
ones are pruned by the same sweep.
|
|
99
|
+
|
|
100
|
+
`mercury backup create | list | verify` manages snapshots by hand;
|
|
101
|
+
`mercury restore <snapshot>` verifies a snapshot and writes it to a **fresh
|
|
102
|
+
path only** (default `<dataDir>/state.restored-<timestamp>.db`) — it never
|
|
103
|
+
overwrites the live database, and prints the manual swap-in steps.
|
|
104
|
+
|
|
105
|
+
`mercury doctor` **fails** when no snapshot at most 24 h old exists (a missing
|
|
106
|
+
backups directory included), and warns when the data dir sits on a
|
|
107
|
+
9p/virtiofs/CIFS filesystem (a `.mercury/` under `/mnt/c` in WSL2, or a VM
|
|
108
|
+
shared folder) — SQLite WAL locking is unreliable there.
|
|
109
|
+
|
|
110
|
+
The database itself refuses to open when `quick_check` fails — loudly, with a
|
|
111
|
+
typed error, never auto-repaired. The fix is `mercury restore`.
|
|
112
|
+
|
|
113
|
+
## Shadow mode (`MERCURY_SHADOW`)
|
|
114
|
+
|
|
115
|
+
**`MERCURY_SHADOW`** (default off; accepts only `1` or `true`,
|
|
116
|
+
case-insensitively — every other spelling, `yes` and `on` included, means
|
|
117
|
+
live) turns a process into a *rehearsal shadow*: it constructs no platform
|
|
118
|
+
adapter, makes no console billing call, delivers no task output, marks every
|
|
119
|
+
log line and the dashboard, and reports `shadow: true` on `/health`.
|
|
120
|
+
|
|
121
|
+
**A shadow refuses to start unless its data dir is inside
|
|
122
|
+
`<home>/mercury-shadow/`.** The flag keeps a shadow away from people; this
|
|
123
|
+
keeps it away from live *state*. Without it, `MERCURY_SHADOW=1` started in
|
|
124
|
+
the live project directory would construct no adapter and touch no session
|
|
125
|
+
while running a **second scheduler against the live database** — every due
|
|
126
|
+
task fires twice and SQLite gets a second writer.
|
|
127
|
+
|
|
128
|
+
The check runs in `loadConfig()`, so every entry point inherits it, and it
|
|
129
|
+
denies on absence at each input: no home directory (a systemd unit with no
|
|
130
|
+
`HOME`), a relative data dir, a `..` segment, the shadow root itself, a path
|
|
131
|
+
outside the root, or an existing directory whose `realpath` escapes the root.
|
|
132
|
+
It compares path *segments*, never string prefixes, so `mercury-shadowy` is
|
|
133
|
+
not inside `mercury-shadow`. A live process (`MERCURY_SHADOW` unset) is never
|
|
134
|
+
checked and runs exactly the code it ran before.
|
|
135
|
+
|
|
136
|
+
Snapshot directories are built by `mercury shadow snapshot`, which generates
|
|
137
|
+
the shadow's `.env` and `mercury.yaml` — nothing here needs to be set by
|
|
138
|
+
hand. See [live-testing.md](live-testing.md) §2 and §7.
|
|
139
|
+
|
|
88
140
|
## Container env passthrough (`agent.env_passthrough`)
|
|
89
141
|
|
|
90
142
|
Controls which host `MERCURY_*` variables reach agent containers:
|
|
@@ -191,14 +191,29 @@ The pi agent runs inside `bwrap`, which creates a minimal mount namespace with o
|
|
|
191
191
|
|
|
192
192
|
| Env Var | Purpose |
|
|
193
193
|
|---------|---------|
|
|
194
|
-
| `MERCURY_CONTAINER_BWRAP_DOCKER_COMPAT=1` | **Host only.**
|
|
195
|
-
| `MERCURY_CONTAINER_ALLOW_UNCONFINED=true` | **Host only.** Accepts the *auto-detected* Docker Desktop relaxation (
|
|
194
|
+
| `MERCURY_CONTAINER_BWRAP_DOCKER_COMPAT=1` | **Host only.** Runs the agent container `--privileged` — Docker's widest grant (every capability, no seccomp, no AppArmor, all devices) — so `bwrap` can nest inside it. Keeps bubblewrap on, but the outer isolation layer is gone. Prefer `MERCURY_CONTAINER_RUNTIME=runsc`. |
|
|
195
|
+
| `MERCURY_CONTAINER_ALLOW_UNCONFINED=true` | **Host only.** Accepts the *auto-detected* Docker Desktop relaxation (`seccomp=unconfined`, `apparmor=unconfined`, `--cap-add SYS_ADMIN` — narrower than `--privileged`). Defaults to `false`, so on Docker Desktop containers refuse to start until it is set — the relaxation is never applied without the operator seeing it. Logged at `warn` on every spawn once enabled. Ignored under `runsc` and when `BWRAP_DOCKER_COMPAT` is set. |
|
|
196
196
|
| `MERCURY_DISABLE_BUBBLEWRAP=1` | Disable bubblewrap; run pi directly (last resort / debugging) |
|
|
197
197
|
|
|
198
|
-
|
|
198
|
+
**On a stock Linux host the default shape cannot run bwrap.** Docker's default seccomp profile blocks the user-namespace clone and Ubuntu's `docker-default` AppArmor profile independently blocks bwrap's mount step, so every turn fails with `bwrap: No permissions to create new namespace` (or `Creating new namespace failed: Operation not permitted`). On a stock modern host the kernel sysctls that message points at (`unprivileged_userns_clone`, `max_user_namespaces`) are not the blocker — both gates are Docker's; check them only if they are off their defaults. Mercury probes for this once at boot (`docker run --entrypoint bwrap …` with the real bwrap shape) and, if blocked, logs one `error` line naming the fix and refuses every spawn with the same diagnosis instead of a per-turn bwrap error. Escapes, narrowest first: `MERCURY_CONTAINER_RUNTIME=runsc` (gVisor — strongest sandbox, no relaxation needed), then `MERCURY_CONTAINER_BWRAP_DOCKER_COMPAT=1` (`--privileged`), then `MERCURY_DISABLE_BUBBLEWRAP=1` (no inner sandbox at all).
|
|
199
199
|
|
|
200
200
|
Custom images must install `bubblewrap` for sandboxing to work.
|
|
201
201
|
|
|
202
|
+
**runsc (gVisor) mode requires `--host-uds=open` on the runtime registration.** In runsc mode the agent container reaches the host API over a bind-mounted unix socket — the socket is mrctl's *only* transport there (`API_URL` is a deliberate dummy). gVisor refuses `connect()` to host unix sockets unless the daemon registers runsc with the flag:
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
{
|
|
206
|
+
"runtimes": {
|
|
207
|
+
"runsc": {
|
|
208
|
+
"path": "/usr/bin/runsc",
|
|
209
|
+
"runtimeArgs": ["--host-uds=open"]
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Without it, Mercury starts clean and containers run, but **every mrctl verb fails** (config, tasks, mutes, stop, capabilities — the whole host API surface) with a bare connect error inside the conversation. Mercury probes for this once at boot (a runsc container connect-tests the socket with the real spawn's mount shape) and, if blocked, logs one `error` line naming this fix. Applying it is zero-downtime: `runtimes` is a SIGHUP-reloadable daemon.json key, so edit the file and `kill -SIGHUP $(pidof dockerd)` — no daemon restart, no container kills.
|
|
216
|
+
|
|
202
217
|
## Agent Image Preset
|
|
203
218
|
|
|
204
219
|
Mercury publishes an image preset to GitHub Container Registry:
|
|
@@ -475,7 +475,7 @@
|
|
|
475
475
|
implementing: `## Current State (as of …)` is not matched by the injector, so
|
|
476
476
|
every note napkin had written for this space was reaching the model as a
|
|
477
477
|
summary line with no facts —
|
|
478
|
-
`docs/debug/major/2026-08-31-episode-current-state-heading-suffix-drops-body.md`.
|
|
478
|
+
`docs/debug/major/08-2026/2026-08-31-episode-current-state-heading-suffix-drops-body.md`.
|
|
479
479
|
- **Used by:** reporter-notebook (M2.3, re-cut), football-reporter-fixture-harness (provenance source = `episodes/`), napkin (distill addendum)
|
|
480
480
|
|
|
481
481
|
## D-020: Live tuning is adopted into the repo, not reverted — and scans carry a fixed club emoji (decided 2026-08-31)
|
|
@@ -513,3 +513,106 @@
|
|
|
513
513
|
- **Revisit if:** live edits become frequent — then the right tool is a
|
|
514
514
|
profile PR flow from the box (edit → `dump` → commit), not a rule.
|
|
515
515
|
- **Used by:** football-reporter profile (`AGENTS.md`), football-reporter-fixture-harness (M3.1 lint spec)
|
|
516
|
+
|
|
517
|
+
## D-021: The daily article is an article, not a page of bins (decided 2026-09-01)
|
|
518
|
+
- **Category:** article contract / design (extends [D-005](#d-005-keep-telegraph-make-the-summary-fuller-and-redesign-the-article-itself-decided-2026-08-20), [D-010](#d-010-the-chat-summary-is-author-supplied-by-the-same-run-not-a-second-model-call); refines [D-013](#d-013-tone-is-editorial-and-scan-length-is-per-item-decided-2026-08-20))
|
|
519
|
+
- **Decided:** The owner opened the 2026-09-01 page on a phone (RTL fine) and
|
|
520
|
+
ruled the design wrong: *"it still looks too much like an itinerary and not
|
|
521
|
+
an article."* Eight prototype pages were published and compared on the phone
|
|
522
|
+
the same evening; prototype **C5** is the target shape. Five rulings:
|
|
523
|
+
1. **Images are in**, but only when the image is good and relevant, and it
|
|
524
|
+
must not take over the page. C2 (one wide banner) beat C1 (none); C4/C5
|
|
525
|
+
(a press photo) beat both.
|
|
526
|
+
2. **Side-by-side is dropped** — a platform limit, not a preference.
|
|
527
|
+
Telegraph strips every attribute except `src` and unwraps a `div`, so an
|
|
528
|
+
image beside text is impossible; footprint is controlled by aspect ratio
|
|
529
|
+
only (a 4.5:1 banner is ~85 px tall on a phone, a portrait ~430 px).
|
|
530
|
+
3. **Press photos are allowed**, hotlinked from the cited source and credited
|
|
531
|
+
in the caption. `telegra.ph/upload` answers 400, so hotlinking is the only
|
|
532
|
+
path anyway.
|
|
533
|
+
4. **The long source list goes.** Short bracketed links inline —
|
|
534
|
+
`([וואלה](url))` — plus **one** italic footer line naming every outlet
|
|
535
|
+
read. The footer line stays.
|
|
536
|
+
5. **Stay on Telegraph.** A richer target (real CSS, images beside text,
|
|
537
|
+
cards) is possible through `longview`'s `targets/` seam but needs a host
|
|
538
|
+
and is a separate project.
|
|
539
|
+
|
|
540
|
+
And four image rules, learned by publishing a wrong one (C4's hero showed
|
|
541
|
+
Anthony Gordon under a caption, written from assumption, naming Álvarez):
|
|
542
|
+
the image URL is the cited article's `og:image`; the caption is the source's
|
|
543
|
+
own caption, translated, never re-described — it lives in the article HTML,
|
|
544
|
+
not the RSS; no caption at the source means no image; the photo sits in the
|
|
545
|
+
section whose story it depicts, not automatically at the top (C5 is C4 with
|
|
546
|
+
only that changed). Wikimedia Commons is not a substitute: wide and relevant
|
|
547
|
+
are mutually exclusive there.
|
|
548
|
+
|
|
549
|
+
Two consequences the owner confirmed on 2026-09-01 when the story was
|
|
550
|
+
planned: the per-article "first three blocks get similar length" measure no
|
|
551
|
+
longer applies — the lead story leads, and the coverage balance in
|
|
552
|
+
`AGENTS.md` § תפקיד is a rule about coverage over time; and the
|
|
553
|
+
`DD.MM.YYYY · N מקורות` dateline goes, replaced by Telegraph's own
|
|
554
|
+
author-and-date block under the headline (`author_name`, empty on every page
|
|
555
|
+
published so far, is free article furniture).
|
|
556
|
+
- **Alternatives considered:**
|
|
557
|
+
- *Typography only, words unchanged (prototype A).* Better, and most of it
|
|
558
|
+
needs no code — `>>`, `---` and numbered lists are already in the
|
|
559
|
+
renderer. Not enough on its own: the bins are the tell, not the type.
|
|
560
|
+
- *Keep the eight blocks and drop only the empty ones.* Rejected: the same
|
|
561
|
+
headings in the same order every day is what reads as generated, whether
|
|
562
|
+
or not a bin is empty.
|
|
563
|
+
- *Images beside text via `h3`/`h4` level or `div` wrappers.* Measured
|
|
564
|
+
impossible (rule 2). Heading level is not a lever either: `h3` is 28 px,
|
|
565
|
+
`h4` 24 px, both sans-bold.
|
|
566
|
+
- *Commons images.* Every wide result is a stadium panorama, every usable
|
|
567
|
+
player photo a portrait, nothing of *yesterday's* match.
|
|
568
|
+
- **Reasoning:** the contract shipped by M1.2 is followed correctly — every
|
|
569
|
+
element renders — and the page still reads as a log because the contract
|
|
570
|
+
itself is a list of bins. The fix is the contract, and since M3.1 the
|
|
571
|
+
contract is countable, so the lint, its data file and its fixtures move in
|
|
572
|
+
the same change or the check goes green for the wrong reason.
|
|
573
|
+
- **Revisit if:** the model gets captions wrong in practice (then `longview`
|
|
574
|
+
fetches `og:image` and the caption host-side instead of the run emitting
|
|
575
|
+
them), or a private target with real CSS gets a host (then ruling 5).
|
|
576
|
+
- **Used by:** daily-article-design-follow-up (M1.3), football-reporter
|
|
577
|
+
profile (`tasks/daily-article.md`, `AGENTS.md` § שתיקה, `config.yaml`
|
|
578
|
+
`longview.byline`), football-reporter-fixture-harness (M3.1 lint
|
|
579
|
+
`article-structure` / `article-image`)
|
|
580
|
+
|
|
581
|
+
## D-022: M2.2 is closed as accepted by construction, with two revisit triggers (decided 2026-09-01)
|
|
582
|
+
- **Category:** scope / sequencing (closes [D-015](#d-015-m22-is-deferred-until-after-m31-not-dropped-decided-2026-08-21); keeps [D-006](#d-006-scheduled-research-runs-post-from-the-primary-model-leg-or-not-at-all) as the standing rule)
|
|
583
|
+
- **Decided:** `scheduled-task-model-leg-policy` (M2.2) is **closed**, not
|
|
584
|
+
built. The property it was written for — a scheduled research run posts from
|
|
585
|
+
the primary leg or not at all — has held by construction since the
|
|
586
|
+
2026-08-20 pin: every space carries a `model.active` row, the runtime turns
|
|
587
|
+
it into a one-element chain, a leg failure is a task error that retries and
|
|
588
|
+
reports to the admin and posts nothing, and the M3.2 ledger records the
|
|
589
|
+
model of every run. The backlog doc moves to `docs/ideas/` with its design
|
|
590
|
+
intact, re-labelled as what the same column is really worth: **per-task
|
|
591
|
+
model choice** (the article on one leg, verify runs on another — the Opus
|
|
592
|
+
A/B that was never started), with the safety property as a side effect.
|
|
593
|
+
- **Revisit triggers (either reopens it, as a prerequisite, not an option):**
|
|
594
|
+
1. anyone clears a `model.active` pin, or a `/model reset` verb is built —
|
|
595
|
+
the fix the `model-switch-disables-fallback` bug asks for. That verb must
|
|
596
|
+
not ship before a scheduled-run guard exists; the bug doc now says so.
|
|
597
|
+
2. the ledger records a scheduled run on a non-primary leg.
|
|
598
|
+
- **Alternatives considered:**
|
|
599
|
+
- *Build it as designed* (nullable `tasks.model_policy`, `primary_only`, a
|
|
600
|
+
`skipped` status, one truncation site). Sound, about a session of work,
|
|
601
|
+
and it buys nothing while the pin policy stands. Rejected for now, not on
|
|
602
|
+
merit.
|
|
603
|
+
- *Keep it deferred a third time.* Rejected: D-015's own revisit condition
|
|
604
|
+
(M3.1 in hand) has been met and the doc's open question — is this worth
|
|
605
|
+
its cost while every space is pinned — had gone unanswered since
|
|
606
|
+
2026-08-20. A story that cannot be scheduled or dropped is noise on the
|
|
607
|
+
roadmap.
|
|
608
|
+
- *Re-cut it as per-task model choice now.* Not rejected — parked as the
|
|
609
|
+
idea, to be planned on its own merits if the article A/B is wanted.
|
|
610
|
+
- **Reasoning:** the pin was a deliberate choice ("Opus-only, no fallback,
|
|
611
|
+
all legs switchable", 2026-08-20), not an accident to be guarded against.
|
|
612
|
+
The cost argument for re-opening died with the digest-replay fix
|
|
613
|
+
(M2 checkpoint, 2026-08-31). What remained was a guarantee against a future
|
|
614
|
+
the owner has not asked for, and that future has a clear trigger. 300-plus
|
|
615
|
+
scheduled runs since the pin, zero on a non-primary leg.
|
|
616
|
+
- **Used by:** football-reporter-profile roadmap (M2.2 row),
|
|
617
|
+
`docs/bugs/model-switch-disables-fallback.md` (the reset-verb constraint),
|
|
618
|
+
`docs/ideas/scheduled-task-model-leg-policy.md`
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
{
|
|
2
|
+
"generatedAt": "2026-09-01T15:16:17.114Z",
|
|
3
|
+
"mode": "live",
|
|
4
|
+
"space": "football-friends",
|
|
5
|
+
"caller": "cli:harness",
|
|
6
|
+
"links": [
|
|
7
|
+
{
|
|
8
|
+
"platform": "whatsapp",
|
|
9
|
+
"kind": "group",
|
|
10
|
+
"title": null
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"startedAt": "2026-09-01T15:04:43.882Z",
|
|
14
|
+
"finishedAt": "2026-09-01T15:16:17.114Z",
|
|
15
|
+
"note": "Numbers are reported, not gated, for the first two weeks (D-008). The offline suite is gated separately via bun test.",
|
|
16
|
+
"totals": {
|
|
17
|
+
"errors": 0,
|
|
18
|
+
"warnings": 0,
|
|
19
|
+
"unknown": 0,
|
|
20
|
+
"mismatches": 0
|
|
21
|
+
},
|
|
22
|
+
"rows": [
|
|
23
|
+
{
|
|
24
|
+
"id": "fresh-official",
|
|
25
|
+
"case": "A club's own announcement, published inside the window. Must be reported, with a source and a dd/mm.",
|
|
26
|
+
"mode": "live",
|
|
27
|
+
"surface": "scan",
|
|
28
|
+
"model": "anthropic:claude-sonnet-5",
|
|
29
|
+
"modelSource": "space_config.model.active",
|
|
30
|
+
"expected": "posts",
|
|
31
|
+
"observed": "posts",
|
|
32
|
+
"observedItemCount": 0,
|
|
33
|
+
"reply": "",
|
|
34
|
+
"findings": [],
|
|
35
|
+
"summary": {
|
|
36
|
+
"errors": 0,
|
|
37
|
+
"warnings": 0,
|
|
38
|
+
"unknown": 0,
|
|
39
|
+
"byRule": {}
|
|
40
|
+
},
|
|
41
|
+
"verdict": "reported"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "stale-real-article",
|
|
45
|
+
"case": "F4's test rather than F4's memory: a real article published forty days ago must be dropped, not reported.",
|
|
46
|
+
"mode": "live",
|
|
47
|
+
"surface": "scan",
|
|
48
|
+
"model": "anthropic:claude-sonnet-5",
|
|
49
|
+
"modelSource": "space_config.model.active",
|
|
50
|
+
"expected": "silence",
|
|
51
|
+
"observed": "posts",
|
|
52
|
+
"observedItemCount": 0,
|
|
53
|
+
"reply": "",
|
|
54
|
+
"findings": [],
|
|
55
|
+
"summary": {
|
|
56
|
+
"errors": 0,
|
|
57
|
+
"warnings": 0,
|
|
58
|
+
"unknown": 0,
|
|
59
|
+
"byRule": {}
|
|
60
|
+
},
|
|
61
|
+
"verdict": "reported"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "undated-item",
|
|
65
|
+
"case": "An item whose publication date cannot be read. Without a date it is deleted, so the run is silent.",
|
|
66
|
+
"mode": "live",
|
|
67
|
+
"surface": "scan",
|
|
68
|
+
"model": "anthropic:claude-sonnet-5",
|
|
69
|
+
"modelSource": "space_config.model.active",
|
|
70
|
+
"expected": "silence",
|
|
71
|
+
"observed": "posts",
|
|
72
|
+
"observedItemCount": 0,
|
|
73
|
+
"reply": "",
|
|
74
|
+
"findings": [],
|
|
75
|
+
"summary": {
|
|
76
|
+
"errors": 0,
|
|
77
|
+
"warnings": 0,
|
|
78
|
+
"unknown": 0,
|
|
79
|
+
"byRule": {}
|
|
80
|
+
},
|
|
81
|
+
"verdict": "reported"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"id": "duplicate-across-outlets",
|
|
85
|
+
"case": "Four outlets carrying one story. The verify prompt asks for them to be merged, so the reply is one item, not four.",
|
|
86
|
+
"mode": "live",
|
|
87
|
+
"surface": "scan",
|
|
88
|
+
"model": "anthropic:claude-sonnet-5",
|
|
89
|
+
"modelSource": "space_config.model.active",
|
|
90
|
+
"expected": "posts-subset",
|
|
91
|
+
"observed": "posts",
|
|
92
|
+
"observedItemCount": 0,
|
|
93
|
+
"reply": "",
|
|
94
|
+
"findings": [],
|
|
95
|
+
"summary": {
|
|
96
|
+
"errors": 0,
|
|
97
|
+
"warnings": 0,
|
|
98
|
+
"unknown": 0,
|
|
99
|
+
"byRule": {}
|
|
100
|
+
},
|
|
101
|
+
"verdict": "reported"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"id": "rumour-tier-3",
|
|
105
|
+
"case": "A rumour from an outlet on the never-cite list. Not reportable at any certainty grade.",
|
|
106
|
+
"mode": "live",
|
|
107
|
+
"surface": "scan",
|
|
108
|
+
"model": "anthropic:claude-sonnet-5",
|
|
109
|
+
"modelSource": "space_config.model.active",
|
|
110
|
+
"expected": "silence",
|
|
111
|
+
"observed": "posts",
|
|
112
|
+
"observedItemCount": 0,
|
|
113
|
+
"reply": "",
|
|
114
|
+
"findings": [],
|
|
115
|
+
"summary": {
|
|
116
|
+
"errors": 0,
|
|
117
|
+
"warnings": 0,
|
|
118
|
+
"unknown": 0,
|
|
119
|
+
"byRule": {}
|
|
120
|
+
},
|
|
121
|
+
"verdict": "reported"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"id": "rumour-tier-1-named",
|
|
125
|
+
"case": "The same shape from a Tier-1 journalist by name. Reportable, but only with an explicit certainty grade.",
|
|
126
|
+
"mode": "live",
|
|
127
|
+
"surface": "scan",
|
|
128
|
+
"model": "anthropic:claude-sonnet-5",
|
|
129
|
+
"modelSource": "space_config.model.active",
|
|
130
|
+
"expected": "posts",
|
|
131
|
+
"observed": "posts",
|
|
132
|
+
"observedItemCount": 0,
|
|
133
|
+
"reply": "",
|
|
134
|
+
"findings": [],
|
|
135
|
+
"summary": {
|
|
136
|
+
"errors": 0,
|
|
137
|
+
"warnings": 0,
|
|
138
|
+
"unknown": 0,
|
|
139
|
+
"byRule": {}
|
|
140
|
+
},
|
|
141
|
+
"verdict": "reported"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"id": "preview-not-an-event",
|
|
145
|
+
"case": "A match preview is speculation, not a concrete new event. The verify prompt rules it out explicitly.",
|
|
146
|
+
"mode": "live",
|
|
147
|
+
"surface": "scan",
|
|
148
|
+
"model": "anthropic:claude-sonnet-5",
|
|
149
|
+
"modelSource": "space_config.model.active",
|
|
150
|
+
"expected": "silence",
|
|
151
|
+
"observed": "posts",
|
|
152
|
+
"observedItemCount": 0,
|
|
153
|
+
"reply": "",
|
|
154
|
+
"findings": [],
|
|
155
|
+
"summary": {
|
|
156
|
+
"errors": 0,
|
|
157
|
+
"warnings": 0,
|
|
158
|
+
"unknown": 0,
|
|
159
|
+
"byRule": {}
|
|
160
|
+
},
|
|
161
|
+
"verdict": "reported"
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"id": "player-ratings-not-an-event",
|
|
165
|
+
"case": "Player ratings are an opinion piece — the exclude list's shape, checked end to end.",
|
|
166
|
+
"mode": "live",
|
|
167
|
+
"surface": "scan",
|
|
168
|
+
"model": "anthropic:claude-sonnet-5",
|
|
169
|
+
"modelSource": "space_config.model.active",
|
|
170
|
+
"expected": "silence",
|
|
171
|
+
"observed": "posts",
|
|
172
|
+
"observedItemCount": 0,
|
|
173
|
+
"reply": "",
|
|
174
|
+
"findings": [],
|
|
175
|
+
"summary": {
|
|
176
|
+
"errors": 0,
|
|
177
|
+
"warnings": 0,
|
|
178
|
+
"unknown": 0,
|
|
179
|
+
"byRule": {}
|
|
180
|
+
},
|
|
181
|
+
"verdict": "reported"
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"id": "agreement-is-not-a-completed-move",
|
|
185
|
+
"case": "Step 4 of the verify prompt: an outlet reporting an agreement is not a completed transfer, and the grade must say so.",
|
|
186
|
+
"mode": "live",
|
|
187
|
+
"surface": "scan",
|
|
188
|
+
"model": "anthropic:claude-sonnet-5",
|
|
189
|
+
"modelSource": "space_config.model.active",
|
|
190
|
+
"expected": "posts",
|
|
191
|
+
"observed": "posts",
|
|
192
|
+
"observedItemCount": 0,
|
|
193
|
+
"reply": "",
|
|
194
|
+
"findings": [],
|
|
195
|
+
"summary": {
|
|
196
|
+
"errors": 0,
|
|
197
|
+
"warnings": 0,
|
|
198
|
+
"unknown": 0,
|
|
199
|
+
"byRule": {}
|
|
200
|
+
},
|
|
201
|
+
"verdict": "reported"
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"id": "lineup-renders-as-a-formation",
|
|
205
|
+
"case": "The one exception to one-line-per-item: a lineup is a formation block, never a sentence of eleven names.",
|
|
206
|
+
"mode": "live",
|
|
207
|
+
"surface": "scan",
|
|
208
|
+
"model": "anthropic:claude-sonnet-5",
|
|
209
|
+
"modelSource": "space_config.model.active",
|
|
210
|
+
"expected": "posts",
|
|
211
|
+
"observed": "posts",
|
|
212
|
+
"observedItemCount": 0,
|
|
213
|
+
"reply": "",
|
|
214
|
+
"findings": [],
|
|
215
|
+
"summary": {
|
|
216
|
+
"errors": 0,
|
|
217
|
+
"warnings": 0,
|
|
218
|
+
"unknown": 0,
|
|
219
|
+
"byRule": {}
|
|
220
|
+
},
|
|
221
|
+
"verdict": "reported"
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
"id": "no-club-takes-no-emoji",
|
|
225
|
+
"case": "An Israeli at a club outside the six gets no emoji at all — absence is correct, and one must not be invented.",
|
|
226
|
+
"mode": "live",
|
|
227
|
+
"surface": "scan",
|
|
228
|
+
"model": "anthropic:claude-sonnet-5",
|
|
229
|
+
"modelSource": "space_config.model.active",
|
|
230
|
+
"expected": "posts",
|
|
231
|
+
"observed": "posts",
|
|
232
|
+
"observedItemCount": 0,
|
|
233
|
+
"reply": "",
|
|
234
|
+
"findings": [],
|
|
235
|
+
"summary": {
|
|
236
|
+
"errors": 0,
|
|
237
|
+
"warnings": 0,
|
|
238
|
+
"unknown": 0,
|
|
239
|
+
"byRule": {}
|
|
240
|
+
},
|
|
241
|
+
"verdict": "reported"
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"id": "empty-window",
|
|
245
|
+
"case": "The commonest run of all: nothing matched, so the reply is exactly NO_UPDATE and nothing reaches the group.",
|
|
246
|
+
"mode": "live",
|
|
247
|
+
"surface": "scan",
|
|
248
|
+
"model": "anthropic:claude-sonnet-5",
|
|
249
|
+
"modelSource": "space_config.model.active",
|
|
250
|
+
"expected": "silence",
|
|
251
|
+
"observed": "posts",
|
|
252
|
+
"observedItemCount": 0,
|
|
253
|
+
"reply": "",
|
|
254
|
+
"findings": [],
|
|
255
|
+
"summary": {
|
|
256
|
+
"errors": 0,
|
|
257
|
+
"warnings": 0,
|
|
258
|
+
"unknown": 0,
|
|
259
|
+
"byRule": {}
|
|
260
|
+
},
|
|
261
|
+
"verdict": "reported"
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"id": "register-joke-instruction",
|
|
265
|
+
"case": "D-016: a message built like an instruction but said as a joke. Answered as banter, in at most two lines, with no permission process.",
|
|
266
|
+
"mode": "live",
|
|
267
|
+
"surface": "chat",
|
|
268
|
+
"model": "anthropic:claude-sonnet-5",
|
|
269
|
+
"modelSource": "space_config.model.active",
|
|
270
|
+
"expected": "chat",
|
|
271
|
+
"observed": "chat",
|
|
272
|
+
"reply": "תשכנע קודם שמישהו פה שואל על משהו חוץ מהפועל.",
|
|
273
|
+
"findings": [],
|
|
274
|
+
"summary": {
|
|
275
|
+
"errors": 0,
|
|
276
|
+
"warnings": 0,
|
|
277
|
+
"unknown": 0,
|
|
278
|
+
"byRule": {}
|
|
279
|
+
},
|
|
280
|
+
"verdict": "reported"
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
"id": "wrong-colour-emoji-regression",
|
|
284
|
+
"case": "D-020: the live 🟡-for-Beitar case, driven from the input side. Beitar's emoji is 💛 and nothing else.",
|
|
285
|
+
"mode": "live",
|
|
286
|
+
"surface": "scan",
|
|
287
|
+
"model": "anthropic:claude-sonnet-5",
|
|
288
|
+
"modelSource": "space_config.model.active",
|
|
289
|
+
"expected": "posts",
|
|
290
|
+
"observed": "posts",
|
|
291
|
+
"observedItemCount": 0,
|
|
292
|
+
"reply": "",
|
|
293
|
+
"findings": [],
|
|
294
|
+
"summary": {
|
|
295
|
+
"errors": 0,
|
|
296
|
+
"warnings": 0,
|
|
297
|
+
"unknown": 0,
|
|
298
|
+
"byRule": {}
|
|
299
|
+
},
|
|
300
|
+
"verdict": "reported"
|
|
301
|
+
}
|
|
302
|
+
]
|
|
303
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Fixture harness — first live pass, 2026-09-01
|
|
2
|
+
|
|
3
|
+
Raw report: [`harness-2026-09-01.json`](harness-2026-09-01.json), written by
|
|
4
|
+
`scripts/football-harness.ts --live --space football-friends --allow-linked`
|
|
5
|
+
from `~/whatsapp-bot`, script mirrored from the M3.1 branch at `78a7954`.
|
|
6
|
+
The running server was `dev` — see the reading note on `observed` below.
|
|
7
|
+
|
|
8
|
+
| | |
|
|
9
|
+
|---|---|
|
|
10
|
+
| Space | `football-friends`, linked to one WhatsApp group (owner the only member, 2026-09-01) |
|
|
11
|
+
| Model | `anthropic:claude-sonnet-5` on every row (`space_config.model.active`) |
|
|
12
|
+
| Window | 15:04:43 – 15:16:17 UTC (11½ min, 14 runs) |
|
|
13
|
+
| Cost | **$1.87** over 14 `token_usage` rows (`ids 1389–1402`) — inside the $2–4 estimate |
|
|
14
|
+
| Lint | 0 errors, 0 warnings, 0 unverifiable |
|
|
15
|
+
|
|
16
|
+
## What the bot did
|
|
17
|
+
|
|
18
|
+
| Fixture | Expected | Bot | Read as |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
| `fresh-official` | posts | `NO_UPDATE` | could not open the invented link → silence (see below) |
|
|
21
|
+
| `stale-real-article` | silence | `NO_UPDATE` | ✓ consistent — but silence was over-determined, see below |
|
|
22
|
+
| `undated-item` | silence | `NO_UPDATE` | ✓ same caveat |
|
|
23
|
+
| `duplicate-across-outlets` | posts-subset | `NO_UPDATE` | invented links |
|
|
24
|
+
| `rumour-tier-3` | silence | `NO_UPDATE` | ✓ same caveat |
|
|
25
|
+
| `rumour-tier-1-named` | posts | `NO_UPDATE` | invented link |
|
|
26
|
+
| `preview-not-an-event` | silence | `NO_UPDATE` | ✓ same caveat |
|
|
27
|
+
| `player-ratings-not-an-event` | silence | `NO_UPDATE` | ✓ same caveat |
|
|
28
|
+
| `agreement-is-not-a-completed-move` | posts | `NO_UPDATE` | invented link |
|
|
29
|
+
| `lineup-renders-as-a-formation` | posts | `NO_UPDATE` | invented link |
|
|
30
|
+
| `no-club-takes-no-emoji` | posts | `NO_UPDATE` | invented link |
|
|
31
|
+
| `empty-window` | silence | `NO_UPDATE` | ✓ the one silence case with no caveat |
|
|
32
|
+
| `register-joke-instruction` | chat | `תשכנע קודם שמישהו פה שואל על משהו חוץ מהפועל.` | ✓ banter, one line, no permission process (D-016) |
|
|
33
|
+
| `wrong-colour-emoji-regression` | posts | `NO_UPDATE` | invented link |
|
|
34
|
+
|
|
35
|
+
Evidence that every scan row was the sentinel and not an empty run: the
|
|
36
|
+
service journal logs `Chat turn answered with silence — sending nothing …
|
|
37
|
+
suppressed=NO_UPDATE` for user rows 4050–4061 and 4064, one per fixture.
|
|
38
|
+
|
|
39
|
+
**The finding is about the fixtures, not the bot.** Step 1 of the production
|
|
40
|
+
verify prompt is "open the link", and every fixture link is fabricated. The
|
|
41
|
+
ledger shows the bot trying — `fresh-official` read 730 K cached tokens across
|
|
42
|
+
its rounds, `wrong-colour-emoji-regression` 1.06 M — and then, correctly,
|
|
43
|
+
reporting nothing it could not verify. That means the seven "expected posts"
|
|
44
|
+
rows cannot pass as written, and the six silence rows are consistent with the
|
|
45
|
+
standard but do not prove the rule they were written for (a stale item would
|
|
46
|
+
have been dropped for its date *or* for its dead link — the run cannot say
|
|
47
|
+
which). Filed as `docs/ideas/football-harness-live-fixtures-need-real-links.md`.
|
|
48
|
+
|
|
49
|
+
## Reading `observed` in the JSON
|
|
50
|
+
|
|
51
|
+
The JSON says `observed: "posts", observedItemCount: 0` on every scan row.
|
|
52
|
+
That is a harness defect found by this pass: the runtime turns a `NO_UPDATE`
|
|
53
|
+
chat reply into `""` (`runtime.ts`, chat silence) and the `/chat` route on
|
|
54
|
+
`dev` did not pass the runtime's `silent` flag through, so the harness saw an
|
|
55
|
+
empty string, which `isSilenceSentinel` does not match. Fixed in the same
|
|
56
|
+
branch — `/chat` now returns `silent: true` and the harness records `silence`
|
|
57
|
+
for it and `empty` for a reply that is `""` without the flag. The JSON is
|
|
58
|
+
kept as written; this note is the correction.
|
|
59
|
+
|
|
60
|
+
## What the pass left behind, and the cleanup
|
|
61
|
+
|
|
62
|
+
- 15 `messages` rows (`source='cli'`, ids 4050–4064) — **deleted** with the
|
|
63
|
+
printed `DELETE`, bounded by the run window; the five older `cli` rows in
|
|
64
|
+
the space were untouched.
|
|
65
|
+
- No file under the space's `knowledge/` or `MEMORY.md` was modified during
|
|
66
|
+
the window (`find -newermt` over the workspace), so the notebook needed no
|
|
67
|
+
re-seed. napkin's next nightly pass has nothing of the harness to read.
|
|
68
|
+
- `space_roles` gained `cli:harness` as `admin` (`granted_by='seed'`), the
|
|
69
|
+
same seeding any `mercury chat` caller gets. **Deleted 2026-09-01 18:57 IDT**
|
|
70
|
+
(one row, backup at `~/db-backups/2026-09-01-drop-harness-role/state.db`);
|
|
71
|
+
the space is back to 2 admins / 8 members and the two legitimate `cli:user`
|
|
72
|
+
rows were left alone. The next live pass re-creates it — the row is a
|
|
73
|
+
by-product of `/chat` seeding its caller, not something the harness needs
|
|
74
|
+
to keep.
|
|
75
|
+
- Backup taken before the run: `~/db-backups/2026-09-01-harness/state.db`.
|