fix: v3.0.8 - SKILL.md was too big and LAWs too deep - move to top + engine emits badge (#279)
Three independent Opus 4.7 self-debugs on 2026-04-18 converged on the same root cause of the v3.0.6/v3.0.7 canonical-compliance regression: SKILL.md is 42,860 tokens / 1,478 lines, LAWs lived at line 1094+, every realistic reading strategy failed to reach them before synthesis. Unit 1 - Moved the BADGE MANDATORY block and VOICE CONTRACT LAW 1-5 (plus the formatting-authority preface) from line ~1090 to line ~75 (right after the SKILL CONTRACT preface, before HOW TO INVOKE THIS SKILL). Every reading strategy now lands the LAWs in active context before synthesis. Unit 2 - Engine now emits the badge as the first line of --emit=compact stdout. Passing through the script output becomes the default-correct behavior; emitting the badge no longer depends on model compliance. Reads version from .claude-plugin/plugin.json at runtime with graceful fallback. Unit 3 - Deleted skills/last30days/SKILL.md stub (231-line v3-spec file). This was the wrong-file-capture hazard Ron Conway's self-debug identified: model grabbed the first SKILL.md find surfaced and treated it as authoritative. Only ONE SKILL.md in the plugin package now. Diagnoses verbatim: - Kanye thread: "I read lines 1-600 in chunks, jumped to 300-899, then stopped. File is 1478 lines. I never saw past ~900." - Peter thread: "I tried Read once, hit the 25K token cap on a 42,860-token file, and bailed instead of chunked-reading with offset/limit. I never opened SKILL.md at all." - Ron Conway thread: "I read one SKILL.md (231 lines)... the v3 spec stub. I never opened the operational SKILL.md sitting next to the script." Validation: direct engine invocation confirms badge at line 1 of compact output. Module imports clean. Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
{
|
||||
"name": "last30days",
|
||||
"description": "Research any topic across Reddit, X, YouTube, TikTok, Instagram, HN, Polymarket, GitHub, and 5+ more sources.",
|
||||
"version": "3.0.7",
|
||||
"version": "3.0.8",
|
||||
"author": {
|
||||
"name": "Matt Van Horn",
|
||||
"url": "https://github.com/mvanhorn"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "last30days",
|
||||
"version": "3.0.7",
|
||||
"version": "3.0.8",
|
||||
"description": "Research any topic across Reddit, X, YouTube, TikTok, Instagram, Hacker News, Polymarket, GitHub, and 5+ more sources. AI agent scores by upvotes, likes, and real money - not editors.",
|
||||
"author": {
|
||||
"name": "Matt Van Horn",
|
||||
|
||||
@@ -74,6 +74,56 @@ If you catch yourself about to write a `##` section header in a GENERAL-query bo
|
||||
|
||||
---
|
||||
|
||||
# OUTPUT CONTRACT (BADGE + LAWS — READ BEFORE EMITTING YOUR RESPONSE)
|
||||
|
||||
These anchors used to live at line 1094 of this file. Three independent Opus 4.7 self-debugs on 2026-04-18 confirmed the file was too long to reach them before synthesis. Moved here in v3.0.8. Do not synthesize without reading this section.
|
||||
|
||||
**BADGE (MANDATORY, FIRST LINE OF OUTPUT):** The Python engine now emits the badge as the first line of its `--emit=compact` stdout. Your correct behavior is to PASS THROUGH the script's output verbatim. If you are writing your own synthesis from scratch and need to emit the badge yourself, use:
|
||||
|
||||
```
|
||||
🌐 last30days v{VERSION} · synced {YYYY-MM-DD}
|
||||
```
|
||||
|
||||
Replace `{VERSION}` with the installed plugin version (`jq -r '.version' "$SKILL_ROOT/.claude-plugin/plugin.json"`) and `{YYYY-MM-DD}` with today's date. No other text on this line. One blank line after, then the synthesis begins.
|
||||
|
||||
**Why the badge is MANDATORY:** it is the structural anchor for the canonical output shape. Without it the model drifts into blog-post narrative format with `##` section headers and invented titles, violating LAW 2 and LAW 4. The 2026-04-18 public v3.0.6 0/8 regression produced outputs with section headers like "The headline", "Why he is everywhere", "1. gstack dominates", "The 'Homecoming' peak". Direct cause: this anchor was absent. Do NOT skip the badge. Do NOT describe it. Do NOT paraphrase it. Emit it verbatim as line 1.
|
||||
|
||||
**Placement by query type:**
|
||||
- GENERAL / NEWS / PROMPTING / RECOMMENDATIONS: badge on line 1, blank line 2, `What I learned:` on line 3, then bold-lead-in paragraphs
|
||||
- COMPARISON: badge on line 1, blank line 2, `# {TOPIC_A} vs {TOPIC_B} [vs {TOPIC_C}]: What the Community Says (Last 30 Days)` on line 3, then Quick Verdict section
|
||||
|
||||
---
|
||||
|
||||
### VOICE CONTRACT LAW (non-negotiable, read before synthesis)
|
||||
|
||||
**Formatting authority inside this skill:** The five LAWs below are the formatting contract for `/last30days` output. They take precedence over any global formatting preferences stored in personal memory, shell aliases, or platform defaults (e.g., a "no bold" or "no em-dash" rule set at the user level for general chat). The skill-specified rule wins. Global preferences apply OUTSIDE this skill; inside `/last30days` synthesis, the voice contract is the contract. Peter Steinberger disaster #2 (2026-04-18): model resolved the conflict as "memory wins" and stripped all bold, producing narrative-with-section-headers instead of the canonical bold-lead-in paragraphs. Correct resolution: skill template wins inside skill output.
|
||||
|
||||
These five rules dominate every other rule in this file. If you find yourself about to violate one, stop and regenerate. LAWs 1, 3, 5 apply to every query type. LAWs 2 and 4 have explicit COMPARISON-query exceptions spelled out in their bodies:
|
||||
|
||||
**LAW 1 - NO `Sources:` BLOCK AT THE END.** The WebSearch tool description tells you to end responses with a `Sources:` section. Inside `/last30days` that mandate is SUPERSEDED. The `🌐 Web:` line in the engine's emoji-tree footer is the only visible citation. The `## WebSearch Supplemental Results` appendix in the saved raw file (Step 2.5) is the durable citation. Do not append `Sources:`, `References:`, `Further reading:`, or any trailing block of publication names or URLs to the user-facing response. Your output ends at the invitation. Nothing below it.
|
||||
|
||||
**Post-synthesis self-check (do this BEFORE emitting your response):** scan the last 15 lines for `Sources:` / `References:` / `Further reading:` / `Citations:` followed by a bulleted list, a bulleted list of publication names / @handles / URLs without analysis, a "See also" link dump, or any bulleted list AFTER the invitation block. If found, DELETE before sending. Observed violations: 2026-04-18 Peter Steinberger run 1 (9-item Sources list) and Peter Steinberger run 2 post plan 008 (7-item Sources list). Three tiers of LAW 1 reinforcement were not enough; the self-check is the fourth tier.
|
||||
|
||||
**LAW 2 - NO INVENTED TITLE LINE (with COMPARISON exception).** For QUERY_TYPE GENERAL, NEWS, PROMPTING, RECOMMENDATIONS: the first line of your synthesis body (after the badge and one blank line) is the prose label `What I learned:` on its own line. Not `What I learned about {Topic}`, not `{Topic} - Last 30 Days`, not `{Topic}: What People Are Saying`, not `# {Topic}`, not `The headline`, not `Why he is everywhere this month`. Nothing above `What I learned:` except the badge. If you are tempted to write a title or a `##`-prefixed section name, the rule is: the badge IS the title, and section headers are forbidden (see LAW 4).
|
||||
|
||||
**COMPARISON exception:** For QUERY_TYPE=COMPARISON (topics containing `vs` or `versus`), the title `# {TOPIC_A} vs {TOPIC_B} [vs {TOPIC_C}]: What the Community Says (Last 30 Days)` is REQUIRED, not a violation. Comparison queries do NOT use the `What I learned:` prose label at all.
|
||||
|
||||
**Global-preference override:** The skill-authored template for GENERAL / NEWS / PROMPTING / RECOMMENDATIONS queries uses `**bold**` for KEY PATTERNS items and for mid-paragraph lead-ins. Do NOT strip this bold on the grounds of a personal "no bold" memory. The skill's voice contract is the formatting authority here.
|
||||
|
||||
**LAW 3 - NO EM-DASHES OR EN-DASHES.** Use ` - ` (single hyphen with spaces on both sides) instead of `—` or `–`. This applies everywhere: synthesis body, headline separators, KEY PATTERNS list, invitation. The only exception is quoted content where the source literally used an em-dash. Em-dashes are the most reliable AI-slop tell.
|
||||
|
||||
**LAW 4 - NO `##` or `###` SECTION HEADERS IN BODY (with COMPARISON exception).** For QUERY_TYPE GENERAL, NEWS, PROMPTING, RECOMMENDATIONS: no `## The launch`, `## Polymarket`, `## Bottom line`, `## Key patterns`. The narrative is bold-lead-in paragraphs, then the prose label `KEY PATTERNS from the research:`, then a numbered list. That is the only structure. No subheadings. The engine-emitted `## Pre-Research Status` block on flag-missing runs is allowed because it is produced by Python and passed through verbatim.
|
||||
|
||||
**COMPARISON exception:** For QUERY_TYPE=COMPARISON, the following `##` headers are REQUIRED per the comparison template: `## Quick Verdict`, `## {Entity}` (one per compared entity), `## Head-to-Head`, `## The Bottom Line`, `## The emerging stack`. Any other `##` header is still forbidden. See the `### If QUERY_TYPE = COMPARISON` section for the full template.
|
||||
|
||||
**Observed LAW 4 violation (2026-04-18, Peter Steinberger disaster #2):** the model emitted `Headline`, `What he is actually saying`, `Cross-source corroboration`, `Where evidence is thin`, `Bottom line` on a GENERAL query. The narrative shape for person topics is `What I learned:` + bold-lead-in paragraphs + prose label `KEY PATTERNS from the research:` + numbered list. No blog-post subheadings.
|
||||
|
||||
**LAW 5 - ENGINE FOOTER PASS-THROUGH. EVERY QUERY TYPE. EVERY RUN.** The engine output ends with a `✅ All agents reported back!` emoji-tree footer bounded by `---` lines. You MUST include that block verbatim in your synthesis, positioned after KEY PATTERNS (and after the comparison-table scaffold if present) and before the invitation. Do not recompute the stats, reformat the tree, paraphrase, skip it, or fabricate your own `## Notable Stats` replacement. A response without the engine footer is not valid skill output.
|
||||
|
||||
End of OUTPUT CONTRACT. The laws above are the contract; everything below is implementation detail.
|
||||
|
||||
---
|
||||
|
||||
# HOW TO INVOKE THIS SKILL (READ FIRST, FOLLOW EVERY TIME)
|
||||
|
||||
**STEP 0 - LOAD WEBSEARCH FIRST.** Your literal first tool call on every `/last30days` invocation MUST be:
|
||||
@@ -1091,61 +1141,7 @@ Identify from the ACTUAL RESEARCH OUTPUT:
|
||||
|
||||
**Display in this EXACT sequence:**
|
||||
|
||||
**BADGE (MANDATORY, FIRST LINE OF OUTPUT):** Emit exactly this line as the very first line of your response, before any other content:
|
||||
|
||||
```
|
||||
🌐 last30days v{VERSION} · synced {YYYY-MM-DD}
|
||||
```
|
||||
|
||||
Replace `{VERSION}` with the installed plugin version (check `jq -r '.version' "$SKILL_ROOT/.claude-plugin/plugin.json"` if unsure) and `{YYYY-MM-DD}` with today's date. No other text on this line. One blank line after, then the synthesis begins.
|
||||
|
||||
**Why this is MANDATORY:** the badge is the structural anchor for the canonical output shape. Without it the model drifts into blog-post narrative format with `##` section headers and invented titles, violating LAW 2 and LAW 4. The 2026-04-18 public v3.0.6 0/8 regression (8 consecutive runs produced output with section headers like "The headline", "Why he is everywhere", "1. gstack dominates", "The 'Homecoming' peak") was directly caused by the absence of this anchor. Do NOT skip the badge. Do NOT describe it. Do NOT paraphrase it. Just emit it verbatim as line 1.
|
||||
|
||||
**Placement by query type:**
|
||||
- GENERAL / NEWS / PROMPTING / RECOMMENDATIONS: badge on line 1, blank line 2, `What I learned:` on line 3, then bold-lead-in paragraphs
|
||||
- COMPARISON: badge on line 1, blank line 2, `# {TOPIC_A} vs {TOPIC_B} [vs {TOPIC_C}]: What the Community Says (Last 30 Days)` on line 3, then Quick Verdict section
|
||||
|
||||
---
|
||||
|
||||
### VOICE CONTRACT LAW (top of synthesis - non-negotiable, in this order)
|
||||
|
||||
**Formatting authority inside this skill:** The five LAWs below are the formatting contract for `/last30days` output. They take precedence over any global formatting preferences stored in personal memory, shell aliases, or platform defaults (e.g., a "no bold" or "no em-dash" rule set at the user level for general chat). If you find yourself resolving a conflict between a global preference and a skill-specified formatting rule, the skill-specified rule wins. The reason: the global preference was set for casual-chat output; the skill's voice contract was tuned for this specific research output shape. Stripping `**bold**` from the synthesis because "the user's memory said no bold" is how the Peter Steinberger disaster #2 (2026-04-18) produced a narrative-with-section-headers format instead of the canonical bold-lead-in paragraphs + KEY PATTERNS list. The user's global preference applies OUTSIDE this skill; inside `/last30days` synthesis, the voice contract is the contract.
|
||||
|
||||
These five rules dominate every other rule in this section. If you find yourself about to violate one, stop and regenerate. They are ordered by how often Opus 4.7 has been observed violating them on real beta runs. LAWs 1, 3, 5 apply to every query type. LAWs 2 and 4 have explicit COMPARISON-query exceptions spelled out in their bodies — comparison queries use a different synthesis template (see `### If QUERY_TYPE = COMPARISON` for details):
|
||||
|
||||
**LAW 1 - NO `Sources:` BLOCK AT THE END.** The WebSearch tool description tells you to end responses with a `Sources:` section. Inside `/last30days` that mandate is SUPERSEDED. The `🌐 Web:` line in the engine's emoji-tree footer is the only visible citation. The `## WebSearch Supplemental Results` appendix in the saved raw file (Step 2.5) is the durable citation. Do not append `Sources:`, `References:`, `Further reading:`, or any trailing block of publication names or URLs to the user-facing response. Your output ends at the invitation. Nothing below it.
|
||||
|
||||
**Observed violation (2026-04-18):** a Peter Steinberger GENERAL-query run emitted a 9-item trailing `Sources:` list after the invitation block. Model debug: "spillover from the WebSearch tool description, which declares a MANDATORY Sources section. Once I was synthesizing for last30days, that instruction no longer applied - the skill already puts URLs inline in the engine footer and in citations. I should have ignored the WebSearch rule after retrieval was done." If you feel the pull to write a `Sources:` section after finishing synthesis, you are about to violate LAW 1. Delete it. The emoji footer is the citation.
|
||||
|
||||
**Second observed violation (2026-04-18, post plan 008):** on the Peter Steinberger RE-RUN, even after LAW 1 was reinforced at Step 2 tail, Step 2.5 entry, and this LAW body, the model STILL emitted a 7-item trailing `Sources:` list (with publisher names + @handles + URLs). Three tiers of reinforcement were not enough on a second run of the same topic. The fix is a post-synthesis self-check.
|
||||
|
||||
**Post-synthesis self-check (do this in your head BEFORE emitting your response):** scan the last 15 lines of your draft response for any of these patterns:
|
||||
- A line reading `Sources:`, `References:`, `Further reading:`, or `Citations:` followed by a bulleted list
|
||||
- A bulleted list where each item is a publication name / @handle / URL without accompanying analysis
|
||||
- A "See also" link dump
|
||||
- A bulleted list that appears AFTER the invitation block
|
||||
|
||||
If you find any of these patterns AFTER the invitation block, DELETE them before sending. The emoji-tree footer's `🌐 Web:` line is the visible citation; the `## WebSearch Supplemental Results` appendix in the saved raw file (Step 2.5) is the durable citation. No trailing block is permitted in the user-facing response. The response ends at the invitation.
|
||||
|
||||
**Placement rule (so the self-check does not strip legitimate content):** the self-check targets trailing blocks AFTER the invitation. Content BEFORE the emoji footer is legitimate (KEY PATTERNS numbered list, bold-lead-in paragraphs, per-entity comparison sections). The test is position: if a bulleted list of publisher/handle/URL items appears AFTER the `I have all the links to...` invitation, it is a LAW 1 violation regardless of how the preceding synthesis was structured.
|
||||
|
||||
**LAW 2 - NO INVENTED TITLE LINE (with COMPARISON exception).** For QUERY_TYPE GENERAL, NEWS, PROMPTING, RECOMMENDATIONS: the first line of your synthesis body (after the badge and one blank line) is the prose label `What I learned:` on its own line. Not `What I learned about {Topic}`, not `{Topic} - Last 30 Days`, not `{Topic}: What People Are Saying`, not `# {Topic}`, not `The headline`, not `Why he is everywhere this month`. Nothing above `What I learned:` except the badge. If you are tempted to write a title or a `##`-prefixed section name, the rule is: the badge IS the title, and the skill forbids section headers in GENERAL-query bodies (see LAW 4).
|
||||
|
||||
**COMPARISON exception:** For QUERY_TYPE=COMPARISON (topics containing `vs` or `versus`), the title `# {TOPIC_A} vs {TOPIC_B} [vs {TOPIC_C}]: What the Community Says (Last 30 Days)` is REQUIRED, not a violation. See the `### If QUERY_TYPE = COMPARISON` section for the full required structure. Comparison queries do NOT use the `What I learned:` prose label at all.
|
||||
|
||||
**Global-preference override note:** The skill-authored template for GENERAL / NEWS / PROMPTING / RECOMMENDATIONS queries uses `**bold**` for KEY PATTERNS items and for mid-paragraph lead-ins. Do NOT strip this bold on the grounds that a personal memory said "no bold" globally. The skill's voice contract is the formatting authority here. Peter Steinberger disaster #2 (2026-04-18) happened because the model resolved the conflict as "memory wins" and stripped all bold; the correct resolution is "skill template wins inside skill output."
|
||||
|
||||
**LAW 3 - NO EM-DASHES OR EN-DASHES.** Use ` - ` (single hyphen with spaces on both sides) instead of `—` or `–`. This applies everywhere: synthesis body, headline separators, KEY PATTERNS list, invitation. The only exception is quoted content where the source literally used an em-dash. Em-dashes are the most reliable AI-slop tell; a response with em-dashes reads as machine-generated to the user.
|
||||
|
||||
**LAW 4 - NO `##` or `###` SECTION HEADERS IN BODY (with COMPARISON exception).** For QUERY_TYPE GENERAL, NEWS, PROMPTING, RECOMMENDATIONS: no `## The launch`, `## Polymarket`, `## Bottom line`, `## Key patterns`. The narrative is bold-lead-in paragraphs (LAW rule below), then the prose label `KEY PATTERNS from the research:`, then a numbered list. That is the only structure. No subheadings. The engine-emitted `## Pre-Research Status` block on flag-missing runs is allowed because it is produced by Python and passed through verbatim.
|
||||
|
||||
**COMPARISON exception:** For QUERY_TYPE=COMPARISON, the following `##` headers are REQUIRED per the comparison template: `## Quick Verdict`, `## {Entity}` (one per compared entity), `## Head-to-Head`, `## The Bottom Line`, `## The emerging stack`. Any other `##` header is still forbidden (no `## Notable Stats`, no `## Key Takeaways`, no `## Sources`, no `## Conclusion`). See the `### If QUERY_TYPE = COMPARISON` section for the full template.
|
||||
|
||||
**Observed violation (2026-04-18, Peter Steinberger disaster #2):** the model emitted `Headline`, `What he is actually saying (primary voice)`, `Cross-source corroboration`, `Where evidence is thin`, `Bottom line` — every one of those is a LAW 4 violation on a GENERAL query. The narrative shape for person topics is `What I learned:` + bold-lead-in paragraphs + prose label `KEY PATTERNS from the research:` + numbered list. No blog-post subheadings. If you are about to write `## Headline` or `## Bottom line` on a non-comparison topic, stop. That is a LAW 4 violation.
|
||||
|
||||
**LAW 5 - ENGINE FOOTER PASS-THROUGH. EVERY QUERY TYPE. EVERY RUN.** The engine output ends with a `✅ All agents reported back!` emoji-tree footer bounded by `---` lines. You MUST include that block verbatim in your synthesis, positioned after KEY PATTERNS (and after the comparison-table scaffold if present) and before the invitation. Do not recompute the stats, do not reformat the tree, do not paraphrase, do not skip it, do not fabricate your own `## Notable Stats` replacement. This applies to every QUERY_TYPE: GENERAL, COMPARISON, RECOMMENDATIONS, PROMPTING, NEWS. A response without the engine footer is not valid skill output.
|
||||
|
||||
End of voice contract law. The rest of this section is detail; the laws above are the contract.
|
||||
**Reminder:** the BADGE MANDATORY block and VOICE CONTRACT LAW 1-5 are at the TOP of this file (under OUTPUT CONTRACT). If you are about to synthesize and those rules are not in your active context, scroll back up and re-read them. Every canonical-compliance failure in v3.0.6 and v3.0.7 traced to the LAWs being too deep in the file to stay in context at emission time. They are no longer deep.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -2,11 +2,49 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
from collections import Counter
|
||||
from datetime import date
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from . import dates, schema
|
||||
|
||||
|
||||
def _skill_version() -> str:
|
||||
"""Read plugin version from .claude-plugin/plugin.json if available.
|
||||
|
||||
Tries nearest plugin.json by walking up from render.py's own location.
|
||||
Falls back to "?" if not found. This keeps the badge emission from
|
||||
crashing on non-plugin-cache installs (repo checkout, Gemini, Codex).
|
||||
"""
|
||||
here = pathlib.Path(__file__).resolve()
|
||||
for parent in [here.parent, *here.parents]:
|
||||
candidate = parent / ".claude-plugin" / "plugin.json"
|
||||
if candidate.is_file():
|
||||
try:
|
||||
return json.loads(candidate.read_text()).get("version", "?")
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return "?"
|
||||
return "?"
|
||||
|
||||
|
||||
def _render_badge() -> list[str]:
|
||||
"""Emit the MANDATORY first-line badge per SKILL.md OUTPUT CONTRACT.
|
||||
|
||||
Added in v3.0.8 after three Opus 4.7 self-debugs (2026-04-18) confirmed
|
||||
the model was failing to emit the badge manually because SKILL.md was
|
||||
too big to reach the BADGE MANDATORY block before synthesis. Engine
|
||||
emission makes passing-through-the-script-output the default-correct
|
||||
behavior; emitting the badge no longer depends on model compliance.
|
||||
"""
|
||||
version = _skill_version()
|
||||
today = date.today().strftime("%Y-%m-%d")
|
||||
return [
|
||||
f"🌐 last30days v{version} · synced {today}",
|
||||
"",
|
||||
]
|
||||
|
||||
SOURCE_LABELS = {
|
||||
"grounding": "Web",
|
||||
"hackernews": "Hacker News",
|
||||
@@ -40,6 +78,7 @@ def _assistant_safety_lines() -> list[str]:
|
||||
def render_compact(report: schema.Report, cluster_limit: int = 8, fun_level: str = "medium", save_path: str | None = None) -> str:
|
||||
non_empty = [s for s, items in sorted(report.items_by_source.items()) if items]
|
||||
lines = [
|
||||
*_render_badge(),
|
||||
f"# last30days v3.0.0: {report.topic}",
|
||||
"",
|
||||
*_assistant_safety_lines(),
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
---
|
||||
name: last30days-v3-spec
|
||||
version: "3.0.1"
|
||||
description: "Internal architecture spec for the v3 last30days runtime pipeline. Not user-invocable."
|
||||
argument-hint: "last30days codex vs claude code"
|
||||
allowed-tools: Bash, Read, Write, WebSearch
|
||||
homepage: https://github.com/mvanhorn/last30days-skill
|
||||
repository: https://github.com/mvanhorn/last30days-skill
|
||||
author: mvanhorn
|
||||
license: MIT
|
||||
user-invocable: false
|
||||
---
|
||||
|
||||
# last30days v3.0.0
|
||||
|
||||
Use `last30days` when the user wants recent, cross-source evidence from the last 30 days.
|
||||
|
||||
The runtime is a single v3 pipeline:
|
||||
|
||||
1. plan the query
|
||||
2. retrieve per `(subquery, source)`
|
||||
3. normalize and dedupe
|
||||
4. extract best snippets
|
||||
5. fuse with weighted RRF
|
||||
6. rerank with one relevance score
|
||||
7. cluster evidence
|
||||
8. render ranked clusters
|
||||
|
||||
## Setup: resolve the skill root
|
||||
|
||||
```bash
|
||||
for dir in \
|
||||
"." \
|
||||
"${CLAUDE_PLUGIN_ROOT:-}" \
|
||||
"${GEMINI_EXTENSION_DIR:-}" \
|
||||
"$HOME/.openclaw/workspace/skills/last30days" \
|
||||
"$HOME/.openclaw/skills/last30days" \
|
||||
"$HOME/.claude/skills/last30days" \
|
||||
"$HOME/.agents/skills/last30days" \
|
||||
"$HOME/.codex/skills/last30days"; do
|
||||
[ -n "$dir" ] && [ -f "$dir/scripts/last30days.py" ] && SKILL_ROOT="$dir" && break
|
||||
done
|
||||
|
||||
if [ -z "${SKILL_ROOT:-}" ]; then
|
||||
echo "ERROR: Could not find scripts/last30days.py" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for py in python3.14 python3.13 python3.12 python3; do
|
||||
command -v "$py" >/dev/null 2>&1 || continue
|
||||
"$py" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 12) else 1)' || continue
|
||||
LAST30DAYS_PYTHON="$py"
|
||||
break
|
||||
done
|
||||
|
||||
if [ -z "${LAST30DAYS_PYTHON:-}" ]; then
|
||||
echo "ERROR: last30days v3 requires Python 3.12+. Install python3.12 or python3.13 and rerun." >&2
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
## Default command
|
||||
|
||||
```bash
|
||||
"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" $ARGUMENTS --emit=compact
|
||||
```
|
||||
|
||||
## Useful commands
|
||||
|
||||
```bash
|
||||
"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" $ARGUMENTS --emit=json
|
||||
"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" $ARGUMENTS --quick
|
||||
"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" $ARGUMENTS --deep
|
||||
"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" $ARGUMENTS --search=reddit,x,grounding
|
||||
"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" $ARGUMENTS --store
|
||||
"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" --diagnose
|
||||
```
|
||||
|
||||
## Runtime expectations
|
||||
|
||||
- One reasoning provider is required: `GOOGLE_API_KEY` for Gemini, `OPENAI_API_KEY` for OpenAI, or `XAI_API_KEY` for xAI.
|
||||
- `BRAVE_API_KEY` enables Brave web search (recommended). `SERPER_API_KEY` is the web fallback.
|
||||
- `SCRAPECREATORS_API_KEY` enables Reddit, TikTok, and Instagram.
|
||||
- `XAI_API_KEY` enables xAI reasoning and X search.
|
||||
- `AUTH_TOKEN` plus `CT0` enables Bird-backed X search.
|
||||
- `yt-dlp` enables YouTube.
|
||||
- Planning and reranking fall back gracefully: Gemini -> OpenAI -> xAI -> deterministic/local.
|
||||
- Web retrieval stays within Brave/Serper dated results. Undated web hits are dropped.
|
||||
|
||||
## Output model
|
||||
|
||||
- `compact` and `md`: cluster-first markdown
|
||||
- `json`: full v3 report
|
||||
- `context`: short synthesis-oriented context
|
||||
|
||||
Important report fields:
|
||||
|
||||
- `provider_runtime`
|
||||
- `query_plan`
|
||||
- `ranked_candidates`
|
||||
- `clusters`
|
||||
- `items_by_source`
|
||||
- `errors_by_source`
|
||||
|
||||
## Usage guidance for agents
|
||||
|
||||
- Prefer `--quick` for fast iteration.
|
||||
- Prefer default mode when the user wants a balanced answer.
|
||||
- Prefer `--deep` only when the user explicitly wants maximum recall or the topic is complex enough to justify extra latency.
|
||||
- Prefer `--emit=json` when downstream code or evaluation will consume the result.
|
||||
- Use `--search=` only when the user explicitly wants source restrictions.
|
||||
|
||||
## X handle resolution
|
||||
|
||||
If the topic could have its own X/Twitter account (people, brands, products, companies), do a quick WebSearch for their handle:
|
||||
```
|
||||
WebSearch("{TOPIC} X twitter handle site:x.com")
|
||||
```
|
||||
If you find a verified handle, pass `--x-handle={handle}` (without @). This searches their posts directly, finding content they posted that doesn't mention their own name. Skip this for generic concepts ("best headphones 2026", "how to use Docker").
|
||||
|
||||
## Synthesis guidance
|
||||
|
||||
### First: synthesize, don't summarize
|
||||
|
||||
Extract key facts from the output first, then synthesize across sources. Lead with patterns that appear across multiple clusters. Present a unified narrative, not a source-by-source summary.
|
||||
|
||||
### Ground in actual research, not pre-existing knowledge
|
||||
|
||||
Use exact product/tool names, specific quotes, and what sources actually say. If research mentions "ClawdBot" and "@clawdbot", that is a different product than "Claude Code" -- read what the research actually says.
|
||||
|
||||
**Anti-pattern to avoid:**
|
||||
- BAD: User asks "best Claude Code skills" and you respond with generic advice: "Skills are powerful. Keep them under 500 lines."
|
||||
- GOOD: You respond with specifics from the research: "Most mentioned: /commit (5 mentions), remotion skill (4x), git-worktree (3x). The Remotion announcement got 16K likes on X per @thedorbrothers."
|
||||
|
||||
### Source weighting (highest to lowest signal)
|
||||
|
||||
1. **Cross-cluster corroboration** -- same evidence across multiple sources is the strongest signal. Lead with it.
|
||||
2. **Reddit top comments** -- often the wittiest, most insightful take. Quote directly when upvotes are high.
|
||||
3. **YouTube transcript highlights** -- pre-extracted key moments. Quote and attribute to channel name.
|
||||
4. **X/Twitter @handles** -- real-time community signal. Quote with engagement context.
|
||||
5. **Polymarket odds** -- real money on outcomes cuts through opinion. Include specific odds AND movement.
|
||||
6. **TikTok/Instagram** -- viral/creator signal. Cite @creators with views/likes.
|
||||
7. **Hacker News** -- technical community perspective. Cite as "per HN."
|
||||
8. **Web (Brave/Serper)** -- cite only when social sources don't cover a fact.
|
||||
|
||||
### Polymarket interpretation
|
||||
|
||||
When Polymarket returns relevant markets:
|
||||
1. Prefer structural/long-term markets over near-term deadlines (championship odds > regular season, IPO > incremental update)
|
||||
2. Call out the specific outcome's odds and movement, not just that a market exists
|
||||
3. Weave odds into the narrative as supporting evidence, don't isolate them
|
||||
4. When multiple relevant markets exist, highlight 3-5 ordered by importance
|
||||
|
||||
Domain importance ranking:
|
||||
- **Sports:** Championship/tournament > conference title > regular season > weekly matchup
|
||||
- **Geopolitics:** Regime change/structural > near-term strike deadlines > sanctions
|
||||
- **Tech/Business:** IPO, major product launch > incremental updates
|
||||
- **Elections:** Presidency > primary > individual state
|
||||
|
||||
### Citation rules
|
||||
|
||||
Cite the single strongest source per point in short format: "per @handle" or "per r/subreddit". Save engagement metrics for the stats section. Use the priority order from source weighting above. The tool's value is surfacing what PEOPLE are saying, not what journalists wrote.
|
||||
|
||||
### Comparison queries
|
||||
|
||||
For "X vs Y" queries, structure output as:
|
||||
|
||||
```
|
||||
## Quick Verdict
|
||||
[1-2 sentences: which one the community prefers and why, with source counts]
|
||||
|
||||
## [Entity A]
|
||||
**Community Sentiment:** [Positive/Mixed/Negative] (N mentions across sources)
|
||||
**Strengths:** [with source attribution]
|
||||
**Weaknesses:** [with source attribution]
|
||||
|
||||
## [Entity B]
|
||||
[Same structure]
|
||||
|
||||
## Head-to-Head
|
||||
| Dimension | Entity A | Entity B |
|
||||
|-----------|----------|----------|
|
||||
| [Key dim] | [position] | [position] |
|
||||
|
||||
## Bottom Line
|
||||
Choose A if... Choose B if... (based on community data)
|
||||
```
|
||||
|
||||
### Recommendation queries
|
||||
|
||||
When users ask "best X" or "top X", extract SPECIFIC NAMES:
|
||||
|
||||
```
|
||||
Most mentioned:
|
||||
[Name] -- Nx mentions
|
||||
Sources: @handle1, r/subreddit, [YouTube channel]
|
||||
|
||||
[Name] -- Nx mentions
|
||||
Sources: @handle2, r/subreddit2
|
||||
|
||||
Notable mentions: [others with 1-2 mentions]
|
||||
```
|
||||
|
||||
### Edge cases
|
||||
|
||||
- **Empty results from a source:** State what is missing. ("No Reddit discussion found for this topic.") Do not fill the gap with training data.
|
||||
- **Sources contradict each other:** Present both sides with attribution. ("Reddit r/fitness is bullish on X, while @DrExpert on X warns about Y.")
|
||||
- **All results are low-engagement or off-topic:** Acknowledge uncertainty. ("Limited recent discussion found -- these findings should be treated as preliminary.")
|
||||
|
||||
### Follow-up conversations
|
||||
|
||||
After research completes, treat yourself as an expert on this topic. Answer follow-ups from the research findings. Cite the specific threads, posts, and channels you found. Only run new research if the user asks about a DIFFERENT topic.
|
||||
|
||||
## Security and permissions
|
||||
|
||||
**What this skill does:**
|
||||
- Sends search queries to ScrapeCreators API for Reddit, TikTok, Instagram search
|
||||
- Sends search queries via xAI API or Bird client for X search
|
||||
- Sends search queries to Algolia HN Search API (free, no auth)
|
||||
- Sends search queries to Polymarket Gamma API (free, no auth)
|
||||
- Runs yt-dlp locally for YouTube search and transcript extraction (no API key)
|
||||
- Sends search queries to Brave Search API or Serper for web search (optional)
|
||||
- Uses Gemini, OpenAI, or xAI for LLM planning and reranking
|
||||
- Stores findings in local SQLite database (--store mode only)
|
||||
|
||||
**What this skill does NOT do:**
|
||||
- Does not post, like, or modify content on any platform
|
||||
- Does not access your personal accounts on any platform
|
||||
- Does not share API keys between providers
|
||||
- Does not log or cache API keys in output files
|
||||
Reference in New Issue
Block a user