Files
clawmates/templates/workflows/continuous_research.toml
T
Omar SobhandClaude Opus 5 f4adc8d0f9
deploy / test (push) Successful in 4m11s
deploy / build (push) Successful in 5m28s
fix(podcast): stop reading identifiers aloud, and pitch the episode at a teenager
Two things the operator found by listening to a real episode.

**1. Identifiers were spoken as digit soup.** The script genuinely said
"arxiv 2608.12888", which the voice reads as "two six zero eight point one two
eight eight eight". Same for three-decimal values: "0.506" and "0.004" became
long strings of spoken digits. A listener on a treadmill cannot write an
identifier down and does not need a third decimal place.

`speakable()` strips arXiv references and bare identifier-shaped numbers, and
rounds decimals to two places — with a carve-out that matters: 0.004 rounds to
0.00, which would claim the value was ZERO when the whole point was that it
collapsed to nearly nothing, so it says "under 0.01" instead.

Deliberately narrow: it removes identifiers and shortens over-precise decimals,
and does not paraphrase, reorder or summarise. The agents' words are still the
episode. It also preserves the sentence's full stop — swallowing it turned
"…financial retrieval, arxiv 2608.00183. This one's a catch." into one run-on
sentence, and the pause is how a listener knows a thought ended.

Note that `podcast-dialogue-writing.md` ALREADY said "no arXiv ids" and the
writer included them anyway. That is this project's recurring lesson restated:
an instruction is a request, and a listener deserves a guarantee. The prose asks
and the code enforces.

**2. It was written for someone who already knew the field.** The skill and the
script phase's task now target a bright sixteen-year-old: define an acronym in
the sentence that first uses it, describe the mechanism rather than naming it
("a road map with motorways and side streets" instead of "a hierarchical
navigable small world graph"), one idea per sentence. The test offered is
whether the listener could explain the finding to a friend afterwards.

That is not dumbing down — it is the constraint that forces a writer to say what
a thing actually does rather than what it is called.

Tested against the exact lines from the episode that was listened to.
366 tests pass.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-18 10:42:48 -07:00

110 lines
5.6 KiB
TOML

key = "continuous_research"
title = "Continuous Research"
blurb = "Harvest new arXiv papers, read them against your projects, and write a two-host podcast script for the morning."
requires_repo = true
# The vault, not `rust_sdlc`. Every other recipe defaults to the Rust SDLC team,
# which is why `continuous_research` has only ever been reachable as a checkbox
# in Advanced rather than as a mission type.
default_team_template = "continuous_research"
# ── How the papers arrive ────────────────────────────────────────────
#
# The harvest is NOT an agent phase. `library::run_to_vault` already does
# arXiv search → seen-set check → PDF fetch → blob shelf → vault note, and it
# takes a `mission_id` so the run is attributed. Asking an agent to redo that
# would be slower, non-deterministic, and would lose the `corpus_items`
# seen-set that is the entire reason a recurring mission knows what it already
# covered. `mission_orchestrator::on_launch` runs it before the phases start
# and drops a manifest at `ContinuousResearch/<date>/harvest.jsonl` — the path
# `templates/teams/continuous_research.toml` already tells the harvester role to
# write, now produced by the code that actually does the harvesting.
#
# The mission binds the VAULT repo, so `/mission/repo` is the card catalogue:
# the agents read the notes the harvest just wrote and commit their analysis and
# script back onto the run's own branch, never `main`.
[[phases]]
kind = "research"
order_idx = 0
[phases.config]
produces = ["md"]
default_topology = "hub_spoke"
# `research`, not a new `read` kind. An unrecognised kind falls through to
# `purposes_for`'s `_ => ["mission"]` and is absent from `PRODUCING_KINDS`
# (phase_runner.rs), so it would get a generic directive AND be exempt from the
# empty-delivery rule — a phase that could produce nothing and still pass. Two
# `research` phases differentiated by `task` keeps both guards.
task = """
Read today's harvested papers and judge them against the operator's projects.
Start from ContinuousResearch/<today>/harvest.jsonl — that is the list of papers \
that are NEW since the last run. Papers already covered are not in it, and you \
should not go looking for them.
For each paper write an entry in ContinuousResearch/<today>/analysis.md \
containing: what it actually does (not what its abstract claims), whether the \
evidence supports it, and — the part that matters — WHICH of the operator's \
projects it bears on and what concrete change it would imply. Name a file, a module or a \
roadmap item wherever you can.
Depth comes from the paper itself: the note carries the abstract, and `curl` on \
the arXiv abstract page gets you the rest. Do not review a paper from its title.
A paper with no bearing on any project is a real and useful finding — say so in \
one line and move on. Do not manufacture relevance.
THE PROJECTS THIS SERVES ARE IN THE BRIEF ABOVE. `phase_task_text` puts the \
mission's description there verbatim, so that is where the operator names what \
they are working on and what each project needs. If the brief names no \
projects, say so plainly in analysis.md rather than inventing a target — a \
digest that guesses at relevance is worse than one that admits it has no \
context.
"""
done_when = "ContinuousResearch/<today>/analysis.md exists and contains, for every paper in that day's harvest.jsonl, a judgement of the work and a statement of which project it bears on or that it bears on none"
max_iterations = 2
# Nothing here is compiled, so `on_green_tests` would gate on a suite that does
# not exist and land every branch `-wip`. The vault is prose.
commit_policy = "always"
[[phases]]
kind = "research"
order_idx = 1
[phases.config]
produces = ["md"]
default_topology = "hub_spoke"
task = """
Turn today's analysis into a podcast script for two hosts.
Read ContinuousResearch/<today>/analysis.md and write \
ContinuousResearch/<today>/script.md as a conversation between HOST and GUEST. \
Lead with what changed for our projects, not with a list of papers — the \
listener is on a treadmill, not at a desk.
Also write ContinuousResearch/<today>/episode.json:
{ "title": "<one line, under 80 chars>",
"highlights": ["<10-70 chars each, at most 5>"] }
Those bounds are the podcast API's, not a style preference — a highlight \
outside them is rejected.
Pitch it at a bright sixteen-year-old: someone curious who has NOT read the \
paper, does not know the jargon, and cannot pause to look anything up. Define \
an acronym the first time in the sentence, then use it. Describe the mechanism \
rather than naming it — "a road map with motorways and side streets" beats \
"a hierarchical navigable small world graph". That is not dumbing down; it is \
what forces you to say what the thing actually does.
Numbers are HEARD. Never write an arXiv id — "arxiv 2608.12888" is heard as \
"two six zero eight point one two eight eight eight"; name the paper instead. \
Two decimal places at most, and round in speech: "roughly a third faster" \
beats "34.7% faster". No URLs, no figure references.
Target seven minutes of speech, roughly 1,000 words. Say the specific thing: \
"this changes how we prune the search index in clawhdf5" beats "researchers \
propose a novel method". Skip a paper entirely rather than pad the episode \
with one that does not matter.
"""
done_when = "ContinuousResearch/<today>/script.md contains a two-host dialogue covering the analysis, and episode.json contains a title and a highlights array whose entries are each between 10 and 70 characters"
max_iterations = 2
commit_policy = "always"