Skip to content

Instantly share code, notes, and snippets.

@douglaz
Last active May 12, 2026 02:01
Show Gist options
  • Select an option

  • Save douglaz/c7d31f5b5c8ddb96eaeb3c22a3ce6211 to your computer and use it in GitHub Desktop.

Select an option

Save douglaz/c7d31f5b5c8ddb96eaeb3c22a3ce6211 to your computer and use it in GitHub Desktop.
news-lens spec explainer (rev 4) — Wiki-grounded news commentary fork of news-tagger
<!doctype html>
<html lang="en" class="bg-slate-950 text-slate-100">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>news-lens — spec rev 4</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', system-ui, sans-serif; }
.mono, code, pre { font-family: 'JetBrains Mono', ui-monospace, monospace; }
.arrow { position: relative; }
.arrow::after {
content: '';
position: absolute;
left: 50%;
bottom: -1.75rem;
transform: translateX(-50%);
width: 2px;
height: 1.5rem;
background: linear-gradient(to bottom, rgb(148 163 184), rgb(148 163 184 / 0.3));
}
.arrow::before {
content: '';
position: absolute;
left: 50%;
bottom: -1.85rem;
transform: translateX(-50%);
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 8px solid rgb(148 163 184 / 0.3);
}
.strike { text-decoration: line-through; text-decoration-color: rgb(244 63 94); }
</style>
</head>
<body class="antialiased selection:bg-amber-400 selection:text-slate-950">
<!-- =============== HERO =============== -->
<header class="border-b border-slate-800/80 bg-gradient-to-b from-slate-900 to-slate-950">
<div class="max-w-6xl mx-auto px-6 py-16 sm:py-24">
<div class="flex flex-wrap items-center gap-3 text-xs uppercase tracking-widest text-slate-400 mb-6">
<span class="px-2 py-1 rounded bg-amber-500/10 text-amber-300 border border-amber-500/30">SPEC rev 4</span>
<span class="px-2 py-1 rounded bg-slate-800 border border-slate-700">draft / pre-implementation</span>
<span class="px-2 py-1 rounded bg-rose-500/10 text-rose-300 border border-rose-500/30">hard fork of news-tagger</span>
</div>
<h1 class="text-5xl sm:text-7xl font-bold tracking-tight">
<span class="mono">news-lens</span>
</h1>
<p class="mt-6 text-xl sm:text-2xl text-slate-300 max-w-3xl leading-relaxed">
Wiki-grounded news commentary from a defined editorial perspective &mdash; initially Austrian-libertarian, captured in
<code class="text-amber-300">~/wiki/topics/libertarian/</code>.
</p>
<p class="mt-4 text-base text-slate-400 max-w-3xl">
One Rust fetch loop. One harness call per post. Wiki is the system of record. KISS.
</p>
</div>
</header>
<!-- =============== ONE-LINER PITCH =============== -->
<section class="max-w-6xl mx-auto px-6 py-16 grid sm:grid-cols-2 gap-6">
<div class="rounded-lg bg-slate-900/60 border border-slate-800 p-6">
<h3 class="text-sm font-semibold text-emerald-400 uppercase tracking-wider">Goals</h3>
<ul class="mt-4 space-y-3 text-slate-300">
<li class="flex gap-3"><span class="text-emerald-500 mono">·</span> Given a news item, produce commentary written from a defined editorial perspective.</li>
<li class="flex gap-3"><span class="text-emerald-500 mono">·</span> Ground commentary in the wiki: cite specific articles, reuse framings, stay consistent.</li>
<li class="flex gap-3"><span class="text-emerald-500 mono">·</span> Build a persistent archive of contextualized news.</li>
<li class="flex gap-3"><span class="text-emerald-500 mono">·</span><span>Reuse <span class="mono">news-tagger</span>'s plumbing where it still earns its place.</span></li>
</ul>
</div>
<div class="rounded-lg bg-slate-900/60 border border-slate-800 p-6">
<h3 class="text-sm font-semibold text-rose-400 uppercase tracking-wider">Non-goals</h3>
<ul class="mt-4 space-y-3 text-slate-300">
<li class="flex gap-3"><span class="text-rose-500 mono">·</span><span>Multi-label classification into a fixed taxonomy &mdash; that's <span class="mono">news-tagger</span>'s job.</span></li>
<li class="flex gap-3"><span class="text-rose-500 mono">·</span> Editorial perspectives outside what the connected wiki contains.</li>
<li class="flex gap-3"><span class="text-rose-500 mono">·</span> A general-purpose RAG framework.</li>
<li class="flex gap-3"><span class="text-rose-500 mono">·</span> Multi-perspective in one run, translation, images, embeddings, curate TUI.</li>
</ul>
</div>
</section>
<!-- =============== ARCHITECTURE DIAGRAM =============== -->
<section class="border-y border-slate-800 bg-slate-900/30">
<div class="max-w-6xl mx-auto px-6 py-16 sm:py-20">
<h2 class="text-3xl font-bold tracking-tight mb-2">Architecture &mdash; single agent</h2>
<p class="text-slate-400 mb-12">One harness invocation per post, period. Strictly serial.</p>
<div class="grid lg:grid-cols-12 gap-6">
<!-- Pipeline column -->
<div class="lg:col-span-7 space-y-7">
<!-- Step 1 -->
<div class="arrow rounded-lg border border-slate-700 bg-slate-900 p-5">
<div class="flex items-start gap-4">
<div class="flex-shrink-0 w-10 h-10 rounded-full bg-indigo-500/20 border border-indigo-500/40 flex items-center justify-center font-bold text-indigo-300">1</div>
<div>
<h4 class="font-semibold text-slate-100">Fetch loop (Rust)</h4>
<p class="text-sm text-slate-400 mt-1">Pull next new post from X / Nostr / JSONL fixture. One at a time. No mutex, no parallel tasks.</p>
</div>
</div>
</div>
<!-- Step 2 -->
<div class="arrow rounded-lg border border-amber-500/40 bg-amber-500/[0.04] p-5">
<div class="flex items-start gap-4">
<div class="flex-shrink-0 w-10 h-10 rounded-full bg-amber-500/20 border border-amber-500/50 flex items-center justify-center font-bold text-amber-300">2</div>
<div class="flex-1">
<h4 class="font-semibold text-amber-200">Shell out to harness <span class="text-xs text-slate-400 font-normal">— the single agentic call</span></h4>
<p class="text-sm text-slate-400 mt-1 mb-3"><code class="text-amber-300 mono">claude --print</code> / <code class="text-amber-300 mono">codex</code> / <code class="text-amber-300 mono">opencode</code> with <code class="text-amber-300 mono">prompts/process-post.md</code>.</p>
<ol class="mt-2 space-y-1.5 text-sm text-slate-300 list-decimal list-inside marker:text-amber-400">
<li>Read the lens.</li>
<li>File the post as <code class="text-amber-300 mono">raw/news/&lt;slug&gt;.md</code> via <code class="text-amber-300 mono">/wiki:ingest</code>.</li>
<li>Decide if newsworthy enough to comment.</li>
<li class="text-slate-400">If yes:
<ul class="ml-6 mt-1 space-y-1 list-disc marker:text-slate-600 text-slate-400">
<li>Read 5&ndash;12 relevant articles from <code class="mono">wiki/{concepts,topics,references}/</code>.</li>
<li>Draft commentary with <code class="mono">[[wikilinks]]</code> citations.</li>
<li>Write <code class="mono">wiki/theses/&lt;slug&gt;.md</code>.</li>
</ul>
</li>
<li>Run <code class="text-amber-300 mono">/wiki:lint --fix</code>.</li>
<li>Print final-line JSON.</li>
</ol>
</div>
</div>
</div>
<!-- Step 3 -->
<div class="arrow rounded-lg border border-slate-700 bg-slate-900 p-5">
<div class="flex items-start gap-4">
<div class="flex-shrink-0 w-10 h-10 rounded-full bg-emerald-500/20 border border-emerald-500/40 flex items-center justify-center font-bold text-emerald-300">3</div>
<div>
<h4 class="font-semibold text-slate-100">Render &amp; publish <span class="text-xs text-slate-400 font-normal">— only if a thesis was produced</span></h4>
<p class="text-sm text-slate-400 mt-1">Pure-Rust template renders the reply from <code class="mono">one_liner</code>. Publish via existing X/Nostr publisher. Subject to <code class="mono">--dry-run</code> and <code class="mono">--require-approval</code>.</p>
</div>
</div>
</div>
<!-- Step 4 -->
<div class="rounded-lg border border-slate-700 bg-slate-900 p-5">
<div class="flex items-start gap-4">
<div class="flex-shrink-0 w-10 h-10 rounded-full bg-slate-700 border border-slate-600 flex items-center justify-center font-bold text-slate-300">4</div>
<div>
<h4 class="font-semibold text-slate-100">Record</h4>
<p class="text-sm text-slate-400 mt-1">Insert into <code class="mono">processed_posts</code>: post_id, lens_id, stance, raw_path, thesis_slug, platform IDs. Loop.</p>
</div>
</div>
</div>
</div>
<!-- Right column: source of truth -->
<div class="lg:col-span-5 space-y-6">
<div class="rounded-lg border border-slate-700 bg-slate-950 p-5">
<h4 class="text-sm font-semibold text-slate-400 uppercase tracking-wider">Split source of truth</h4>
<div class="mt-4 space-y-4">
<div class="border-l-2 border-amber-500 pl-4">
<div class="text-xs uppercase tracking-wider text-amber-400 font-semibold">Filesystem (the wiki)</div>
<div class="mt-1 text-sm text-slate-300">raw news, theses, indexes, log.md, See Also backlinks</div>
</div>
<div class="border-l-2 border-sky-500 pl-4">
<div class="text-xs uppercase tracking-wider text-sky-400 font-semibold">state.sqlite</div>
<div class="mt-1 text-sm text-slate-300">fetch cursor, X / Nostr post IDs, what news-lens did per post</div>
</div>
</div>
<p class="mt-4 text-xs text-slate-500 italic">"Has post X been processed?" is a PK lookup. "What did we publish for thesis Y?" is a slug filter.</p>
</div>
<div class="rounded-lg border border-slate-700 bg-slate-950 p-5">
<h4 class="text-sm font-semibold text-slate-400 uppercase tracking-wider">The lens</h4>
<p class="mt-3 text-sm text-slate-300">A short markdown file separate from the wiki, declaring voice, register, and the stance vocabulary.</p>
<ul class="mt-4 space-y-2 text-sm">
<li class="flex gap-2"><span class="px-1.5 py-0.5 rounded bg-emerald-500/15 text-emerald-300 text-xs font-semibold mono">ENDORSE</span><span class="text-slate-400">news reports facts that confirm a wiki claim.</span></li>
<li class="flex gap-2"><span class="px-1.5 py-0.5 rounded bg-rose-500/15 text-rose-300 text-xs font-semibold mono">CRITIQUE</span><span class="text-slate-400">news's framing assumes premises the wiki rejects.</span></li>
<li class="flex gap-2"><span class="px-1.5 py-0.5 rounded bg-sky-500/15 text-sky-300 text-xs font-semibold mono">CONTEXT</span><span class="text-slate-400">news is descriptive, wiki adds frame.</span></li>
<li class="flex gap-2"><span class="px-1.5 py-0.5 rounded bg-slate-700 text-slate-300 text-xs font-semibold mono">DECLINE</span><span class="text-slate-400">no wiki article speaks to the substance.</span></li>
</ul>
</div>
</div>
</div>
</div>
</section>
<!-- =============== KEEP / DROP =============== -->
<section class="max-w-6xl mx-auto px-6 py-16">
<h2 class="text-3xl font-bold tracking-tight mb-2">What stays, what goes</h2>
<p class="text-slate-400 mb-10">Roughly &asymp; 2000 LOC deleted. The harness owns provider selection &mdash; news-lens does not.</p>
<div class="grid md:grid-cols-2 gap-6">
<!-- KEEP -->
<div class="rounded-lg border border-emerald-500/30 bg-emerald-500/[0.03]">
<div class="px-5 py-3 border-b border-emerald-500/30 flex items-center justify-between">
<h3 class="font-semibold text-emerald-300">Keep</h3>
<span class="text-xs text-emerald-400/70 mono">§3</span>
</div>
<div class="divide-y divide-emerald-500/10 text-sm">
<div class="px-5 py-3"><code class="mono text-emerald-300">SourcePost</code> &mdash; the post type</div>
<div class="px-5 py-3"><code class="mono text-emerald-300">PostSource</code> port + Jsonl / X / Stub sources</div>
<div class="px-5 py-3"><code class="mono text-emerald-300">Publisher</code> port + X / Nostr / outbox</div>
<div class="px-5 py-3"><code class="mono text-emerald-300">StateStore</code> port + sqlite / memory (simpler schema)</div>
<div class="px-5 py-3"><code class="mono text-emerald-300">Clock</code>, <code class="mono text-emerald-300">RateLimiter</code>, ignore-pattern compilation</div>
<div class="px-5 py-3"><code class="mono text-emerald-300">AppConfig</code> infra, env-var loading, doctor</div>
<div class="px-5 py-3">clap subcommand scaffolding (replace bodies)</div>
</div>
</div>
<!-- DROP -->
<div class="rounded-lg border border-rose-500/30 bg-rose-500/[0.03]">
<div class="px-5 py-3 border-b border-rose-500/30 flex items-center justify-between">
<h3 class="font-semibold text-rose-300">Drop</h3>
<span class="text-xs text-rose-400/70 mono">§4</span>
</div>
<div class="divide-y divide-rose-500/10 text-sm">
<div class="px-5 py-3"><code class="mono text-rose-300">TagDefinition</code>, <code class="mono text-rose-300">Taxonomy</code>, <code class="mono text-rose-300">taxonomy_hash</code></div>
<div class="px-5 py-3"><code class="mono text-rose-300">ClassifyOutput</code>, <code class="mono text-rose-300">TagMatch</code>, classify use case</div>
<div class="px-5 py-3"><code class="mono text-rose-300">render.rs</code> &mdash; agent renders prose</div>
<div class="px-5 py-3"><code class="mono text-rose-300">curate.rs</code> TUI</div>
<div class="px-5 py-3"><code class="mono text-rose-300">policy.forbidden_patterns</code> &mdash; lives in the lens now</div>
<div class="px-5 py-3 bg-rose-500/[0.06]"><code class="mono text-rose-300">adapters/llm/{anthropic,openai,gemini,ollama,opencode,openai_compat,claude_code,codex,local_command}.rs</code> <span class="text-rose-400/70 text-xs">&nbsp;~2000 LOC</span></div>
<div class="px-5 py-3"><code class="mono text-rose-300">FsWikiWriter</code> &mdash; agent calls <code class="mono">/wiki:ingest</code> directly</div>
</div>
</div>
</div>
</section>
<!-- =============== AGENT RETURN CONTRACT =============== -->
<section class="border-y border-slate-800 bg-slate-900/30">
<div class="max-w-6xl mx-auto px-6 py-16 grid lg:grid-cols-2 gap-10 items-start">
<div>
<h2 class="text-3xl font-bold tracking-tight mb-2">Agent return contract</h2>
<p class="text-slate-400 mb-6">The agent prints exactly one JSON object as its final stdout line. news-lens parses, validates, then publishes &mdash; never the other way around.</p>
<h4 class="text-sm font-semibold text-slate-400 uppercase tracking-wider mb-3">Validation before publish</h4>
<ul class="space-y-2 text-sm text-slate-300">
<li class="flex gap-3"><span class="mono text-amber-400">·</span><span><code class="mono">stance</code> is one of <code class="mono text-amber-300">endorse | critique | contextualize | decline | failed</code></span></li>
<li class="flex gap-3"><span class="mono text-amber-400">·</span><span><code class="mono">raw_path</code> exists on disk</span></li>
<li class="flex gap-3"><span class="mono text-amber-400">·</span><span>if not <code class="mono">decline</code>: <code class="mono">thesis_path</code>, <code class="mono">thesis_slug</code>, <code class="mono">one_liner</code> present, path exists</span></li>
<li class="flex gap-3"><span class="mono text-amber-400">·</span><span><code class="mono">one_liner.len() &le; 240</code> (truncate gently if not)</span></li>
</ul>
<h4 class="text-sm font-semibold text-slate-400 uppercase tracking-wider mt-8 mb-3">Failure mode</h4>
<p class="text-sm text-slate-300">Malformed JSON, missing path, timeout, any harness error &rarr; record <code class="mono text-rose-300">stance='failed'</code>, move on. No retries, no quarantine. Re-run by hand via <code class="mono">news-lens process --post &lt;id&gt;</code>.</p>
</div>
<pre class="rounded-lg border border-slate-700 bg-slate-950 p-5 text-sm overflow-x-auto mono leading-relaxed"><span class="text-slate-500">// agent prints exactly this on the final stdout line</span>
{
<span class="text-amber-300">"stance"</span>: <span class="text-emerald-300">"critique"</span>,
<span class="text-amber-300">"raw_path"</span>: <span class="text-emerald-300">"raw/news/2026-05-07-argentina-rent-decontrol.md"</span>,
<span class="text-amber-300">"raw_slug"</span>: <span class="text-emerald-300">"2026-05-07-argentina-rent-decontrol"</span>,
<span class="text-amber-300">"thesis_path"</span>: <span class="text-emerald-300">"wiki/theses/on-argentina-rent-decontrol.md"</span>,
<span class="text-amber-300">"thesis_slug"</span>: <span class="text-emerald-300">"on-argentina-rent-decontrol"</span>,
<span class="text-amber-300">"one_liner"</span>: <span class="text-emerald-300">"The cap simply cuts the supply they were</span>
<span class="text-emerald-300"> going to consume &mdash; see [[state-power-and-intervention]]."</span>
}</pre>
</div>
</section>
<!-- =============== STATE SCHEMA =============== -->
<section class="max-w-6xl mx-auto px-6 py-16 grid lg:grid-cols-2 gap-10 items-start">
<div>
<h2 class="text-3xl font-bold tracking-tight mb-2">State schema</h2>
<p class="text-slate-400 mb-6">Two tables. Intentionally tiny. Reconciliation with the wiki is a separate <code class="mono">wiki status</code> op &mdash; never on the hot path.</p>
<div class="grid grid-cols-2 gap-4 text-sm">
<div class="rounded border border-slate-800 bg-slate-900 p-4">
<div class="text-xs font-semibold text-slate-400 uppercase tracking-wider">No</div>
<ul class="mt-2 space-y-1 text-slate-400">
<li>cost_usd column</li>
<li>taxonomy_hash</li>
<li>retry_count</li>
<li>quarantine flag</li>
</ul>
</div>
<div class="rounded border border-slate-800 bg-slate-900 p-4">
<div class="text-xs font-semibold text-slate-400 uppercase tracking-wider">Yes</div>
<ul class="mt-2 space-y-1 text-slate-300">
<li>processed_posts</li>
<li>account_state</li>
</ul>
</div>
</div>
</div>
<pre class="rounded-lg border border-slate-700 bg-slate-950 p-5 text-sm overflow-x-auto mono leading-relaxed"><span class="text-sky-300">CREATE TABLE</span> processed_posts (
post_id <span class="text-amber-300">TEXT PRIMARY KEY</span>,
lens_id <span class="text-amber-300">TEXT NOT NULL</span>,
processed_at <span class="text-amber-300">TEXT NOT NULL</span>,
stance <span class="text-amber-300">TEXT NOT NULL</span>, <span class="text-slate-500">-- endorse|critique|contextualize|decline|failed</span>
raw_path <span class="text-amber-300">TEXT</span>, <span class="text-slate-500">-- relative to wiki root</span>
thesis_slug <span class="text-amber-300">TEXT</span>, <span class="text-slate-500">-- nullable when stance=decline|failed</span>
x_post_id <span class="text-amber-300">TEXT</span>,
nostr_event_id <span class="text-amber-300">TEXT</span>
);
<span class="text-sky-300">CREATE TABLE</span> account_state (
account <span class="text-amber-300">TEXT PRIMARY KEY</span>,
since_id <span class="text-amber-300">TEXT</span>,
updated_at <span class="text-amber-300">TEXT NOT NULL</span>
);</pre>
</section>
<!-- =============== LOCKED DECISIONS =============== -->
<section class="border-y border-slate-800 bg-slate-900/30">
<div class="max-w-6xl mx-auto px-6 py-16">
<h2 class="text-3xl font-bold tracking-tight mb-2">Locked decisions</h2>
<p class="text-slate-400 mb-10">Settled. The rest of the spec elaborates them. Struck-through items were locked earlier, then superseded.</p>
<div class="grid md:grid-cols-2 gap-x-8 gap-y-3 text-sm">
<div class="flex gap-3"><span class="mono text-amber-400 flex-shrink-0">1.</span><span>Hard fork of <code class="mono">news-tagger</code>, not a mode added to it.</span></div>
<div class="flex gap-3"><span class="mono text-amber-400 flex-shrink-0">2.</span><span>Single-wiki for v1. One topic-wiki path, one lens.</span></div>
<div class="flex gap-3"><span class="mono text-amber-400 flex-shrink-0">3.</span><span>The wiki is the system of record. news-lens is a contributor.</span></div>
<div class="flex gap-3"><span class="mono text-amber-400 flex-shrink-0">4.</span><span>Single agentic call per post. One harness invocation does ingest + commentary + lint.</span></div>
<div class="flex gap-3"><span class="mono text-amber-400 flex-shrink-0">5.</span><span>No upstream skill changes. Hand-crafted prompt orchestrates existing skills.</span></div>
<div class="flex gap-3"><span class="mono text-amber-400 flex-shrink-0">6.</span><span>Block-then-publish. Replies only after agent confirms thesis slug.</span></div>
<div class="flex gap-3"><span class="mono text-amber-400 flex-shrink-0">7.</span><span>Split source of truth: filesystem for wiki facts, <code class="mono">state.sqlite</code> for platform facts.</span></div>
<div class="flex gap-3"><span class="mono text-amber-400 flex-shrink-0">8.</span><span class="font-semibold text-amber-200">KISS for v1: smallest viable answer for every open question.</span></div>
<div class="flex gap-3 text-slate-500"><span class="mono flex-shrink-0">9.</span><span class="strike">Three LLM roles with separate config blocks.</span></div>
<div class="flex gap-3 text-slate-500"><span class="mono flex-shrink-0">10.</span><span class="strike">Three-pass CLI (ingest / comment / publish).</span></div>
<div class="flex gap-3 text-slate-500"><span class="mono flex-shrink-0">11.</span><span class="strike">Outbox staging directory + slug-claim mechanism.</span></div>
<div class="flex gap-3 text-slate-500"><span class="mono flex-shrink-0">12.</span><span class="strike">Per-wiki mutex / concurrency control.</span></div>
<div class="flex gap-3 text-slate-500"><span class="mono flex-shrink-0">13.</span><span class="strike">Per-call cost tracking + daily budget breaker.</span></div>
<div class="flex gap-3 text-slate-500"><span class="mono flex-shrink-0">14.</span><span class="strike">Quarantine + auto-retry on agent failure.</span></div>
</div>
</div>
</section>
<!-- =============== PHASED ROLLOUT =============== -->
<section class="max-w-6xl mx-auto px-6 py-16">
<h2 class="text-3xl font-bold tracking-tight mb-2">Phased rollout</h2>
<p class="text-slate-400 mb-10">Each phase is independently shippable. We do not graduate until the previous one earns trust.</p>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="rounded-lg border border-slate-700 bg-slate-900 p-5">
<div class="flex items-center justify-between mb-3">
<span class="px-2 py-0.5 rounded bg-slate-700 text-slate-300 text-xs font-semibold mono">PHASE 0</span>
<span class="text-xs text-slate-500">done</span>
</div>
<h4 class="font-semibold">Spec lock-in</h4>
<p class="mt-2 text-sm text-slate-400">This doc. Rev 4.</p>
</div>
<div class="rounded-lg border border-indigo-500/40 bg-indigo-500/[0.05] p-5">
<div class="flex items-center justify-between mb-3">
<span class="px-2 py-0.5 rounded bg-indigo-500/20 text-indigo-300 text-xs font-semibold mono">PHASE 1</span>
<span class="text-xs text-indigo-400">smoke</span>
</div>
<h4 class="font-semibold">One-shot agent</h4>
<p class="mt-2 text-sm text-slate-400"><code class="mono text-indigo-300">process --post --text "..."</code> against a real wiki. No publishing, no DB, no fetching.</p>
<p class="mt-3 text-xs text-slate-500 italic">Goal: see whether the prompt produces clean lint output and useful commentary.</p>
</div>
<div class="rounded-lg border border-slate-700 bg-slate-900 p-5">
<div class="flex items-center justify-between mb-3">
<span class="px-2 py-0.5 rounded bg-slate-700 text-slate-300 text-xs font-semibold mono">PHASE 2</span>
<span class="text-xs text-slate-500">fixture</span>
</div>
<h4 class="font-semibold">Fetch + state</h4>
<p class="mt-2 text-sm text-slate-400"><code class="mono">process --jsonl &lt;fixture&gt;</code> processes a batch. Records to state DB. Still no publishing.</p>
</div>
<div class="rounded-lg border border-slate-700 bg-slate-900 p-5">
<div class="flex items-center justify-between mb-3">
<span class="px-2 py-0.5 rounded bg-slate-700 text-slate-300 text-xs font-semibold mono">PHASE 3</span>
<span class="text-xs text-slate-500">live read</span>
</div>
<h4 class="font-semibold">Run loop, dry-run</h4>
<p class="mt-2 text-sm text-slate-400"><code class="mono">run --dry-run</code> polls X, processes posts, writes to wiki, does not publish. Eyeball for a week.</p>
</div>
<div class="rounded-lg border border-emerald-500/40 bg-emerald-500/[0.05] p-5">
<div class="flex items-center justify-between mb-3">
<span class="px-2 py-0.5 rounded bg-emerald-500/20 text-emerald-300 text-xs font-semibold mono">PHASE 4</span>
<span class="text-xs text-emerald-400">approval</span>
</div>
<h4 class="font-semibold">Publish</h4>
<p class="mt-2 text-sm text-slate-400">X / Nostr in <code class="mono text-emerald-300">--require-approval</code> mode (existing outbox). Every commentary reviewed before going out. Stay here a week.</p>
</div>
<div class="rounded-lg border border-slate-800 bg-slate-950/60 p-5">
<div class="flex items-center justify-between mb-3">
<span class="px-2 py-0.5 rounded bg-slate-800 text-slate-400 text-xs font-semibold mono">PHASE 5</span>
<span class="text-xs text-slate-600">re-evaluate</span>
</div>
<h4 class="font-semibold text-slate-300">Split out what measurement demands</h4>
<ul class="mt-2 text-sm text-slate-400 space-y-1">
<li>&middot; Cost too high &rarr; cheap prefilter back</li>
<li>&middot; Bad retrieval &rarr; Rust-side retrieval back</li>
<li>&middot; JSON drift &rarr; direct API with <code class="mono">response_format</code></li>
</ul>
<p class="mt-3 text-xs text-slate-500 italic">None requires rearchitecture. Each is a localized split.</p>
</div>
</div>
</section>
<!-- =============== KISS DEFERRED TABLE =============== -->
<section class="border-y border-slate-800 bg-slate-900/30">
<div class="max-w-6xl mx-auto px-6 py-16">
<h2 class="text-3xl font-bold tracking-tight mb-2">Deferred decisions</h2>
<p class="text-slate-400 mb-10">Every question below has a v1 default that picks the smallest viable answer. Each "no" in v1 saves real code &mdash; and becomes a localized addition when measurement justifies it.</p>
<div class="overflow-x-auto rounded-lg border border-slate-800">
<table class="w-full text-sm">
<thead class="bg-slate-900 border-b border-slate-800 text-slate-400">
<tr>
<th class="text-left font-semibold px-4 py-3 w-12">#</th>
<th class="text-left font-semibold px-4 py-3">Question</th>
<th class="text-left font-semibold px-4 py-3">v1 default</th>
<th class="text-left font-semibold px-4 py-3">Revisit trigger</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800 text-slate-300">
<tr class="hover:bg-slate-900/50">
<td class="px-4 py-3 mono text-slate-500">1</td>
<td class="px-4 py-3 font-medium">News dedup</td>
<td class="px-4 py-3 text-slate-400">No dedup. Process every post.</td>
<td class="px-4 py-3 text-slate-400"><code class="mono">wiki status</code> reports noticeable duplication.</td>
</tr>
<tr class="hover:bg-slate-900/50">
<td class="px-4 py-3 mono text-slate-500">2</td>
<td class="px-4 py-3 font-medium">What counts as news worth fetching</td>
<td class="px-4 py-3 text-slate-400">Existing pre-fetch filters. Agent <span class="mono">Decline</span> handles the rest.</td>
<td class="px-4 py-3 text-slate-400">Decline rate &gt; &asymp;80% &rarr; add <code class="mono">min_text_chars</code>.</td>
</tr>
<tr class="hover:bg-slate-900/50">
<td class="px-4 py-3 mono text-slate-500">3</td>
<td class="px-4 py-3 font-medium">Lens versioning</td>
<td class="px-4 py-3 text-slate-400">No hash, no <code class="mono">--force</code>. Manual <code class="mono">DELETE</code> + rerun.</td>
<td class="px-4 py-3 text-slate-400">Doing it more than once a month.</td>
</tr>
<tr class="hover:bg-slate-900/50">
<td class="px-4 py-3 mono text-slate-500">4</td>
<td class="px-4 py-3 font-medium">Harness failure modes</td>
<td class="px-4 py-3 text-slate-400">No retries, no quarantine. <code class="mono">stance='failed'</code>, move on.</td>
<td class="px-4 py-3 text-slate-400">Transient errors drop posts you wanted.</td>
</tr>
<tr class="hover:bg-slate-900/50">
<td class="px-4 py-3 mono text-slate-500">5</td>
<td class="px-4 py-3 font-medium">Cost runaway</td>
<td class="px-4 py-3 text-slate-400"><code class="mono">timeout_secs</code> only. Watch provider dashboard.</td>
<td class="px-4 py-3 text-slate-400">First surprise bill &rarr; add <code class="mono">max_posts_per_day</code>.</td>
</tr>
<tr class="hover:bg-slate-900/50">
<td class="px-4 py-3 mono text-slate-500">6</td>
<td class="px-4 py-3 font-medium">Concurrency</td>
<td class="px-4 py-3 text-slate-400">Strictly serial.</td>
<td class="px-4 py-3 text-slate-400">Throughput becomes the bottleneck.</td>
</tr>
<tr class="hover:bg-slate-900/50">
<td class="px-4 py-3 mono text-slate-500">7</td>
<td class="px-4 py-3 font-medium">Thesis re-edit on developing stories</td>
<td class="px-4 py-3 text-slate-400">Never. New news &rarr; new thesis with cross-links.</td>
<td class="px-4 py-3 text-slate-400">Story-chain repetition is visibly noisy.</td>
</tr>
<tr class="hover:bg-slate-900/50">
<td class="px-4 py-3 mono text-slate-500">8</td>
<td class="px-4 py-3 font-medium">Agent retrieval discipline</td>
<td class="px-4 py-3 text-slate-400">Trust the prompt's "read 5&ndash;12 articles" line.</td>
<td class="px-4 py-3 text-slate-400"><code class="mono">timeout_secs</code> keeps hitting.</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- =============== CLI SURFACE =============== -->
<section class="max-w-6xl mx-auto px-6 py-16">
<h2 class="text-3xl font-bold tracking-tight mb-2">CLI surface</h2>
<p class="text-slate-400 mb-10"><code class="mono">fetch</code>, <code class="mono">classify</code>, <code class="mono">curate</code> are dropped. Standard flags: <code class="mono">--dry-run</code>, <code class="mono">--require-approval</code>, <code class="mono">--config &lt;path&gt;</code>.</p>
<div class="rounded-lg border border-slate-800 overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-slate-900 border-b border-slate-800 text-slate-400">
<tr>
<th class="text-left font-semibold px-4 py-3">Command</th>
<th class="text-left font-semibold px-4 py-3">Behavior</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800 text-slate-300">
<tr class="hover:bg-slate-900/50"><td class="px-4 py-3 mono text-amber-300">news-lens run</td><td class="px-4 py-3 text-slate-400">Fetch new posts &rarr; agent call + optional publish &rarr; loop.</td></tr>
<tr class="hover:bg-slate-900/50"><td class="px-4 py-3 mono text-amber-300">news-lens run --once</td><td class="px-4 py-3 text-slate-400">One poll cycle.</td></tr>
<tr class="hover:bg-slate-900/50"><td class="px-4 py-3 mono text-amber-300">news-lens process --post &lt;id-or-text&gt;</td><td class="px-4 py-3 text-slate-400">Process a single post (fetched-by-id or ad-hoc text).</td></tr>
<tr class="hover:bg-slate-900/50"><td class="px-4 py-3 mono text-amber-300">news-lens process --jsonl &lt;path&gt;</td><td class="px-4 py-3 text-slate-400">Process posts from a fixture file.</td></tr>
<tr class="hover:bg-slate-900/50"><td class="px-4 py-3 mono text-amber-300">news-lens wiki status</td><td class="px-4 py-3 text-slate-400">Wiki path, raw news count, theses count, uncommented news count.</td></tr>
<tr class="hover:bg-slate-900/50"><td class="px-4 py-3 mono text-amber-300">news-lens lens list | show &lt;id&gt;</td><td class="px-4 py-3 text-slate-400">Inspect lens files.</td></tr>
<tr class="hover:bg-slate-900/50"><td class="px-4 py-3 mono text-amber-300">news-lens doctor</td><td class="px-4 py-3 text-slate-400">Existing doctor + wiki readable, lens parseable, harness reachable.</td></tr>
<tr class="hover:bg-slate-900/50"><td class="px-4 py-3 mono text-amber-300">news-lens config init</td><td class="px-4 py-3 text-slate-400">Generate config skeleton.</td></tr>
</tbody>
</table>
</div>
</section>
<!-- =============== FOOTER =============== -->
<footer class="border-t border-slate-800 bg-slate-950">
<div class="max-w-6xl mx-auto px-6 py-12 grid sm:grid-cols-2 gap-8 items-start">
<div>
<h3 class="font-semibold text-slate-200">Next steps</h3>
<ol class="mt-4 space-y-2 text-sm text-slate-400 list-decimal list-inside marker:text-amber-400">
<li>Stand up <code class="mono">news-lens</code> repo from a fork of <code class="mono">news-tagger@master</code>.</li>
<li>Delete §4 components.</li>
<li>Add the harness subprocess adapter.</li>
<li>Write <code class="mono">prompts/process-post.md</code> and iterate against a stub wiki.</li>
<li>Implement <code class="mono">process --post --text "..."</code> end-to-end. (= Phase 1.)</li>
<li>Wire post sources, state store, publishers back in. (= Phases 2&ndash;4.)</li>
</ol>
</div>
<div class="text-sm text-slate-500">
<div class="font-semibold text-slate-300 mb-2">Working name</div>
<p><span class="mono text-slate-200">news-lens</span>. Bikeshed later.</p>
<div class="mt-6 font-semibold text-slate-300 mb-2">Estimated effort</div>
<p>Phase 1: a couple of days. The codebase shrinks dramatically. Phases 2&ndash;4 are where the prompt engineering lives.</p>
</div>
</div>
<div class="border-t border-slate-800/80 bg-slate-950/80">
<div class="max-w-6xl mx-auto px-6 py-4 text-xs text-slate-600 flex flex-wrap items-center justify-between gap-2">
<span>SPEC-libertarian.md &mdash; rev 4 &mdash; 368 lines</span>
<span class="mono">draft / pre-implementation</span>
</div>
</div>
</footer>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment