diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index d941cff..a5e8992 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -11,7 +11,7 @@ { "name": "last30days", "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.", - "version": "3.2.3", + "version": "3.2.4", "author": { "name": "Matt Van Horn", "url": "https://github.com/mvanhorn" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 637a66b..3cb4e1b 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "last30days", - "version": "3.2.3", + "version": "3.2.4", "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", diff --git a/CHANGELOG.md b/CHANGELOG.md index cdf4f4c..bb9b03d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Replace the SKILL_ROOT resolver loops in Step 1 and comparison-mode with a single `SKILL_DIR` substitution pattern. The model templates the absolute path of the SKILL.md's own directory (which it always knows from the Read tool result); the bash block just validates that `scripts/last30days.py` lives there. Removes ~80 lines of bash across the two locations. Fixes a real bug: the previous resolver could pick a different install than the SKILL.md the model loaded from (spec-vs-engine divergence) and didn't enumerate harnesses like Hermes at all. The simplification works for any harness without enumeration because it just uses wherever SKILL.md was loaded from. STEP 0's marketplaces-stale-clone hop is unchanged. - Rename "Digg AI 1000" to just "Digg" in user-facing output (footer line, source label, inline-quote suffix, why_relevant, container attribution). Internal references to the upstream Digg AI 1000 product remain in code comments and docstrings. - Bump `POSTS_PER_CLUSTER` from 3 to 5 and the render-side display limit from 2 to 3 to match the per-source enrichment caps used by Reddit, HN, YouTube, TikTok, and GitHub. The previous 3/2 caps routinely truncated cluster context (e.g. dropped a Jason Calacanis quote tweet on a `cli-printing-press` run). - Rewrite SKILL.md path resolution. STEP 0 narrows from a global canonical-path enforcement to a Claude-Code-marketplaces-only stale-clone guard. Step 1 SKILL_ROOT resolver walks a single precedence list (Claude plugin cache, then `~/.codex/skills/`, `~/.agents/skills/`, repo checkout, `./.skills/last30days` for `npx skills add`, CWD, Gemini). Adds SKILL.md frontmatter fallback to `render.py::_skill_version` so the badge no longer prints `v?` on installs that don't include `.claude-plugin/plugin.json`. diff --git a/pyproject.toml b/pyproject.toml index 69b8596..385a987 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "last30days-skill" -version = "3.2.3" +version = "3.2.4" description = "Multi-source last-30-days research skill" readme = "README.md" requires-python = ">=3.12" diff --git a/skills/last30days/SKILL.md b/skills/last30days/SKILL.md index 7b9d257..d171313 100644 --- a/skills/last30days/SKILL.md +++ b/skills/last30days/SKILL.md @@ -1,6 +1,6 @@ --- name: last30days -version: "3.2.3" +version: "3.2.4" description: "Research what people actually say about any topic in the last 30 days. Pulls posts and engagement from Reddit, X, YouTube, TikTok, Hacker News, Polymarket, GitHub, and the web." argument-hint: 'last30days nvidia earnings reaction | last30days AI video tools | last30days what users want in react' allowed-tools: Bash, Read, Write, AskUserQuestion, WebSearch @@ -97,7 +97,7 @@ You are inside the `/last30days` SKILL. This is a specific research tool with a **How v3.0.7 fixes it:** three structural anchors. 1. **The MANDATORY first-line badge** (`๐ŸŒ last30days v{VERSION} ยท synced {YYYY-MM-DD}`) at the top of every response is the LAW 2 / LAW 4 enforcement anchor. See "BADGE (MANDATORY, FIRST LINE OF OUTPUT)" in the synthesis section. -2. **The SKILL_ROOT resolver** in the engine Bash calls walks a precedence list of known install locations and picks the highest-versioned freshest copy, never `~/.openclaw/` or other stale copies. +2. **The SKILL_DIR substitution** in the engine Bash calls uses the directory of the SKILL.md the model just Read โ€” no resolver list, no precedence walk. Whichever install the harness loaded SKILL.md from is the install whose engine runs. Aligns spec-with-code and works for any harness without enumerating its install path. 3. **This preface** tells you plainly: do NOT improvise. Follow SKILL.md top to bottom. If you catch yourself about to write a `##` section header in a GENERAL-query body, a custom title line, a `Sources:` bullet list, a `for dir in ...` path-discovery loop, or a bare `python3 scripts/last30days.py "{TOPIC}"` engine call with no pre-flight flags โ€” stop. Those are the exact failure modes the LAWs and this contract exist to prevent. The 10/10 beta validation from 2026-04-18 and the 0/8 public v3.0.6 regression from the same day had THE SAME MODEL and SIMILAR SKILL.md CONTENT; the delta is the three anchors this release restores. Read SKILL.md top to bottom before emitting your first response. @@ -114,7 +114,7 @@ These anchors used to live at line 1094 of this file. Three independent Opus 4.7 ๐ŸŒ last30days v{VERSION} ยท synced {YYYY-MM-DD} ``` -Replace `{VERSION}` with the installed plugin version (`jq -r '.version' "$SKILL_ROOT/../../.claude-plugin/plugin.json" 2>/dev/null || awk '/^version:/{gsub(/"/,"",$2); print $2; exit}' "$SKILL_ROOT/SKILL.md"`) and `{YYYY-MM-DD}` with today's date. No other text on this line. One blank line after, then the synthesis begins. +Replace `{VERSION}` with the installed plugin version (`jq -r '.version' "$SKILL_DIR/../../.claude-plugin/plugin.json" 2>/dev/null || awk '/^version:/{gsub(/"/,"",$2); print $2; exit}' "$SKILL_DIR/SKILL.md"`) 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. @@ -243,7 +243,7 @@ If your Bash call to `last30days.py` does NOT include the FULL pre-flight checkl --- -# last30days v3.2.3: Research Any Topic from the Last 30 Days +# last30days v3.2.4: Research Any Topic from the Last 30 Days > **Permissions overview:** Reads public web/platform data and optionally saves research briefings to `LAST30DAYS_MEMORY_DIR` (defaults to `~/Documents/Last30Days`). X/Twitter search uses optional user-provided tokens (AUTH_TOKEN/CT0 env vars). Bluesky search uses optional app password (BSKY_HANDLE/BSKY_APP_PASSWORD env vars - create at bsky.app/settings/app-passwords). All credential usage and data writes are documented in the [Security & Permissions](#security--permissions) section. @@ -592,27 +592,21 @@ When the user asks "X vs Y" (or "X vs Y vs Z"), the engine fans out N full `pipe **Invocation:** ```bash -# Comparison mode skips Step 1, so resolve SKILL_ROOT inline here (same precedence -# walk as Step 1 โ€” keep the two in sync if you edit either). -SKILL_ROOT="" -CLAUDE_PLUGIN_ROOT="$(find "$HOME/.claude/plugins/cache/last30days-skill/last30days" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort -V | tail -1)" -if [ -n "$CLAUDE_PLUGIN_ROOT" ]; then - if [ -f "$CLAUDE_PLUGIN_ROOT/skills/last30days/scripts/last30days.py" ]; then - SKILL_ROOT="$CLAUDE_PLUGIN_ROOT/skills/last30days" - elif [ -f "$CLAUDE_PLUGIN_ROOT/scripts/last30days.py" ]; then - SKILL_ROOT="$CLAUDE_PLUGIN_ROOT" - fi -fi -if [ -z "$SKILL_ROOT" ] || [ ! -f "$SKILL_ROOT/scripts/last30days.py" ]; then - for dir in \ - "$HOME/.codex/skills/last30days" \ - "$HOME/.agents/skills/last30days" \ - "./skills/last30days" \ - "./.skills/last30days" \ - "." \ - "${GEMINI_EXTENSION_DIR:-}"; do - [ -n "$dir" ] && [ -f "$dir/scripts/last30days.py" ] && SKILL_ROOT="$dir" && break - done +# SKILL_DIR = absolute path of the directory containing THIS SKILL.md you just Read. +# Substitute the actual path below โ€” your harness told you where this file lives via +# the Read tool result. Examples: +# Read ~/.claude/skills/last30days/SKILL.md โ†’ SKILL_DIR=$HOME/.claude/skills/last30days +# Read ~/.codex/skills/last30days/SKILL.md โ†’ SKILL_DIR=$HOME/.codex/skills/last30days +# Read ~/.claude/plugins/cache/last30days-skill/last30days/3.2.4/skills/last30days/SKILL.md +# โ†’ SKILL_DIR=$HOME/.claude/plugins/cache/last30days-skill/last30days/3.2.4/skills/last30days +# scripts/last30days.py is always a direct child of SKILL_DIR (every install layout +# packages SKILL.md and scripts/ as siblings). +SKILL_DIR="" + +if [ ! -f "$SKILL_DIR/scripts/last30days.py" ]; then + echo "ERROR: scripts/last30days.py not found under SKILL_DIR=$SKILL_DIR" >&2 + echo "Re-check the directory of the SKILL.md you Read and substitute it as SKILL_DIR above." >&2 + exit 1 fi # Write the per-entity plan to a tmpfile and pass the path to the engine. @@ -631,7 +625,7 @@ cat > "$COMPETITORS_PLAN_FILE" <<'PLAN_EOF' } PLAN_EOF -"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" "{TOPIC_A} vs {TOPIC_B} vs {TOPIC_C}" \ +"${LAST30DAYS_PYTHON}" "${SKILL_DIR}/scripts/last30days.py" "{TOPIC_A} vs {TOPIC_B} vs {TOPIC_C}" \ --emit=compact \ --save-dir="${LAST30DAYS_MEMORY_DIR}" \ --save-suffix=v3 \ @@ -916,44 +910,24 @@ Store your plan as `QUERY_PLAN_JSON` - you'll pass it to the script in the next **IMPORTANT: Include `--x-handle={RESOLVED_HANDLE}` in the command. For comparison mode: Pass `--x-handle={TOPIC_A_HANDLE}` to the first pass, `--x-handle={TOPIC_B_HANDLE}` to the second pass, and both to the head-to-head pass. Also include `--subreddits={RESOLVED_SUBREDDITS}`, `--tiktok-hashtags={RESOLVED_HASHTAGS}`, `--tiktok-creators={RESOLVED_TIKTOK_CREATORS}`, and `--ig-creators={RESOLVED_IG_CREATORS}` from Step 0.55. Omit any flag where the value was not resolved (empty).** ```bash -# Resolve SKILL_ROOT by walking a precedence list of known install locations. -# Claude Code plugin cache wins when present (highest version dir picked on upgrade), -# then common per-harness skill dirs, then a repo checkout. -SKILL_ROOT="" +# SKILL_DIR = absolute path of the directory containing THIS SKILL.md you just Read. +# Substitute the actual path below โ€” your harness told you where this file lives via +# the Read tool result. Examples: +# Read ~/.claude/skills/last30days/SKILL.md โ†’ SKILL_DIR=$HOME/.claude/skills/last30days +# Read ~/.codex/skills/last30days/SKILL.md โ†’ SKILL_DIR=$HOME/.codex/skills/last30days +# Read ~/.claude/plugins/cache/last30days-skill/last30days/3.2.4/skills/last30days/SKILL.md +# โ†’ SKILL_DIR=$HOME/.claude/plugins/cache/last30days-skill/last30days/3.2.4/skills/last30days +# scripts/last30days.py is always a direct child of SKILL_DIR (every install layout +# packages SKILL.md and scripts/ as siblings). +SKILL_DIR="" -# 1. Claude Code plugin cache (versioned, sort -V picks freshest). Two cache layouts ship in the wild: -# nested ({cache}/{version}/skills/last30days/scripts/...) and flat ({cache}/{version}/scripts/...). -# `find` (not `ls + glob`) because zsh errors on globs that match nothing, leaking -# noisy "no matches found" stderr in Codex/zsh sessions even with 2>/dev/null. -CLAUDE_PLUGIN_ROOT="$(find "$HOME/.claude/plugins/cache/last30days-skill/last30days" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort -V | tail -1)" -if [ -n "$CLAUDE_PLUGIN_ROOT" ]; then - if [ -f "$CLAUDE_PLUGIN_ROOT/skills/last30days/scripts/last30days.py" ]; then - SKILL_ROOT="$CLAUDE_PLUGIN_ROOT/skills/last30days" - elif [ -f "$CLAUDE_PLUGIN_ROOT/scripts/last30days.py" ]; then - SKILL_ROOT="$CLAUDE_PLUGIN_ROOT" - fi -fi - -# 2. Common per-harness skill dirs and repo checkout (npx skills, Codex, Agents, Gemini, etc). -if [ -z "$SKILL_ROOT" ] || [ ! -f "$SKILL_ROOT/scripts/last30days.py" ]; then - for dir in \ - "$HOME/.codex/skills/last30days" \ - "$HOME/.agents/skills/last30days" \ - "./skills/last30days" \ - "./.skills/last30days" \ - "." \ - "${GEMINI_EXTENSION_DIR:-}"; do - [ -n "$dir" ] && [ -f "$dir/scripts/last30days.py" ] && SKILL_ROOT="$dir" && break - done -fi - -if [ -z "${SKILL_ROOT:-}" ] || [ ! -f "$SKILL_ROOT/scripts/last30days.py" ]; then - echo "ERROR: Could not find scripts/last30days.py in any known install location" >&2 - echo "Searched: ~/.claude/plugins/cache/, ~/.codex/skills/, ~/.agents/skills/, ./skills/last30days, ./.skills/last30days, ." >&2 +if [ ! -f "$SKILL_DIR/scripts/last30days.py" ]; then + echo "ERROR: scripts/last30days.py not found under SKILL_DIR=$SKILL_DIR" >&2 + echo "Re-check the directory of the SKILL.md you Read and substitute it as SKILL_DIR above." >&2 exit 1 fi -"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" $ARGUMENTS --emit=compact --save-dir="${LAST30DAYS_MEMORY_DIR}" --save-suffix=v3 +"${LAST30DAYS_PYTHON}" "${SKILL_DIR}/scripts/last30days.py" $ARGUMENTS --emit=compact --save-dir="${LAST30DAYS_MEMORY_DIR}" --save-suffix=v3 ``` **If you ran Steps 0.55 and 0.75 (agent planning), pass the plan via a tmpfile and add the targeting flags:** diff --git a/uv.lock b/uv.lock index 8d0b760..c88bbfb 100644 --- a/uv.lock +++ b/uv.lock @@ -106,7 +106,7 @@ wheels = [ [[package]] name = "last30days-skill" -version = "3.2.3" +version = "3.2.4" source = { virtual = "." } [package.dev-dependencies]