| name | eigenfeed.score |
| description | Score candidates against taste profile, publish top picks, and generate feed. Used by /eigenfeed and /coldstart, also invocable directly. |
| allowed-tools | Agent, Bash(*), Read, Write, Glob, Grep |
Score
Evaluate discovered candidates against the user's taste profile and topics, save scores to the database, publish top picks, and generate the feed.
Key principle: use subagents to score candidates in parallel batches.
Prerequisites
Read the vault state:
vault/profile.md — anti-preferences and curiosity edges
vault/topics/*.md — interest topics (for understanding interests and provenance)
The caller passes candidate data (from the explore step) — a list of candidates with url, title, author, content, and target topics.
Scoring Criteria
Each candidate is evaluated on five criteria (0-3 each):
- RELEVANCE (weight 0.25): How well does this match the user's interests?
- NOVELTY (weight 0.20): Is this something the user wouldn't find on their own?
- QUALITY (weight 0.20): Based on the content, is this quality writing?
- RECENCY (weight 0.15): How fresh? 3=last week, 2=last month, 1=last few months, 0=older/undated
- SERENDIPITY (weight 0.20): Does this bridge multiple interests or open new curiosity paths?
Calculate total_score = relevance*0.25 + novelty*0.2 + quality*0.2 + recency*0.15 + serendipity*0.2
Important principles:
- Optimize for "would the user be glad they read this tomorrow?"
- Penalize sensationalism. Reward depth and originality.
- Read the full content to assess quality, don't just go by title.
- Mark as
skip for spam, low-quality, or content that doesn't match at all.
Parallel Scoring with Subagents
Security: scoring subagents must have NO tools (no Bash, no Write, no Edit). They see untrusted article content, so even if prompt injection succeeds, the subagent cannot act on it — it can only return text.
Split candidates into batches of 3-5. Spawn one subagent per batch, all in parallel.
Each subagent receives:
- The taste profile and topic list
- Its batch of candidates (with full content)
Each subagent returns per candidate:
- Scores (relevance, novelty, quality, recency, serendipity, total_score)
- Status: recommended | skip
- Why This? — 1-2 sentences with provenance: "Connects to [[topics/]] — ."
Save to Database
After scoring, write all scored candidates (both recommended and skipped) as JSON to tmp/scored.json:
[
{
"url": "https://example.com/article",
"title": "Article Title",
"author": "Author Name",
"relevance": 3,
"novelty": 2,
"quality": 3,
"recency": 2,
"serendipity": 2,
"total_score": 2.5,
"reasoning": "Connects to [[topics/slug]] — reason.",
"skip": false
}
]
Set "skip": true for candidates that should not be recommended. Then save to the database:
uv run python ${CLAUDE_SKILL_DIR}/scripts/save_and_generate.py --data-dir local/ --json-file tmp/scored.json --no-publish
Pick and Publish
When the caller requests publishing (passes picks_per_run), pick the top N unpublished recommendations and generate the feed:
uv run python ${CLAUDE_SKILL_DIR}/scripts/save_and_generate.py --data-dir local/ --pick <picks_per_run>
This marks the top N as published in the DB and regenerates local/feed.xml.
Write recommendation nodes to the vault
After publishing, mark previous active recommendations as expired, then write the new batch:
- Find all existing recommendation files with
tags: [recommendation, active] and update them to tags: [recommendation, expired].
- Write new recommendation files for the newly published batch.
mkdir -p vault/recommendations
For each newly published recommendation, write vault/recommendations/<date>-<slug>.md:
---
type: recommendation
tags: [recommendation, active]
url: <url>
title: "<title>"
author: "<author>"
total_score: <score>
published: YYYY-MM-DD
---
# <Title>
**By <Author>** | [source link](<url>)
## Why This?
<1-2 sentence provenance from scoring>
## Related Topics
- [[topics/<slug>]] — why this recommendation connects to this topic
Use today's date as prefix (e.g., 2026-03-28-stop-calling-it-memory.md). Recommendations accumulate over time — filter in Obsidian with tag:#recommendation AND tag:#active for the current batch, or tag:#recommendation for full history.
Return
Report to the parent skill:
- Total candidates scored vs recommended vs skipped
- Unpublished backlog count
- Which articles were published (if pick was requested)