From ff54c07a3bdc3881ef75111660511f26f6dd817b Mon Sep 17 00:00:00 2001 From: Trevin Chow Date: Fri, 15 May 2026 22:43:02 -0700 Subject: [PATCH] fix(skill): write --plan / --competitors-plan to tmpfile, bump 3.2.2 -> 3.2.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #403. The SKILL.md templates instructed the model to invoke the engine with inline single-quoted JSON: `--plan '$JSON'` and `--competitors-plan '{...}'`. When any resolved field value contained an apostrophe (common in `context` strings like "McDonald's", "people's choice", or contracted forms like "don't", "won't"), the inner `'` closed the outer single-quote and broke shell parsing before the engine was even invoked. Observed during PR #400 testing: a Codex run hit the trap and self-healed by re-encoding, wasting one engine invocation and ~30s of latency. Fix: switch both templates to the heredoc + tmpfile pattern. The engine's `parse_plan()` and `parse_competitors_plan()` already check `os.path.isfile(plan_str)` and read from disk — only the SKILL.md prose needed to change. The quoted heredoc marker (<<'PLAN_EOF') is load-bearing: it suppresses shell interpolation so apostrophes, $, backticks, etc. pass through verbatim. A trap on EXIT cleans up the tmpfile after the engine call returns. LAW 7's "MUST contain --plan" self-check guidance and Step 1's invocation example both updated to reference the file form. Comparison-mode invocation block updated the same way for --competitors-plan. Version bump 3.2.2 -> 3.2.3 because this is a behavior change users running comparison-mode queries will notice (no more "shell quoting error, retrying" sequences on apostrophe-containing context strings). --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- CHANGELOG.md | 2 ++ pyproject.toml | 2 +- skills/last30days/SKILL.md | 50 ++++++++++++++++++++++++------- skills/last30days/scripts/sync.sh | 2 +- uv.lock | 2 +- 7 files changed, 46 insertions(+), 16 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 54e78b5..d941cff 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.2", + "version": "3.2.3", "author": { "name": "Matt Van Horn", "url": "https://github.com/mvanhorn" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 7859abc..637a66b 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "last30days", - "version": "3.2.2", + "version": "3.2.3", "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 388e4b0..dfd2eb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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`. +- Switch SKILL.md's `--plan` and `--competitors-plan` invocation templates from inline single-quoted JSON to heredoc-written tmpfiles. Apostrophes in resolved context strings ("McDonald's", "people's choice", "developer's") previously closed the outer single-quote and broke shell parsing before the engine started — observed in a Codex run during PR #400 testing. The engine's `parse_plan()` / `parse_competitors_plan()` already supported file paths (via `os.path.isfile()` probe); only the template prose changed. Fixes [#403](https://github.com/mvanhorn/last30days-skill/issues/403). + ### Removed - **BREAKING for Codex native-plugin users:** `.codex-plugin/plugin.json` and the matching SKILL_ROOT resolver branch in SKILL.md Step 1. Codex users should install via `npx skills add mvanhorn/last30days-skill` or copy the skill to `~/.codex/skills/last30days/` (which `sync.sh` already writes to in this repo). diff --git a/pyproject.toml b/pyproject.toml index 2a77d94..69b8596 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "last30days-skill" -version = "3.2.2" +version = "3.2.3" 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 3075933..44f838f 100644 --- a/skills/last30days/SKILL.md +++ b/skills/last30days/SKILL.md @@ -1,6 +1,6 @@ --- name: last30days -version: "3.2.2" +version: "3.2.3" 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 @@ -186,13 +186,13 @@ The self-evolving loop is the sticky use case. Every 15 tool calls Hermes pauses Cron-scheduled autonomous briefings are the most-cited concrete workflow. r/TunisiaTech's "Use cases of OpenClaw, Hermes Agent" thread says it plainly: "Currently I have daily cron jobs for news briefing, but I know there's much more I can do." ``` -**LAW 7 - YOU ARE THE PLANNER. `--plan` IS MANDATORY ON NAMED-ENTITY TOPICS.** If you are the reasoning model hosting this skill (Claude Code, Codex, Hermes, Gemini, or any agent runtime that invoked `/last30days`), YOU generate the JSON query plan. You do not need an API key, "LLM provider" credentials, or an external planning service - you ARE the LLM. The `--plan` flag exists precisely so a reasoning model generates its own plan upstream and passes it to the engine. The engine's internal planner and deterministic fallback are headless/cron paths only; on any reasoning-model path, bypass them by passing `--plan '$JSON'`. +**LAW 7 - YOU ARE THE PLANNER. `--plan` IS MANDATORY ON NAMED-ENTITY TOPICS.** If you are the reasoning model hosting this skill (Claude Code, Codex, Hermes, Gemini, or any agent runtime that invoked `/last30days`), YOU generate the JSON query plan. You do not need an API key, "LLM provider" credentials, or an external planning service - you ARE the LLM. The `--plan` flag exists precisely so a reasoning model generates its own plan upstream and passes it to the engine. The engine's internal planner and deterministic fallback are headless/cron paths only; on any reasoning-model path, bypass them by passing `--plan "$QUERY_PLAN_FILE"` (the path to a tmpfile you wrote via heredoc — see Step 1 for the pattern; never inline `--plan '$JSON'`, apostrophes in search/ranking strings break shell parsing). -Named-entity topics (capitalized proper nouns, product names, person names, project names, or any topic that would benefit from handle resolution in Step 0.55) REQUIRE `--plan`. Your invocation of `scripts/last30days.py` MUST contain `--plan '$JSON'`. A bare `python3 scripts/last30days.py "$TOPIC" --emit=compact` on a named-entity topic is a LAW 7 violation. Before you invoke Bash, self-check: does my command contain `--plan`? If no, STOP and generate a plan first (see Step 0.75 for the schema). +Named-entity topics (capitalized proper nouns, product names, person names, project names, or any topic that would benefit from handle resolution in Step 0.55) REQUIRE `--plan`. Your invocation of `scripts/last30days.py` MUST contain `--plan "$QUERY_PLAN_FILE"` (or any path the engine can read). A bare `python3 scripts/last30days.py "$TOPIC" --emit=compact` on a named-entity topic is a LAW 7 violation. Before you invoke Bash, self-check: does my command contain `--plan`? If no, STOP and generate a plan first (see Step 0.75 for the schema). **Observed LAW 7 violation (2026-04-19, Hermes Agent Use Cases Run 1):** the model called the engine bare with no `--plan`, no pre-flight handle resolution. The engine emitted a stderr warning ("No --plan and no LLM provider configured. Using deterministic fallback...") which the model read as a capability constraint ("I don't have a key, I can't do LLM stuff") instead of as what it actually was: a reminder that the reasoning model skipped its own planning step. The misread came from the word "provider" - the engine uses "provider" to mean "the key for the engine's INTERNAL planner," but the model parsed it as "I need a provider to plan at all." You do not. You ARE the provider. Run 2 of the same topic (2026-04-19, framed as "best workflows") with the same model and same cache generated the plan itself via `--plan` and produced clean results - the delta was this step. -**Self-check before Bash:** re-read your pending `scripts/last30days.py` command. Does it contain `--plan '$JSON'`? If no, and the topic is a named entity, STOP. Return to Step 0.75 and generate the plan. Do not interpret the word "provider" in any engine message as "you need credentials" - you are the provider. +**Self-check before Bash:** re-read your pending `scripts/last30days.py` command. Does it contain `--plan "$QUERY_PLAN_FILE"` (or another path the engine can read)? If no, and the topic is a named entity, STOP. Return to Step 0.75 and generate the plan, then write it to a tmpfile per the Step 1 pattern. Do not interpret the word "provider" in any engine message as "you need credentials" - you are the provider. **LAW 8 - EVERY CITATION IN THE NARRATIVE IS AN INLINE MARKDOWN LINK `[name](url)`. NEVER A RAW URL STRING. NEVER A PLAIN NAME WHEN A URL IS AVAILABLE.** Applies to every query type. In the "What I learned:" narrative, in KEY PATTERNS, and in the COMPARISON body sections, every cited @handle, r/subreddit, publication, YouTube channel, TikTok creator, Instagram creator, and Polymarket market is wrapped as `[name](url)` at first mention. The URL comes from the raw research dump — every engine item carries a URL; WebSearch supplements carry URLs in their own output. Claude Code renders `[text](url)` as blue CMD-clickable text; the URL is hidden in the rendering, only the link text shows. The stats footer (emoji-tree block) is engine-emitted per LAW 5 and passes through verbatim — do NOT reformat its links yourself. @@ -243,7 +243,7 @@ If your Bash call to `last30days.py` does NOT include the FULL pre-flight checkl --- -# last30days v3.2.2: Research Any Topic from the Last 30 Days +# last30days v3.2.3: 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. @@ -615,18 +615,31 @@ if [ -z "$SKILL_ROOT" ] || [ ! -f "$SKILL_ROOT/scripts/last30days.py" ]; then done fi +# Write the per-entity plan to a tmpfile and pass the path to the engine. +# The engine's parse_competitors_plan() reads file paths transparently. This +# avoids the inline-single-quoted-JSON apostrophe trap (resolved context +# strings like "people's choice" or "McDonald's" otherwise close the outer +# single-quote and break shell parsing before the engine is even invoked). +COMPETITORS_PLAN_FILE=$(mktemp -t last30days-competitors.XXXXXX.json) +trap 'rm -f "$COMPETITORS_PLAN_FILE"' EXIT +cat > "$COMPETITORS_PLAN_FILE" <<'PLAN_EOF' +{ + "{TOPIC_B}": {"x_handle":"{TOPIC_B_HANDLE}","subreddits":["{TOPIC_B_SUB_1}","{TOPIC_B_SUB_2}"],"github_user":"{TOPIC_B_GH}","context":"{TOPIC_B_CONTEXT}"}, + "{TOPIC_C}": {"x_handle":"{TOPIC_C_HANDLE}","subreddits":["{TOPIC_C_SUB_1}"],"github_user":"{TOPIC_C_GH}","context":"{TOPIC_C_CONTEXT}"} +} +PLAN_EOF + "${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" "{TOPIC_A} vs {TOPIC_B} vs {TOPIC_C}" \ --emit=compact \ --save-dir="${LAST30DAYS_MEMORY_DIR}" \ --save-suffix=v3 \ --x-handle={TOPIC_A_HANDLE} \ --subreddits={TOPIC_A_SUBS} \ - --competitors-plan '{ - "{TOPIC_B}": {"x_handle":"{TOPIC_B_HANDLE}","subreddits":["{TOPIC_B_SUB_1}","{TOPIC_B_SUB_2}"],"github_user":"{TOPIC_B_GH}","context":"{TOPIC_B_CONTEXT}"}, - "{TOPIC_C}": {"x_handle":"{TOPIC_C_HANDLE}","subreddits":["{TOPIC_C_SUB_1}"],"github_user":"{TOPIC_C_GH}","context":"{TOPIC_C_CONTEXT}"} - }' + --competitors-plan "$COMPETITORS_PLAN_FILE" ``` +**The quoted heredoc marker `'PLAN_EOF'` is load-bearing** — quoting suppresses shell interpolation so apostrophes, `$`, backticks, etc. pass through verbatim. If you ever switch to an unquoted `< "$QUERY_PLAN_FILE" <<'PLAN_EOF' +{QUERY_PLAN_JSON_FROM_STEP_0.75} +PLAN_EOF +``` + +Then add to the engine command: + +- `--plan "$QUERY_PLAN_FILE"` (path to the file you just wrote) - `--x-handle={RESOLVED_HANDLE}` (from Step 0.5) - `--subreddits={RESOLVED_SUBREDDITS}` (from Step 0.55) - `--tiktok-hashtags={RESOLVED_HASHTAGS}` (from Step 0.55) diff --git a/skills/last30days/scripts/sync.sh b/skills/last30days/scripts/sync.sh index eca793b..09a7e08 100755 --- a/skills/last30days/scripts/sync.sh +++ b/skills/last30days/scripts/sync.sh @@ -15,7 +15,7 @@ COMMON_TARGETS=( # against the working tree so /last30days reflects local dev without # waiting for a release. Do NOT add ~/.claude/skills/last30days - it # creates a duplicate slash-command entry alongside the plugin version. - "$HOME/.claude/plugins/cache/last30days-skill/last30days/3.2.2" + "$HOME/.claude/plugins/cache/last30days-skill/last30days/3.2.3" "$HOME/.agents/skills/last30days" "$HOME/.codex/skills/last30days" ) diff --git a/uv.lock b/uv.lock index 0249fdc..8d0b760 100644 --- a/uv.lock +++ b/uv.lock @@ -106,7 +106,7 @@ wheels = [ [[package]] name = "last30days-skill" -version = "3.2.2" +version = "3.2.3" source = { virtual = "." } [package.dev-dependencies]