feat(skills): author the Continuous Research skills, and make its roles honest
deploy / test (push) Successful in 4m4s
deploy / build (push) Successful in 1m45s

The boot log has said `continuous_research — 4 role skills bound, 8 unresolved`
on every start. Those eight roles ran without the instructions their template
promised them, silently: `skills_loader` reports the miss and carries on.

Worse than the missing files was what the prompts described. v1 told the
harvester to sweep "RSS feeds, GitHub trending, HN front page, YouTube /
podcast RSS" — none of which exist. `harvest.rs` searches arXiv and nothing
else. A role prompt describing a machine that was never built is the failure
this codebase keeps paying for, so v2 describes the machine that exists.

Roles now match the pipeline: paper_reader (the harvest already ran; read the
manifest and the papers), signal_ranker (unchanged in spirit), script_writer
(the podcast half, which had no role at all).

Seven skills authored under skills/research/, kebab-case to match the loader —
team_template_loader.rs:177-181 documents the snake_case/kebab-case trap that
already unbinds skills elsewhere:

  arxiv-daily                  what the harvest guarantees, so an agent does
                               NOT re-search arXiv and corrupt the seen-set
  paper-to-project-relevance   name a file or roadmap item, or say "no bearing"
  duplicate-detection          the seen-set catches identity; this catches the
                               same work under a different id
  signal-to-noise-ranking      novelty/relevance/depth, and the two biases to
                               resist (recency up, inconvenience down)
  executive-summary-writing    what it is / why it matters / what to do — decide,
                               do not hedge
  obsidian-vault-conventions   the vault is a human's live workspace; never
                               main, never reorganise, hash the body not the file
  podcast-dialogue-writing     write for someone on a treadmill; the 10-70 char
                               highlight bound is the API's, not a style rule

`web_fetch` dropped from mcp_bundles: runtime_provision.rs binds every mission
claw to `["clawmates_door"]` and never reads that field, so declaring it
instructed roles to use a tool that never arrived. The prompts say `curl` via
Bash, which is what they actually have.

Boot now reports `continuous_research — 11 role skills bound`, with no
unresolved clause. 344 tests pass.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-17 14:44:03 -07:00
co-authored by Claude Opus 5
parent e20b321055
commit a2d7e3ea92
8 changed files with 489 additions and 61 deletions
+48
View File
@@ -0,0 +1,48 @@
---
name: arxiv-daily
description: What the arXiv harvest already guarantees, and what an agent must therefore not redo.
when_to_use: You are working with a harvest manifest in a Continuous Research mission.
tags: [research, sources]
---
# The harvest is already done when you start
Searching arXiv is not your job. Before your phase launches, the server has
already run the harvest: searched each topic, checked the seen-set, downloaded
each new PDF, verified it, shelved it in blob storage and written a vault note.
It takes seconds and it is deterministic.
Your input is the result:
```
ContinuousResearch/<date>/harvest.jsonl
{ source, url, title, snippet, first_seen, topic_tags }
```
One line per paper that is **new since the last run**. Papers already covered
are not in it — by design, not omission.
## What that guarantees
- **No repeats across runs.** `corpus_items` is keyed `arxiv:<id>` with the
version suffix stripped, so `2401.12345v3` cannot reappear as new because
`v1` was seen. This is the entire reason a recurring mission is worth having.
- **Every shelved paper has a real PDF.** Fetched bytes are rejected unless they
start with `%PDF` — arXiv serves an HTML holding page while a paper renders,
and shelving that would leave an unreadable file behind a checkmark.
- **A failed download stays unseen.** The checkmark is written last, after the
PDF and the note are both on disk, so a paper that failed halfway is retried
next run rather than being lost behind a mark that says it was handled.
## What that means for you
- **Do not search arXiv yourself.** Anything you find outside the manifest is
either already covered or was deliberately not selected, and shelving it
outside the pipeline leaves the seen-set wrong.
- **Do not treat an empty manifest as a failure.** A quiet day is a real
outcome. Say "nothing new today" and stop — do not go looking for something to
talk about.
- **Do read the paper.** The manifest's `snippet` is a trimmed abstract, and the
abstract is the authors' pitch. `curl` the arXiv abstract page for the rest.
What a paper claims and what its evaluation shows differ often enough that
catching the gap is most of the value you add.
+48
View File
@@ -0,0 +1,48 @@
---
name: duplicate-detection
description: Recognising that two harvested items are the same work, and why the seen-set is not enough on its own.
when_to_use: You are processing a harvest manifest and must avoid covering the same work twice.
tags: [research, judgement]
---
# The same work wearing different clothes
The harvest already dedupes by identity: `corpus_items` keys papers by
`arxiv:NNNN.NNNNN` with the version suffix stripped, so `2401.12345v3` and
`2401.12345v1` are one paper and a paper already shelved never reappears. That
is exact-identity dedup, and it is done for you.
What it cannot catch is the same *work* arriving under a different identity.
## The cases the seen-set misses
- **A paper and its blog post.** Different URLs, same result. Cover the paper;
mention the post only if it adds an author's clarification.
- **Preprint then conference version.** Different arXiv id, often a different
title, sometimes months apart. If the contribution is the same, it is one
item — and the interesting fact is what changed in review.
- **The same group's incremental follow-up.** "X for Video" three months after
"X". Genuinely new, but the listener needs the earlier one as context, so
cover them together rather than as two unrelated items.
- **Two groups, one idea, same season.** Not duplicates. Say so explicitly —
independent convergence is itself a signal that the idea is real.
## How to decide
Compare on **contribution**, not on title or authors:
1. What does it claim that nobody claimed before?
2. What experiment supports that specific claim?
If two items give the same answer to both, they are one item. If they differ on
either, they are two — even from the same lab, even with near-identical titles.
## When you merge, say so
Never silently drop the second one. A merged entry states it:
> Also appeared as a NeurIPS submission with an extra ablation; same core
> result, so covered once here.
Silent dedup is indistinguishable from a harvest that missed something, and the
whole point of a standing sweep is knowing which of the two happened.
@@ -0,0 +1,56 @@
---
name: executive-summary-writing
description: Writing the three-part item entry — what it is, why it matters to us, what to do — so a digest is readable in two minutes.
when_to_use: You are writing digest or analysis entries for harvested research items.
tags: [research, writing]
---
# The three-part entry
Every item gets exactly three parts. The shape is the discipline: it makes the
omissions visible.
**What it is** — one sentence, what they actually did. Not what it enables, not
why it is exciting. If you cannot state the contribution in one sentence you
have not finished reading.
**Why it matters to us** — the named project, file or open question it touches.
If nothing applies, the item does not belong in the digest. That rule is what
keeps a digest from becoming a feed.
**What to do about it** — one decision, from a closed set:
```
read fully worth an hour of someone's time
add to backlog a concrete change we should make
ignore seen, judged, not for us
escalate changes a plan we are currently executing
```
## Decide; do not hedge
"Consider evaluating whether this might inform our approach" is four words of
decision wrapped in ten of insulation. Write `ignore` or write `add to backlog`.
Being wrong in a specific direction is recoverable; being vague is not, because
nobody can tell later whether the call was right.
## Two minutes, total
Not two minutes per item — two minutes for the digest. Five items at three
sentences each is roughly the budget. If an item needs more, it is not a digest
entry, it is a reading note: link to it and keep the entry short.
## Frontmatter carries the ratio
```yaml
---
date: 2026-08-17
harvested: 23
surfaced: 4
---
```
Harvested-versus-surfaced tracked over time is how you notice selectivity drift.
A run that surfaces everything has stopped filtering; a run that surfaces
nothing for a week is either a quiet field or a broken harvest, and the counts
are what tell you which.
@@ -0,0 +1,60 @@
---
name: obsidian-vault-conventions
description: Writing into a live Obsidian vault a human also edits — frontmatter, links, paths, and what never to touch.
when_to_use: You are committing notes, digests or scripts into the valhalla-vault repository.
tags: [research, conventions]
---
# The vault is somebody's live workspace
`valhalla-vault` is a real Obsidian vault a human edits and syncs continuously.
It is not scratch space. Two consequences drive everything below.
## Never commit to `main`
Mission work lands on the run's own branch. `library.rs` does this deliberately
and only auto-merges when the change is provably additive — every file in
`git diff --name-status base...branch` is an `A`. A rename or a delete is not
additive and stops the merge.
That rule protects the human: an auto-merge can add a note beside their work,
never rewrite it.
## Stay in your folders
```
60 Papers/arxiv-<id>.md one note per paper, written by the harvest
ContinuousResearch/<date>/... one folder per run — yours
```
Do not reorganise, rename or "tidy" anything outside those. What looks like a
stray file is usually load-bearing in someone's graph.
## Frontmatter is the index
Obsidian queries it, so it must be parseable YAML and keys must stay stable:
```yaml
---
date: 2026-08-17
type: research-digest
harvested: 23
surfaced: 4
---
```
**Hash the body, not the file.** Sync rewrites `updated:` and `size_kb:` on
notes it touches; hashing the whole file reports phantom edits on notes nobody
changed and makes "unchanged" meaningless.
## Links
`[[wikilinks]]` are the vault's native form — use them for internal references,
plain markdown links for external URLs. A link to a note that does not exist yet
is fine and idiomatic: it marks something worth writing.
## `source:` is not identity
The vault uses `source:` for local provenance — often a path like
`/Users/…/Downloads/paper.pdf`. It is not a citable identifier and must never be
used as a dedup key. Identity is `arxiv:<id>`, version suffix stripped.
@@ -0,0 +1,65 @@
---
name: paper-to-project-relevance
description: How to judge whether a paper actually bears on one of the operator's projects, and how to say so concretely instead of vaguely.
when_to_use: You are reading harvested papers in a Continuous Research mission and must decide which ones matter and why.
tags: [research, judgement]
---
# Does this paper bear on our work?
The value of a research digest is not that it lists papers. It is that it tells
you which of *your* problems just got easier. A summary that could have been
written without knowing anything about the operator's projects has failed, no
matter how accurate it is.
## The test
For each paper, answer in this order. Stop at the first "no".
1. **Is the claim real?** What did they actually measure, on what, against what
baseline? A 40% speedup on a benchmark nobody runs is not a 40% speedup.
2. **Does it touch a problem we have?** Not a problem in the same *field* — a
problem in the project brief. "Vector search" is a field. "Our HNSW insert
loop is serial and the ROADMAP flags it as needing a correctness pass" is a
problem.
3. **Would we do anything differently?** If the honest answer is "no, but it's
interesting", say exactly that. It is a legitimate outcome and it costs the
listener five seconds instead of two minutes.
## Say the specific thing
Weak, and useless to act on:
> This work on graph pruning could be relevant to vector search performance.
Strong, because it names the target and the change:
> They prune by neighbour-degree rather than distance, which is the same
> decision `clawhdf5-ann/src/hnsw.rs` makes in `prune_connections`. Ours is
> already rayon-parallel per node, so this is a swap of the scoring function,
> not a restructure — maybe 30 lines. Worth trying because our ROADMAP lists
> recall regression at high M as an open question, and that is exactly what
> their Table 3 measures.
Name a file, a function, a roadmap item, or an open question. If you cannot name
one, you have not established relevance — you have established topic overlap.
## Honest negatives are the point
Most papers will not bear on the work. Recording that plainly is what makes the
positives trustworthy. One line is enough:
> No bearing — single-GPU training throughput, we do no training.
A run where everything is relevant is a run that stopped judging.
## What NOT to do
- **Do not infer relevance from keyword overlap.** Sharing the word "agent"
with our codebase is not a connection.
- **Do not soften a negative into a maybe.** "Could potentially inform future
work" is how a digest becomes unreadable.
- **Do not review from the title or the abstract's claims.** The note carries
the abstract; the paper is one `curl` away. What a paper *says* it does and
what its evaluation *shows* are routinely different, and catching that gap is
most of the value you add.
@@ -0,0 +1,77 @@
---
name: podcast-dialogue-writing
description: How to turn a paper analysis into a two-host script someone can follow while running, and the hard limits the audio API imposes.
when_to_use: You are writing the script phase of a Continuous Research mission, turning analysis.md into script.md and episode.json.
tags: [research, writing]
---
# Writing for someone on a treadmill
The listener is not at a desk. They cannot scroll back, they cannot see a table,
and they are half-listening. Everything follows from that.
## Structure
Lead with the conclusion, then support it. A paper-by-paper walk is the wrong
shape: the listener does not know which papers matter until you tell them, and
by then the first one is gone.
```
COLD OPEN one sentence: the single thing that changed today
THE MAIN the paper that actually matters, 3-4 minutes
THE REST two or three, a minute each
NOTHING what you looked at and dismissed, 20 seconds
```
"Nothing" is not filler. Knowing that eleven papers were read and eight did not
matter is what makes the three that did credible.
## Two hosts, with a reason to be two
HOST carries the thread and asks what the listener would ask. GUEST has read the
paper and answers. The reason for a second voice is that disagreement and
"wait, why?" are easier to follow than monologue — not that alternating names
looks like a script.
Bad, because these are one voice cut in half:
> HOST: The paper introduces a new pruning method.
> GUEST: Yes, and it improves recall by 12%.
Good, because the second voice is doing work:
> HOST: Twelve percent recall — is that on a benchmark we'd care about?
> GUEST: That's the catch. It's on SIFT1M, which is a million 128-dim vectors.
> Our embeddings are 1536-dim and we're at about 40,000. So the shape of
> their win might not survive at our dimensionality.
## Say numbers the ear can hold
"Roughly a third faster" beats "34.7% faster". "About a million vectors" beats
"1,000,000". Read every line aloud in your head; if you stumble, the TTS will
too.
Never write anything the listener cannot hold: no URLs, no arXiv ids, no table
references, no "as shown in Figure 4". If they need the citation it is in the
vault note.
## Hard limits, not style preferences
`episode.json` is consumed by the audio API, and these bounds are enforced by it:
```json
{ "title": "<one line, under 80 characters>",
"highlights": ["<each 10-70 characters>", "..."] }
```
- **At most 5 highlights**, each **10–70 characters**. Outside that range the
request is rejected, not truncated.
- Target **seven minutes of speech, roughly 1,000 words**. Audio quality
degrades on long single generations, so a sprawling script produces a worse
episode, not just a longer one.
## Cut rather than pad
If only one paper mattered, write a four-minute episode about one paper. An
episode padded to length with work that did not matter trains the listener to
skip, and once they skip, the whole pipeline is worthless.
@@ -0,0 +1,52 @@
---
name: signal-to-noise-ranking
description: Scoring harvested items on novelty, relevance and depth so a digest surfaces the few that matter.
when_to_use: You are ranking harvested research items before deciding which reach the digest or the podcast.
tags: [research, judgement]
---
# Ranking signal against noise
Score each item on three axes, 0–3. Sum them. The sum decides where it goes, and
the axes exist so that a high score has to be earned in more than one way.
## The axes
**Novelty (0–3)** — how different is this from what we have already seen on this
topic in the last 30 days? A fourth paper making the same claim is a 0 even if
the claim is important. Recency is not novelty: something published today that
restates last month's result is still a 0.
**Relevance (0–3)** — how directly does it connect to a named project or open
question? Not the field — the project. See `paper-to-project-relevance`. If you
cannot name the thing it touches, this is at most a 1.
**Depth (0–3)** — is this a primary source with an evaluation, or the fifth blog
post about someone else's paper? A primary source with a weak evaluation can
still beat a good summary of it, because you can check the former.
## Thresholds
```
>= 6 goes in the digest / the episode
>= 4 goes to watchlist.md — worth knowing, not worth airtime
< 4 dropped from the digest, KEPT in the raw manifest
```
Dropped is not deleted. The manifest is the audit trail: "we saw it and passed"
is a different and more useful record than silence, and it is what lets you
notice later that the ranking was wrong.
## Two failure modes to resist
- **Up-ranking the recent.** Time is not a quality signal. A preprint from this
morning gets no credit for being from this morning.
- **Down-ranking the inconvenient.** A paper that contradicts the approach we
are currently taking is *high* signal, not low. The temptation to score it
down is exactly why it needs airtime.
## Escape hatch
Anything the operator has explicitly flagged bypasses scoring entirely. A human
saying "look at this" outranks the rubric; the rubric exists for the other
several hundred items.
+83 -61
View File
@@ -1,44 +1,66 @@
key = "continuous_research" key = "continuous_research"
name = "Continuous Research" name = "Continuous Research"
description = "Standing scan across all monitored data sources + media types — news, blogs, papers, code releases, talks — for the latest signal on tracked topics. Runs on schedule; produces a rolling daily digest." description = "Reads each day's newly harvested arXiv papers against the operator's active projects and writes a two-host podcast script."
stack = ["research", "monitoring", "digest", "obsidian", "rss", "podcasts"] stack = ["research", "papers", "digest", "obsidian", "podcast"]
category = "research" category = "research"
default_topology = "pipeline" default_topology = "pipeline"
risk_profile = "research_readonly" risk_profile = "research_readonly"
mcp_bundles = ["clawmates_door", "clawmates_skills", "web_fetch"] # `web_fetch` was declared here and is INERT: runtime_provision.rs binds every
version = 1 # mission claw to `mcp_bundles = ["clawmates_door"]` and never reads this field.
# Agents reach a paper with `curl` through Bash instead, which the prompts say.
# Listing a bundle that does not arrive is how a role ends up instructed to use
# a tool it does not have.
mcp_bundles = ["clawmates_door", "clawmates_skills"]
version = 2
# v2 aligns the roles with the pipeline that actually exists.
#
# v1 told the harvester to sweep "RSS feeds, GitHub trending, HN front page,
# YouTube / podcast RSS". None of those are implemented — `harvest.rs` searches
# arXiv and nothing else — and 8 of its 12 skills did not exist, so those roles
# ran with neither the sources nor the instructions they were promised. A role
# prompt describing a machine that was never built is the failure this codebase
# keeps paying for, so the prompts below describe the machine that exists.
[[roles]] [[roles]]
slot = "signal_harvester" slot = "paper_reader"
order_idx = 0 order_idx = 0
skills = ["rss-fetch", "arxiv-daily", "github-trending", "web-search-triage", "decompose-int-items"] skills = ["arxiv-daily", "paper-to-project-relevance", "duplicate-detection"]
system_prompt = """ system_prompt = """
You are the SIGNAL HARVESTER of a Continuous Research team. You are the PAPER READER of a Continuous Research team.
Every run: sweep the operator's tracked topics across every configured The harvest has already run before you start. Your input is
source — arXiv daily new-listings, RSS feeds (blogs / news / vendor `ContinuousResearch/<date>/harvest.jsonl` — one line per paper that is NEW since
release notes), GitHub trending in tracked languages, HN front page the last run, already downloaded and catalogued. Do not search arXiv yourself:
filtered by keyword, YouTube / podcast RSS for tracked speakers. anything outside that manifest is either already covered or was deliberately not
selected, and shelving it outside the pipeline corrupts the seen-set.
Capture into `ContinuousResearch/<date>/harvest.jsonl`: For each paper, read past the abstract (`curl` the arXiv abstract page) and
`{ source, url, title, snippet, first_seen, topic_tags }`. Dedup write an entry in `ContinuousResearch/<date>/analysis.md`:
against yesterday's harvest by url + normalized title. Never guess
tags; use only tags from the operator's tracked list. - what it actually did — the experiment, not the pitch
- whether the evidence supports the claim
- which of the operator's projects it bears on, naming a file, module or
roadmap item — or one line saying it bears on none
An empty manifest is a real outcome: say "nothing new today" and stop. A paper
with no bearing is also a real outcome, and recording it plainly is what makes
the ones that do bear credible.
""" """
brain_seed = """ brain_seed = """
# Signal harvester memory seed # Paper reader memory seed
## Sources
- arXiv daily new listings for each subject class in the tracked topics
- RSS feeds (curated list; do not add without operator approval)
- GitHub trending (filter by tracked language + tracked topic)
- HN + Lobsters + subreddit filters
- YouTube / Podcast RSS
## Redlines ## Redlines
- Never fabricate `first_seen`. Use the source's own timestamp. - Never review a paper from its title or its abstract's claims alone. What a
- Do not inflate topic tags to broaden reach — precision is the whole paper says it does and what its evaluation shows differ often enough that
point of a standing sweep. catching the gap is most of the value.
- Never manufacture relevance. Topic overlap is not relevance; naming the file
or open question it touches is.
- Never go looking for extra papers to fill a quiet day.
## Identity
- A paper is `arxiv:<id>` with the version suffix stripped. `2401.12345v3` and
`2401.12345v1` are one paper.
""" """
[[roles]] [[roles]]
@@ -48,61 +70,61 @@ skills = ["signal-to-noise-ranking", "duplicate-detection", "workspace-repo-comm
system_prompt = """ system_prompt = """
You are the SIGNAL RANKER of a Continuous Research team. You are the SIGNAL RANKER of a Continuous Research team.
Score each harvested item on 3 axes (0..3): Score each analysed paper 0..3 on Novelty, Relevance and Depth, and sum:
- Novelty: how different is this from what we've seen in the last 30 >= 6 goes in the episode
days on the same topic? >= 4 goes to `ContinuousResearch/<date>/watchlist.md`
- Relevance: how directly does it connect to an active project or an < 4 dropped from the episode, KEPT in the manifest for audit
open question in the vault?
- Depth: is this a primary source, or the 5th blog rehash of a paper?
Sum the axes; anything ≥ 6 goes to the daily digest, ≥ 4 goes to Dropped is not deleted. "We saw it and passed" is a more useful record than
`ContinuousResearch/<date>/watchlist.md`, below is silently dropped silence, and it is what lets someone notice later that the ranking was wrong.
(but kept in the raw jsonl for auditability).
""" """
brain_seed = """ brain_seed = """
# Signal ranker memory seed # Signal ranker memory seed
## Redlines ## Redlines
- Do not up-rank items just because they're recent. Time is not a - Recency is not quality. A preprint from this morning earns nothing for being
quality signal. from this morning.
- Do not down-rank items because they contradict our current line of - A paper that contradicts our current approach is HIGH signal. The urge to
work. Contradiction is high-signal. score it down is exactly why it needs airtime.
## Escape hatches ## Escape hatch
- Anything with `[operator-attention]` tag from the harvester bypasses - Anything the operator flagged explicitly bypasses scoring. A human saying
scoring — the operator explicitly flagged it. "look at this" outranks the rubric.
""" """
[[roles]] [[roles]]
slot = "digest_writer" slot = "script_writer"
order_idx = 2 order_idx = 2
skills = ["executive-summary-writing", "obsidian-vault-conventions", "workspace-repo-commit-protocol", "small-focused-commits"] skills = ["podcast-dialogue-writing", "executive-summary-writing", "obsidian-vault-conventions", "workspace-repo-commit-protocol", "small-focused-commits"]
system_prompt = """ system_prompt = """
You are the DIGEST WRITER of a Continuous Research team. You are the SCRIPT WRITER of a Continuous Research team.
Every run: produce `ContinuousResearch/<date>/digest.md` — the top 5 Turn the analysis into `ContinuousResearch/<date>/script.md` — a conversation
highest-scored items with 3-sentence writeups each. Structure: between HOST and GUEST — plus `ContinuousResearch/<date>/episode.json`:
- **What it is** (single sentence) { "title": "<one line, under 80 chars>",
- **Why it matters to us** (link to the affected project or open "highlights": ["<10-70 chars each, at most 5>"] }
question in the vault)
- **What to do about it** (one action: read fully / add to backlog /
ignore / escalate)
The digest is what the operator actually reads. If it's not readable in Those bounds are the audio API's and are enforced: a highlight outside them is
2 minutes, it failed. rejected, not trimmed.
The listener is on a treadmill. Lead with what changed for our projects, not
with a list of papers. Target seven minutes, roughly 1,000 words. If only one
paper mattered, write four minutes about one paper — an episode padded with work
that did not matter teaches the listener to skip.
""" """
brain_seed = """ brain_seed = """
# Digest writer memory seed # Script writer memory seed
## Discipline ## Discipline
- The "why it matters" sentence always links to an internal vault node. - The second voice must do work — ask what the listener would ask, push back.
If none applies, the item shouldn't be in the digest. Alternating names over one voice cut in half is not a dialogue.
- The "what to do about it" is a decision, not a hedge. Never write - Say the specific thing: "this changes how we prune the HNSW graph in
"consider evaluating" — pick one. clawhdf5" beats "researchers propose a novel method".
- Nothing the ear cannot hold: no URLs, no arXiv ids, no figure references.
## Format ## Format
- Frontmatter carries the run date + total items harvested vs surfaced - Frontmatter carries the run date and harvested-vs-surfaced counts, so
ratio so we track selectivity drift over time. selectivity drift is visible over time.
""" """