From be12fb8b63c3ba9e0f838d7e7281d641114092b2 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Mon, 22 Jun 2026 14:51:50 +0200 Subject: [PATCH] =?UTF-8?q?feat(linkedin-studio):=20wire=20trend-spotter?= =?UTF-8?q?=20into=20the=20research=20engine=20=E2=80=94=20store=20+=20MCP?= =?UTF-8?q?=20routing=20+=20scoring=20SSOT=20(=C2=A75=20slice=202b)=20[ski?= =?UTF-8?q?p-docs]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01RBMKqPSVbvSZHtQ4heM1UY --- agents/trend-spotter.md | 172 +++++++++++++++++++++++++--------------- scripts/test-runner.sh | 38 ++++++++- 2 files changed, 142 insertions(+), 68 deletions(-) diff --git a/agents/trend-spotter.md b/agents/trend-spotter.md index 299d1ab..002c8cf 100644 --- a/agents/trend-spotter.md +++ b/agents/trend-spotter.md @@ -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____` 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 "" + ``` + 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 24–48h | +| **Tier 2 — Analysis & research** | where developments get interpreted, not just reported | 2–3×/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 1–10 per the mode's table, take the weighted composite +(both modes stay on the same 0–10 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 4–6 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 "" \ + --url "" \ + --topics "" \ + --source "" \ + --summary "" +``` + +`--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`) diff --git a/scripts/test-runner.sh b/scripts/test-runner.sh index 6b6ddd7..a807abb 100755 --- a/scripts/test-runner.sh +++ b/scripts/test-runner.sh @@ -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 8–16). +# 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 8–17). # # 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 14–15 (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.