mercury-agent 0.16.2 → 0.17.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 +12 -0
- package/container/Dockerfile +1 -0
- package/container/Dockerfile.base +1 -0
- package/docs/autopilot/config.yaml +43 -0
- package/docs/behavior-layers.md +26 -12
- package/docs/container-lifecycle.md +1 -0
- package/docs/extensions.md +43 -0
- package/docs/goals/football-reporter-profile/decisions.md +37 -1
- package/docs/goals/football-reporter-profile/roadmap.md +63 -5
- package/docs/permissions.md +15 -0
- package/docs/profile-guide.md +304 -18
- package/docs/skills-guide.md +24 -0
- package/examples/extensions/feed-watch/config.ts +36 -0
- package/examples/extensions/feed-watch/digest.ts +230 -32
- package/examples/extensions/feed-watch/index.ts +8 -0
- package/examples/extensions/feed-watch/items.ts +48 -4
- package/examples/extensions/overview/actions.ts +264 -0
- package/examples/extensions/overview/gate.ts +82 -0
- package/examples/extensions/overview/index.ts +123 -0
- package/examples/extensions/overview/skill/SKILL.md +80 -0
- package/examples/profiles/_template/config.yaml +34 -0
- package/examples/profiles/football-reporter/AGENTS.md +24 -6
- package/examples/profiles/football-reporter/README.md +9 -1
- package/examples/profiles/football-reporter/config.yaml +31 -2
- package/examples/profiles/football-reporter/tasks/daily-article.md +6 -4
- package/package.json +7 -3
- package/resources/templates/mercury.example.yaml +5 -0
- package/src/adapters/whatsapp-media.ts +80 -3
- package/src/agent/container-entry.ts +143 -21
- package/src/agent/container-env.ts +11 -0
- package/src/agent/container-error.ts +12 -1
- package/src/agent/container-runner.ts +91 -9
- package/src/agent/pi-jsonl-parser.ts +166 -5
- package/src/bridges/discord.ts +4 -0
- package/src/bridges/slack.ts +4 -0
- package/src/bridges/teams.ts +4 -0
- package/src/bridges/telegram.ts +33 -1
- package/src/cli/build-image-tags.ts +34 -0
- package/src/cli/mercury.ts +51 -4
- package/src/config-file.ts +6 -0
- package/src/config.ts +28 -3
- package/src/core/attachment-notes.ts +4 -0
- package/src/core/confirmation.ts +3 -1
- package/src/core/handler.ts +2 -1
- package/src/core/media.ts +104 -2
- package/src/core/outbox.ts +23 -3
- package/src/core/reply-context.ts +61 -0
- package/src/core/routes/chat.ts +70 -3
- package/src/core/routes/config-builtin.ts +7 -0
- package/src/core/routes/dashboard.ts +91 -5
- package/src/core/runtime.ts +170 -22
- package/src/core/task-output.ts +36 -18
- package/src/core/task-scheduler.ts +18 -1
- package/src/extensions/catalog.ts +1 -0
- package/src/extensions/hooks.ts +8 -0
- package/src/extensions/loader.ts +68 -7
- package/src/extensions/types.ts +35 -2
- package/src/main.ts +19 -0
- package/src/server.ts +15 -8
- package/src/storage/db.ts +231 -12
- package/src/text/sanitize-text.ts +118 -0
- package/src/types.ts +73 -1
package/docs/profile-guide.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
This is the how-to for turning Mercury into *a particular assistant*: a
|
|
4
4
|
barber's booking line, a copywriter's studio assistant, a football reporter in
|
|
5
5
|
a friends' group, a trial-tier business helper. It is written from the
|
|
6
|
-
profiles that exist — the
|
|
7
|
-
(barber, lawyers, copywriter, trial) and the football reporter in this repo —
|
|
6
|
+
profiles that exist — the five in `tagula-agents/tagula-agent-profiles`
|
|
7
|
+
(barber, lawyers, copywriter, trial, investor) and the football reporter in this repo —
|
|
8
8
|
and from what went wrong while building them. Where it states how Mercury
|
|
9
9
|
behaves, that was verified against the code and the running agent image on
|
|
10
10
|
2026-08-21; the pointers are there so you can re-verify.
|
|
@@ -44,7 +44,7 @@ image — not against the diff.
|
|
|
44
44
|
| | Applicative profile (`mercury-profile.yaml`) | Space profile (`config.yaml`) |
|
|
45
45
|
|---|---|---|
|
|
46
46
|
| Scope | the whole deployment: every space, every DM auto-space | one space |
|
|
47
|
-
| What it carries | global `AGENTS.md`, extensions (capabilities, CLIs, skills), **exhaustive `member_permissions`**, `profile_prompt`, required capabilities, env | the space `AGENTS.md`, task prompts by live id, extension config keys, preferences to delete, workspace files to remove |
|
|
47
|
+
| What it carries | global `AGENTS.md`, extensions (capabilities, CLIs, skills), **exhaustive `member_permissions`**, `profile_prompt`, required capabilities, env | the space `AGENTS.md`, task prompts by live id, extension config keys (incl. `role.member.permissions`, `persona.exclusive`), preferences to delete, workspace files to remove, **workspace files to seed once** (`MEMORY.md`, episode notes, a napkin addendum) |
|
|
48
48
|
| Applied with | `mercury profiles apply <dir>` (non-interactive) or `mercury setup --profile`; needs a restart, and an image rebuild if it adds CLIs | `bun scripts/space-profile.ts apply --project <dir> --profile <dir>`; no restart |
|
|
49
49
|
| Verified with | `mercury doctor`, a DM from a non-admin number, the startup warning about excluded member permissions | `space-profile check` (exit 1 + diff on drift); runs on every `~/sync-mercury.sh` |
|
|
50
50
|
| Loader | `src/core/profiles.ts` | `src/profile/space-profile.ts` |
|
|
@@ -134,9 +134,20 @@ reads top-down and the first sections frame the rest.
|
|
|
134
134
|
## Silence when to say nothing and how (NO_UPDATE, "אין חדש")
|
|
135
135
|
## Integrity about what was done "I wrote it" only after a successful write; who can change what
|
|
136
136
|
## Dedupe / memory routine which file to read before posting and append after
|
|
137
|
+
## Privacy "you may see it, you may not keep it" — never a denial of what is visible
|
|
138
|
+
## What this space does NOT do one line each; the reader's expectations, closed explicitly
|
|
137
139
|
## Overrides of platform text the explicit sentences (see 4.4)
|
|
138
140
|
```
|
|
139
141
|
|
|
142
|
+
Two of those sections are easy to skip and expensive to skip. **Privacy** phrased
|
|
143
|
+
as a denial makes the bot lie — the investor profile's line is the honest
|
|
144
|
+
shape: *"you do see the recent messages and the senders' names — do not deny
|
|
145
|
+
it and do not answer 'I have no information'; what is forbidden is to
|
|
146
|
+
**retain** it"* (no names in memory, notes or ledger). **What this space does
|
|
147
|
+
not do** closes the door the persona leaves open: no portfolio, no scheduled
|
|
148
|
+
posts, no legal advice, "you cannot send a message on your own initiative" —
|
|
149
|
+
each one a promise the bot would otherwise make.
|
|
150
|
+
|
|
140
151
|
Write in the language the bot speaks. Keep every rule **testable from the
|
|
141
152
|
transcript** ("never more than 3 time slots in one message") rather than
|
|
142
153
|
aspirational ("be helpful").
|
|
@@ -154,6 +165,14 @@ Compare:
|
|
|
154
165
|
The second names the tool, the trigger, the reason, and the edge case
|
|
155
166
|
(volunteering). That is what survives contact with a model under pressure.
|
|
156
167
|
|
|
168
|
+
**Attach the failure mode, especially when it is invisible.** The most
|
|
169
|
+
consistent habit across the tagula profiles is a clause after the rule saying
|
|
170
|
+
what goes wrong and why nobody would notice: *"an invented date poisons the
|
|
171
|
+
ledger silently — the record looks perfectly fine, only its score is wrong"*;
|
|
172
|
+
*"no error, the skill just never fires"*; *"the file still looks right"*. A
|
|
173
|
+
model told *why* a shortcut is dangerous takes it less often than one told
|
|
174
|
+
not to; and the sentence doubles as the test case for the fixture harness.
|
|
175
|
+
|
|
157
176
|
### 4.3 The no-clock rule (every profile needs one)
|
|
158
177
|
|
|
159
178
|
The model has a `Current date` line (UTC) and a shell; it has no reliable
|
|
@@ -191,10 +210,17 @@ win by being specific:
|
|
|
191
210
|
default global `AGENTS.md` — a reporter that must give a full factual
|
|
192
211
|
answer, or a bot that must not ask before acting, says so.
|
|
193
212
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
213
|
+
Two of the four voices can be removed instead of out-argued. A space that owns
|
|
214
|
+
its whole standard sets `persona.exclusive: "true"` in `extension_config`
|
|
215
|
+
(shipped 2026-08-25, `behavior-layers.md` §1): the host then leaves the
|
|
216
|
+
owner's global Bot Character out of the prompt and does not mount the global
|
|
217
|
+
`AGENTS.md` (`mercury init`'s "helpful AI assistant… ask before acting") for
|
|
218
|
+
that space. The platform paragraphs above still arrive and still need their
|
|
219
|
+
override sentences; and the two capability paragraphs ("set a preference",
|
|
220
|
+
"warn then mute") are already left out for a caller whose permissions cannot
|
|
221
|
+
use them, so a member without `prefs.set` is no longer told to ask the admin.
|
|
222
|
+
Removing a voice beats ranking it — precedence is a coin flip
|
|
223
|
+
(`whatsapp-bot-hardening` A-3) and a fifth voice does not fix four.
|
|
198
224
|
|
|
199
225
|
### 4.5 Contradictions inside the file
|
|
200
226
|
|
|
@@ -215,6 +241,13 @@ failure turned into a rule with the exact wording to use. That is how a
|
|
|
215
241
|
profile improves: a fixture fails → one sentence in the file → the fixture
|
|
216
242
|
passes. Keep the sentences, drop the narrative.
|
|
217
243
|
|
|
244
|
+
Date the rules that came from a decision — the investor skill writes
|
|
245
|
+
*"Actionable, not flattering (est. 2026-08-11)"* and *"Horizons (approved
|
|
246
|
+
2026-08-22)"* in the headings. A dated rule can be revisited; an undated one
|
|
247
|
+
is argued from memory. The football profile keeps the dates in
|
|
248
|
+
`decisions.md` instead (every `D-0NN` names the day and its `Revisit if:`),
|
|
249
|
+
which works as long as the file's section names match the decision ids.
|
|
250
|
+
|
|
218
251
|
---
|
|
219
252
|
|
|
220
253
|
## 5. Accuracy: put the computation in code
|
|
@@ -257,6 +290,17 @@ And the honest fallback, in the global character and every profile: *"If you
|
|
|
257
290
|
are not sure, say so instead of guessing. Never say you searched, read or
|
|
258
291
|
changed anything unless you just ran the tool that does it."*
|
|
259
292
|
|
|
293
|
+
**Make the bot measurable, not just fluent.** A profile that issues
|
|
294
|
+
judgements — verdicts, predictions, "official / here-we-go / rumour" grades —
|
|
295
|
+
records each one as an append-only row with a horizon and a due date, and
|
|
296
|
+
re-analysis writes a *new* row that `supersedes` the old, never an edit. The
|
|
297
|
+
investor profile does it with a tracker (`skills/stock-analysis/SKILL.md` §8:
|
|
298
|
+
one `add` per verdict, a 14-day duplicate rule, "never record who asked");
|
|
299
|
+
the football reporter does it with the notebook's `## Current State` /
|
|
300
|
+
`## History` split plus the host's `task_runs` ledger. Either way the bot's
|
|
301
|
+
own claims become something a script can grade a month later, which is what
|
|
302
|
+
turns "the bot was wrong about X" from an anecdote into a rate.
|
|
303
|
+
|
|
260
304
|
---
|
|
261
305
|
|
|
262
306
|
## 6. Memory: design it, do not hope for it
|
|
@@ -296,12 +340,15 @@ What that means for a profile:
|
|
|
296
340
|
in the engine. "The tracker is the source of truth — never answer 'what is
|
|
297
341
|
the status?' from memory."
|
|
298
342
|
6. **Size the history window to the space.** `context.mode` defaults to
|
|
299
|
-
`clear` for a hand-created space; `context.window_size` counts
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
343
|
+
`clear` for a hand-created space; `context.window_size` counts turns of
|
|
344
|
+
*conversation* — a person's message (reply included), or one of the bot's
|
|
345
|
+
own scheduled posts. Since `chat-window-and-silence` (M2.5) a scheduled
|
|
346
|
+
run's prompt is not in the window at all, so procedure can no longer crowd
|
|
347
|
+
out chat — which is how the football bot once contradicted its own lineup.
|
|
348
|
+
The bot's scheduled *posts* still hold a slot each, so a space that mostly
|
|
349
|
+
publishes still fills its window with its own articles: size the window
|
|
350
|
+
for how far back a follow-up should reach, and rely on the notebook, not
|
|
351
|
+
the window, for facts.
|
|
305
352
|
|
|
306
353
|
---
|
|
307
354
|
|
|
@@ -319,6 +366,24 @@ rules are:
|
|
|
319
366
|
- When the host can do the work, prefer a **marker the host expands**
|
|
320
367
|
(`[feed-watch:digest]`) over a skill the model must remember to run.
|
|
321
368
|
- Verify use with a trace (`agent.trace_runs`), not with a correct answer.
|
|
369
|
+
- **Formatting is a skill plus a lint, sharing one rule list.** The investor
|
|
370
|
+
profile moves every platform-rendering rule out of `AGENTS.md` into
|
|
371
|
+
`skills/whatsapp-formatting/SKILL.md` — "load this before composing *any*
|
|
372
|
+
message; not optional" — and keeps a one-line digest in `AGENTS.md` so the
|
|
373
|
+
rule survives an unread file. The skill is **grounded in the send
|
|
374
|
+
pipeline** and says so: Mercury's `applyRtlDirection` (`src/text/rtl.ts`)
|
|
375
|
+
strips leading bidi marks and prepends its own RLM per line, so a skill
|
|
376
|
+
telling the model to add RLE/RLM is telling it to do work the host
|
|
377
|
+
discards; the rule that remains is "start every line with a Hebrew word or
|
|
378
|
+
an emoji, never a ticker or a number". Likewise `normalizeChatMarkdown`
|
|
379
|
+
(`src/text/markdown.ts`) rewrites `**` and headings at send — the DB row is
|
|
380
|
+
what the model wrote, not what WhatsApp showed. Before writing a formatting
|
|
381
|
+
rule, read those two files and state which rules the host already
|
|
382
|
+
enforces. The countable rules (line length, emoji policy, `(source, dd/mm)`
|
|
383
|
+
per item, no HTML, no `**`) live in exactly one list that both the skill's
|
|
384
|
+
"checklist before sending" and the output lint (§11) are generated from —
|
|
385
|
+
tagula has the skill and no lint; the football harness is the first lint,
|
|
386
|
+
and its spec must not drift from the `## פורמט` section it measures.
|
|
322
387
|
|
|
323
388
|
---
|
|
324
389
|
|
|
@@ -344,6 +409,23 @@ rules are:
|
|
|
344
409
|
cron in the README; `check` reports a deleted row as drift.
|
|
345
410
|
- Restarting the service kills in-flight runs and loses the retry — check
|
|
346
411
|
the crons before a restart.
|
|
412
|
+
- **A quiet run is not a cheap run.** Every scheduled run — a verify one-shot
|
|
413
|
+
that answers `NO_UPDATE`, a scan that finds nothing — assembles the same
|
|
414
|
+
prompt stack as chat and pays for it before it says a word: the space
|
|
415
|
+
`AGENTS.md` (17 KB for the football reporter), `MEMORY.md`, the injected
|
|
416
|
+
notes, the digest, plus whatever the notebook rule makes it read. Feed-watch
|
|
417
|
+
replaced five blind scans a day (~$3) with ~46 verify runs a day, 70 % of
|
|
418
|
+
them `no_update` at $0.25–0.75 each — **$14–20/day** for one space. Budget a
|
|
419
|
+
profile by *runs per day × cost per run* from the ledger after the first
|
|
420
|
+
day, and cap the number of runs (`max_per_hour`, `batch_minutes`, a tighter
|
|
421
|
+
`exclude`, quiet hours) before touching prompt length. A host-side
|
|
422
|
+
pre-filter that opens no container is the only run that is free.
|
|
423
|
+
- **Model is per space and changes without a deploy.** `model.active` is a
|
|
424
|
+
space-config row the owner can flip from chat, and a profile tuned on one
|
|
425
|
+
model behaves differently on another — Sonnet narrates "Now sending the
|
|
426
|
+
chat reply." into the sent text where Opus did not. The ledger records the
|
|
427
|
+
model per run; read it before blaming the prompt, and re-run the fixtures
|
|
428
|
+
after a switch.
|
|
347
429
|
|
|
348
430
|
---
|
|
349
431
|
|
|
@@ -400,8 +482,37 @@ live `AGENTS.md` or a task row by hand — `check` calls it drift and the next
|
|
|
400
482
|
tests/ runbook + fixtures
|
|
401
483
|
```
|
|
402
484
|
|
|
403
|
-
Lessons from
|
|
404
|
-
|
|
485
|
+
Lessons from five deployments of this shape (barber, lawyers, copywriter,
|
|
486
|
+
trial, investor):
|
|
487
|
+
|
|
488
|
+
- **Split every file by one question — *does the agent write this at
|
|
489
|
+
runtime?*** Profile-owned files (`AGENTS.md`, task prompts, skills,
|
|
490
|
+
preferences, config keys) are replaced on every install: shipping a newer
|
|
491
|
+
one *is* the upgrade. Agent-writable files (`MEMORY.md`, `knowledge/`, a
|
|
492
|
+
registry, a ledger) are seeded once and never clobbered — overwriting one
|
|
493
|
+
destroys data that exists nowhere else, and there is no flag that makes
|
|
494
|
+
that safe. In a space profile the line is `agents_md`/`tasks`/
|
|
495
|
+
`extension_config` versus `workspace_seed`; `check` reports a changed
|
|
496
|
+
profile-owned file as drift and a changed seed as the feature working.
|
|
497
|
+
- **Ledgers and databases never ship.** A profile creates them empty at
|
|
498
|
+
install and a gate fails on any committed `*.db` — that is the exact
|
|
499
|
+
mechanism by which one client's records would be installed into another
|
|
500
|
+
client's space. Paths the runtime creates (`outbox/`, `trackers/`) are
|
|
501
|
+
declared per profile (`runtime-paths.txt`) so the "every referenced path
|
|
502
|
+
exists" check can exempt them *and print the exemption count*.
|
|
503
|
+
- **A profile installed into several spaces authors owner-dependent prose
|
|
504
|
+
twice**, in `<!-- OWNER:BEGIN -->…<!-- NOOWNER:BEGIN -->…` blocks, and the
|
|
505
|
+
installer deletes the losing block — never substitutes an empty string into
|
|
506
|
+
a sentence that then reads as a bug. A gate checks the blocks balance,
|
|
507
|
+
scanning only the file set the installer rewrites.
|
|
508
|
+
- **Stage, verify, then write.** The investor installer builds the whole tree
|
|
509
|
+
in a temp dir, asserts no placeholder and no marker survives and every
|
|
510
|
+
absolute `/spaces/<id>/…` path it names exists in the stage, and only then
|
|
511
|
+
touches the live space — a failure leaves the space untouched rather than
|
|
512
|
+
half-installed. It also reads the last `messages` row from `state.db` and
|
|
513
|
+
refuses to overwrite while a turn is open: the space is mounted per run, so
|
|
514
|
+
a mid-turn write lands half the old persona and half the new one on a
|
|
515
|
+
request already executing, and nothing reports it.
|
|
405
516
|
- **Typecheck the extension against the deployed layout, not the source
|
|
406
517
|
tree.** Mercury copies only the extension dir; a deep relative import into
|
|
407
518
|
a sibling engine compiles locally and fails on the box. tagula's
|
|
@@ -428,12 +539,35 @@ Lessons from four deployments of this shape:
|
|
|
428
539
|
runbook and `test-full.sh`; the football goal has
|
|
429
540
|
`football-reporter-fixture-harness` planned. Probe callers must be
|
|
430
541
|
dedicated — probes spend the same per-caller daily budget as the test.
|
|
542
|
+
1b. **Two tiers, named as such.** The deterministic tier (engine unit tests,
|
|
543
|
+
the output lint) is the *done gate*; the live conversational tier is
|
|
544
|
+
*post-deploy evidence* — a deferred live run is then a tracked residual
|
|
545
|
+
risk, not an assumed pass. The tagula scenario suite is explicit that it
|
|
546
|
+
is "a data gatherer, NOT a pass/fail test suite": `id|severity|category|
|
|
547
|
+
message|expected_behavior` rows (196 for the barber), one dedicated caller
|
|
548
|
+
per stateful flow so conversations do not pollute each other, a cleanup
|
|
549
|
+
flow for anything that creates real state, a circuit breaker after three
|
|
550
|
+
consecutive failures, incremental skipping via `passed-ids.txt`, and a
|
|
551
|
+
JSON report carrying reply + raw response + timing per scenario. A row is
|
|
552
|
+
"passed" when a reply was *collected*; judging it is a separate step —
|
|
553
|
+
parallel LLM graders into PASS / PARTIAL / FAIL per category, findings
|
|
554
|
+
split into **engine bugs** (get a test) and **persona fixes** (get a line
|
|
555
|
+
in `AGENTS.md`). Every scenario the harness cannot really exercise (a
|
|
556
|
+
voice note sent as text, a state-dependent slot) is labelled so the
|
|
557
|
+
report says "state-dependent, not a bug" instead of red.
|
|
431
558
|
2. **Trace the runs you are tuning** (`agent.trace_runs: true`, image
|
|
432
559
|
rebuild, bounded window). A trace answers "which sources did it open, did
|
|
433
560
|
it read the skill, where did the time go"; a reply does not.
|
|
434
561
|
3. **Drift-check after every deploy** (`space-profile check`,
|
|
435
562
|
`compose-grounding.sh check`, `check-extensions.sh`). A gate whose
|
|
436
|
-
coverage is a hand-maintained list will shrink; derive it from disk
|
|
563
|
+
coverage is a hand-maintained list will shrink; derive it from disk, and
|
|
564
|
+
require every profile to be either covered or exempted *with a written
|
|
565
|
+
reason* in the gate itself. Two checks tagula's `check-profile-skills.sh`
|
|
566
|
+
runs that `space-profile check` does not yet: every absolute
|
|
567
|
+
`/spaces/<id>/…` path an `AGENTS.md` or `SKILL.md` names must resolve
|
|
568
|
+
("an agent told to load this finds nothing, and NOTHING reports it"), and
|
|
569
|
+
every shipped skill directory must be named by the file that indexes it
|
|
570
|
+
(`ideas/space-profile-check-paths-and-inflight-guard.md`).
|
|
437
571
|
4. **Read the rendered output the way the user does** — on a phone, in the
|
|
438
572
|
group, right-to-left. A mirrored RTL grid and a three-paragraph "short"
|
|
439
573
|
answer both pass every structural test.
|
|
@@ -442,10 +576,143 @@ Lessons from four deployments of this shape:
|
|
|
442
576
|
restart).
|
|
443
577
|
6. **Before calling it done**, the manual check is in
|
|
444
578
|
`docs/pending-verification.md` and the change has run on the live bot.
|
|
579
|
+
7. **Read status from the box, never from the planning docs.** On 2026-08-31
|
|
580
|
+
three docs said "deploy not yet run" and "space paused" while
|
|
581
|
+
`space_config` said feed-watch had been on for six days, the ledger held
|
|
582
|
+
250 runs, and `check` was red on an owner edit nobody had recorded. The
|
|
583
|
+
status of a profile is: `space-profile check`, `SELECT … FROM task_runs`
|
|
584
|
+
grouped by day and outcome, `model.active`, and the newest assistant rows
|
|
585
|
+
— then the docs are corrected to match.
|
|
586
|
+
|
|
587
|
+
---
|
|
588
|
+
|
|
589
|
+
## 12. The plan shape — from audit to regression
|
|
590
|
+
|
|
591
|
+
The football profile was built as a four-milestone goal, and the sequence is
|
|
592
|
+
the reusable part. It is the order in which each step makes the next one
|
|
593
|
+
*measurable*; skipping ahead is how a change ships that nobody can tell
|
|
594
|
+
worked.
|
|
595
|
+
|
|
596
|
+
```
|
|
597
|
+
0. Audit read N hours of the real chat; one row per defect with the
|
|
598
|
+
message id, the rule it broke, and the layer that owned it
|
|
599
|
+
1. One owner profile folder + AGENTS.md + apply/check; zero code change
|
|
600
|
+
to the bot; every rule now has one home and a drift check
|
|
601
|
+
2. Deterministic put code in front of the model: a feed, an engine, a
|
|
602
|
+
input host-expanded marker — so the model verifies instead of
|
|
603
|
+
discovering
|
|
604
|
+
3. Proof a ledger (one row per run: model, cost, outcome) BEFORE the
|
|
605
|
+
change it measures; a pure output lint; fixtures through the
|
|
606
|
+
real prompts in a private space; numbers reported, not gated
|
|
607
|
+
4. Regression one check per audit defect inside `bun run check`; a
|
|
608
|
+
re-audit note two weeks later, read from the ledger first
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
Rules that came with it, each from a decision that was tested against a real
|
|
612
|
+
failure (`docs/goals/football-reporter-profile/decisions.md`):
|
|
613
|
+
|
|
614
|
+
- **Instrument before the change** (D-012): the ledger was built ahead of
|
|
615
|
+
feed-watch so the before existed. It is what showed, ten days later, that
|
|
616
|
+
the deterministic feed cost five times the blind scans.
|
|
617
|
+
- **Mechanisms over rules** (D-016): a defect that comes from *what the model
|
|
618
|
+
could see* is fixed by changing what it can see — a notebook it reads by
|
|
619
|
+
rule, a voice removed from the stack — never by a rule about the case. The
|
|
620
|
+
test: if the fix is a sentence in `AGENTS.md` describing the failure, it is
|
|
621
|
+
edge-case chasing.
|
|
622
|
+
- **Measure before gating** (D-008): a lint reports for two weeks before it
|
|
623
|
+
blocks anything; thresholds come from the measured low end, not the median.
|
|
624
|
+
- **Bugs on a parallel track** (D-007): a defect found while building a story
|
|
625
|
+
is filed, not fixed in place; the story's regression check lands in M4.
|
|
626
|
+
- **Checkpoints are countable** ("0 emoji", "1 `#` title", "one model call per
|
|
627
|
+
run confirmed in `token_usage`", "$/day reported") and are passed on
|
|
628
|
+
evidence from the box, with the failures written down next to the passes —
|
|
629
|
+
the M2 checkpoint records a failed cost direction, which is what makes it
|
|
630
|
+
worth having.
|
|
631
|
+
- **A decision log with `Revisit if:`** on every entry. Half the football
|
|
632
|
+
decisions were revisited within ten days (D-013 refined D-003, D-019 re-cut
|
|
633
|
+
M2.3, D-020 reversed the M1 emoji rule) — a log without the trigger written
|
|
634
|
+
down is a log that gets argued from memory.
|
|
635
|
+
|
|
636
|
+
---
|
|
637
|
+
|
|
638
|
+
## 13. Operating a live profile
|
|
639
|
+
|
|
640
|
+
What changes once the profile is running and someone else is in the room.
|
|
641
|
+
|
|
642
|
+
- **The owner will edit the live file.** It is the fastest tuning loop there
|
|
643
|
+
is — a Hebrew sentence on the box, read by the next run — and it happened
|
|
644
|
+
four days after "never edit the live `AGENTS.md`" was written. When `check`
|
|
645
|
+
goes red on the standard itself, **adopt, do not revert**: copy live → repo,
|
|
646
|
+
`git diff` to see exactly what changed, read the *whole* file for the
|
|
647
|
+
contradiction the edit introduced (the 08-27 edit allowed club emoji in
|
|
648
|
+
`## פורמט` while `## סגנון` still said zero), fix that in the repo, commit,
|
|
649
|
+
then `apply` — which now writes back only the reconciliation (D-020).
|
|
650
|
+
`apply` over an owner edit deletes four days of judgement to restore a
|
|
651
|
+
stale copy.
|
|
652
|
+
- **A prose rule that appears on the box is a bug report in disguise.** "No
|
|
653
|
+
English working sentence as a first line" is the owner describing a host
|
|
654
|
+
defect in the only layer they can reach. Adopt the sentence (it records
|
|
655
|
+
intent), file the mechanism (`docs/bugs/…`), and expect the sentence not to
|
|
656
|
+
hold — four of ten leaks came after it.
|
|
657
|
+
- **Switches with a setter stay out of the manifest, and stay recorded.**
|
|
658
|
+
`feed-watch.enabled` was deliberately left to a named admin (a verify run
|
|
659
|
+
executes as whoever set the key). That is right — and it is also how the
|
|
660
|
+
extension went live with none of its 19 runbook checks walked. When the
|
|
661
|
+
switch is flipped from chat, the ledger is the record: the first run's
|
|
662
|
+
timestamp, model and cost go into `pending-verification.md` the same day.
|
|
663
|
+
- **Cost is a per-day number, per space, per outcome.** Read it every few
|
|
664
|
+
days for the first two weeks:
|
|
665
|
+
```sql
|
|
666
|
+
SELECT date(started_at/1000,'unixepoch') d, outcome, count(*) n, round(sum(cost),2) usd
|
|
667
|
+
FROM task_runs WHERE space_id='<space>' GROUP BY d, outcome ORDER BY d DESC;
|
|
668
|
+
```
|
|
669
|
+
A space that costs more after the deterministic feed than before it has a
|
|
670
|
+
configuration problem (run cap, batching, exclusions, a cheaper leg for the
|
|
671
|
+
quiet path), not a prompt problem.
|
|
672
|
+
- **The model pin is part of the profile's contract but not of its files.**
|
|
673
|
+
`model.active` lives in `space_config` and the owner switches it from chat.
|
|
674
|
+
A profile README should say which model it was tuned on; a fixture result
|
|
675
|
+
should carry the model; a behaviour change that coincides with a switch is
|
|
676
|
+
the switch until proven otherwise.
|
|
677
|
+
- **Memory needs hygiene, not re-seeding.** After a week the football space had
|
|
678
|
+
19 episode notes, five of them seeded and the rest napkin's. `check` cannot
|
|
679
|
+
see a wrong note (a seed is compared for existence only), so the notebook's
|
|
680
|
+
correctness is verified the way the audit verified the chat: open a sample,
|
|
681
|
+
check the `## Current State` lines carry `(source, date)`, and look for two
|
|
682
|
+
notes on one topic — the split-topic failure the seeds were designed to
|
|
683
|
+
avoid.
|
|
684
|
+
- **Keep the planning docs behind the box, never ahead of it.** The roadmap
|
|
685
|
+
row says "deployed <date>", the checkpoint block says what passed and what
|
|
686
|
+
failed with the numbers, and `pending-verification.md` holds only what a
|
|
687
|
+
person still has to look at. A "deploy not yet run" line that survives the
|
|
688
|
+
deploy by six days costs the next session an hour.
|
|
689
|
+
- **Ask who is in the room before reading engagement out of the numbers.** On
|
|
690
|
+
2026-08-31 every mechanical signal on the box — a ledger of successful runs,
|
|
691
|
+
ambient counts falling to zero, `conversations.last_seen_at` frozen four days
|
|
692
|
+
back, another space as a live control on the same connection — supported the
|
|
693
|
+
conclusion that the audience had walked out under the bot's output volume. The
|
|
694
|
+
space had been re-pointed at a private testing group with one member in it.
|
|
695
|
+
The box records what was *sent* and what *arrived*; it has no column for what
|
|
696
|
+
the room is **for**. Membership, purpose and whether a space is a test harness
|
|
697
|
+
are owner facts. Read the counts, then ask — and write the answer into the
|
|
698
|
+
profile README, because the next session will re-derive the same wrong story
|
|
699
|
+
from the same true numbers.
|
|
700
|
+
- **Price the prefix, not the run.** On the Anthropic leg a run's cost is
|
|
701
|
+
`prefix tokens × $2.50/M` for the first call, then ~1/10 of that per tool
|
|
702
|
+
round — so `cost/day ≈ runs/day × prefix tokens`. Every byte that reaches the
|
|
703
|
+
per-run prompt is paid for on every run, whether or not the run reads it. Two
|
|
704
|
+
things put unbounded content there on `football-friends` in the same week: an
|
|
705
|
+
extension's `promptAppend` that the host persisted and then replayed through
|
|
706
|
+
the history window (103 KB), and episode notes whose `## Current State` had no
|
|
707
|
+
working ceiling (11 KB). Same failure, two doors. The rule: anything injected
|
|
708
|
+
per run — digests, notebook sections, history — needs a cap that **logs when it
|
|
709
|
+
bites**, and the stored copy of a turn must be the marker, never the
|
|
710
|
+
expansion. And read the cost from `cache_write_tokens`, not `input_tokens`;
|
|
711
|
+
the latter is the uncached sliver and explains nothing.
|
|
445
712
|
|
|
446
713
|
---
|
|
447
714
|
|
|
448
|
-
##
|
|
715
|
+
## 14. Anti-patterns (each one has happened)
|
|
449
716
|
|
|
450
717
|
| Anti-pattern | What it did | Do instead |
|
|
451
718
|
|---|---|---|
|
|
@@ -463,10 +730,20 @@ Lessons from four deployments of this shape:
|
|
|
463
730
|
| Blanket-`cp` of a skill over its deployed copy | `{SPACE}` placeholders reverted; commands broke silently | copy → substitute → diff |
|
|
464
731
|
| Verifying a deploy against the repo | fixes "deployed" that were never built into the image | grep the image / the installed copy |
|
|
465
732
|
| A restart to deploy `container-entry.ts` changes | tracing silently did nothing | image rebuild, then verify in the image |
|
|
733
|
+
| `apply` over an owner's live edit | would have reverted four days of scan-format tuning | adopt live → repo, fix the contradiction, then `apply` (D-020) |
|
|
734
|
+
| A prose rule against a host defect ("no English first line") | four leaks after the rule | file the mechanism; keep the sentence as a record of intent |
|
|
735
|
+
| "Empty runs are cheap" | 46 verify runs/day, 70 % `NO_UPDATE`, $14–20/day for one space | cap runs per hour, batch, exclude; a host pre-filter is the only free run |
|
|
736
|
+
| Assuming the model the profile was tuned on | Sonnet narrated into the sent text where Opus had not | read `model.active` and the ledger's model column first; fixtures carry the model |
|
|
737
|
+
| Status from the planning docs | "deploy not yet run" six days into the deploy | `check` + ledger + `space_config`, then correct the docs |
|
|
738
|
+
| Copying a DM space's permission row into a group | members held `tasks.delete` on the daily article | start from the live row and remove; audit per room |
|
|
739
|
+
| A formatting rule the send pipeline undoes | the model added RLM/RLE marks the host strips before adding its own | read `src/text/rtl.ts` and `markdown.ts`; write only the rules the host does not enforce |
|
|
740
|
+
| "Passed" = a reply was collected | 196 green rows, quality unjudged | grade separately (PASS / PARTIAL / FAIL); split engine bugs from persona fixes |
|
|
741
|
+
| Writing to a live space dir mid-turn | half the old persona, half the new, on a running request | check `state.db`'s last `messages` row before any live write |
|
|
742
|
+
| A ledger or `.db` committed with the profile | one client's records installed into another client's space | create it empty at install; a gate fails on any committed `*.db` |
|
|
466
743
|
|
|
467
744
|
---
|
|
468
745
|
|
|
469
|
-
##
|
|
746
|
+
## 15. Checklist
|
|
470
747
|
|
|
471
748
|
- [ ] Kind chosen (applicative vs space) and the layer map written in the README.
|
|
472
749
|
- [ ] Facts inventory done; every fact has a source of truth; every "no answer" has its sentence.
|
|
@@ -481,3 +758,12 @@ Lessons from four deployments of this shape:
|
|
|
481
758
|
- [ ] Extension config owned by the profile where a hand edit would matter; the `enabled`-style switch deliberately not.
|
|
482
759
|
- [ ] Fixtures exist for the rules that matter; a traced run shows the tools/skills being used.
|
|
483
760
|
- [ ] Deployed and verified inside the artefact; `check` clean; the change has run on the live bot.
|
|
761
|
+
- [ ] The ledger existed before the change it measures; the first day's runs × cost per run is written down next to the target.
|
|
762
|
+
- [ ] `persona.exclusive` decided deliberately (owns its standard → on; should still sound like the deployment → off).
|
|
763
|
+
- [ ] Seeds (`workspace_seed`) carry facts and indexes only — no rule that `AGENTS.md` already owns; thin where evidence is thin.
|
|
764
|
+
- [ ] The README names the model the profile was tuned on; the fixture report records the model per result.
|
|
765
|
+
- [ ] Every switch the profile does not own (`*.enabled`) has a line in `pending-verification.md` saying who flipped it and when, taken from `space_config.updated_by` and the ledger.
|
|
766
|
+
- [ ] Every file is classified profile-owned or agent-writable; nothing agent-writable is re-asserted; no `*.db` ships.
|
|
767
|
+
- [ ] Formatting rules are checked against `src/text/rtl.ts` and `src/text/markdown.ts`; the skill's checklist and the lint read one rule list.
|
|
768
|
+
- [ ] `AGENTS.md` has a "what this space does not do" section and a privacy rule phrased as no-retention, not denial.
|
|
769
|
+
- [ ] The bot's own judgements land in an append-only record with a date (notebook `## History`, a tracker, the ledger) so they can be graded later.
|
package/docs/skills-guide.md
CHANGED
|
@@ -99,6 +99,23 @@ The `description` is the trigger. It is limited to 1024 characters, and the
|
|
|
99
99
|
- **Declare state the skill owns** — a DB path, a registry file, a seed —
|
|
100
100
|
and the rule for it ("register every new tracker in `registries.json` or it
|
|
101
101
|
disappears").
|
|
102
|
+
- **A formatting skill states what the host already does, then only what it
|
|
103
|
+
does not.** The investor `whatsapp-formatting` skill opens by saying that
|
|
104
|
+
`applyRtlDirection` (`src/text/rtl.ts`) strips leading bidi marks and adds
|
|
105
|
+
its own RLM per line — "verified against this install's runtime" — so the
|
|
106
|
+
model must *not* add RLE/RLM; what remains its job is to start every line
|
|
107
|
+
with a Hebrew word or an emoji, never a ticker or a number. Same for
|
|
108
|
+
`normalizeChatMarkdown` (`src/text/markdown.ts`): headings and `**` are
|
|
109
|
+
rewritten at send, so a rule about them is a rule about the stored row,
|
|
110
|
+
not the rendered message. End the skill with a short checklist the model
|
|
111
|
+
runs before sending; the same list is what the output lint asserts
|
|
112
|
+
(`profile-guide.md` §7, §11) — one list, two readers.
|
|
113
|
+
- **Exit codes are a table with a "what you do" column.** The investor
|
|
114
|
+
`chart` skill lists every code, says which are structural (never retry —
|
|
115
|
+
"a chartless analysis is a fine answer, a stalled reply is not") and adds
|
|
116
|
+
a "how it works, for whoever maintains this" section so the next editor
|
|
117
|
+
does not "simplify" it into a different data source with silently wrong
|
|
118
|
+
numbers.
|
|
102
119
|
|
|
103
120
|
## 5. Making a profile actually use its skills
|
|
104
121
|
|
|
@@ -152,3 +169,10 @@ then `diff` and confirm the only difference is the placeholder.
|
|
|
152
169
|
a container (`docker run --rm --entrypoint ls <image> /home/mercury/.pi/agent/skills`
|
|
153
170
|
for global ones) or check `<available_skills>` in a trace.
|
|
154
171
|
- [ ] A trace of one real run shows the `read` of `SKILL.md`.
|
|
172
|
+
- [ ] Every absolute path the skill or `AGENTS.md` names resolves inside the
|
|
173
|
+
space (tagula gates this with `check-profile-skills.sh`; for a space
|
|
174
|
+
profile, `ls` them from the box until `space-profile check` learns to —
|
|
175
|
+
`docs/ideas/space-profile-check-paths-and-inflight-guard.md`).
|
|
176
|
+
- [ ] If the skill is about formatting, it names the host passes that already
|
|
177
|
+
run at send (`src/text/rtl.ts`, `src/text/markdown.ts`) and only rules
|
|
178
|
+
beyond them.
|
|
@@ -21,6 +21,16 @@ export const MAX_TERMS = 50;
|
|
|
21
21
|
export const MAX_PER_HOUR_CEILING = 12;
|
|
22
22
|
export const MAX_BATCH_MINUTES = 30;
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Most items the daily digest will carry. A bound, not a tuning knob: the
|
|
26
|
+
* buffer is already capped at `BUFFER_CAP` items, but that ceiling was chosen
|
|
27
|
+
* for the store, not for a prompt, and 264 items came to 103 KB before anyone
|
|
28
|
+
* measured it. Whatever the watcher collects, the article's prompt stops here
|
|
29
|
+
* — and says so in the log when it does.
|
|
30
|
+
*/
|
|
31
|
+
export const DEFAULT_DIGEST_MAX_ITEMS = 200;
|
|
32
|
+
export const DIGEST_MAX_ITEMS_CEILING = 400;
|
|
33
|
+
|
|
24
34
|
/** An RSS/Atom URL, polled as-is. */
|
|
25
35
|
export interface RssSource {
|
|
26
36
|
type: "rss";
|
|
@@ -60,6 +70,8 @@ export interface WatchConfig {
|
|
|
60
70
|
quietHours: QuietHours | null;
|
|
61
71
|
maxPerHour: number;
|
|
62
72
|
batchMinutes: number;
|
|
73
|
+
/** Hard ceiling on how many items the daily digest hands the article. */
|
|
74
|
+
digestMaxItems: number;
|
|
63
75
|
/** IANA zone, or "" to fall back to the deployment default and then UTC. */
|
|
64
76
|
timezone: string;
|
|
65
77
|
}
|
|
@@ -158,6 +170,14 @@ export function isValidBatchMinutes(value: string): boolean {
|
|
|
158
170
|
return isIntInRange(value, 0, MAX_BATCH_MINUTES);
|
|
159
171
|
}
|
|
160
172
|
|
|
173
|
+
/**
|
|
174
|
+
* At least one: a digest of zero items is indistinguishable from "the watcher
|
|
175
|
+
* is not running", which the article is explicitly told to treat differently.
|
|
176
|
+
*/
|
|
177
|
+
export function isValidDigestMaxItems(value: string): boolean {
|
|
178
|
+
return isIntInRange(value, 1, DIGEST_MAX_ITEMS_CEILING);
|
|
179
|
+
}
|
|
180
|
+
|
|
161
181
|
/**
|
|
162
182
|
* An IANA zone name, checked by asking `Intl` to use it. Empty means "unset",
|
|
163
183
|
* which is a valid state — the poller falls back to the deployment default.
|
|
@@ -270,6 +290,16 @@ export function readSpaceConfig(
|
|
|
270
290
|
log,
|
|
271
291
|
),
|
|
272
292
|
),
|
|
293
|
+
digestMaxItems: Number(
|
|
294
|
+
read(
|
|
295
|
+
getConfig,
|
|
296
|
+
spaceId,
|
|
297
|
+
"digest_max_items",
|
|
298
|
+
String(DEFAULT_DIGEST_MAX_ITEMS),
|
|
299
|
+
isValidDigestMaxItems,
|
|
300
|
+
log,
|
|
301
|
+
),
|
|
302
|
+
),
|
|
273
303
|
timezone: read(
|
|
274
304
|
getConfig,
|
|
275
305
|
spaceId,
|
|
@@ -344,6 +374,12 @@ export const CONFIG_KEYS: ConfigKeyDef[] = [
|
|
|
344
374
|
default: String(DEFAULT_BATCH_MINUTES),
|
|
345
375
|
validate: isValidBatchMinutes,
|
|
346
376
|
},
|
|
377
|
+
{
|
|
378
|
+
key: "digest_max_items",
|
|
379
|
+
description: `Most items the daily digest carries into the article's prompt, newest first (1–${DIGEST_MAX_ITEMS_CEILING}, default ${DEFAULT_DIGEST_MAX_ITEMS}). Older matches stay in the workspace digest file. A bound on prompt size, not a filter.`,
|
|
380
|
+
default: String(DEFAULT_DIGEST_MAX_ITEMS),
|
|
381
|
+
validate: isValidDigestMaxItems,
|
|
382
|
+
},
|
|
347
383
|
{
|
|
348
384
|
key: "timezone",
|
|
349
385
|
description:
|