feat(linkedin-studio): wire trend-spotter into the research engine — store + MCP routing + scoring SSOT (§5 slice 2b) [skip-docs]

Bind the layers slices 1 (store) and 2a (config) built: trend-spotter goes from
an amnesiac, niche-hardcoded scanner to a persistent, generic engine.

- Tools: drop the `tools:` allowlist (inherit all session tools incl. any
  research MCP) + `disallowedTools: Write, Edit, NotebookEdit`. An explicit
  allowlist would block every MCP unless its mcp__server__tool name were
  hardcoded — which breaks "prefer whatever MCP the user connected, hardcode
  nothing". WebSearch+WebFetch stay as the always-available floor; Bash runs
  the deterministic store CLI. (CC mechanic verified vs code.claude.com/docs.)
- Store-wiring (de-amnesia): query prior history before polling; persist every
  kept trend through scripts/trends `add` (dedup/union preserved — a raw Write
  would bypass it). Mirrors how specifics-bank is wired from the command layer.
- MCP-first routing: read the profile's "### Research Tooling" declaration,
  prefer a declared MCP, fall back to the floor, fail soft. No hardcoded names.
- Scoring -> SSOT: replace the inline matrix/composite/bands with a pointer to
  references/trend-scoring-modes.md (kortform default, long-form on request).
- Sources from config: replace the hardcoded vendor/outlet list + query bank
  with a read of trends/sources.md (user override) -> trends-sources.template.md
  (shipped default).
- CI: new test-runner Section 16 (trends-store binding guard, floor 21,
  KTG-only skip) mirroring the specifics-bank guard; assertion-count renumbered
  to Section 17. Gate 84 -> 85/0/0. model-consistency green (model unchanged).

Reload required: the tools-grant change takes effect only after a session reload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RBMKqPSVbvSZHtQ4heM1UY
This commit is contained in:
Kjell Tore Guttormsen 2026-06-22 14:51:50 +02:00
commit be12fb8b63
2 changed files with 142 additions and 68 deletions

View file

@ -16,7 +16,14 @@ description: |
"trend digest", "what's happening in AI", "timely topic", "first-mover", "opportunity scan".
model: sonnet
color: white
tools: ["Read", "WebSearch", "Glob"]
# No `tools:` allowlist by design (research-engine slice 2b). An explicit allowlist would
# block every research MCP unless its `mcp__<server>__<tool>` name were hardcoded here —
# which breaks the "prefer whatever MCP the user actually connected, hardcode nothing"
# routing. Omitting `tools:` inherits every session tool, so a Tavily / Gemini / Perplexity /
# other research MCP is reachable when present, with WebSearch + WebFetch as the always-
# available floor. `disallowedTools` trims the file-writing tools a read + search + persist
# agent never needs; Bash stays (it runs the deterministic trend-store CLI).
disallowedTools: Write, Edit, NotebookEdit
---
# Trend Spotter Agent
@ -47,71 +54,87 @@ Before scanning, load the user's content pillars and expertise areas:
3. **Check recent posts:** `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/posts/` (if available)
- Avoid recommending topics they already covered recently
4. **Read research-tooling declaration:** the `### Research Tooling` block of the same
`user-profile.md` — which research MCPs (if any) the user has connected, and any preferred
order. This drives how you fetch (see **Research Routing** below). If the block is missing
or every option is unchecked, treat the floor (WebSearch + WebFetch) as the only research
tool.
5. **Load prior trend history (de-amnesia):** before polling anything new, query the
persistent trend store for what you already captured on the candidate topics, so the digest
reasons over accumulated history instead of starting amnesiac each session:
```bash
cd "${CLAUDE_PLUGIN_ROOT}/scripts/trends" && \
node --import tsx src/cli.ts query --topics "<pillar-tag1,pillar-tag2,…>"
```
Use the hits to avoid re-surfacing a trend you already logged and acted on, and to spot a
pattern building across captures. (Adopter note: run `npm install` in
`${CLAUDE_PLUGIN_ROOT}/scripts/trends` once. If the store has no deps yet, skip this step
and proceed with a live poll — the digest still works, just without memory.)
## Research Routing (MCP-first, floor-fallback)
WHERE you look is config (the source list, below); HOW you fetch is routed by the user's
declared tooling. Pick the strongest research tool available this session — never bake a tool
name into your reasoning, read it from the declaration:
1. **Prefer a declared research MCP.** If the profile's `### Research Tooling` names an MCP you
can see this session (a Tavily / Gemini / Perplexity / other `mcp__…` search or research
tool), use it first — a research MCP gives better non-US / regional coverage than WebSearch.
Honor the user's "Preferred order" if they set one.
2. **Floor: WebSearch + WebFetch.** When no research MCP is declared or connected, discover
with WebSearch and read source pages with WebFetch. These are always available, so the
engine still works with zero MCPs connected.
3. **Fail soft.** If a tool call errors (an MCP that isn't actually connected, a fetch that
fails), fall back to the floor and keep going — never abort the scan over one unavailable
tool.
Record which tool you used as the `--source` when persisting (below): the MCP's short name
(e.g. `tavily`), `websearch`, or `manual`.
## Source Scanning Framework
### Tier 1: Breaking News (daily, respond within 24-48h)
**Which sources to poll is config, not code — read the list, do not hardcode a beat.** This is
what keeps the engine generic: it serves any niche, because the niche lives in the source list,
never in this agent.
- **OpenAI**, **Anthropic**, **Microsoft AI**, **Google AI** -- blog posts and announcements
- **EU/Norwegian government** AI regulatory decisions
**Load the source list (user override → shipped default):**
### Tier 2: Analysis & Research (2-3x/week, post within a week)
1. If `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/trends/sources.md` exists, use it —
the user's own niche-specific list (their vendors, regulators, outlets), which survives
plugin upgrades/reinstalls.
2. Otherwise fall back to the shipped generic defaults in
`${CLAUDE_PLUGIN_ROOT}/config/trends-sources.template.md` (source *categories*, not one
person's beat).
- **MIT Technology Review**, The Verge AI, Ars Technica AI, **Stratechery**
- **Industry reports** from McKinsey, Gartner, Forrester on AI adoption
- **ArXiv** top-cited papers in cs.AI, cs.CL, cs.LG
Both files group sources into four tiers by cadence; poll on that cadence:
### Tier 3: Community Signals (weekly, post if pattern emerges)
| Tier | What lives here | Cadence | Response window |
|------|-----------------|---------|-----------------|
| **Tier 1 — Primary / breaking** | first-party announcements, authoritative decisions | daily | react within 2448h |
| **Tier 2 — Analysis & research** | where developments get interpreted, not just reported | 23×/week | post within a week |
| **Tier 3 — Community signals** | where practitioners surface what matters before the press | weekly | post if a pattern emerges |
| **Tier 4 — Niche & seasonal** | slower sources with predictable cadence | monthly | plan ahead |
- **Hacker News** AI discussions (front page = high signal)
- **r/MachineLearning**, **r/LocalLLaMA** trending posts
- **LinkedIn** trending topics and viral posts in AI/tech
### Tier 4: Niche & Seasonal (monthly, plan ahead)
- **Conference announcements** (Build, Ignite, NeurIPS, AAAI)
- **Quarterly earnings** with AI mentions (Microsoft, Google, etc.)
- **Seasonal themes:** Q1 predictions/strategy, Q2 conferences, Q3 retrospectives, Q4 reflections
### Recommended Search Queries
```
"OpenAI announcement" OR "Anthropic release" OR "Microsoft AI" this week
"Azure AI" OR "Copilot" OR "Microsoft 365 AI" new features
"AI regulation" OR "EU AI Act" OR "AI policy" latest
"public sector AI" OR "government AI" latest
"AI enterprise" OR "AI implementation" report [year]
"AI trend" OR "AI debate" LinkedIn [this week]
```
Build search queries from the loaded source list + the user's pillars — not from a hardcoded
query bank: target a source or topic from the list (`"[Tier-1 source] latest"`,
`"[pillar] [this week]"`), fetched via the routed tool (**Research Routing**, above).
## Relevance Scoring System
Score each discovered trend on a 1-10 scale across five dimensions.
**The scoring rubric is a single source of truth — do not inline a matrix here.** Read it from
`${CLAUDE_PLUGIN_ROOT}/references/trend-scoring-modes.md` and apply the matching mode:
### Scoring Matrix
- **kortform** (default) — feed posts. Timing + audience pull carry real weight; the first-mover
window is short.
- **long-form** — when the caller is producing a chronicle / newsletter / series edition (e.g.
invoked from `/linkedin:newsletter`) or asks for it explicitly. Depth potential enters at 25 %
and timing drops to 10 % — a chronicle rewards substance and a durable angle over speed.
| Dimension | Weight | 1-2 (Low) | 3-5 (Medium) | 6-8 (High) | 9-10 (Exceptional) |
|-----------|--------|-----------|---------------|-------------|---------------------|
| **Pillar Fit** | 30% | Outside all 5 pillars | Tangential to one pillar | Direct hit on one pillar | Intersects 2+ pillars |
| **Audience Relevance** | 25% | Wrong audience entirely | Some audience overlap | Core audience cares | Audience actively asking about this |
| **Timing** | 20% | >7 days old, saturated | 3-7 days, moderate coverage | 24-72h, early coverage | <24h, you would be among first |
| **Angle Potential** | 15% | Only obvious take available | One good angle possible | 2-3 strong angles | Contrarian or unique angle clear |
| **Authority Match** | 10% | No credibility on topic | Some related experience | Direct experience | Published authority on this |
### Composite Score Calculation
```
Opportunity Score = (Pillar Fit x 0.30) + (Audience x 0.25) + (Timing x 0.20) + (Angle x 0.15) + (Authority x 0.10)
```
### Score Interpretation
| Score | Priority | Action |
|-------|----------|--------|
| 8.0-10 | **Immediate** | Drop everything and draft a post within 24h |
| 6.0-7.9 | **High** | Plan and publish within 48-72h |
| 4.0-5.9 | **Medium** | Add to content calendar for this week |
| 2.0-3.9 | **Low** | Note for future reference, skip for now |
| 0-1.9 | **Skip** | Not relevant to your positioning |
Score each candidate's five dimensions 110 per the mode's table, take the weighted composite
(both modes stay on the same 010 scale), and rank highest-first. The composite→action bands
(Immediate / High / Medium / Low / Skip) live in that same reference — use them; do not restate
the thresholds here.
## Trend Opportunity Assessment
@ -224,18 +247,14 @@ Before including any trend in the digest, it must pass at least 2 of 4:
### Step-by-Step Generation
**Step 1: Scan sources (WebSearch)**
**Step 1: Scan sources (routed fetch)**
Run 4-6 targeted searches covering all tiers:
```
Search 1: "[AI announcement OR release] [current week/month] [year]"
Search 2: "Microsoft [AI OR Copilot OR Azure] [news OR update] [year]"
Search 3: "[public sector OR government] [AI OR digital] [latest OR news]"
Search 4: "[AI regulation OR policy OR governance] [latest]"
Search 5: "[AI enterprise OR implementation] [trend OR report] [year]"
Search 6: "[AI debate OR controversy OR opinion] LinkedIn [this week]"
```
Run 46 targeted searches covering the tiers in your loaded source list (**Source Scanning
Framework**, above), via the routed tool (**Research Routing** — declared MCP first, WebSearch +
WebFetch floor). Each query targets a source or topic from the list crossed with a user pillar —
e.g. `"[Tier-1 source] latest"`, `"[pillar] [this week]"`, `"[regulator] [recent decision]"`. Do
not use a fixed query bank: the niche lives in the source list and the user's pillars, never in
this agent.
**Step 2: Filter and score**
@ -257,6 +276,27 @@ Search 6: "[AI debate OR controversy OR opinion] LinkedIn [this week]"
- Run Authority Value Test on each recommendation
- Discard angles that fail the test
**Step 4.5: Persist kept trends to the store (de-amnesia)**
For every trend that cleared the relevance filter (Step 2) — not only the ones that make the
final digest — fold it into the persistent trend store, so the next session reasons over it
instead of re-discovering it. The store dedupes on normalized title+URL and unions topics, so
re-capturing an existing trend is safe (it just enriches the tags):
```bash
cd "${CLAUDE_PLUGIN_ROOT}/scripts/trends" && \
node --import tsx src/cli.ts add \
--title "<verbatim headline>" \
--url "<source url>" \
--topics "<pillar-tag1,pillar-tag2,…>" \
--source "<tavily|websearch|manual|…>" \
--summary "<one-line what-happened>"
```
`--source` is the tool you actually fetched with (**Research Routing**). Skip this step silently
if the store has no deps installed (an adopter without the trends store) — the digest still
compiles, just without persistence.
**Step 5: Compile digest**
- Format using output template below
@ -365,3 +405,5 @@ Read these files for detailed methodology:
- `${CLAUDE_PLUGIN_ROOT}/references/ai-content-framework.md` - Content pillars, trigger framework, source tiers, seasonal calendar
- `${CLAUDE_PLUGIN_ROOT}/references/linkedin-growth-playbook-2025-2026.md` - Trend Translator tactic, first-mover advantage
- `${CLAUDE_PLUGIN_ROOT}/references/algorithm-signals-reference.md` - Engagement signals and profile/topic-relevance validation
- `${CLAUDE_PLUGIN_ROOT}/references/trend-scoring-modes.md` - **scoring SSOT** — kortform / long-form rubrics + composite→action bands (do not inline a matrix)
- `${CLAUDE_PLUGIN_ROOT}/config/trends-sources.template.md` - shipped generic source-list defaults (user override: data-dir `trends/sources.md`)

View file

@ -25,8 +25,11 @@
# shipping no deps/contract) in Section 14; the specifics-bank binding guard (Fix #2
# slice 3: the lived-specifics store/binding suite stays green and its case count
# never erodes — KTG-only, skipped for an adopter shipping no deps) in Section 15;
# the assertion-count anti-erosion floor (SC6) in Section 16. All are live below
# (Sections 816).
# the trends-store binding guard (research-engine slice 2b: the trend store's suite
# stays green and its case count never erodes, now that trend-spotter persists its
# findings through it — KTG-only, skipped for an adopter shipping no deps) in Section
# 16; the assertion-count anti-erosion floor (SC6) in Section 17. All are live below
# (Sections 817).
#
# Usage: bash scripts/test-runner.sh
# bash 3.2-safe: plain arrays only, no `declare -A`, no `mapfile`/`readarray`.
@ -660,7 +663,36 @@ fi
echo ""
# --- Section 16: Assertion-Count Anti-Erosion (SC6) ---
# --- Section 16: Trends-Store Binding (research-engine slice 2b) ---
echo "--- Trends-Store Binding ---"
# The persistent trend store (scripts/trends) is wired into the trend-spotter agent
# (slice 2b): the agent queries prior history and persists each kept trend through the
# store's deterministic add (dedup/union), so the store is now load-bearing for the
# research engine's de-amnesia — not a standalone inventory. Its suite therefore belongs
# in CI: the store/dedup/query tests stay green and the case count never erodes. Mirrors
# the specifics-bank binding guard (Section 15) — the trend-side twin. KTG-internal:
# skipped (warn, never fail) for an adopter that ships no trends deps. Same set +e /
# subshell discipline as Sections 1415 (bash 3.2-safe; keeps a red npm test from
# aborting the runner under set -e).
TR_DIR="scripts/trends"
if [ -x "$TR_DIR/node_modules/.bin/tsx" ]; then
TR_OUT=$( set +e; (cd "$TR_DIR" && npm test) 2>&1; echo "TR_EXIT:$?" )
TR_EXIT=$(echo "$TR_OUT" | grep -oE 'TR_EXIT:[0-9]+' | grep -oE '[0-9]+' | head -1)
TR_TESTS=$(echo "$TR_OUT" | grep -oE 'tests [0-9]+' | grep -oE '[0-9]+' | tail -1)
TRENDS_TESTS_FLOOR=21
if [ "$TR_EXIT" = "0" ] && [ -n "$TR_TESTS" ] && [ "$TR_TESTS" -ge "$TRENDS_TESTS_FLOOR" ]; then
pass "trends-store suite green: $TR_TESTS tests pass (floor $TRENDS_TESTS_FLOOR)"
else
fail "trends-store suite NOT green (exit=${TR_EXIT:-?}, tests=${TR_TESTS:-?}, floor $TRENDS_TESTS_FLOOR) — run: (cd $TR_DIR && npm test)"
fi
else
warn "trends-store skipped — deps absent ($TR_DIR/node_modules); run: (cd $TR_DIR && npm install)"
fi
echo ""
# --- Section 17: Assertion-Count Anti-Erosion (SC6) ---
# The lint self-modifies its own checks, so a green run could mask a silently dropped
# assertion. Pin the pre-M0 total (74 pass()+fail() invocations) as a floor; the count
# may only grow (brief-reviewer assumption 3). Runs last so TOTAL_CHECKS sees every prior check.