Agent-readable docs
last30days Documentation
Technical reference for the multi-source Agent Skills research package: slash-command and engine entry points, v3 retrieval pipeline, configuration layers, synthesis output contract, and operator workflows across 50+ agent harnesses.
Pages
- OverviewWhat the Skill exposes, primary slash-command vs engine paths, zero-config sources, and the shortest successful research path.
- InstallationInstall via Claude Code marketplace, npx skills add for Agent Skills hosts, Hermes, live-edit symlinks, and version sync expectations.
- QuickstartFirst /last30days invocation, first-run setup wizard signals, saved output location, and --diagnose verification.
- Skill, engine, and harnessProject-specific boundaries between the Agent Skills package, Python engine, and multi-harness runtimes that load the skill.
- Query typesQUERY_TYPE classification (GENERAL, NEWS, PROMPTING, RECOMMENDATIONS, COMPARISON), intent parsing, and engine pre-flight refusal paths.
- Research pipelinev3 orchestration: planner sub-queries, parallel source fan-out, fusion, clustering, dedupe, rerank, and depth profiles (--quick / default / --deep).
- Output contractBadge line, LAWs voice contract, --emit modes (compact, json, context, md, html), evidence blocks, and footer pass-through rules.
- Configure sourcesCredential layers (.env paths, keychain), per-source API keys, INCLUDE_SOURCES, setup wizard auto-actions, and --diagnose availability checks.
- Model clientsPer-harness install patterns: Claude Code marketplace, npx skills -a targets, Codex/Cursor/Gemini, Hermes, OpenClaw, and stale-install avoidance.
- Comparison mode--competitors discovery fan-out, --competitors-list and --competitors-plan JSON schema, per-entity Step 0.55 targeting, and comparison synthesis template.
- HTML briefsShareable offline HTML artifacts via --emit=html, --synthesis-file, save paths under LAST30DAYS_MEMORY_DIR, and SKILL.md-driven save flow.
- Trend monitoringSQLite --store persistence, watchlist.py scheduling and delivery, briefing.py digests, and recommended baseline cadence.
Complete Markdown
# last30days Documentation
> Technical reference for the multi-source Agent Skills research package: slash-command and engine entry points, v3 retrieval pipeline, configuration layers, synthesis output contract, and operator workflows across 50+ agent harnesses.
## Context Links
- [Agent index](https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/llms.txt)
- [Human interactive docs](https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca)
- [GitHub repository](https://github.com/mvanhorn/last30days-skill)
## Repository Metadata
- Repository: mvanhorn/last30days-skill
- Generated: 2026-06-04T23:26:24.258Z
- Updated: 2026-06-05T19:21:29.664Z
- Runtime: Grok CLI
- Format: Documentation
- Pages: 22
## Page Index
- 01. [Overview](https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/01-overview.md) - What the Skill exposes, primary slash-command vs engine paths, zero-config sources, and the shortest successful research path.
- 02. [Installation](https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/02-installation.md) - Install via Claude Code marketplace, npx skills add for Agent Skills hosts, Hermes, live-edit symlinks, and version sync expectations.
- 03. [Quickstart](https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/03-quickstart.md) - First /last30days invocation, first-run setup wizard signals, saved output location, and --diagnose verification.
- 04. [Skill, engine, and harness](https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/04-skill-engine-and-harness.md) - Project-specific boundaries between the Agent Skills package, Python engine, and multi-harness runtimes that load the skill.
- 05. [Query types](https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/05-query-types.md) - QUERY_TYPE classification (GENERAL, NEWS, PROMPTING, RECOMMENDATIONS, COMPARISON), intent parsing, and engine pre-flight refusal paths.
- 06. [Research pipeline](https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/06-research-pipeline.md) - v3 orchestration: planner sub-queries, parallel source fan-out, fusion, clustering, dedupe, rerank, and depth profiles (--quick / default / --deep).
- 07. [Output contract](https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/07-output-contract.md) - Badge line, LAWs voice contract, --emit modes (compact, json, context, md, html), evidence blocks, and footer pass-through rules.
- 08. [Configure sources](https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/08-configure-sources.md) - Credential layers (.env paths, keychain), per-source API keys, INCLUDE_SOURCES, setup wizard auto-actions, and --diagnose availability checks.
- 09. [Model clients](https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/09-model-clients.md) - Per-harness install patterns: Claude Code marketplace, npx skills -a targets, Codex/Cursor/Gemini, Hermes, OpenClaw, and stale-install avoidance.
- 10. [Comparison mode](https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/10-comparison-mode.md) - --competitors discovery fan-out, --competitors-list and --competitors-plan JSON schema, per-entity Step 0.55 targeting, and comparison synthesis template.
- 11. [HTML briefs](https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/11-html-briefs.md) - Shareable offline HTML artifacts via --emit=html, --synthesis-file, save paths under LAST30DAYS_MEMORY_DIR, and SKILL.md-driven save flow.
- 12. [Trend monitoring](https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/12-trend-monitoring.md) - SQLite --store persistence, watchlist.py scheduling and delivery, briefing.py digests, and recommended baseline cadence.
- 13. [Per-client setup](https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/13-per-client-setup.md) - Project-scoped .claude/last30days.env, save-dir and --save-suffix isolation, category peer subreddits, and recurring --competitors-plan templates.
- 14. [Configuration reference](https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/14-configuration-reference.md) - Environment variables, .env lookup order, reasoning provider priority, web-backend priority, output paths, and trend-monitoring toggles.
- 15. [CLI reference](https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/15-cli-reference.md) - last30days.py positional topic, all argparse flags, --emit choices, targeting flags, and direct CLI vs slash-command constraints.
- 16. [Data sources](https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/16-data-sources.md) - Per-platform retrieval modules, keyless vs keyed sources, --search alias map, and engagement scoring inputs by source.
- 17. [Skill contract](https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/17-skill-contract.md) - SKILL.md runtime authority: STEP 0 stale-clone guard, pre-flight protocol, engine invocation rules, and SKILL_DIR substitution.
- 18. [Comparison recipes](https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/18-comparison-recipes.md) - Copy-paste comparison workflows: vs-topic phrasing, --competitors fan-out, and multi-entity Head-to-Head table expectations.
- 19. [Prompting recipes](https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/19-prompting-recipes.md) - PROMPTING query workflows: community technique extraction, KEY PATTERNS output shape, and copy-paste prompt generation patterns.
- 20. [Troubleshooting](https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/20-troubleshooting.md) - --diagnose probes, missing-source recovery, stale Claude marketplace clones, thin results, and documented failure modes.
- 21. [Develop and test](https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/21-develop-and-test.md) - uv/pytest workflow, coverage omit rules, validate and security CI, optional search-quality eval, and contributor constraints from AGENTS.md.
- 22. [Beta channel](https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/22-beta-channel.md) - Parallel /last30days-beta install from the private repo, promotion workflow, and where client-specific customizations belong.
## Source File Index
- `.agents/plugins/marketplace.json`
- `.claude-plugin/marketplace.json`
- `.claude-plugin/plugin.json`
- `.github/workflows/security.yml`
- `.github/workflows/validate.yml`
- `AGENTS.md`
- `CHANGELOG.md`
- `CONCEPTS.md`
- `CONFIGURATION.md`
- `docs/how-search-works.md`
- `docs/releases/v3.0.9.md`
- `docs/search-quality-eval.md`
- `docs/solutions/architecture/search-quality-eval-manual-by-default-2026-05-10.md`
- `docs/solutions/workflow-issues/release-consistency-test-cascade-2026-05-16.md`
- `fixtures/eval_topics.json`
- `gemini-extension.json`
- `HERMES_SETUP.md`
- `pyproject.toml`
- `README.md`
- `skills/last30days/agents/openai.yaml`
- `skills/last30days/references/save-html-brief.md`
- `skills/last30days/scripts/briefing.py`
- `skills/last30days/scripts/evaluate_search_quality.py`
- `skills/last30days/scripts/last30days.py`
- `skills/last30days/scripts/lib/bird_x.py`
- `skills/last30days/scripts/lib/categories.py`
- `skills/last30days/scripts/lib/cluster.py`
- `skills/last30days/scripts/lib/competitors.py`
- `skills/last30days/scripts/lib/dedupe.py`
- `skills/last30days/scripts/lib/env.py`
- `skills/last30days/scripts/lib/fanout.py`
- `skills/last30days/scripts/lib/fusion.py`
- `skills/last30days/scripts/lib/github.py`
- `skills/last30days/scripts/lib/html_render.py`
- `skills/last30days/scripts/lib/pipeline.py`
- `skills/last30days/scripts/lib/planner.py`
- `skills/last30days/scripts/lib/polymarket.py`
- `skills/last30days/scripts/lib/preflight.py`
- `skills/last30days/scripts/lib/providers.py`
- `skills/last30days/scripts/lib/query.py`
- `skills/last30days/scripts/lib/reddit_public.py`
- `skills/last30days/scripts/lib/render.py`
- `skills/last30days/scripts/lib/rerank.py`
- `skills/last30days/scripts/lib/schema.py`
- `skills/last30days/scripts/lib/setup_wizard.py`
- `skills/last30days/scripts/lib/skill_meta.py`
- `skills/last30days/scripts/lib/youtube_yt.py`
- `skills/last30days/scripts/setup-keychain.sh`
- `skills/last30days/scripts/store.py`
- `skills/last30days/scripts/verify_v3.py`
- `skills/last30days/scripts/watchlist.py`
- `skills/last30days/SKILL.md`
- `tests/test_cli_competitors.py`
- `tests/test_competitor_fanout.py`
- `tests/test_pipeline_v3.py`
- `tests/test_regression.py`
- `tests/test_render_comparison_multi.py`
- `tests/test_watchlist_commands.py`
---
## 01. Overview
> What the Skill exposes, primary slash-command vs engine paths, zero-config sources, and the shortest successful research path.
- Page Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/01-overview.md
- Generated: 2026-06-04T23:18:54.834Z
### Source Files
- `README.md`
- `CONCEPTS.md`
- `skills/last30days/SKILL.md`
- `skills/last30days/scripts/last30days.py`
- `AGENTS.md`
- `pyproject.toml`
---
title: "Overview"
description: "What the Skill exposes, primary slash-command vs engine paths, zero-config sources, and the shortest successful research path."
---
`/last30days` is an [Agent Skills](https://agentskills.io) package (`skills/last30days/`) whose runtime contract lives in `SKILL.md` (v3.3.1) and whose Python engine is `scripts/last30days.py`. The product surface is a harness-invoked slash command; the engine aggregates Reddit, Hacker News, Polymarket, GitHub, YouTube, optional social sources, and grounded web retrieval into ranked evidence the hosting model synthesizes under a fixed output contract (badge, LAWs voice rules, pass-through footer).
## Skill, engine, and harness
Three boundaries define every run:
| Layer | Artifact | Role |
|-------|----------|------|
| **Skill** | `skills/last30days/SKILL.md` + sibling `scripts/` | Tells the hosting model how to parse intent, run pre-flight (handles, subreddits, plans), invoke the engine, and format the user-facing brief |
| **Engine** | `skills/last30days/scripts/last30days.py` → `lib/pipeline.py` | Plans sub-queries, fans out parallel source retrieval, clusters/dedupes/reranks, emits `--emit` payloads |
| **Harness** | Claude Code, Codex, Cursor, Gemini CLI, Copilot, Hermes, OpenClaw, 50+ Agent Skills hosts | Loads the skill, exposes `/last30days`, and (on reasoning-model paths) acts as planner and synthesizer |
```mermaid
flowchart TB
subgraph harness["Harness (agent runtime)"]
SC["/last30days topic"]
WS["WebSearch supplement"]
end
subgraph skill["Skill package skills/last30days/"]
SM["SKILL.md contract"]
end
subgraph engine["Engine scripts/last30days.py"]
PL["lib/pipeline.run"]
REN["lib/render compact/md/html"]
end
subgraph sources["Retrieval modules lib/*"]
RD["reddit_keyless"]
HN["hackernews"]
PM["polymarket"]
GH["github"]
YT["youtube_yt"]
MORE["x, tiktok, grounding, …"]
end
SC --> SM
SM -->|"Bash: SKILL_DIR/scripts/last30days.py"| PL
PL --> RD & HN & PM & GH & YT & MORE
PL --> REN
REN -->|"stdout --emit=compact"| SM
SM --> WS
SM -->|"synthesis + footer pass-through"| OUT["User brief"]
```
<Note>
On a reasoning-model path, the harness model **is** the planner: named-entity topics require `--plan` written to a tmpfile (LAW 7). The engine’s internal LLM planner is for headless/cron runs without a hosting model.
</Note>
## What the skill exposes
| Surface | Entry | Typical use |
|---------|-------|-------------|
| **Slash command** | `/last30days <topic>` | Primary UX in Claude Code (`/plugin install last30days`) and Agent Skills hosts (`npx skills add mvanhorn/last30days-skill -g`) |
| **Direct CLI** | `python3 <SKILL_DIR>/scripts/last30days.py "<topic>"` | Scripting, cron, engine testing, `--diagnose`, HTML export with `--emit=html` |
| **Setup** | First run via SKILL.md Step 0 → `nux-wizard.md`, or `last30days.py setup` | Writes `~/.config/last30days/.env` with `SETUP_COMPLETE=true` |
| **Trend stack** (optional) | `--store`, `watchlist.py`, `briefing.py` | SQLite persistence and scheduled digests |
The skill frontmatter declares `user-invocable: true`, optional env keys (`SCRAPECREATORS_API_KEY`, `OPENROUTER_API_KEY`, X cookies, Bluesky app password, etc.), and required bins `node` + `python3` (Python **3.12+** enforced at engine startup).
### Slash command vs direct CLI
| Constraint | Slash command | Direct CLI |
|------------|---------------|------------|
| Shell flags/pipes | Invalid — harness does not pass `|`, `>`, or arbitrary flags through | Full `argparse` surface (`--emit`, `--plan`, `--competitors`, …) |
| Who plans | Hosting model generates `--plan` JSON for named entities | Optional `--plan` file; otherwise engine `providers.resolve_runtime` |
| Who synthesizes | Model follows SKILL.md LAWs on engine stdout | `--emit=compact` still targets model synthesis in skill flows; `html`/`json` can be consumed headlessly |
| Engine path | `SKILL_DIR` = directory of the `SKILL.md` the harness loaded | Caller supplies path (repo: `skills/last30days/scripts/last30days.py`) |
<Warning>
Treating `/last30days` as a generic “research the last 30 days” prompt and answering from WebSearch alone violates the skill contract: valid output always includes an engine run and the `✅ All agents reported back!` footer block.
</Warning>
## Zero-config and baseline sources
With no `.env` file, the engine still activates keyless or CLI-backed sources. `lib/pipeline.available_sources` always includes **Reddit** (public/keyless pipeline), **Hacker News**, and **Polymarket**. Additional sources join when tools or keys are present:
| Source | Activation without API keys in `.env` |
|--------|----------------------------------------|
| Reddit | Always (`reddit` — no `SCRAPECREATORS` required) |
| Hacker News | Always |
| Polymarket | Always |
| GitHub | `gh` on `PATH` (uses GitHub CLI auth) |
| YouTube | `yt-dlp` on `PATH` |
| Digg | `digg-pp-cli` on `PATH` |
| X / Twitter | `AUTH_TOKEN`+`CT0`, `XAI_API_KEY`, `SCRAPECREATORS_API_KEY`, `FROM_BROWSER`, or authenticated Bird/xurl |
| TikTok, Instagram, Threads | `SCRAPECREATORS_API_KEY` (+ `INCLUDE_SOURCES` where applicable) |
| Bluesky | `BSKY_HANDLE` + `BSKY_APP_PASSWORD` |
| Web grounding | `BRAVE_API_KEY`, `EXA_API_KEY`, `SERPER_API_KEY`, or `PARALLEL_API_KEY` |
| Perplexity | `OPENROUTER_API_KEY` and `INCLUDE_SOURCES` containing `perplexity` |
README’s “zero config” line matches this: Reddit, HN, Polymarket, and GitHub (with `gh`) work on first install; the first-run wizard optionally unlocks X, YouTube enrichment, TikTok, and ScrapeCreators-backed sources in one pass.
### Active source announcement
Before research, SKILL.md instructs the model to build `ACTIVE_SOURCES_LIST` from live checks (`which gh`, `which yt-dlp`, env keys, `EXCLUDE_SOURCES`) and emit a single branded line, for example:
```
/last30days - searching Reddit, Hacker News, Polymarket, and GitHub for what people are saying about {TOPIC}.
```
Only configured sources are named — the skill must not promise platforms that `--diagnose` would omit.
## Shortest successful research path
<Steps>
<Step title="Install the skill package">
<Tabs>
<Tab title="Claude Code (marketplace)">
```text
/plugin marketplace add mvanhorn/last30days-skill
/plugin install last30days
```
</Tab>
<Tab title="Agent Skills hosts">
```bash
npx skills add mvanhorn/last30days-skill -g
```
</Tab>
</Tabs>
</Step>
<Step title="Invoke with a concrete topic">
```text
/last30days OpenClaw
```
If `~/.config/last30days/.env` is missing, Step 0 runs `nux-wizard.md` once, then continues.
</Step>
<Step title="Let the contract run (automatic in skill mode)">
The hosting model should: load WebSearch (`ToolSearch select:WebSearch`), parse `QUERY_TYPE`, run Step 0.45 pre-flight, resolve handles/subreddits (Step 0.5 / 0.55), write `--plan` for named entities, and execute:
```bash
"${SKILL_DIR}/scripts/last30days.py" "<topic>" --emit=compact --plan "$QUERY_PLAN_FILE" ...
```
Stdout supplies badge + evidence blocks + footer; the model synthesizes `What I learned:` prose and passes the footer through verbatim.
</Step>
<Step title="Verify configuration (optional)">
```bash
python3 skills/last30days/scripts/last30days.py --diagnose
```
Prints JSON: `providers`, `available_sources`, `x_backend`, `native_web_backend`, `has_scrapecreators`, etc., without running a full search.
</Step>
</Steps>
### Saved artifacts
Default raw dumps land under `LAST30DAYS_MEMORY_DIR` (default `~/Documents/Last30Days/`) as `<slug>-raw.md`. The engine footer points to the saved path; HTML briefs use `--emit=html` or skill-driven save flow under the same directory.
### Query types (intent routing)
SKILL.md classifies input before retrieval shape and synthesis template:
| QUERY_TYPE | Trigger examples | Synthesis shape |
|------------|------------------|-----------------|
| GENERAL | Default topic research | Badge → `What I learned:` → KEY PATTERNS |
| NEWS | “latest on X”, “X news” | Same as GENERAL |
| RECOMMENDATIONS | “best X”, “top X” | Signal-weighted picks |
| PROMPTING | “X prompts”, “prompting for X” | Techniques + copy-paste prompts |
| COMPARISON | “X vs Y”, `--competitors` | Required `##` comparison sections + Head-to-Head table |
Depth profiles on the engine: `--quick`, default, `--deep` (maps to `DEPTH_SETTINGS` in `lib/pipeline.py`).
## Repository layout (skill product)
:::files
last30days-skill/
├── skills/last30days/
│ ├── SKILL.md # Runtime authority for slash-command runs
│ ├── nux-wizard.md # First-run setup (Step 0)
│ └── scripts/
│ ├── last30days.py # CLI entry
│ ├── lib/ # pipeline, sources, render, env
│ ├── watchlist.py
│ └── briefing.py
├── CONFIGURATION.md # User-facing knobs (env, flags, paths)
├── CONCEPTS.md # Skill / Engine / Harness vocabulary
└── pyproject.toml # Python 3.12+, pytest suite
:::
<Info>
`AGENTS.md` states the distribution unit is the Agent Skills package, not a standalone CLI product. Engine flags without matching `SKILL.md` integration are considered incomplete for multi-harness behavior.
</Info>
## Engine output modes
<ParamField body="--emit" type="string" default="compact">
Choices: `compact`, `md`, `json`, `context`, `html`. Skill runs normally use `compact` for model synthesis; `html` produces shareable offline briefs; `json` exposes the structured `schema.Report`.
</ParamField>
<ParamField body="topic" type="string" required>
Positional words joined into the research topic. Empty topic with no `--diagnose` prints usage and exits 2.
</ParamField>
The compact emitter places ranked evidence inside `<!-- EVIDENCE FOR SYNTHESIS -->` comments; LAW 6 forbids dumping those clusters verbatim to the user.
## Related pages
<CardGroup>
<Card title="Installation" href="/installation">
Claude Code marketplace, `npx skills add`, Hermes, live-edit symlinks, and version sync.
</Card>
<Card title="Quickstart" href="/quickstart">
First invocation, setup wizard signals, saved output location, and `--diagnose`.
</Card>
<Card title="Skill, engine, and harness" href="/skill-engine-harness">
Boundaries between SKILL.md, `last30days.py`, and multi-harness runtimes.
</Card>
<Card title="Research pipeline" href="/research-pipeline">
v3 planner fan-out, fusion, clustering, dedupe, rerank, depth profiles.
</Card>
<Card title="Output contract" href="/output-contract">
Badge, LAWs, `--emit` modes, evidence blocks, footer pass-through.
</Card>
<Card title="Configure sources" href="/configure-sources">
`.env` layers, API keys, `INCLUDE_SOURCES`, setup wizard actions.
</Card>
</CardGroup>
---
## 02. Installation
> Install via Claude Code marketplace, npx skills add for Agent Skills hosts, Hermes, live-edit symlinks, and version sync expectations.
- Page Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/02-installation.md
- Generated: 2026-06-04T23:19:06.798Z
### Source Files
- `README.md`
- `HERMES_SETUP.md`
- `.claude-plugin/plugin.json`
- `.claude-plugin/marketplace.json`
- `gemini-extension.json`
- `AGENTS.md`
---
title: "Installation"
description: "Install via Claude Code marketplace, npx skills add for Agent Skills hosts, Hermes, live-edit symlinks, and version sync expectations."
---
The distributable unit is the Agent Skills package at `skills/last30days/` (`SKILL.md` plus `scripts/last30days.py` and `scripts/lib/`). Harnesses load that package through marketplace plugins, `npx skills`, harness-native installers, or a manual symlink; the engine always runs from whichever directory contains the `SKILL.md` the model read (`SKILL_DIR`).
## Prerequisites
| Requirement | Used for | Notes |
|---|---|---|
| Python **3.12+** | Engine (`scripts/last30days.py`) | `requires-python = ">=3.12"` in `pyproject.toml`; Hermes docs recommend `python3.12` explicitly |
| **Node.js** | Vendored Bird client for X search | Listed under `metadata.openclaw.requires.bins` in `SKILL.md` |
| **yt-dlp** (optional) | YouTube search and transcripts | `brew install yt-dlp` or `pip install yt-dlp` |
| **gh** CLI (optional) | GitHub source | Uses your existing GitHub auth when present |
Reddit (public JSON), Hacker News, Polymarket, and GitHub (with `gh`) work with zero API configuration after install. Additional sources unlock via the first-run setup wizard or keys documented on [Configure sources](/configure-sources).
## Install surfaces
| Surface | Command | Update | On-disk layout (typical) |
|---|---|---|---|
| **Claude Code** (recommended) | `/plugin marketplace add mvanhorn/last30days-skill` then `/plugin install last30days` | Marketplace auto-refresh; `claude plugin update last30days@last30days-skill` | `~/.claude/plugins/cache/last30days-skill/last30days/{version}/skills/last30days/` |
| **Agent Skills hosts** (Codex, Cursor, Copilot, Gemini CLI, 50+ others) | `npx skills add mvanhorn/last30days-skill -g` | `npx skills update last30days -g` | `~/.agents/skills/last30days/` (copy; see sync below) |
| **Claude Code via Agent Skills CLI** | `npx skills add mvanhorn/last30days-skill -g -a claude-code` | Same as above | Per-host symlink under `~/.claude/skills/` pointing at `~/.agents/skills/last30days` when supported |
| **claude.ai** (web) | Download `last30days.skill` from latest release; upload in Settings | Re-download and re-upload | Hosted skill bundle (not local engine path) |
| **Hermes** | `hermes skills install mvanhorn/last30days-skill --force` | Re-run install, or `git pull` if symlinked | `~/.hermes/skills/research/last30days/` |
| **OpenClaw** | `clawhub install last30days-official` | `clawhub update last30days-official` | OpenClaw skill dir (see OpenClaw metadata in `SKILL.md`) |
| **Developer / live-edit** | `git clone` + symlink into a harness skill dir | `git pull` in the clone | Symlink targets `skills/last30days/` in your checkout |
Current release version is **3.3.1**, pinned consistently in `pyproject.toml`, `skills/last30days/SKILL.md` frontmatter, `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, and `gemini-extension.json` (enforced by `tests/test_plugin_contract.py`).
```text
skills/last30days/ # canonical package in the repo
├── SKILL.md # runtime contract (slash command reads this)
└── scripts/
├── last30days.py # Python engine
└── lib/ # search, render, env modules
Harness install → copy or symlink of skills/last30days/
Model sets SKILL_DIR = directory containing the SKILL.md it Read
Engine invoked as: ${SKILL_DIR}/scripts/last30days.py
```
## Claude Code marketplace
<Steps>
<Step title="Add the marketplace">
In Claude Code:
```
/plugin marketplace add mvanhorn/last30days-skill
```
The marketplace manifest (`.claude-plugin/marketplace.json`) registers plugin name `last30days` with `source: "./"` from the public repo.
</Step>
<Step title="Install the plugin">
```
/plugin install last30days
```
Claude Code auto-discovers `skills/*/SKILL.md` when the plugin manifest omits a `"skills"` key (current `.claude-plugin/plugin.json` shape).
</Step>
<Step title="Verify version in cache">
```bash
cat ~/.claude/plugins/cache/last30days-skill/last30days/*/.claude-plugin/plugin.json | grep version
```
Expect `"version": "3.3.1"` (or whatever release you installed).
</Step>
</Steps>
<Tip>
Force an update check with `claude plugin update last30days@last30days-skill`. After upgrades, run `/plugin update last30days` then `/reload-plugins` if slash-command registration or `/doctor` errors persist.
</Tip>
### Stale marketplace clone (Claude Code only)
Claude Code maintains two plugin paths:
- **Versioned cache** — `~/.claude/plugins/cache/last30days-skill/last30days/{version}/` (preferred)
- **Marketplaces git clone** — `~/.claude/plugins/marketplaces/last30days-skill/` (can lag `origin/main`)
`SKILL.md` **STEP 0** tells the model: if the loaded path contains `/.claude/plugins/marketplaces/` and a newer cache `SKILL.md` exists, stop and re-read from the cache. Symptoms of a stale marketplace load include missing CLI flags (for example `--competitors`) and improvised comparison flows.
<Warning>
Do not run engine `--help` or plan against `marketplaces/` when the versioned cache is available. Users cannot fix this with `git pull` alone — rely on marketplace update or the STEP 0 re-read behavior baked into the skill contract.
</Warning>
### One install method per machine
The native marketplace plugin and `npx skills add … -a claude-code` can coexist. Claude Code does **not** dedupe slash commands across methods — both active installs surface two `/last30days` entries. Pick one path per machine.
## Agent Skills CLI (`npx skills`)
Default install for Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, Continue, Roo, OpenCode, Goose, and other [Agent Skills](https://agentskills.io) hosts:
```bash
npx skills add mvanhorn/last30days-skill -g
```
| Flag | Effect |
|---|---|
| `-g` | Global user install (`~/.agents/skills/last30days/`) — available in all projects |
| (omit `-g`) | Project-local install into `./.skills/` (committed with the repo) |
| `-a <harness>` | Target specific harness(es), e.g. `-a codex`, `-a cursor`, `-a gemini-cli` |
| `-y` | Non-interactive (used in contributor docs: `npx skills add . -g -y` from repo root) |
<Tabs>
<Tab title="Single harness">
```bash
npx skills add mvanhorn/last30days-skill -g -a codex
```
</Tab>
<Tab title="Multiple harnesses">
```bash
npx skills add mvanhorn/last30days-skill -g -a codex -a cursor
```
</Tab>
<Tab title="Claude Code via skills CLI">
```bash
npx skills add mvanhorn/last30days-skill -g -a claude-code
```
</Tab>
</Tabs>
Maintenance commands:
```bash
npx skills update last30days -g # update this skill globally
npx skills update -g # update all globally installed skills
npx skills list -g
npx skills remove last30days -g
```
<Note>
`.codex-plugin/` was removed from the repo; Codex should use `npx skills add` or a copy/symlink under `~/.codex/skills/last30days/`. `tests/test_plugin_contract.py` asserts the Codex plugin scaffold stays removed to avoid a forked install surface.
</Note>
## Hermes
Prerequisites: Hermes installed, Python 3.12+, optional `yt-dlp` for YouTube. See `HERMES_SETUP.md`.
<Steps>
<Step title="Install from GitHub">
```bash
hermes skills install mvanhorn/last30days-skill --force
```
Deploys to `~/.hermes/skills/research/last30days/`. `--force` reinstalls over an existing copy.
</Step>
<Step title="Invoke in Hermes">
```
last30days "your research topic"
```
Options pass through to the engine, e.g. `last30days "AI news" --days=7 --deep`.
</Step>
<Step title="Diagnose from install dir">
```bash
cd ~/.hermes/skills/research/last30days
python3.12 scripts/last30days.py --diagnose
```
</Step>
</Steps>
### Hermes live-edit symlink
```bash
git clone https://github.com/mvanhorn/last30days-skill.git
mkdir -p ~/.hermes/skills/research
ln -s "$(pwd)/last30days-skill/skills/last30days" ~/.hermes/skills/research/last30days
```
Edits in the clone propagate immediately; update with `git pull` instead of re-running `hermes skills install`.
## claude.ai (web)
<Steps>
<Step title="Enable code execution">
In [claude.ai Settings → Capabilities](https://claude.ai/settings/capabilities), enable **Code execution and file creation** — skills do not run without it.
</Step>
<Step title="Download the bundle">
[Download `last30days.skill`](https://github.com/mvanhorn/last30days-skill/releases/latest/download/last30days.skill) from the latest GitHub release.
</Step>
<Step title="Upload">
Settings → Capabilities → Skills → `+`, drop the file.
</Step>
</Steps>
Maintainers rebuild the upload artifact from a clean tree:
```bash
bash skills/last30days/scripts/build-skill.sh
```
Produces `dist/last30days.skill` (zip with top-level `last30days/`, capped at 200 files for claude.ai). Requires a clean git working tree.
## OpenClaw
```bash
clawhub install last30days-official
clawhub update last30days-official
```
`SKILL.md` includes `metadata.openclaw` (optional env keys, required bins `node` and `python3`, `primaryEnv: SCRAPECREATORS_API_KEY`).
## Developer install and live-edit symlinks
For contributors hacking the repo, two patterns matter: **frozen copy** vs **live symlink**.
### Frozen copy (`npx skills` default)
From the repository root:
```bash
npx skills add . -g -y
```
This copies `skills/last30days` into `~/.agents/skills/last30days/`. **Working-tree edits do not propagate** until you re-run `npx skills add . -g -y`.
### Live symlink (recommended for local dev)
From the repository root:
```bash
ln -sfn "$PWD/skills/last30days" ~/.agents/skills/last30days
```
Equivalent manual paths from the README:
```bash
git clone https://github.com/mvanhorn/last30days-skill.git
ln -s "$(pwd)/last30days-skill/skills/last30days" ~/.claude/skills/last30days
```
Symlinked installs track `git pull` in the clone with no re-copy step.
<Check>
After either pattern, confirm the engine exists:
```bash
test -f ~/.agents/skills/last30days/scripts/last30days.py && echo OK
```
For Claude marketplace cache, substitute the resolved `SKILL_DIR` under `~/.claude/plugins/cache/.../skills/last30days/`.
</Check>
## Version sync expectations
| Install model | When you get new releases | What to run |
|---|---|---|
| Claude Code marketplace | Auto-refresh on new GitHub releases; versioned cache directory per release | `claude plugin update last30days@last30days-skill` or `/plugin update last30days` |
| `npx skills add` (copy) | **Not** automatic — install is a snapshot | `npx skills update last30days -g` or re-run `npx skills add mvanhorn/last30days-skill -g` |
| Git symlink / repo checkout | Immediate for local edits; upstream fixes via git | `git pull` in the clone |
| claude.ai `.skill` upload | Manual | Re-download release artifact and re-upload |
| Hermes `hermes skills install` | On demand | `hermes skills install mvanhorn/last30days-skill --force` |
| OpenClaw ClawHub | On demand | `clawhub update last30days-official` |
Manifest version drift historically broke Claude marketplace resolution (marketplace.json lagging plugin.json). CI now requires `pyproject.toml`, `SKILL.md`, `plugin.json`, `marketplace.json` plugins[0].version, and `gemini-extension.json` to match.
The synthesizing model reports installed version in the mandatory badge (`🌐 last30days v{VERSION} · synced {YYYY-MM-DD}`), resolved from `SKILL_DIR/../../.claude-plugin/plugin.json` or `SKILL.md` frontmatter.
## Verify installation
| Check | Command / signal |
|---|---|
| Slash command registered | `/last30days` appears once (not duplicated) in your harness |
| Engine reachable | `python3 skills/last30days/scripts/last30days.py --diagnose` from `SKILL_DIR` |
| Sources available | `--diagnose` prints per-source keys, CLIs, and backends without a full search |
| Version alignment | Marketplace cache `plugin.json` version matches `SKILL.md` `version:` frontmatter |
First successful `/last30days` invocation triggers the setup wizard (browser cookie scan for X, `yt-dlp` check, optional ScrapeCreators signup). See [Quickstart](/quickstart).
## Beta channel (parallel install)
Experimental work ships from private repo `mvanhorn/last30days-skill-private` as slash command `/last30days-beta`. Install and promotion workflow live in `BETA.md` (private repo). Do not ship beta-only changes to the public marketplace without a review PR here.
## Related pages
<CardGroup>
<Card title="Quickstart" href="/quickstart">
First `/last30days` run, setup wizard signals, saved output location, and `--diagnose`.
</Card>
<Card title="Model clients" href="/model-clients">
Per-harness install targets, `-a` flags, and stale-install avoidance in depth.
</Card>
<Card title="Configure sources" href="/configure-sources">
API keys, `.env` lookup order, and source enablement after install.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
`--diagnose`, missing sources, stale marketplace clones, and thin results.
</Card>
<Card title="Skill, engine, and harness" href="/skill-engine-harness">
Boundaries between the Agent Skills package, Python engine, and harness runtimes.
</Card>
<Card title="Beta channel" href="/beta-channel">
Parallel `/last30days-beta` install and promotion workflow.
</Card>
</CardGroup>
---
## 03. Quickstart
> First /last30days invocation, first-run setup wizard signals, saved output location, and --diagnose verification.
- Page Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/03-quickstart.md
- Generated: 2026-06-04T23:19:11.649Z
### Source Files
- `skills/last30days/SKILL.md`
- `skills/last30days/scripts/lib/setup_wizard.py`
- `skills/last30days/scripts/last30days.py`
- `CONFIGURATION.md`
- `README.md`
---
title: "Quickstart"
description: "First /last30days invocation, first-run setup wizard signals, saved output location, and --diagnose verification."
---
The `/last30days` slash command loads `skills/last30days/SKILL.md`, runs Python 3.12+ preflight, optionally executes Step 0 first-run setup, then invokes `scripts/last30days.py` with `--emit=compact` and `--save-dir` pointed at `LAST30DAYS_MEMORY_DIR` (default `~/Documents/Last30Days/`). Reddit, Hacker News, Polymarket, and GitHub (when `gh` is installed) work with no API keys on the first run.
## Prerequisites
| Requirement | Detail |
|---|---|
| Skill installed | Claude Code marketplace, `npx skills add`, Hermes, or another Agent Skills host — see [Installation](/installation) |
| Python | 3.12+ (`python3.12`, `python3.13`, or `python3.14`); the skill resolves `LAST30DAYS_PYTHON` before any engine call |
| Optional keys | None required for the zero-config sources above |
<Note>
The product path is the slash command in your harness. Direct CLI (`python3 …/last30days.py`) is for scripting, cron, and verification — not a substitute for `/last30days` in chat.
</Note>
## First invocation
<Steps>
<Step title="Install the skill">
Install once per machine (marketplace plugin or `npx skills add mvanhorn/last30days-skill -g`). Re-run install or update when syncing a dev checkout.
</Step>
<Step title="Run a topic">
In Claude Code, Codex, Cursor, Gemini CLI, or any host that exposes the skill:
```
/last30days OpenClaw
```
The hosting model reads `SKILL.md`, parses intent (GENERAL by default), builds `ACTIVE_SOURCES_LIST` from your config, and prints a one-line confirmation such as:
```
/last30days - searching Reddit, Hacker News, Polymarket, and GitHub for what people are saying about OpenClaw.
```
It then runs pre-research (when WebSearch is available), plans sub-queries, and calls the engine with `--emit=compact --save-dir="${LAST30DAYS_MEMORY_DIR}" --save-suffix=v3`.
</Step>
<Step title="Read the response">
Valid output starts with the engine badge (`🌐 last30days v{VERSION} · synced {YYYY-MM-DD}`), then `What I learned:` (or the comparison title for `vs` topics), KEY PATTERNS, the pass-through emoji-tree footer, and an invitation — no trailing `Sources:` block.
</Step>
</Steps>
```mermaid
sequenceDiagram
participant User
participant Harness as Agent harness
participant Skill as SKILL.md contract
participant Engine as last30days.py
User->>Harness: /last30days topic
Harness->>Skill: Read + Step 0 check
alt First run
Skill->>Harness: Setup wizard (nux-wizard flow)
Harness->>Engine: optional setup subcommand
end
Harness->>Engine: --emit=compact --save-dir=MEMORY_DIR
Engine-->>Harness: compact stdout + footer save path
Harness-->>User: Synthesis + saved raw path
```
## First-run setup wizard
Setup splits into **detection** (Python), **UI** (hosting model via `SKILL.md`), and **auto-actions** (engine `setup` subcommand).
### Detection signals
| Signal | Meaning |
|---|---|
| `~/.config/last30days/.env` missing | Treated as first run (`SKILL.md` Step 0) |
| `SETUP_COMPLETE` absent or empty in config | `setup_wizard.is_first_run()` returns true |
| `SETUP_COMPLETE=true` present | Step 0 skipped silently — no setup banner on every run |
Config merge order: process environment → `.claude/last30days.env` (walked up from cwd) → `~/.config/last30days/.env` (override with `LAST30DAYS_CONFIG_DIR`).
### What the hosting model does (Step 0)
On first run the agent must:
1. Read `skills/last30days/nux-wizard.md` (relative to the skill root) and follow platform-specific flow (Claude Code vs OpenClaw, ScrapeCreators opt-in, topic picker).
2. After setup, ensure `SETUP_COMPLETE=true` is written to `~/.config/last30days/.env`.
3. Proceed to intent parsing and research without repeating setup on later runs.
<Warning>
`nux-wizard.md` is referenced in `SKILL.md` Step 0 as the wizard script. If that file is missing from your install tree, first-run UX falls back to manual configuration — use the engine `setup` subcommand and [Configure sources](/configure-sources) instead.
</Warning>
### Engine auto-setup (`setup` subcommand)
The model or you can run auto-setup directly (dev/fallback path):
```bash
# From the directory that contains scripts/last30days.py
python3 scripts/last30days.py setup
```
`run_auto_setup()` then:
- Extracts browser cookies for registered domains (X/Twitter and related cookie-jar sources) via `FROM_BROWSER=auto`
- Checks `yt-dlp`; on macOS with Homebrew, may run `brew install yt-dlp`
- Writes `SETUP_COMPLETE=true` and `FROM_BROWSER={browser}` to `~/.config/last30days/.env` (append-only, no overwrite of existing keys)
Optional flags after `setup` (parsed as extra argv): `--openclaw`, `--github`, `--device-auth` for platform-specific auth flows.
### Status text you should recognize
stderr/stdout from `setup` ends with a summary from `get_setup_status_text()`, for example:
<RequestExample>
```text
Setup complete! Here's what I found:
- X cookies found in chrome
- yt-dlp already installed
Configuration saved. Future runs will auto-detect your browsers.
```
</RequestExample>
Other branches include `No browser cookies found for X/Twitter`, `Installed yt-dlp via Homebrew`, `yt-dlp install failed — run brew install yt-dlp manually`, and `yt-dlp not found. Install Homebrew first…`.
<Info>
README describes “run it once and the setup wizard unlocks X, YouTube, TikTok, and more in 30 seconds” — that unlock is cookie extraction + `yt-dlp` plus optional ScrapeCreators keys, not a separate product install.
</Info>
## Where output is saved
| Artifact | Path pattern | When |
|---|---|---|
| Raw research dump | `{LAST30DAYS_MEMORY_DIR}/{slug}-raw[-suffix].md` | Every agent-mode run (`--save-suffix=v3` in `SKILL.md`) |
| Same-day collision | `{slug}-raw[-suffix]-{YYYY-MM-DD}.md` | If the target file already exists |
| HTML brief (optional) | `{LAST30DAYS_MEMORY_DIR}/{slug}-brief.html` | User asks for shareable HTML |
| Engine pointer | Footer line `📎 Raw results saved to …` | Confirms resolved path in chat |
<ParamField body="LAST30DAYS_MEMORY_DIR" type="string">
Default `~/Documents/Last30Days/` on Linux/macOS; `C:\Users\<you>\Documents\Last30Days\` on Windows. Set before invoking the skill to redirect all saves.
</ParamField>
<ParamField body="--save-dir" type="path">
Per-run override passed by the skill or CLI. Creates the directory if needed.
</ParamField>
<ParamField body="--save-suffix" type="string">
Distinguishes parallel runs of the same topic (e.g. `v3`, client slug).
</ParamField>
Slug rules: topic lowercased, non-alphanumerics folded to hyphens (`save_output` / `slugify` in `last30days.py`).
```text
~/Documents/Last30Days/
├── openclaw-raw-v3.md # default agent run
├── openclaw-brief.html # optional HTML export
└── peter-steinberger-raw.md # person-topic example
```
Step 2.5 in `SKILL.md` requires the hosting model to append WebSearch supplements into the saved raw file under `## WebSearch Supplemental Results` — the on-disk file is the durable citation surface, not only chat output.
## Verify with `--diagnose`
`--diagnose` runs availability probes only — no topic search, no synthesis.
```bash
python3 skills/last30days/scripts/last30days.py --diagnose
```
<ResponseExample>
```json
{
"available_sources": ["reddit", "hackernews", "polymarket", "github", "grounding"],
"bird_authenticated": false,
"bird_installed": true,
"bird_username": null,
"has_github": true,
"has_scrapecreators": false,
"local_mode": true,
"native_web_backend": null,
"providers": {
"google": false,
"openai": false,
"xai": false,
"openrouter": false
},
"reasoning_provider": "auto",
"x_backend": null
}
```
</ResponseExample>
| Field | Use when verifying |
|---|---|
| `available_sources` | Which platforms the next run can query |
| `providers` / `local_mode` | Whether headless planning needs API keys (`local_mode: true` → deterministic fallback unless the host passes `--plan`) |
| `x_backend`, `bird_*` | X/Twitter auth path (cookies, Bird CLI, xAI, ScrapeCreators) |
| `has_scrapecreators` | TikTok, Instagram, Threads unlock |
| `native_web_backend` | First configured Brave → Exa → Serper → Parallel key |
<Check>
After setup, confirm `youtube` appears in `available_sources` when `yt-dlp` is on PATH, and `x` when cookies or `XAI_API_KEY` / Bird auth is configured. Missing sources with no keys is expected — add credentials per [Configure sources](/configure-sources).
</Check>
`verify_v3.py` in the repo uses the same `--diagnose` call as a release gate.
## Direct CLI smoke test (optional)
For engine testing without the skill contract:
```bash
cd skills/last30days/scripts
python3 last30days.py "test query" --emit=compact --mock
```
Full research requires the skill’s pre-flight flags (`--plan`, handles, subreddits). A bare `python3 last30days.py "Topic"` from cron is supported but uses the engine’s internal planner, not the hosting model’s Step 0.55 / 0.75 resolution.
## Common first-run outcomes
| Outcome | Cause | Next step |
|---|---|---|
| Thin social coverage | No X / TikTok / YouTube keys yet | Run `setup` or add keys; re-check `--diagnose` |
| `## Pre-Research Status` in output | Model skipped Step 0.55 on a named entity | Re-run via slash command with WebSearch; see [Skill contract](/skill-contract) |
| Stale behavior / missing flags | Claude marketplace clone lagging cache | `SKILL.md` STEP 0 stale-clone guard — see [Troubleshooting](/troubleshooting) |
| Permission warning on `.env` | File mode looser than `600` | `chmod 600 ~/.config/last30days/.env` |
## Related pages
<CardGroup>
<Card title="Installation" href="/installation">
Marketplace, `npx skills add`, Hermes, and live-edit symlinks.
</Card>
<Card title="Overview" href="/overview">
Slash command vs engine paths and zero-config sources.
</Card>
<Card title="Configure sources" href="/configure-sources">
API keys, `.env` layers, and wizard auto-actions in depth.
</Card>
<Card title="CLI reference" href="/cli-reference">
All `last30days.py` flags including `--emit` and `--save-*`.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
`--diagnose` recovery, thin results, and stale installs.
</Card>
</CardGroup>
---
## 04. Skill, engine, and harness
> Project-specific boundaries between the Agent Skills package, Python engine, and multi-harness runtimes that load the skill.
- Page Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/04-skill-engine-and-harness.md
- Generated: 2026-06-04T23:19:02.027Z
### Source Files
- `CONCEPTS.md`
- `skills/last30days/SKILL.md`
- `skills/last30days/scripts/last30days.py`
- `AGENTS.md`
- `skills/last30days/scripts/lib/skill_meta.py`
---
title: "Skill, engine, and harness"
description: "Project-specific boundaries between the Agent Skills package, Python engine, and multi-harness runtimes that load the skill."
---
`mvanhorn/last30days-skill` ships as an [Agent Skills](https://agentskills.io) package under `skills/last30days/`: `SKILL.md` is the runtime contract the hosting model follows, and `scripts/last30days.py` plus `scripts/lib/` perform retrieval, fusion, and rendering. The product surface is harness-invoked research (`/last30days <topic>` on most hosts); direct `python3 …/last30days.py` is a dev, cron, and scripting fallback only.
## Three layers
| Layer | Location | Owns | Does not own |
|-------|----------|------|----------------|
| **Skill** | `skills/last30days/SKILL.md` + sibling `scripts/`, `references/`, `assets/` | Slash-command UX, pre-flight (Step 0–0.75), query planning on reasoning hosts, synthesis LAWs, which engine flags to pass | Platform HTTP clients, clustering, dedupe, rerank |
| **Engine** | `scripts/last30days.py`, `scripts/lib/*` | v3 pipeline (`pipeline.run`), `--emit` output shapes, `--diagnose`, SQLite `--store`, headless `--auto-resolve` / internal planner | User-facing prose contract, WebSearch supplements, invitation copy |
| **Harness** | Claude Code, Codex, Cursor, Copilot, Gemini CLI, Hermes, OpenClaw, 50+ Agent Skills hosts | Loads `SKILL.md`, exposes tools (Bash, Read, WebSearch), routes `/last30days` | Research algorithms, source scoring |
<Note>
Project vocabulary is fixed in `CONCEPTS.md`: **Skill** = distribution unit; **Engine** = Python implementation; **Harness** = agent runtime that loads the Skill. “Multi-harness” means a feature must work on every install path the Skill supports—not only Claude Code.
</Note>
```mermaid
flowchart TB
subgraph harness["Harness (Claude Code, Codex, Cursor, Hermes, …)"]
SC["/last30days slash command"]
M["Reasoning model"]
WS["WebSearch / Read / Bash tools"]
SC --> M
M --> WS
end
subgraph skill["Skill package skills/last30days/"]
SM["SKILL.md contract"]
SM -->|"SKILL_DIR/scripts/last30days.py"| ENG
end
subgraph engine["Engine scripts/"]
ENG["last30days.py"]
LIB["lib/ pipeline · render · env · …"]
ENG --> LIB
LIB --> SRC["Reddit · X · YouTube · …"]
end
M -->|"Read SKILL.md"| SM
WS -->|"Bash invoke"| ENG
ENG -->|"--emit=compact stdout"| M
M -->|"synthesis + LAW pass-through"| U["User response"]
```
## Skill package layout
Every install layout keeps **`SKILL.md` and `scripts/` as siblings** under one directory (`SKILL_DIR`). The engine entrypoint is always `${SKILL_DIR}/scripts/last30days.py`.
:::files
skills/last30days/
├── SKILL.md # Runtime spec (~1400+ lines); harness reads this on invoke
├── scripts/
│ ├── last30days.py # CLI entry (Python 3.12+)
│ ├── lib/ # pipeline, render, env, per-source modules
│ ├── briefing.py # Trend-monitoring digests (optional)
│ ├── watchlist.py # Scheduled runs (optional)
│ └── store.py # SQLite persistence (optional)
├── references/ # Supplemental skill docs (e.g. HTML brief flow)
└── assets/ # Static assets referenced from SKILL.md
:::
The Skill conforms to the Agent Skills open format (`name`, `version`, `description`, `allowed-tools`, `metadata` in YAML frontmatter). Version strings in output come from `SKILL.md` frontmatter via `lib/skill_meta.py`, with an optional `.claude-plugin/plugin.json` walk in `lib/render.py` for marketplace cache installs.
Distribution paths include:
- **Claude Code marketplace** — plugin cache under `~/.claude/plugins/cache/last30days-skill/…` (versioned) plus a known-stale `~/.claude/plugins/marketplaces/` clone (STEP 0 guard).
- **`npx skills add`** — copies into `~/.agents/skills/last30days/` (frozen until re-run); per-host symlinks when supported.
- **Per-harness dirs** — e.g. `~/.codex/skills/last30days/`, `~/.hermes/skills/research/last30days/`.
- **Repo checkout** — `skills/last30days/` in the working tree for development.
<Warning>
Working-tree edits do **not** propagate to `~/.agents/skills/` automatically. Re-run `npx skills add . -g -y` or symlink the working tree for live dev (`ln -sfn "$PWD/skills/last30days" ~/.agents/skills/last30days`).
</Warning>
## Engine responsibilities
`last30days.py` is the sole supported engine entry. It requires **Python 3.12+**, inserts `SCRIPT_DIR` on `sys.path`, and delegates to `lib.pipeline` for orchestration (planner input, parallel source fan-out, fusion, clustering, dedupe, rerank, depth profiles via `--quick` / default / `--deep`).
| Concern | Engine module / surface |
|---------|-------------------------|
| Config & keys | `lib/env.py`, `.env` / keychain patterns |
| Retrieval | `lib/reddit.py`, `lib/bird_x.py`, `lib/youtube_yt.py`, … |
| Output wire format | `lib/render.py` (`--emit=compact|md|json|context|html`) |
| Badge line | `render._render_badge()` — first line of compact/md stdout |
| Class-1 refusal | `lib/preflight.py` before pipeline |
| Headless planning | `lib/planner.py` + `--auto-resolve` when no host WebSearch |
| Version string | `lib/skill_meta.read_skill_version` + `render._skill_version()` |
`lib/__init__.py` is intentionally a bare package marker (no eager imports)—contributor constraint from `AGENTS.md`.
Direct CLI invocation (fallback only):
```bash
python3 skills/last30days/scripts/last30days.py "topic" --emit=compact --diagnose
```
Headless/cron paths may rely on engine `--plan` omission (internal/deterministic planner) or `--auto-resolve` when the harness lacks WebSearch—documented in-engine as the OpenClaw / raw CLI equivalent of SKILL.md Steps 0.55 and 0.75.
## Harness responsibilities
The harness loads `SKILL.md` when the user runs `/last30days <topic>` (or host-specific equivalent). It does **not** forward raw shell syntax from the slash line: pipes, inline flags, and `| pbcopy` on `/last30days …` are invalid; the model maps intent to engine flags, or the operator uses the direct CLI with a real shell.
On reasoning harnesses (Claude Code, Codex with WebSearch, Gemini, Hermes, etc.):
1. **Read** the Skill contract (including STEP 0 stale-clone check for Claude marketplace paths).
2. **Plan** — generate `QUERY_PLAN_JSON` and Step 0.55 targeting; pass `--plan` via a tmpfile (never inline quoted JSON).
3. **Invoke** — Bash `${LAST30DAYS_PYTHON} "${SKILL_DIR}/scripts/last30days.py" … --emit=compact`.
4. **Supplement** — WebSearch steps SKILL.md defines post-engine.
5. **Synthesize** — transform evidence blocks; pass through badge and emoji-tree footer per LAWs.
`SKILL_DIR` is the directory of the **SKILL.md the harness actually loaded**—not a searched precedence list. That keeps spec and code aligned across install layouts without enumerating every host path.
```mermaid
sequenceDiagram
participant U as User
participant H as Harness
participant M as Model
participant S as SKILL.md
participant E as last30days.py
U->>H: /last30days topic
H->>M: Load skill + tools
M->>S: Read (STEP 0 if Claude marketplace)
M->>M: Step 0.55 / 0.75 plan + resolve
M->>E: Bash SKILL_DIR/scripts/last30days.py --plan … --emit=compact
E-->>M: Badge + evidence + footer stdout
M->>M: WebSearch supplements (if available)
M-->>U: Synthesis + pass-through footer
```
### Platform branch
| Harness capability | Skill path | Engine flags |
|--------------------|------------|--------------|
| WebSearch available | Steps 0.55 + 0.75 mandatory; model is planner (LAW 7) | `--plan "$QUERY_PLAN_FILE"`, targeting flags, `--emit=compact` |
| No WebSearch (OpenClaw, some Codex) | Skip 0.55/0.75 | `--auto-resolve` |
| Agent mode (`--agent` in args) | Skip intro, AskUserQuestion, invitation | Same engine run; structured report template |
Resolve once per session:
- `LAST30DAYS_PYTHON` — Python 3.12+ interpreter for all engine calls.
- `LAST30DAYS_MEMORY_DIR` — raw save root (default `~/Documents/Last30Days`); passed as `--save-dir`.
## Skill ↔ engine contract
The contract is bidirectional: **SKILL.md → flags**, **engine → output shape**.
### SKILL.md → engine
The model must translate user intent into explicit flags the engine understands, including:
| Flag family | Purpose |
|-------------|---------|
| `--plan` | External JSON query plan (reasoning-model path) |
| `--x-handle`, `--subreddits`, `--github-user`, `--github-repo`, TikTok/IG flags | Step 0.55 targeting |
| `--competitors`, `--competitors-plan` | Comparison / vs-mode fan-out |
| `--emit` | Wire format (`compact` is the primary user-facing mode) |
| `--save-dir`, `--save-suffix` | Artifact paths |
| `--quick` / `--deep` | Depth profiles |
| `--diagnose` | Source availability probe (verification) |
<Info>
A new engine flag without a matching `SKILL.md` section is considered **incomplete**: the invoking model will not know the flag exists. Feature design starts from slash-command UX, per `AGENTS.md`.
</Info>
### Engine → model
Default `--emit=compact` stdout includes:
- **Badge** — `🌐 last30days v{version} · synced {date}` (engine-emitted since v3.0.8).
- **Evidence for synthesis** — `<!-- EVIDENCE FOR SYNTHESIS -->` bounded blocks (read, do not emit verbatim — LAW 6).
- **Pass-through footer** — `<!-- PASS-THROUGH FOOTER -->` emoji-tree ending in raw save path (LAW 5).
The model adds narrative (`What I learned:`, KEY PATTERNS, comparison template sections) and must not improvise alternate titles, `Sources:` blocks, or bare `python3 scripts/last30days.py "$TOPIC"` on named entities without `--plan`.
## Install surfaces vs runtime
| Install mechanism | Typical `SKILL_DIR` | Sync model |
|-------------------|---------------------|------------|
| Claude marketplace | `~/.claude/plugins/cache/…/skills/last30days` | Marketplace/plugin update; watch STEP 0 |
| `npx skills add -g` | `~/.agents/skills/last30days` | Re-run add or dev symlink |
| Codex / Cursor skills dir | `~/.codex/skills/last30days` | Host-specific refresh |
| Hermes | `~/.hermes/skills/research/last30days` | `hermes skills install … --force` or symlink |
| Repo dev | `$REPO/skills/last30days` | Immediate on save |
Beta channel (`/last30days-beta` from private repo) is a **parallel Skill install**, not a different engine—see beta docs for promotion boundaries.
## Contributor boundaries
When extending the project:
1. **User-facing behavior** — update `SKILL.md` first (steps, flags, LAWs, examples).
2. **Implementation** — `scripts/last30days.py` + `scripts/lib/`.
3. **Operator knobs** — mirror env vars and CLI flags in `CONFIGURATION.md`.
4. **Tests** — `uv run pytest` (~89 files); version consistency via `tests/test_plugin_contract.py` and `skill_meta.read_skill_version`.
Do not treat the repo as a standalone CLI product in docs or UX: README and PR examples lead with `/last30days <topic>`; direct Python invocation is labeled fallback only.
## Related pages
<CardGroup>
<Card title="Overview" href="/overview">
Slash-command vs engine paths, zero-config sources, shortest successful research path.
</Card>
<Card title="Skill contract" href="/skill-contract">
STEP 0 stale-clone guard, pre-flight protocol, SKILL_DIR substitution, invocation rules.
</Card>
<Card title="Research pipeline" href="/research-pipeline">
v3 orchestration inside the engine: planner, fan-out, fusion, clustering, dedupe, rerank.
</Card>
<Card title="Output contract" href="/output-contract">
Badge, LAWs voice rules, --emit modes, evidence blocks, footer pass-through.
</Card>
<Card title="Installation" href="/installation">
Marketplace, npx skills add, Hermes, live-edit symlinks, version sync.
</Card>
<Card title="Model clients" href="/model-clients">
Per-harness install patterns and stale-install avoidance.
</Card>
<Card title="CLI reference" href="/cli-reference">
Direct last30days.py flags and constraints vs slash-command usage.
</Card>
</CardGroup>
---
## 05. Query types
> QUERY_TYPE classification (GENERAL, NEWS, PROMPTING, RECOMMENDATIONS, COMPARISON), intent parsing, and engine pre-flight refusal paths.
- Page Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/05-query-types.md
- Generated: 2026-06-04T23:19:52.899Z
### Source Files
- `skills/last30days/SKILL.md`
- `skills/last30days/scripts/lib/preflight.py`
- `skills/last30days/scripts/lib/planner.py`
- `skills/last30days/scripts/lib/categories.py`
- `skills/last30days/scripts/lib/query.py`
---
title: "Query types"
description: "QUERY_TYPE classification (GENERAL, NEWS, PROMPTING, RECOMMENDATIONS, COMPARISON), intent parsing, and engine pre-flight refusal paths."
---
`/last30days` classifies each user topic twice: the hosting model assigns a **QUERY_TYPE** for synthesis shape and WebSearch supplements (`skills/last30days/SKILL.md`), while the Python engine assigns a planner **intent** for sub-queries, source weights, freshness, and clustering (`skills/last30days/scripts/lib/planner.py`). A separate engine refuse-gate in `preflight.py` blocks Class 1 demographic-shopping topics before any pipeline work runs.
## Two classification layers
| Layer | Where it runs | Identifier set | Primary effect |
|-------|---------------|----------------|----------------|
| Skill / harness | Model before engine (Step “Parse User Intent”) | `GENERAL`, `NEWS`, `PROMPTING`, `RECOMMENDATIONS`, `COMPARISON` | Branded status line, WebSearch supplement queries, synthesis template (LAWs 2/4), invitation copy |
| Engine planner | `plan_query()` in `planner.py` (or `--plan` JSON from Step 0.75) | `factual`, `product`, `concept`, `opinion`, `how_to`, `comparison`, `breaking_news`, `prediction` | Sub-query fan-out, `freshness_mode`, `cluster_mode`, per-source weights and quick-depth caps |
The skill layer and engine layer are related but not identical. A `RECOMMENDATIONS` slash query often becomes a `product` or `opinion` plan intent; `PROMPTING` queries align with `how_to` planner behavior (YouTube/tutorial weighting). `COMPARISON` is the one type where both layers share the same name and force a deterministic per-entity plan when two or more entities parse from `vs` / `versus` / `/` splits.
```mermaid
flowchart LR
subgraph harness["Harness (SKILL.md)"]
U[User topic] --> PI[Parse intent → QUERY_TYPE]
PI --> PF45[Step 0.45 keyword-trap check]
PF45 --> PF5[Step 0.5 / 0.55 flags]
PF5 --> P75[Step 0.75 → QueryPlan JSON]
end
subgraph engine["Engine (last30days.py)"]
P75 -->|"--plan"| RUN[Pipeline]
PI -->|positional topic| PRE[preflight.check_class_1_trap]
PRE -->|REFUSE exit 2| STOP[No pipeline]
PRE -->|pass| RUN
RUN --> PLAN[plan_query / _infer_intent]
PLAN --> SRC[Source fan-out + fusion]
end
SRC --> SYN[Synthesis by QUERY_TYPE template]
```
## QUERY_TYPE (skill contract)
The model parses the user message **before** engine invocation and stores `TOPIC`, optional `TARGET_TOOL`, `QUERY_TYPE`, and for comparisons `TOPIC_A` / `TOPIC_B`. Do not emit a multi-line “Parsed intent” block; show the branded one-liner instead.
| QUERY_TYPE | Trigger patterns (from user text) | Status line shape | Synthesis shape |
|------------|-----------------------------------|-------------------|-----------------|
| **PROMPTING** | “X prompts”, “prompting for X”, “X best practices” | `searching … for what people are saying about {TOPIC}` | Badge → `What I learned:` → KEY PATTERNS; invitation offers copy-paste prompt help for `TARGET_TOOL` |
| **RECOMMENDATIONS** | “best X”, “top X”, “what X should I use”, “recommended X” | Same as non-comparison types | Signal-weighted picks (not raw mention counts); invitation suggests compare/explain picks |
| **NEWS** | “what’s happening with X”, “X news”, “latest on X” | Same | Current-events narrative; WebSearch targets news/announcements |
| **COMPARISON** | “X vs Y”, “X versus Y”, “compare X and Y” (split on spaced ` vs ` / ` versus `) | `comparing {TOPIC_A} vs {TOPIC_B} across …` | Required `# {A} vs {B}` title and fixed `##` sections (`Quick Verdict`, per-entity, `Head-to-Head`, etc.); skips `What I learned:` |
| **GENERAL** | Default when no other pattern matches | Same as non-comparison types | Badge → `What I learned:` → KEY PATTERNS; broad understanding |
<Note>
The stored-variable line in SKILL.md lists `HOW-TO` among allowed `QUERY_TYPE` values, but classification rules and LAWs throughout the file use **PROMPTING** for technique-and-prompt workflows. Treat **PROMPTING** as the operative label for those queries.
</Note>
**TARGET_TOOL** is parsed from `[topic] for [tool]` or `[topic] prompts for [tool]`. The skill explicitly does **not** ask for a missing tool before research; it runs first, then may ask after results.
**Common pattern shortcuts:**
- `best [topic]` / `top [topic]` → `RECOMMENDATIONS`
- `X vs Y` (with spaces) → `COMPARISON` with entity split
- `[topic] for [tool]` → tool captured, type otherwise inferred
## Engine planner intents
When no `--plan` JSON is supplied, `plan_query()` calls `_infer_intent()` with ordered regex guards, then builds a `QueryPlan` (`schema.QueryPlan`: `intent`, `freshness_mode`, `cluster_mode`, `raw_topic`, `subqueries`, `source_weights`, `notes`).
| Intent | Detection signals (topic, lowercased) | Default `freshness_mode` | Default `cluster_mode` | Source bias (examples) |
|--------|--------------------------------------|--------------------------|------------------------|-------------------------|
| `comparison` | `vs`, `versus`, `compare`, slash-separated proper nouns (`React/Vue`) | `balanced_recent` | `debate` | Reddit, X, HN, YouTube |
| `prediction` | odds, predict, forecast, probability | `strict_recent` | `market` | Polymarket +2.5 weight |
| `how_to` | how to, tutorial, guide, setup, deploy, install | `evergreen_ok` | `workflow` | YouTube +2.0 |
| `factual` | what is/are, who is, release date, parameter count | `balanced_recent` | `none` | HN, Reddit, X |
| `opinion` | thoughts on, worth it, should I, review | `balanced_recent` | `debate` | Reddit, X, YouTube |
| `breaking_news` | latest, news, announced, launched; sports/ceremony keywords; trending/this week | `strict_recent` | `story` | X +1.5, Reddit +1.3 |
| `product` | pricing, feature(s), best/top … for | `balanced_recent` | `none` | YouTube, Reddit, TikTok |
| `concept` | explain, protocol, architecture; **default fallback** | `evergreen_ok` | `none` | HN, Reddit, X |
Comparison topics with ≥2 extracted entities skip LLM planning and use `_fallback_plan()` with per-entity sub-queries (`_should_force_deterministic_plan`).
Allowed intents are validated in `ALLOWED_INTENTS`; invalid LLM output is coerced or replaced by the deterministic fallback. Quick depth applies `SOURCE_LIMITS` caps (typically two sources per intent); default depth searches all configured sources.
## Query preprocessing (`query.py`)
`query.py` does not assign QUERY_TYPE. It supplies shared preprocessing for search modules:
- **`PREFIXES` / `SUFFIXES` / `NOISE_WORDS`** — strip meta-phrasing (“what are the best”, “prompting techniques”, “vs”, “news”, etc.) from platform queries.
- **`extract_core_subject()`** — compact keyword core after prefix/suffix/noise removal.
- **`extract_compound_terms()`** — hyphenated and Title Case multi-word phrases for quoting in planner `_keyword_query()`.
Planner and Reddit modules call these helpers so literal user phrasing does not get echoed verbatim into APIs (the documented “Hermes Agent use cases” failure mode).
## Category peers (`categories.py`)
`detect_category()` and `peer_subs_for()` are orthogonal to QUERY_TYPE. They map product-shaped topics to curated category-peer subreddits (AI image gen, coding agents, prediction markets, etc.) for Step 0.55 Reddit targeting. First pattern match wins; this is code-reviewed data, not user-editable config.
## Pre-flight: skill vs engine
Pre-flight runs at two boundaries with different coverage.
### Step 0.45 (skill — model-side)
Mandatory before Step 0.5. The model classifies keyword-trap **classes** and may stop for a clarifying question:
| Class | Pattern | Action |
|-------|---------|--------|
| **1** Demographic shopping | gift/present for age/gender/relationship; best X for men/women/kids | Ask hobbies / relationship / budget **or** reframe + gift subreddits; do not run engine on literal phrase without user OK |
| **2** Numeric / age trap | Bare numbers that collide (42, 40, 50) | Strip number from engine query unless semantically load-bearing |
| **3** Overly-literal tutorial | “how to use X”, “tutorial for Z” | Reframe to discussion vocabulary (“Docker tips workflows”) |
| **4** Generic single noun | `bread`, `sneakers` without hook | Ask which facet before running |
Emit `Pre-Flight: …` before the Resolved block. If the action is a clarifying question, **stop** until the user responds. Inline qualifiers on Class 1 (budget, hobbies, “cooking-obsessed”) skip the question and proceed with reframe.
### Engine refuse-gate (`preflight.py`)
Only **Class 1** is enforced in Python at `main()` before `ProgressDisplay` starts:
```text
topic → check_class_1_trap()
├─ no Class 1 match → continue pipeline
├─ Class 1 + qualifier (budget, hobbies, $, activity noun) → continue
└─ Class 1 bare demographic → stderr REFUSE, exit code 2
```
<ParamField body="LAST30DAYS_SKIP_PREFLIGHT" type="env (optional)">
Set to any non-empty value to bypass the engine gate. Use only when the user insists on running the literal demographic-shopping phrase anyway.
</ParamField>
The REFUSE message includes `Class 1`, the echoed topic, and instructions to ask for hobbies, relationship, or budget, then re-run. Tests in `tests/test_preflight.py` lock match/skip behavior (e.g. `birthday gift for 40 year old` refuses; `gift for my cooking-obsessed husband` passes).
Classes 2–4 exist only in SKILL.md; the engine does not regex-block them. The hosting model must reframe before invocation.
## WebSearch supplements by QUERY_TYPE
After the engine returns (Step 2), supplement queries differ by skill QUERY_TYPE (2–3 searches, exclude reddit.com/x.com):
| QUERY_TYPE | Supplement focus |
|------------|------------------|
| RECOMMENDATIONS | `best {TOPIC} recommendations`, lists, popular examples |
| NEWS | `{TOPIC} news 2026`, announcements |
| PROMPTING | `{TOPIC} prompts examples`, techniques/tips |
| GENERAL | `{TOPIC} 2026`, discussion |
| COMPARISON | Follow comparison-mode workflow; per-entity engine runs may apply |
Use the user’s exact terminology; do not substitute model-known aliases unless disambiguation is required.
## Output contract differences
LAWs 2 and 4 split on QUERY_TYPE:
- **GENERAL / NEWS / PROMPTING / RECOMMENDATIONS:** No invented title; no `##` body headers; body opens with `What I learned:` after the version badge.
- **COMPARISON:** Required markdown title and mandated `##` sections; engine may emit a `## Head-to-Head` table scaffold to fill verbatim.
RECOMMENDATIONS synthesis must rank by signal quality, not mention frequency. PROMPTING invitations reference concrete techniques from research and offer paste-ready prompts when `TARGET_TOOL` is known.
## Passing intent into the engine
On WebSearch-capable harnesses, Step 0.75 expects the model to emit JSON aligned with `QueryPlan` and pass `--plan`. Intent in that JSON should reflect the topic (often mirroring QUERY_TYPE, e.g. `breaking_news` for NEWS, `how_to` for PROMPTING). Without `--plan`, stderr warns (LAW 7) and `_infer_intent()` supplies a deterministic plan—acceptable for headless/cron, weaker for agent-hosted runs.
Engine-internal competitor fan-out sets `internal_subrun=True` on `plan_query()` to suppress the false-positive “No --plan passed” warning.
## Verification signals
| Check | Expected signal |
|-------|-----------------|
| Class 1 trap (bare) | `[last30days] REFUSE: … Class 1` on stderr; exit `2`; no emoji-tree footer |
| Class 1 with hobby/budget | Engine runs normally |
| Comparison topic | Status line uses “comparing A vs B”; synthesis uses comparison `##` template |
| Skill pre-flight only | `Pre-Flight: topic matches Class N` in model output before bash |
| Planner fallback | `[Planner] No --plan passed` on stderr when hosting model skipped Step 0.75 |
## Related pages
<CardGroup>
<Card title="Skill contract" href="/skill-contract">
STEP 0 stale-clone guard, pre-flight protocol, engine invocation, and LAW anchors that depend on QUERY_TYPE.
</Card>
<Card title="Research pipeline" href="/research-pipeline">
How planner intents drive sub-query fan-out, fusion, clustering, and depth profiles after classification.
</Card>
<Card title="Comparison mode" href="/comparison-mode">
Competitor discovery, per-entity targeting, and comparison synthesis when QUERY_TYPE is COMPARISON.
</Card>
<Card title="Prompting recipes" href="/prompting-recipes">
PROMPTING workflows, KEY PATTERNS shape, and copy-paste prompt patterns.
</Card>
<Card title="Output contract" href="/output-contract">
Badge line, LAWs voice rules, emit modes, and footer pass-through by query type.
</Card>
</CardGroup>
---
## 06. Research pipeline
> v3 orchestration: planner sub-queries, parallel source fan-out, fusion, clustering, dedupe, rerank, and depth profiles (--quick / default / --deep).
- Page Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/06-research-pipeline.md
- Generated: 2026-06-04T23:19:52.528Z
### Source Files
- `skills/last30days/scripts/lib/pipeline.py`
- `skills/last30days/scripts/lib/schema.py`
- `skills/last30days/scripts/lib/fusion.py`
- `skills/last30days/scripts/lib/cluster.py`
- `skills/last30days/scripts/lib/dedupe.py`
- `skills/last30days/scripts/lib/rerank.py`
- `docs/how-search-works.md`
---
title: "Research pipeline"
description: "v3 orchestration: planner sub-queries, parallel source fan-out, fusion, clustering, dedupe, rerank, and depth profiles (--quick / default / --deep)."
---
The v3 research engine in `skills/last30days/scripts/lib/pipeline.py` turns a topic string into a `schema.Report`: a `QueryPlan`, per-source evidence, fused `Candidate` rows, optional `Cluster` groups, and warnings. `last30days.py` resolves depth from `--quick` / `--deep`, calls `pipeline.run()`, then hands the report to renderers (`--emit=compact`, `json`, `context`, `md`, `html`). Comparison and vs-topic flows invoke the same pipeline once per entity via `fanout.run_competitor_fanout`.
## Pipeline overview
```mermaid
flowchart TB
subgraph entry [CLI entry]
CLI[last30days.py]
end
subgraph plan [Planning]
PL[planner.plan_query / --plan]
RT[providers.resolve_runtime]
end
subgraph phase1 [Phase 1 — parallel retrieval]
TP[ThreadPoolExecutor]
RS[_retrieve_stream per subquery × source]
NSD[_normalize_score_dedupe]
end
subgraph phase2 [Phase 2 — recovery]
SUP[_run_supplemental_searches]
RET[_retry_thin_sources]
end
subgraph rank [Global ranking]
FUS[weighted_rrf]
RR[rerank.rerank_candidates]
FUN[rerank.score_fun]
GH[github.enrich_candidates_with_stars]
CL[cluster_candidates]
end
CLI --> PL
CLI --> RT
PL --> TP
RT --> PL
TP --> RS --> NSD
NSD --> SUP
SUP --> RET
RET --> FUS --> RR --> FUN --> GH --> CL
CL --> REP[schema.Report]
```
| Stage | Module | Output |
| --- | --- | --- |
| Plan | `planner.py` | `QueryPlan` with 1–5 `SubQuery` rows |
| Fan-out | `pipeline.py` | `RetrievalBundle` keyed by `(subquery_label, source)` |
| Per-stream cleanup | `normalize`, `signals`, `dedupe`, `snippet` | `SourceItem` lists capped by depth |
| Fusion | `fusion.py` | `Candidate` pool (RRF + diversity) |
| Rerank | `rerank.py` | `rerank_score`, `final_score`, `fun_score` |
| Cluster | `cluster.py` | `Cluster` list with MMR representatives |
## Depth profiles
Depth is a single string: `"quick"`, `"default"`, or `"deep"`. The CLI sets it as `deep` if `--deep`, else `quick` if `--quick`, else `default`.
### Global pool limits (`DEPTH_SETTINGS`)
These caps apply after each retrieval stream is normalized; they do not replace per-source `DEPTH_CONFIG` inside Reddit, X, YouTube, and similar adapters.
| Depth | `per_stream_limit` | `pool_limit` (fusion) | `rerank_limit` (LLM shortlist) |
| --- | ---: | ---: | ---: |
| `quick` | 6 | 15 | 12 |
| `default` | 12 | 40 | 40 |
| `deep` | 20 | 60 | 60 |
### Planner behavior by depth
| Depth | Subqueries | Sources per subquery |
| --- | --- | --- |
| `quick` | At most **1** after sanitization | Up to **2** per intent via `SOURCE_LIMITS` and `QUICK_SOURCE_PRIORITY` |
| `default` | LLM or fallback (typically 2–5) | **All** eligible sources expanded per intent (`_trim_subqueries_for_depth` overrides narrow LLM source lists) |
| `deep` | Same as default | Same as default; higher retrieval caps downstream |
### Phases skipped at `quick`
- **Phase 2 supplemental** (`_run_supplemental_searches`): entity-derived X handle searches from Phase 1 Reddit/X text — skipped when `depth == "quick"` or `mock`.
- **Phase 2b thin retry** (`_retry_thin_sources`): simplified core-subject re-query for sources with fewer than three items — skipped when `depth == "quick"`.
### Reasoning models
`providers.resolve_runtime(config, depth)` picks planner and rerank models. With `LAST30DAYS_REASONING_PROVIDER=auto`, the first configured key wins: Gemini → OpenAI → xAI → OpenRouter; otherwise `reasoning_provider="local"` and planner/rerank fall back to deterministic scoring.
<ParamField body="--quick" type="flag">
Lower-latency profile: one planner subquery, tight source budget, no supplemental or thin-source retry, smaller fusion/rerank pools.
</ParamField>
<ParamField body="--deep" type="flag">
Higher-recall profile: larger per-stream and pool limits. When the reasoning provider is Gemini, default rerank model upgrades to Gemini Pro (`providers._resolve_model_pins`).
</ParamField>
<ParamField body="LAST30DAYS_PLANNER_MODEL" type="string">
Overrides default planner model for the resolved reasoning provider.
</ParamField>
<ParamField body="LAST30DAYS_RERANK_MODEL" type="string">
Overrides default rerank model for the resolved reasoning provider.
</ParamField>
## Query planning
`planner.plan_query()` returns a `QueryPlan`:
- **`intent`**: `factual`, `product`, `concept`, `opinion`, `how_to`, `comparison`, `breaking_news`, or `prediction`
- **`freshness_mode`**: `strict_recent`, `balanced_recent`, or `evergreen_ok`
- **`cluster_mode`**: `none`, `story`, `workflow`, `market`, or `debate` (drives downstream clustering)
- **`subqueries`**: each `SubQuery` has `label`, `search_query`, `ranking_query`, `sources`, and `weight`
- **`source_weights`**: per-source multipliers applied during fusion
### Plan sources (priority order)
1. **`--plan` JSON** (hosting agent or file path): sanitized through `_sanitize_plan`; `plan_source="external"`.
2. **LLM planner** when a reasoning provider and `planner_model` are configured: `provider.generate_json`; `plan_source="llm"`.
3. **Deterministic fallback** when LLM fails or no provider keys exist: comparison entities, intent heuristics, or generic primary subquery; `plan_source="deterministic"`.
<Warning>
When no `--plan` is passed and the engine has no internal reasoning credentials, stderr emits a LAW 7 reminder: the **hosting** agent (Claude Code, Codex, Hermes, etc.) should generate the plan JSON and pass `--plan`. The deterministic fallback is intended for headless/cron runs.
</Warning>
Planner traces always go to stderr (not user stdout):
```
[Planner] Plan: intent=..., freshness=..., cluster_mode=..., subqueries=N, source=llm|deterministic|external
[Planner] sq1 label=... search="..." sources=[reddit,x,...]
```
If `grounding` is available and `--web-backend` is not `none`, every subquery gets `grounding` appended as a safety net even when the planner omits it.
## Phase 1: parallel source fan-out
For each `(subquery, source)` in the plan where `source` is in `available_sources(config)`:
1. Submit `_retrieve_stream` to a `ThreadPoolExecutor` (`max_workers` between 4 and 16, scaled to stream count).
2. Enforce **`MAX_SOURCE_FETCHES`**: X is capped at **2** submissions per run to limit 429 cascades.
3. On **429**, mark the source rate-limited (thread-safe set) so sibling futures skip it.
4. On **5xx**, sleep 3s and retry the stream once.
5. Run **`_normalize_score_dedupe`** on raw dicts, then truncate to `per_stream_limit`.
### GitHub pre-pass
Before the main loop, optional **`--github-repo`** (project mode) or **`--github-user`** (person mode) runs a dedicated GitHub search and injects results under the primary subquery label, skipping redundant keyword GitHub streams later.
### `available_sources`
Sources are included only when credentials or binaries exist (for example `reddit` always, `x` when Bird/xAI/xurl is configured, `grounding` when Brave/Exa/Serper/Parallel keys exist). `EXCLUDE_SOURCES` and `INCLUDE_SOURCES` further filter the set. `pipeline.diagnose()` reports the resolved list for `--diagnose`.
### Per-stream normalization (`_normalize_score_dedupe`)
| Step | Module | Role |
| --- | --- | --- |
| Normalize | `normalize.normalize_source_items` | Dates, URLs, engagement fields |
| Annotate | `signals.annotate_stream` | `local_relevance`, freshness, engagement, `local_rank_score` |
| Prune | `signals.prune_low_relevance` | Drops items below relevance floor (default 0.15) |
| Dedupe | `dedupe.dedupe_items` | Hybrid n-gram + token Jaccard (threshold 0.7) |
| Snippet | `snippet.extract_best_snippet` | Best excerpt for rerank prompts |
Reddit and YouTube often use **`raw_topic`** (original user string) inside `_retrieve_stream` so query expansion helpers see the full topic, not only the planner’s narrowed `search_query`.
## Phase 2: supplemental and thin-source retry
**Supplemental** (default/deep only): `entity_extract` pulls X handles and subreddits from Phase 1 Reddit/X payloads (plus `--x-handle` / `--x-related`). With Bird as the X backend, `bird_x.search_handles` adds targeted posts; related handles use label `supplemental-related` at weight **0.3**.
**Thin retry**: Sources with fewer than three items (and no error) get a second fetch using `query.extract_core_subject(topic, max_words=3)` as `search_query`, weight **0.3**, merged without duplicate URLs. GitHub is skipped when project/person mode already ran.
## Fusion (`weighted_rrf`)
`fusion.weighted_rrf` merges all `(label, source)` streams:
- **Score**: `subquery.weight × plan.source_weights[source] / (RRF_K + rank)` with `RRF_K = 60`
- **Dedup key**: normalized URL, or `source:item_id`
- **Merge**: accumulates `rrf_score`, max relevance/freshness/engagement, provenance metadata
- **Per-author cap**: at most **3** items per author/handle in the fused list
- **Diversity pool**: reserves up to **2** slots per source whose best item has `local_relevance ≥ 0.25`, then fills to `pool_limit`
## Rerank and fun scoring
`rerank.rerank_candidates` scores the fused shortlist (size `rerank_limit`):
- **LLM path**: JSON relevance 0–100 per `candidate_id`, with intent-specific hints and entity-grounding rules for the primary entity extracted from the topic
- **Fallback**: weighted mix of `local_relevance`, freshness, and `source_quality`; **−25** `ENTITY_MISS_PENALTY` when the primary entity is absent from title/snippet/transcript/comments
- **Tail**: candidates beyond the shortlist get fallback scores only
`rerank.score_fun` adds optional `fun_score` / `fun_explanation` (humor/virality judge) on up to 60 candidates.
Post-rerank, **`github.enrich_candidates_with_stars`** fills star counts for GitHub survivors when not in mock mode.
## Clustering (`cluster_candidates`)
| `plan.intent` / `cluster_mode` | Behavior |
| --- | --- |
| Intent not in `CLUSTERABLE_INTENTS` or `cluster_mode == "none"` | One cluster per candidate (no merging) |
| `breaking_news`, `opinion`, `comparison`, `prediction` + story/workflow/market/debate | Greedy text-similarity groups (threshold 0.42 news / 0.48 otherwise), then **entity overlap merge** across sources for small clusters |
| Representatives | MMR (`diversity_lambda=0.75`), up to 3 per cluster |
Uncertainty flags: `single-source` or `thin-evidence` when group scores or source diversity are weak.
## Final report and warnings
`pipeline.run` returns `schema.Report` with:
- `query_plan`, `ranked_candidates`, `clusters`, `items_by_source`
- `errors_by_source` (cleared when that source still returned items)
- `warnings` (thin evidence, single-source concentration, partial source failures)
- `artifacts` (grounding bundles, `plan_source` for degraded-run banners)
`_finalize_items_by_source` re-sorts, re-dedupes per source, applies Polymarket topic/keyword filters, and runs Digg X-post enrichment on survivors only.
## Invocation examples
<RequestExample>
```bash
# Default depth — full planner expansion, supplemental + thin retry
python3 skills/last30days/scripts/last30days.py "Hermes agent workflows" --emit=compact
```
</RequestExample>
<RequestExample>
```bash
# Quick profile — latency-biased
python3 skills/last30days/scripts/last30days.py "OpenClaw plugins" --quick --search=reddit,hackernews --emit=json
```
</RequestExample>
<RequestExample>
```bash
# Hosting agent supplies plan (headless-quality planning without engine API keys)
python3 skills/last30days/scripts/last30days.py "Topic" --plan /path/to/plan.json --emit=context
```
</RequestExample>
<RequestExample>
```bash
# Mock pipeline (no live credentials) — used in tests
python3 skills/last30days/scripts/last30days.py "test" --mock --quick --emit=compact
```
</RequestExample>
## Operational signals
| Signal | Where | Meaning |
| --- | --- | --- |
| `[Planner] Plan: ... source=deterministic` | stderr | No LLM plan; consider passing `--plan` from the hosting agent |
| `DEGRADED RUN` banner | compact output | `plan_source=deterministic` on named-entity topics without pre-research flags |
| `Some sources failed: ...` | `Report.warnings` | Partial retrieval; check keys via `--diagnose` |
| `Evidence is thin` | `Report.warnings` | Fewer than five ranked candidates |
<Note>
Slash-command invocations (`/last30days topic`) do not pass shell flags. The hosting model maps user intent (quick vs deep, source subset, comparison) into engine flags or a `--plan` file before calling `last30days.py`.
</Note>
## Key modules
| File | Responsibility |
| --- | --- |
| `lib/pipeline.py` | Orchestration, depth settings, phases, `run()` |
| `lib/planner.py` | LLM/deterministic `QueryPlan` |
| `lib/schema.py` | `SubQuery`, `QueryPlan`, `SourceItem`, `Candidate`, `Cluster`, `Report` |
| `lib/fusion.py` | Weighted RRF and pool diversification |
| `lib/dedupe.py` | Within-stream near-duplicate removal |
| `lib/rerank.py` | LLM relevance + fun judge + entity demotion |
| `lib/cluster.py` | Story grouping and cross-source entity merge |
| `lib/providers.py` | Reasoning provider and model pins |
Per-platform retrieval limits and auth live in each source module; see the data-sources page for adapter-specific `DEPTH_CONFIG` tables.
## Related pages
<CardGroup>
<Card title="Skill, engine, and harness" href="/skill-engine-harness">
Boundaries between SKILL.md, the Python engine, and harness runtimes that invoke `/last30days`.
</Card>
<Card title="Query types" href="/query-types">
Intent classification, pre-flight refusal, and how intent shapes planner defaults.
</Card>
<Card title="Data sources" href="/data-sources">
Per-platform retrieval modules, keys, and source-level depth configs.
</Card>
<Card title="Output contract" href="/output-contract">
How `Report` fields map to compact, JSON, and synthesis output.
</Card>
<Card title="CLI reference" href="/cli-reference">
All flags including `--plan`, `--search`, `--quick`, and `--deep`.
</Card>
<Card title="Comparison mode" href="/comparison-mode">
Multi-entity fan-out that reuses `pipeline.run` per competitor.
</Card>
</CardGroup>
---
## 07. Output contract
> Badge line, LAWs voice contract, --emit modes (compact, json, context, md, html), evidence blocks, and footer pass-through rules.
- Page Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/07-output-contract.md
- Generated: 2026-06-04T23:20:12.838Z
### Source Files
- `skills/last30days/SKILL.md`
- `skills/last30days/scripts/lib/render.py`
- `skills/last30days/scripts/lib/html_render.py`
- `skills/last30days/scripts/last30days.py`
- `skills/last30days/scripts/lib/skill_meta.py`
---
title: "Output contract"
description: "Badge line, LAWs voice contract, --emit modes (compact, json, context, md, html), evidence blocks, and footer pass-through rules."
---
The last30days output contract spans three layers: Python rendering in `skills/last30days/scripts/lib/render.py` and `html_render.py`, the `--emit` router in `last30days.py`, and the hosting model’s synthesis rules in `SKILL.md` (badge, eight LAWs, evidence envelope, footer pass-through, invitation). Slash-command runs should use `--emit=compact` on stdout; saved artifacts and wire formats diverge by emit mode.
## Contract layers
```text
┌─────────────────────────────────────────────────────────────────┐
│ SKILL.md OUTPUT CONTRACT (LAWs 1–8, badge, synthesis shape) │
│ → governs what the hosting model emits to the user │
└────────────────────────────┬────────────────────────────────────┘
│ reads stdout / saves files
┌────────────────────────────▼────────────────────────────────────┐
│ last30days.py: emit_output() / emit_comparison_output() │
│ --emit compact|md|json|context|html │
└────────────────────────────┬────────────────────────────────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
render_compact render_context html_render.render_html
(model scratchpad) (abbreviated) (shareable artifact)
```
| Layer | Owner | Primary consumer |
| --- | --- | --- |
| Badge + evidence + footer envelopes | `render.py` | Hosting model (compact/md stdout) |
| LAWs + invitation | `SKILL.md` | Hosting model (final chat response) |
| `--emit` routing | `last30days.py` | CLI, cron, skill Bash step |
| Saved `*-raw.md` | `render_full()` via `save_output()` | Durable debug / WebSearch appendix target |
## Badge line
The engine emits the mandatory first line in every compact, md, and HTML-prep path via `_render_badge()`:
```text
🌐 last30days v{VERSION} · synced {YYYY-MM-DD}
```
<ParamField body="VERSION" type="string">
Resolved from the nearest ancestor `.claude-plugin/plugin.json` `version` field; if missing (typical on per-harness skill installs), falls back to `SKILL.md` frontmatter via `skill_meta.read_skill_version()`. Returns `?` only when neither source yields a version.
</ParamField>
<ParamField body="synced date" type="string">
`date.today()` at render time (`YYYY-MM-DD`), not the research date range.
</ParamField>
<Note>
`SKILL.md` requires the hosting model to pass through the engine badge verbatim on line 1. Manual badge emission is a fallback when synthesizing without reusing engine stdout.
</Note>
**Placement by query type** (from `SKILL.md`):
| Query types | Line 1 | Line 3 (after blank line 2) |
| --- | --- | --- |
| GENERAL, NEWS, PROMPTING, RECOMMENDATIONS | Badge | `What I learned:` (prose label, not a title) |
| COMPARISON (`vs` / `versus`) | Badge | `# {A} vs {B} [vs {C}]: What the Community Says (/Last30Days)` |
## Voice contract (LAWs)
The eight LAWs in `SKILL.md` § OUTPUT CONTRACT override global formatting preferences inside `/last30days`. They apply at synthesis time; the engine enforces structure via HTML comments and warning blocks.
| LAW | Rule | Engine support |
| --- | --- | --- |
| **1** | No trailing `Sources:` / `References:` / link dumps; visible citation is the `🌐 Web:` footer line | Canonical boundary text; saved raw file holds WebSearch appendix |
| **2** | No invented title; body starts with `What I learned:` (COMPARISON uses required `# … vs …` title) | Badge as structural anchor |
| **3** | No em-dashes or en-dashes; use ` - ` | — |
| **4** | No `##` body headers except COMPARISON template sections and Python-pass-through blocks | Comparison scaffold emits allowed `## Head-to-Head` |
| **5** | Pass through emoji-tree footer verbatim | `<!-- PASS-THROUGH FOOTER -->` envelope |
| **6** | Do not emit raw ranked evidence clusters in user output | `<!-- EVIDENCE FOR SYNTHESIS -->` envelope |
| **7** | Reasoning-model hosts must pass `--plan` on named-entity topics | `## DEGRADED RUN WARNING` when `plan_source=deterministic` and flags missing |
| **8** | Inline `[name](url)` citations in narrative; footer links untouched | Evidence items carry URLs in compact output |
<LAW 6 failure signal>: User output containing `### 1.` followed by `(score N, M items, sources: …)` or `- Uncertainty: single-source` means evidence was dumped instead of synthesized.
## `--emit` modes
Default: `compact`. Choices are registered in `last30days.py` and routed through `emit_output()` / `emit_comparison_output()`.
| Mode | Stdout renderer | Typical use | Saved file when `--save-dir` set |
| --- | --- | --- | --- |
| `compact` | `render_compact()` | Primary skill path; model reads evidence + footer | `render_full()` → `{slug}-raw{suffix}.md` |
| `md` | `render_compact()` (identical to compact) | Debug/inspection only; **disallowed** as primary user-facing flow per `SKILL.md` | Same as compact: `render_full()` |
| `json` | `json.dumps(schema.to_dict(report))` | Scripting, watchlist, verify | `{slug}-raw{suffix}.json` (wire JSON) |
| `context` | `render_context()` or `render_comparison_multi_context()` | Token-efficient agent context | `render_full()` if saved as `.md` |
| `html` | `html_render.render_html()` | Shareable offline brief | `{slug}-raw-html{suffix}.html`; accepts `--synthesis-file` |
<Warning>
`--emit=md` and `--emit=compact` produce the same stdout (including evidence and footer envelopes). Do not treat `md` as a different user-facing format. The distinction matters for saved files: non-JSON/HTML saves always use `render_full()`, not compact stdout.
</Warning>
### `compact` / `md` stdout structure
`render_compact()` emits blocks in this order:
1. Badge + blank line
2. Debug header `# last30days vX: {topic}`
3. Model-facing safety blockquote (untrusted internet content)
4. Date range and active source list
5. Optional `## Freshness`, `## Warnings`
6. Optional `## DEGRADED RUN WARNING` (before evidence; LAW 7 backstop)
7. **Evidence envelope** (see below)
8. Optional `## Pre-Research Status` (after evidence closes)
9. Optional `## Head-to-Head` table scaffold (comparison topics)
10. **Pass-through footer** envelope
11. **Canonical boundary** (`# END OF last30days CANONICAL OUTPUT`)
`fun_level` (`low` / `medium` / `high`, from `FUN_LEVEL` env) adjusts Best Takes threshold and count inside the evidence envelope. Default cluster cap is 8 (4 per entity in comparison multi-render).
### `json`
Single-entity: full `Report` as dict via `schema.to_dict()`. Comparison: wrapper `{"comparison": true, "entities": [...], "reports": [{"entity", "report"}, ...]}`.
### `context`
Abbreviated cluster titles, representative items, safety note, optional freshness warning. No badge, evidence envelope, or emoji footer. Comparison context adds entity sections and resolved-entity summary when artifacts exist.
### `html`
Pipeline: `render_for_html()` → strip evidence / invitation / canonical boundary → promote prose labels → markdown-to-HTML → wrap engine footer → embed in styled template.
<Info>
HTML artifacts omit the evidence scratchpad, debug header, safety note, and invitation. Data-quality warnings go to **stderr** via `collect_html_warnings()`, not into the shareable file. Pass `--synthesis-file` with model-authored markdown (post-chat synthesis) for a populated brief body.
</Info>
## Evidence blocks
Raw research for the hosting model lives inside HTML comment boundaries:
```text
<!-- EVIDENCE FOR SYNTHESIS: read this, do not emit verbatim. ... -->
## Ranked Evidence Clusters
### N. {title} (score X, M items, sources: ...)
...
## Stats
## Best Takes (fun_level-gated)
## Source Coverage
<!-- END EVIDENCE FOR SYNTHESIS -->
```
<Check>
LAW 6 scope: synthesize from this block into `What I learned:` prose (or COMPARISON template sections). Do **not** pass the envelope through to the user. Only the PASS-THROUGH FOOTER is verbatim in the final response (plus Python-emitted warning/scaffold blocks when present).
</Check>
Comparison multi-entity runs use one envelope with per-entity `## {label}` subsections, `#### Ranked Evidence Clusters`, and a shared `## Head-to-Head` scaffold after the envelope closes.
## Footer pass-through
The emoji-tree footer is wrapped for model copy-paste:
```text
<!-- PASS-THROUGH FOOTER: emit verbatim in the model response per LAW 5. -->
---
✅ All agents reported back!
├─ 🟠 Reddit: … │ …
├─ 🔵 X: … │ …
└─ 📎 Raw results saved to ~/…/slug-raw.md (when --save-dir set)
---
<!-- END PASS-THROUGH FOOTER -->
```
Per-source lines are built from `_FOOTER_SOURCES` (Reddit, X, YouTube, TikTok, Instagram, Threads, Pinterest, HN, Bluesky, Truth Social, GitHub, Digg), plus Polymarket odds summaries and `🌐 Web:` publication names. Optional `🗣️ Top voices:` aggregates handles and subreddits.
<ParamField body="save_path" type="string | null">
Passed into `render_compact()` when `--save-dir` is set; `compute_save_path_display()` shows a `~/{relative}` path in the `📎 Raw results saved to` line.
</ParamField>
The canonical boundary after the footer explicitly scopes verbatim pass-through to the footer only and repeats LAW 1 / LAW 6 self-check strings.
## Final user-facing response shape
The engine does **not** emit the invitation block. The hosting model composes the chat response on top of compact stdout:
```text
[Badge — pass through line 1 from engine]
What I learned: ← model synthesis (LAW 2/4/8)
**Lead-in** - paragraph …
KEY PATTERNS from the research: ← numbered list
1. …
[PASS-THROUGH FOOTER — verbatim] ← LAW 5
---
I'm now an expert on {TOPIC}… ← model invitation (SKILL.md templates)
```
| Block | Source | Pass through? |
| --- | --- | --- |
| Badge | Engine | Yes (line 1) |
| Evidence envelope | Engine | No (read only) |
| Head-to-Head scaffold | Engine (comparison) | Yes, fill cells |
| Emoji-tree footer | Engine | Yes (verbatim) |
| Invitation | Model | N/A (not in engine stdout) |
| Trailing `Sources:` | Model must omit | LAW 1 |
## Comparison and JSON/HTML notes
- **Comparison stdout**: `render_comparison_multi()` shares badge, one evidence envelope, Head-to-Head scaffold, single footer from the main report, and the same canonical boundary.
- **HTML footer**: `_wrap_engine_footer()` preserves the tree through markdown conversion; other HTML comments are stripped except `<!-- META: … -->` (date range and sources).
- **Zero-source runs**: If `_render_emoji_footer()` returns empty, `SKILL.md` allows skipping the footer and proceeding from KEY PATTERNS to the invitation.
## Verification signals
| Check | Command / test |
| --- | --- |
| Envelope order | `tests/test_render_v3.py` (`OutputEnvelopeTests`) |
| compact ≡ md stdout | `test_envelopes_appear_in_md_emit_mode` |
| HTML strips evidence | `tests/test_html_render.py` |
| Emit choices | `last30days.py` `--emit` argparse `choices=[...]` |
<Steps>
<Step title="Run compact research">
<code>python3 skills/last30days/scripts/last30days.py "topic" --emit=compact --save-dir="${LAST30DAYS_MEMORY_DIR:-$HOME/Documents/Last30Days}"</code>
</Step>
<Step title="Confirm stdout markers">
Stdout should start with the badge, contain exactly one `<!-- EVIDENCE FOR SYNTHESIS` / `<!-- END EVIDENCE FOR SYNTHESIS -->` pair, and one `<!-- PASS-THROUGH FOOTER` / `<!-- END PASS-THROUGH FOOTER -->` pair when sources returned data.
</Step>
<Step title="Synthesize per LAWs">
Transform evidence into `What I learned:` prose; copy the footer verbatim; end at the invitation with no trailing `Sources:` block.
</Step>
</Steps>
## Related pages
<CardGroup>
<Card title="Skill contract" href="/skill-contract">
SKILL.md runtime authority, engine invocation, and STEP 0 guards that precede output emission.
</Card>
<Card title="CLI reference" href="/cli-reference">
Full `--emit` flag listing, `--synthesis-file`, and direct CLI vs slash-command constraints.
</Card>
<Card title="HTML briefs" href="/html-briefs">
Shareable `--emit=html` artifacts, synthesis embedding, and save paths.
</Card>
<Card title="Comparison mode" href="/comparison-mode">
Multi-entity evidence layout, Head-to-Head scaffold, and comparison synthesis template.
</Card>
<Card title="Query types" href="/query-types">
QUERY_TYPE-specific placement rules for badge, titles, and invitation variants.
</Card>
</CardGroup>
---
## 08. Configure sources
> Credential layers (.env paths, keychain), per-source API keys, INCLUDE_SOURCES, setup wizard auto-actions, and --diagnose availability checks.
- Page Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/08-configure-sources.md
- Generated: 2026-06-04T23:20:24.498Z
### Source Files
- `CONFIGURATION.md`
- `skills/last30days/scripts/lib/env.py`
- `skills/last30days/scripts/lib/setup_wizard.py`
- `skills/last30days/scripts/lib/providers.py`
- `skills/last30days/scripts/setup-keychain.sh`
- `skills/last30days/scripts/last30days.py`
---
title: "Configure sources"
description: "Credential layers (.env paths, keychain), per-source API keys, INCLUDE_SOURCES, setup wizard auto-actions, and --diagnose availability checks."
---
The last30days engine loads credentials through `lib/env.py::get_config()`, merges project and global `.env` files with optional macOS Keychain entries, and computes which retrieval backends are runnable via `lib/pipeline.py::available_sources()` and `diagnose()`. Slash-command hosts read the same keys when they invoke `scripts/last30days.py`; direct CLI users can verify wiring with `--diagnose` before spending a full research run.
## Credential resolution order
Configuration is assembled once per engine invocation. Effective value for each key follows this precedence (highest wins):
| Priority | Source | Path or mechanism |
| --- | --- | --- |
| 1 | Process environment | `export KEY=value` or harness-injected env |
| 2 | Project-scoped file | `.claude/last30days.env` (walks up from `cwd`) |
| 3 | Global file | `~/.config/last30days/.env` (override dir with `LAST30DAYS_CONFIG_DIR`) |
| 4 | macOS Keychain | Generic password service `last30days-<KEY>` (Darwin only) |
<Note>
`LAST30DAYS_CONFIG_DIR=""` disables global file loading entirely (clean/no-config mode). `LAST30DAYS_CONFIG_DIR=/path` points the global file at `/path/.env`.
</Note>
```text
os.environ ──────────────┐
▼
.claude/last30days.env ──┼──► merged config dict
│ │
~/.config/last30days/.env┘ │ _CONFIG_SOURCE label
▼
Keychain (last30days-*) ──► lowest file priority
```
POSIX hosts warn on stderr when a loaded secrets file is group- or world-readable; target mode `600`.
### macOS Keychain
`scripts/setup-keychain.sh` stores generic passwords for the keys listed in `lib/env.py::KEYCHAIN_KEYS` (kept in sync via `tests/test_env_keychain.py`). Each item uses service name `last30days-<KEY>` for the current user.
<CodeGroup>
```bash title="Interactive store all keys"
./skills/last30days/scripts/setup-keychain.sh
```
```bash title="List stored items"
./skills/last30days/scripts/setup-keychain.sh --list
```
```bash title="Store one key"
security add-generic-password -a "$USER" -s last30days-XAI_API_KEY -w "xai-..."
```
</CodeGroup>
Keychain values never override `.env` or process env; they only fill gaps.
## Environment file locations
| File | When used |
| --- | --- |
| `.claude/last30days.env` | Present in `cwd` or any parent up to `$HOME` |
| `$LAST30DAYS_CONFIG_DIR/.env` | Custom global path |
| `~/.config/last30days/.env` | Default global fallback |
Per-client workflows typically place keys and `INCLUDE_SOURCES` in `.claude/last30days.env`, then `cd` into that project so the engine picks up the file without extra flags.
## Per-source credentials
### Always-on (no API key)
| Source | Requirement |
| --- | --- |
| Reddit | Public Algolia API |
| Hacker News | Free Algolia API |
| Polymarket | Free Gamma API |
### CLI-backed (no skill API key)
| Source | Requirement |
| --- | --- |
| GitHub | `gh` CLI installed and authenticated |
| YouTube | `yt-dlp` on `PATH`, or `SCRAPECREATORS_API_KEY` as SC fallback |
| Digg | `digg-pp-cli` on `PATH` |
### Keyed social and enrichment
| Source | Key(s) | Engine gate |
| --- | --- | --- |
| X / Twitter | `XAI_API_KEY`, or `AUTH_TOKEN` + `CT0`, or `FROM_BROWSER`, or `xurl` OAuth CLI | `env.get_x_source()` |
| TikTok | `SCRAPECREATORS_API_KEY` or `APIFY_API_TOKEN` | On when SC/Apify key set; opt out with `EXCLUDE_SOURCES` |
| Instagram | `SCRAPECREATORS_API_KEY` | Same as TikTok |
| Threads | `SCRAPECREATORS_API_KEY` | On with SC key; opt out with `EXCLUDE_SOURCES=threads` |
| Pinterest | `SCRAPECREATORS_API_KEY` | Only when `--search=pinterest` (or planner requests `pinterest`) |
| Bluesky | `BSKY_HANDLE` + `BSKY_APP_PASSWORD` | `env.is_bluesky_available()` |
| Truth Social | `TRUTHSOCIAL_TOKEN` or browser cookie via `FROM_BROWSER` | `env.is_truthsocial_available()` |
| Xiaohongshu | `XIAOHONGSHU_API_BASE` (HTTP API health + login probe) | `--search=xiaohongshu` when reachable |
| Xquik | `XQUIK_API_KEY` | `env.is_xquik_available()` |
### Web grounding and deep research
| Capability | Key(s) | Notes |
| --- | --- | --- |
| Native web search (Step 2 / `--auto-resolve`) | `BRAVE_API_KEY`, `EXA_API_KEY`, `SERPER_API_KEY`, or `PARALLEL_API_KEY` | Auto priority: Brave → Exa → Serper → Parallel; override with `--web-backend` |
| Perplexity Sonar | `OPENROUTER_API_KEY` + `INCLUDE_SOURCES=perplexity` or `--search=perplexity` | Also auto-added when `--deep-research` runs |
| Deep Research (~$0.90/query) | `OPENROUTER_API_KEY` | `--deep-research` flag |
### Reasoning (headless runs only)
When the host model is not doing plan/rerank, `lib/providers.py::resolve_runtime()` auto-picks: Gemini (`GOOGLE_API_KEY` / `GEMINI_API_KEY` / `GOOGLE_GENAI_API_KEY`) → OpenAI (`OPENAI_API_KEY`) → xAI → OpenRouter → local deterministic fallback. Pin with `LAST30DAYS_REASONING_PROVIDER`.
<Warning>
Do not commit real API keys, cookies, or `.env` contents. Tests use obvious dummy values only.
</Warning>
### Legacy and rotation notes
- `SCRAPE_CREATORS_API_KEY` (underscore spelling) is accepted as an alias when the canonical `SCRAPECREATORS_API_KEY` is unset.
- Comma-separated `SCRAPECREATORS_API_KEY` values rotate per run via `random.choice`.
- Browser cookie extraction (`FROM_BROWSER`): default tries Firefox and Safari silently; `FROM_BROWSER=auto` adds Chrome; `FROM_BROWSER=off` disables extraction.
## INCLUDE_SOURCES and EXCLUDE_SOURCES
Both are comma-separated, case-insensitive lists in `.env` or process env.
### EXCLUDE_SOURCES (runtime denylist)
`pipeline.available_sources()` removes any listed source after computing availability. This is the supported way to disable TikTok, Instagram, or Threads while keeping `SCRAPECREATORS_API_KEY` set.
```bash
EXCLUDE_SOURCES=tiktok,instagram,threads
```
### INCLUDE_SOURCES (opt-in extensions)
Empty `INCLUDE_SOURCES` means no allowlist filter in the pipeline. Non-empty values gate specific add-ons:
| Token | Effect |
| --- | --- |
| `perplexity` | Enables Perplexity Sonar when `OPENROUTER_API_KEY` is set |
| `youtube_comments` | Comment enrichment on YouTube items (requires SC key) |
| `tiktok_comments` | Comment enrichment on TikTok items (requires SC key) |
| `threads`, `pinterest` | Mentioned in quality nudges; Threads is already default-on with SC key; Pinterest still needs `--search=pinterest` |
`--deep-research` temporarily appends `perplexity` to `INCLUDE_SOURCES` for that run.
<Info>
Quality scoring (`lib/quality_nudge.py`) treats a non-empty `INCLUDE_SOURCES` as an intentional allowlist when detecting “silent” Instagram failures. That does not change which sources the pipeline fetches unless combined with `EXCLUDE_SOURCES` or `--search`.
</Info>
### Per-run source selection (`--search`)
`--search` (alias map: `hn`, `bsky`, `truth`, `web`, `xhs`, `xquik`) intersects the availability list. Explicit `--search=perplexity` or `--search=threads` can enable those sources without editing `INCLUDE_SOURCES` (see `tests/test_cli_v3.py`).
## Example `.env` skeleton
```bash
# Reasoning (headless / cron)
GOOGLE_API_KEY=<your-gemini-key>
# Web grounding
BRAVE_API_KEY=<your-brave-key>
# ScrapeCreators family (TikTok, Instagram, Threads default-on)
SCRAPECREATORS_API_KEY=<your-sc-key>
EXCLUDE_SOURCES=threads # optional denylist
# Opt-in enrichments
INCLUDE_SOURCES=youtube_comments,perplexity
# X (pick one path)
XAI_API_KEY=<your-xai-key>
# FROM_BROWSER=firefox
# Bluesky
BSKY_HANDLE=<handle>.bsky.social
BSKY_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx
BSKY_SEARCH_HOST=api.bsky.app
SETUP_COMPLETE=true
```
After editing: `chmod 600` on the secrets file.
## First-run setup wizard
Setup spans the SKILL.md Step 0 UX (host model reads `nux-wizard.md` when referenced) and engine-side automation in `lib/setup_wizard.py`.
### Detection
- **SKILL path:** missing `~/.config/last30days/.env`, or file without `SETUP_COMPLETE=true` → run wizard; otherwise skip silently.
- **Engine path:** `setup_wizard.is_first_run(config)` when `SETUP_COMPLETE` is unset.
### CLI auto-setup (`setup` subcommand)
From the skill scripts directory (or with full path):
```bash
python3 skills/last30days/scripts/last30days.py setup
```
<Steps>
<Step title="Auto actions">
`run_auto_setup()` probes browsers in `auto` mode for X and Truth Social cookies (`COOKIE_DOMAINS`), checks `yt-dlp`, and runs `brew install yt-dlp` when Homebrew exists.
</Step>
<Step title="Persist flags">
`write_setup_config()` appends `SETUP_COMPLETE=true` and `FROM_BROWSER=<browser>` to the global `.env` without overwriting existing keys.
</Step>
<Step title="Optional auth flows">
`setup --github` — PAT via `gh auth token`, else GitHub device flow through ScrapeCreators (returns JSON API key). `setup --device-auth` — device flow only. `setup --openclaw` — JSON probe of CLI + key presence (no cookies).
</Step>
</Steps>
Status text for auto-setup is returned by `get_setup_status_text()` on stderr.
## Verify with `--diagnose`
`--diagnose` loads config, calls `pipeline.diagnose()`, prints JSON, and exits without running research. No topic required.
```bash
python3 skills/last30days/scripts/last30days.py --diagnose
```
<ParamField body="--search" type="string">
Optional comma-separated list; narrows `available_sources` the same way as a real run (e.g. `--diagnose --search=threads,pinterest`).
</ParamField>
### Response shape
| Field | Meaning |
| --- | --- |
| `providers` | `google`, `openai`, `xai`, `openrouter` booleans |
| `local_mode` | `true` when no reasoning API keys resolve |
| `reasoning_provider` | `LAST30DAYS_REASONING_PROVIDER` or `auto` |
| `x_backend` | Active X backend: `bird`, `xai`, `xurl`, or `null` |
| `bird_installed` / `bird_authenticated` / `bird_username` | Bird CLI state |
| `native_web_backend` | `brave`, `exa`, `serper`, `parallel`, or `null` |
| `has_scrapecreators` | SC key detected |
| `has_github` | `GITHUB_TOKEN` or `gh` on PATH |
| `available_sources` | Sources that would run for this config (and `--search` filter) |
During normal runs, `ui.show_diagnostic_banner()` and post-run promos consume the same `diag` object when core sources are missing.
### Quick interpretation
| Symptom in `available_sources` | Typical fix |
| --- | --- |
| No `x` | Add `XAI_API_KEY`, Bird cookies, or install/authenticate `xurl` |
| No `youtube` | `brew install yt-dlp` or set SC key |
| No `grounding` | Add a web search API key or use a host with native WebSearch |
| No `tiktok` / `instagram` | Set `SCRAPECREATORS_API_KEY`; check `EXCLUDE_SOURCES` |
| No `perplexity` | `OPENROUTER_API_KEY` + `INCLUDE_SOURCES=perplexity` or `--search=perplexity` |
| No `bluesky` | `BSKY_HANDLE` + `BSKY_APP_PASSWORD` |
## Configuration vs skill contract
- **Engine truth:** `get_config()`, `available_sources()`, `diagnose()` in `scripts/lib/`.
- **Slash-command truth:** `SKILL.md` Step 0 (wizard), intent parsing, and `ACTIVE_SOURCES_LIST` messaging must match keys you actually set.
- **User reference:** `CONFIGURATION.md` mirrors knobs for operators; if it diverges from code, code wins.
New env vars or CLI flags that affect sources should update `CONFIGURATION.md`, `SKILL.md`, and this page together.
## Related pages
<CardGroup>
<Card title="Configuration reference" href="/configuration-reference">
Full env var catalog, web-backend priority, output paths, and trend-monitoring toggles.
</Card>
<Card title="Data sources" href="/data-sources">
Per-platform retrieval modules, scoring inputs, and `--search` alias map.
</Card>
<Card title="Quickstart" href="/quickstart">
First `/last30days` run, wizard signals, and initial `--diagnose` check.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Missing-source recovery, thin results, and documented failure modes.
</Card>
<Card title="Per-client setup" href="/per-client-setup">
Project-scoped `.claude/last30days.env` and save-dir isolation patterns.
</Card>
</CardGroup>
---
## 09. Model clients
> Per-harness install patterns: Claude Code marketplace, npx skills -a targets, Codex/Cursor/Gemini, Hermes, OpenClaw, and stale-install avoidance.
- Page Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/09-model-clients.md
- Generated: 2026-06-04T23:20:54.008Z
### Source Files
- `README.md`
- `CONFIGURATION.md`
- `HERMES_SETUP.md`
- `.claude-plugin/marketplace.json`
- `.agents/plugins/marketplace.json`
- `gemini-extension.json`
- `skills/last30days/agents/openai.yaml`
---
title: "Model clients"
description: "Per-harness install patterns: Claude Code marketplace, npx skills -a targets, Codex/Cursor/Gemini, Hermes, OpenClaw, and stale-install avoidance."
---
The last30days Agent Skills package (`skills/last30days/`) installs into harness-specific skill directories; each host loads `SKILL.md` and invokes `scripts/last30days.py` from that install via `SKILL_DIR` (the directory containing the `SKILL.md` the model read). Distribution is provider-neutral: one skill tree, multiple install surfaces, no requirement for a single model vendor.
## Harness vs engine
| Layer | Role | Canonical artifact |
| --- | --- | --- |
| **Harness** | Agent runtime that exposes the slash command or skill invocation | Claude Code, Codex, Cursor, Gemini CLI, Hermes, OpenClaw, 50+ Agent Skills hosts |
| **Skill** | Prose contract + flags the model must pass | `skills/last30days/SKILL.md` |
| **Engine** | Python research pipeline | `skills/last30days/scripts/last30days.py` |
```mermaid
flowchart LR
subgraph hosts [Harnesses]
CC[Claude Code marketplace]
AS[npx skills add -a targets]
HM[Hermes skills install]
OC[OpenClaw clawhub]
end
subgraph install [Install tree]
SM[SKILL.md]
PY[scripts/last30days.py]
end
hosts --> SM
SM --> PY
```
When a reasoning harness runs `/last30days`, the host model is the planner and synthesizer; external reasoning API keys are only required for headless runs (cron, watchlist, direct CLI without a host). See the configuration reference for provider priority.
## Install matrix
Current release version is **3.3.1** (aligned across `skills/last30days/SKILL.md`, `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, and `gemini-extension.json`).
| Harness / surface | Install command | Update | Typical on-disk path |
| --- | --- | --- | --- |
| **Claude Code** (recommended) | `/plugin marketplace add mvanhorn/last30days-skill` then `/plugin install last30days@last30days-skill` | Auto via marketplace; `claude plugin update last30days@last30days-skill` | `~/.claude/plugins/cache/last30days-skill/last30days/{version}/skills/last30days/` |
| **Agent Skills hosts** (Codex, Cursor, Copilot, Gemini CLI, Windsurf, Cline, Continue, Roo, OpenCode, Goose, …) | `npx skills add mvanhorn/last30days-skill -g` | `npx skills update last30days -g` | `~/.codex/skills/last30days`, `~/.agents/skills/last30days`, or host-specific dir |
| **claude.ai** (web) | Download `last30days.skill` from [latest release](https://github.com/mvanhorn/last30days-skill/releases/latest/download/last30days.skill); upload in Settings → Capabilities → Skills | Re-download and re-upload | Hosted skill bundle (not a local tree) |
| **Hermes** | `hermes skills install mvanhorn/last30days-skill --force` | Same command | `~/.hermes/skills/research/last30days/` |
| **OpenClaw** | `clawhub install last30days-official` | `clawhub update last30days-official` | `~/.openclaw/skills/last30days/` (per OpenClaw layout) |
| **Developer live-edit** | `ln -sfn "$PWD/skills/last30days" ~/.agents/skills/last30days` (from repo root) | `git pull` in the repo | Symlink to working tree |
<Note>
Claude Code does **not** dedupe across install methods. If both the marketplace plugin and an `npx skills` copy are active, `/last30days` can appear twice in the command picker. Pick one install method per machine.
</Note>
## Claude Code marketplace
Claude Code is the most common harness. The marketplace manifest lives at `.claude-plugin/marketplace.json` (plugin name `last30days`, source `./`). Plugin metadata is in `.claude-plugin/plugin.json`; Claude Code auto-discovers `skills/*/SKILL.md` without a `"skills": ["./"]` entry (that pattern was removed after path-escape errors on newer Claude Code builds).
<Steps>
<Step title="Add marketplace and install plugin">
```text
/plugin marketplace add mvanhorn/last30days-skill
/plugin install last30days@last30days-skill
```
</Step>
<Step title="Force an update when needed">
```text
claude plugin update last30days@last30days-skill
```
Or in-session: `/plugin update last30days` then `/reload-plugins`.
</Step>
<Step title="Verify cached version">
```bash
cat ~/.claude/plugins/cache/last30days-skill/last30days/*/.claude-plugin/plugin.json | grep version
```
Expect `"version": "3.3.1"` (or your target release).
</Step>
</Steps>
Optional Agent Skills path on Claude Code (coexists with marketplace — avoid unless you intend dual installs):
```bash
npx skills add mvanhorn/last30days-skill -g -a claude-code
```
Invocation: `/last30days <topic>` (user-invocable skill; `argument-hint` documents example topics).
## Agent Skills CLI (`npx skills`)
Canonical install for Codex, Cursor, GitHub Copilot, Gemini CLI, and [50+ hosts](https://agentskills.io) via [vercel-labs/skills](https://github.com/vercel-labs/skills).
<CodeGroup>
```bash title="Global (recommended)"
npx skills add mvanhorn/last30days-skill -g
```
```bash title="Project-local"
npx skills add mvanhorn/last30days-skill
```
```bash title="Target specific harnesses"
npx skills add mvanhorn/last30days-skill -g -a codex
npx skills add mvanhorn/last30days-skill -g -a cursor
npx skills add mvanhorn/last30days-skill -g -a gemini-cli
npx skills add mvanhorn/last30days-skill -g -a codex -a cursor
```
</CodeGroup>
| Flag | Effect |
| --- | --- |
| `-g` | User-global install (available across projects) |
| `-a <harness>` | Target one or more harness adapters (`codex`, `cursor`, `gemini-cli`, `claude-code`, `github-copilot`, `windsurf`, `cline`, `continue`, `roo`, `aider-desk`, `opencode`, `goose`, …) |
| (no `-a`) | Installs for whichever harness `npx skills` detects |
Maintenance:
```bash
npx skills update last30days -g # this skill only
npx skills update -g # all global skills
npx skills list -g
npx skills remove last30days -g
```
<Warning>
`npx skills add` copies into `~/.agents/skills/last30days/` and symlinks per-host dirs where supported. That copy is **frozen at install time** — repo edits do not propagate until you re-run `npx skills add mvanhorn/last30days-skill -g` (or use a dev symlink). Contributors syncing a working tree use `npx skills add . -g -y` from the repo root.
</Warning>
### Codex
The `.codex-plugin/` scaffold was removed (v3.3.0). Codex users install via `npx skills add` or manual copy to `~/.codex/skills/last30days/`. Agent metadata for Codex-style hosts is in `skills/last30days/agents/openai.yaml` (`display_name`, `default_prompt`, `allow_implicit_invocation`).
Historical note: early Codex docs referenced `$last30days`; current installs follow the host’s skill/slash-command naming.
### Cursor, Copilot, Windsurf, and other `-a` targets
Same `npx skills add` flow with `-a <harness>`. Invocation is the host’s skill mechanism (slash command or skill picker per client). Do not pass shell pipes or flags through the slash form — express intent in natural language and let the model map to engine flags, or call the engine directly for scripting.
### Gemini CLI
README no longer documents a separate native-extension install path; use `npx skills add … -a gemini-cli`. The repo still ships `gemini-extension.json` (version **3.3.1**) listing optional env vars (`SCRAPECREATORS_API_KEY`, `BRAVE_API_KEY`, `XAI_API_KEY`, cookie vars, etc.) for extension-style configuration if your Gemini setup consumes that manifest.
## claude.ai (web)
Web Claude does not use the marketplace cache. Package a uploadable bundle from source:
```bash
bash skills/last30days/scripts/build-skill.sh # from repo root; requires clean git tree
```
Produces `dist/last30days.skill` (zip with top-level `last30days/`, ≤200 files). Upload via [claude.ai Settings → Capabilities → Skills](https://claude.ai/settings/capabilities). Enable **Code execution and file creation** or skills will not run.
## Hermes
Hermes uses its own installer (not `npx skills`):
```bash
hermes skills install mvanhorn/last30days-skill --force
```
Deploys to `~/.hermes/skills/research/last30days/`. Invoke as:
```text
last30days "your research topic"
last30days "best mechanical keyboards 2025" --search=reddit,youtube
```
Prerequisites: Python 3.12+, optional `yt-dlp`. Diagnose from the install dir:
```bash
cd ~/.hermes/skills/research/last30days
python3.12 scripts/last30days.py --diagnose
```
Developer live-edit: symlink `skills/last30days` into `~/.hermes/skills/research/last30days` (see `HERMES_SETUP.md`).
## OpenClaw
OpenClaw distributes via ClawHub, not the public GitHub marketplace:
```bash
clawhub install last30days-official
clawhub update last30days-official
```
`SKILL.md` frontmatter includes `metadata.openclaw` (required bins `node`, `python3`, optional env keys, `clawhub` tag). SKILL contract documents a failure mode where models improvised `for dir in …` path discovery and hit a stale `~/.openclaw/skills/last30days/` engine — use `SKILL_DIR` from the loaded `SKILL.md`, not ad hoc directory walks.
## SKILL_DIR binding (all harnesses)
Every engine invocation must use the install that loaded `SKILL.md`:
```bash
SKILL_DIR="<absolute path of the directory containing the SKILL.md you Read>"
"${LAST30DAYS_PYTHON}" "${SKILL_DIR}/scripts/last30days.py" "<topic>" --emit=compact ...
```
Valid examples from the skill contract:
- `~/.claude/skills/last30days`
- `~/.codex/skills/last30days`
- `~/.claude/plugins/cache/last30days-skill/last30days/3.3.1/skills/last30days`
- `~/.agents/skills/last30days` (npx global copy)
- Repo checkout `…/skills/last30days`
If `scripts/last30days.py` is missing under `SKILL_DIR`, the install is broken or the model substituted the wrong path.
Badge version resolution: per-harness installs may lack `.claude-plugin/plugin.json`; the engine falls back to `SKILL.md` frontmatter `version` (see `scripts/lib/render.py`).
## Stale-install avoidance
### Claude Code marketplace clone (STEP 0)
`~/.claude/plugins/marketplaces/last30days-skill/` is a git clone Claude Code can restore to `origin/main` on session start. It may lag the **versioned cache** under `~/.claude/plugins/cache/last30days-skill/last30days/{version}/`.
`SKILL.md` **STEP 0** instructs the model: if the loaded path contains `/.claude/plugins/marketplaces/` and a newer cache `SKILL.md` exists, stop and re-read from the cache before proceeding. Documented impact: stale `--help` missed flags such as `--competitors` and broke comparison workflows.
Cache layout may be nested (`{version}/skills/last30days/SKILL.md`) or flat (`{version}/SKILL.md`); STEP 0 resolves whichever exists.
<Check>
Other paths (`~/.codex/skills/`, `~/.agents/skills/`, npx install dirs, repo checkouts) are valid load points — STEP 0 does not force a cache hop on those paths.
</Check>
### Frozen `npx skills` copy
Re-run `npx skills add` or `npx skills update` after upgrading the GitHub release. For local development, symlink: `ln -sfn "$PWD/skills/last30days" ~/.agents/skills/last30days`.
### Dual installs
Marketplace + `npx skills` on the same Claude Code machine → duplicate `/last30days` entries (fixed in v3.3.1 by removing redundant `commands/last30days.md` wrapper; dual skill trees can still coexist).
### OpenClaw and path-discovery loops
Do not implement custom install discovery. Follow `SKILL_DIR` from the `Read` of `SKILL.md`. Update via `clawhub update last30days-official` when behavior drifts.
### Verification smoke test
After updating Claude Code cache:
```text
/last30days birthday gift for 40 year old
```
A healthy install asks a clarifying question before running the engine. If it runs immediately with thin or improvised output, repeat plugin update and cache verification.
## Manifest and repo anchors
| File | Purpose |
| --- | --- |
| `.claude-plugin/marketplace.json` | Claude Code marketplace listing (`plugins[0].version`) |
| `.claude-plugin/plugin.json` | Plugin identity for cache installs |
| `.agents/plugins/marketplace.json` | Agents-style marketplace metadata (local `source`) |
| `gemini-extension.json` | Optional Gemini extension env schema |
| `skills/last30days/agents/openai.yaml` | Codex/OpenAI-agent interface display metadata |
| `skills/last30days/SKILL.md` | Runtime authority (STEP 0, `SKILL_DIR`, invocation contract) |
`tests/test_plugin_contract.py` enforces version alignment across manifests and asserts `.codex-plugin/` stays removed.
## Beta and experimental installs
Experimental work ships from `mvanhorn/last30days-skill-private` as `/last30days-beta` (parallel slash command). Do not mix beta-only behavior into the public marketplace without a review PR to the public repo.
## Related pages
<CardGroup>
<Card title="Installation" href="/installation">
Marketplace add, `npx skills` global vs project scope, Hermes, symlinks, and version sync expectations.
</Card>
<Card title="Skill contract" href="/skill-contract">
STEP 0 stale-clone guard, pre-flight protocol, engine invocation rules, and `SKILL_DIR` substitution.
</Card>
<Card title="Skill, engine, and harness" href="/skill-engine-harness">
Boundaries between the Agent Skills package, Python engine, and multi-harness runtimes.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
`--diagnose`, missing sources, stale marketplace clones, and thin results.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
Env vars, reasoning provider priority when running headless, and web-backend order.
</Card>
<Card title="Beta channel" href="/beta-channel">
Parallel `/last30days-beta` install and promotion workflow.
</Card>
</CardGroup>
---
## 10. Comparison mode
> --competitors discovery fan-out, --competitors-list and --competitors-plan JSON schema, per-entity Step 0.55 targeting, and comparison synthesis template.
- Page Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/10-comparison-mode.md
- Generated: 2026-06-04T23:21:24.129Z
### Source Files
- `skills/last30days/SKILL.md`
- `skills/last30days/scripts/last30days.py`
- `skills/last30days/scripts/lib/competitors.py`
- `skills/last30days/scripts/lib/fanout.py`
- `tests/test_cli_competitors.py`
- `tests/test_competitor_fanout.py`
---
title: "Comparison mode"
description: "--competitors discovery fan-out, --competitors-list and --competitors-plan JSON schema, per-entity Step 0.55 targeting, and comparison synthesis template."
---
Comparison mode runs one full `pipeline.run()` per entity in parallel, merges evidence for synthesis, and emits a Head-to-Head scaffold. Entry paths are a `vs`/`versus` topic string (automatic vs-mode), `--competitors` with optional engine-side peer discovery, or `--competitors-list` with explicit peer names. Per-entity Step 0.55 targeting is supplied through `--competitors-plan` (preferred) or engine `auto_resolve` when a web backend is configured.
## When comparison mode activates
| Trigger | Behavior |
| --- | --- |
| Topic contains ` vs `, ` versus `, ` vs.`, or `/` between entities | `planner._comparison_entities()` splits the topic; first entity is main, rest become peers; stderr logs `[Competitors] vs-mode: routing to N-pass fanout` |
| `--competitors` or `--competitors-list` | Enables fanout even without a vs-string; peers from discovery or the list |
| Neither of the above | Single-entity pipeline only |
Vs-mode detection uses a richer splitter than render (supports `difference between X and Y`, `compared to`, slash-separated lists, trailing context stripping, dedup). Entity count from the topic is capped at four subqueries for comparison intent. Render’s `_parse_comparison_entities()` caps table columns at four for readability.
`--competitors-plan` does not enable fanout by itself. Pass it together with a vs-topic or `--competitors` / `--competitors-list` so `parse_competitors_plan()` can thread targeting into peer sub-runs.
## End-to-end flow
```mermaid
sequenceDiagram
participant Harness as Hosting model / CLI
participant CLI as last30days.py
participant Disc as competitors.discover_competitors
participant Fan as fanout.run_competitor_fanout
participant Pipe as pipeline.run
participant Rend as render.render_comparison_multi
Harness->>CLI: topic + flags (--competitors-plan, outer --x-handle, ...)
CLI->>CLI: resolve_competitors_args / vs_entities
alt explicit --competitors-list
CLI->>CLI: peers = list
else --competitors N and web backend
CLI->>Disc: 3 parallel SERP queries
Disc-->>CLI: ranked peer names
else no backend (hosted path)
CLI-->>Harness: exit 2 + SKILL.md recovery text
end
CLI->>Fan: main_runner + competitor_runner × N
par MAX_PARALLEL_SUBRUNS=6
Fan->>Pipe: main topic
Fan->>Pipe: each peer (internal_subrun=True)
end
Fan-->>CLI: ordered (entity, Report)[]
CLI->>Rend: merged compact/md/json/context/html
Rend-->>Harness: evidence envelope + Head-to-Head scaffold
```
## CLI flags
<ParamField body="--competitors" type="integer (optional)" default="2 when flag present without value">
Auto-discover N peer entities and fan out. Bare `--competitors` defaults to N=2 (main + two peers → three-way). Range 1–6; values below 1 exit 2; values above 6 clamp with stderr warning. Ignored when `--competitors-list` is set (list length wins).
</ParamField>
<ParamField body="--competitors-list" type="comma-separated string">
Explicit peer names; whitespace-trimmed. Enables comparison mode without `--competitors`. Empty list exits 2. More than six entries clamp to `COMPETITORS_MAX` (6).
</ParamField>
<ParamField body="--competitors-plan" type="inline JSON or file path">
Per-entity Step 0.55 targeting for peer sub-runs. Parsed by `parse_competitors_plan()`; keys normalized to lowercase entity names. Preferred over list-only mode when the hosting model has already resolved handles and subreddits.
</ParamField>
<ParamField body="--polymarket-keywords" type="comma-separated string">
Optional disambiguation for ambiguous single-token topics during comparison runs (documented alongside competitor mode in SKILL.md).
</ParamField>
### Resolution precedence (`resolve_competitors_args`)
1. No flags → comparison disabled.
2. `--competitors-list` present → enabled; count = list length; numeric `--competitors` only warns if mismatched.
3. `--competitors=N` only → enabled; discovery count = N (after clamp).
## Engine-side discovery (`--competitors` without a list)
`lib/competitors.discover_competitors()` runs when auto-discovery is needed and a web backend exists (`BRAVE_API_KEY`, `EXA_API_KEY`, `SERPER_API_KEY`, etc., via `resolve._has_backend`).
<Steps>
<Step title="Fan out three SERP queries in parallel">
`{topic} competitors`, `{topic} alternatives`, `{topic} vs` — each through `grounding.web_search()` with the run’s lookback window.
</Step>
<Step title="Extract brand-shaped phrases">
Regex mining on titles and snippets; stopword filtering; reject candidates overlapping topic tokens.
</Step>
<Step title="Rank and return">
Frequency across SERP items; deterministic tie-break; return up to `count` names.
</Step>
</Steps>
If no backend is configured and `--mock` is not set, the engine exits 2 and prints recovery steps: hosting models should WebSearch peers, run Step 0.55 per entity, then re-invoke with a vs-topic and `--competitors-plan`; headless runs should configure API keys or pass `--competitors-list`.
Empty discovery also exits 2 (`Pass --competitors-list to override`).
## Parallel fan-out (`lib/fanout.py`)
`run_competitor_fanout()` schedules the main topic plus each peer in a `ThreadPoolExecutor` with `max_workers = min(len(competitors) + 1, MAX_PARALLEL_SUBRUNS)` where `MAX_PARALLEL_SUBRUNS = 6`.
- Per-entity failures log to stderr and are dropped; order preserved from submission order (main first, then peers in list order).
- Surviving reports must be at least two total; otherwise exit 1 (`Fewer than 2 sub-runs survived`).
- Empty competitor list degenerates to a single main run.
Each peer sub-run sets `internal_subrun=True`, deep-copies config, and records `report.artifacts["resolved"]` for the Resolved Entities block.
## `--competitors-plan` JSON schema
Top-level object: entity name → targeting object. Entity keys are case-insensitive after parse (stored lowercased).
| Field | Type | Role |
| --- | --- | --- |
| `x_handle` | string | Primary X handle (`@` stripped) |
| `x_related` | string[] | Related handles (`@` stripped) |
| `subreddits` | string[] | Subreddit names (`r/` prefix stripped) |
| `github_user` | string | GitHub username (lowercased, `@` stripped) |
| `github_repos` | string[] | `owner/repo` pairs; entries without `/` filtered out |
| `context` | string | News/events context for planner subqueries |
Unknown fields log a stderr warning and are dropped. Malformed JSON or a non-object top level exits 2. Non-dict entries for an entity are skipped with a warning.
`subrun_kwargs_for()` merges plan entries over `auto_resolve` results. Plan values win when present.
### Engine `auto_resolve` skip
When a plan entry includes both `x_handle` and `subreddits`, the engine skips `resolve.auto_resolve()` for that peer (hosting-model-driven Step 0.55). Otherwise, if a web backend exists, `auto_resolve(entity)` runs before `pipeline.run()`.
### File path vs inline JSON
`parse_competitors_plan()` accepts a filesystem path (same pattern as `--plan`). SKILL.md recommends a quoted heredoc tmpfile to avoid shell apostrophe breaks in `context` strings.
<RequestExample>
```bash
COMPETITORS_PLAN_FILE=$(mktemp "${TMPDIR:-/tmp}/last30days-competitors.XXXXXX")
trap 'rm -f "$COMPETITORS_PLAN_FILE"' EXIT
cat > "$COMPETITORS_PLAN_FILE" <<'PLAN_EOF'
{
"Anthropic": {"x_handle":"AnthropicAI","subreddits":["ClaudeAI","MachineLearning"],"github_user":"anthropics","context":"Claude 4 rollout"},
"xAI": {"x_handle":"xai","subreddits":["singularity"],"github_user":"xai-org"}
}
PLAN_EOF
python3 skills/last30days/scripts/last30days.py "OpenAI vs Anthropic vs xAI" \
--emit=compact \
--x-handle=OpenAI \
--subreddits=OpenAI,MachineLearning \
--competitors-plan "$COMPETITORS_PLAN_FILE"
```
</RequestExample>
Main topic (first entity in the vs-string) uses outer CLI flags (`--x-handle`, `--subreddits`, `--github-user`, `--github-repo`, TikTok/IG flags). Peers use `--competitors-plan` entries keyed by entity name.
## Per-entity Step 0.55 (hosting model)
On platforms with WebSearch (Claude Code, Codex, Hermes, Gemini, etc.), SKILL.md requires Step 0.55 once per entity before engine invocation: X handles, subreddits (including category-peer expansion for product topics), GitHub user/repos, and news context.
The four-step competitor protocol when the user asks for comparison without naming peers:
1. WebSearch `"{topic} competitors"` / `"{topic} alternatives"` (default N=2 peers unless `--competitors=N`).
2. Step 0.55 for main topic and each peer.
3. Build vs-topic: `"{main} vs {peer1} vs {peer2}"`.
4. Invoke engine with vs-topic, outer flags for main, and `--competitors-plan` for peers.
<Warning>
List-only mode (`--competitors-list` without plan) leaves peers on planner defaults — visibly thinner evidence. Dashes in `## Resolved Entities` for a peer mean Step 0.55 was skipped for that entity.
</Warning>
Platforms without WebSearch should use `--auto-resolve` on the engine command so `resolve.auto_resolve()` backs each sub-run when keys exist.
## Engine output shape
`render_comparison_multi()` produces:
- Badge and comparison header (`# last30days v{VERSION}: {entity1} vs {entity2} ...`)
- Optional aggregated `## Warnings`
- `<!-- EVIDENCE FOR SYNTHESIS -->` with per-entity `## {label}` evidence subsections (lower `cluster_limit` than single-entity runs)
- `## Resolved Entities` — per-entity X / Subs / GitHub / Context summary from artifacts
- `## Head-to-Head` empty table scaffold (nine axes from the April 9 exemplar: What it is, GitHub stars, Philosophy, Skills, Memory, Models, Security, Best for, Install)
- Pass-through emoji footer from the main report
`--emit=json` returns `{"comparison": true, "entities": [...], "reports": [...]}`. With `--save-dir`, the merged comparison saves once; each peer also gets an individual `{slug}-raw.md` (historical N-pass behavior).
Passing `--competitors-plan` or `--plan` suppresses the runtime web-promo nudge in the UI layer.
## Hosting model synthesis template
Comparison queries use a dedicated template (not `What I learned:` / KEY PATTERNS). LAW 2 and LAW 4 comparison exceptions apply.
Required structure after the badge:
| Section | Purpose |
| --- | --- |
| `# {A} vs {B} [vs {C}]: What the Community Says (/Last30Days)` | Title line |
| `## Quick Verdict` | Thesis, scale stats, one quotable community framing |
| `## {Entity}` (one per entity) | Community Sentiment; Strengths; Weaknesses bullets with `per <source>` |
| `## Head-to-Head` | Fill engine scaffold cells (5–15 words; ` - ` not em-dashes) |
| `## The Bottom Line` | `**Choose {Entity} if**` per entity |
| `## The emerging stack` | Combination pattern or explicit “no pattern yet” |
Forbidden for comparisons: `What I learned:`, bold-lead-in general voice, `KEY PATTERNS` lists, extra `##` headers beyond the six above, fabricated `## Notable Stats` (footer is LAW 5 pass-through).
Supplement after engine run: WebSearch `{A} vs {B} comparison {YEAR}` and `{A} vs {B} which is better` for rivalry coverage.
## Hosting model vs headless CLI
| Concern | Hosting model (slash command) | Direct CLI / cron |
| --- | --- | --- |
| Peer discovery | Model WebSearch + vs-topic + `--competitors-plan` | `--competitors` + web API keys, or `--competitors-list` |
| Per-entity targeting | `--competitors-plan` from Step 0.55 | Plan file or `auto_resolve` per peer |
| Synthesis | Model follows SKILL.md comparison template | Engine emits evidence + scaffold; synthesis still model-driven in skill hosts |
| Thin peer data | Avoid `--competitors-list` alone | Same; check Resolved Entities block |
`--competitors` on the engine signals intent in skill hosts; discovery and Step 0.55 remain the hosting model’s job before the vs-topic invocation.
## Failure modes and exit codes
| Condition | Exit / signal |
| --- | --- |
| `--competitors` below 1 | Exit 2 |
| Empty `--competitors-list` | Exit 2 |
| Auto-discovery with no web backend (non-mock) | Exit 2 + recovery stderr |
| Zero peers discovered | Exit 2 |
| Fewer than two sub-runs survived | Exit 1 |
| Invalid `--competitors-plan` JSON | Exit 2 |
Partial peer failure is graceful: remaining entities still render if at least two reports survive.
## Related pages
<CardGroup>
<Card title="Query types" href="/query-types">
COMPARISON intent detection, vs-string parsing, and pre-flight refusal paths.
</Card>
<Card title="Comparison recipes" href="/comparison-recipes">
Copy-paste workflows for vs-topic phrasing, fan-out, and Head-to-Head expectations.
</Card>
<Card title="Skill contract" href="/skill-contract">
STEP 0 stale-clone guard, LAW exceptions for comparison voice, and engine invocation rules.
</Card>
<Card title="CLI reference" href="/cli-reference">
Full flag inventory including `--competitors*` and emit modes.
</Card>
<Card title="Output contract" href="/output-contract">
Badge line, evidence envelope boundaries, and footer pass-through (LAW 5).
</Card>
<Card title="Per-client setup" href="/per-client-setup">
Recurring `--competitors-plan` templates and project-scoped env isolation.
</Card>
</CardGroup>
---
## 11. HTML briefs
> Shareable offline HTML artifacts via --emit=html, --synthesis-file, save paths under LAST30DAYS_MEMORY_DIR, and SKILL.md-driven save flow.
- Page Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/11-html-briefs.md
- Generated: 2026-06-04T23:21:50.794Z
### Source Files
- `skills/last30days/references/save-html-brief.md`
- `skills/last30days/scripts/lib/html_render.py`
- `skills/last30days/SKILL.md`
- `skills/last30days/scripts/last30days.py`
- `README.md`
---
title: "HTML briefs"
description: "Shareable offline HTML artifacts via --emit=html, --synthesis-file, save paths under LAST30DAYS_MEMORY_DIR, and SKILL.md-driven save flow."
---
Shareable HTML briefs are produced by the Python engine’s `--emit=html` mode (`html_render.py` wrapping `render.render_for_html`), with the harness agent writing model synthesis to a temp file via `--synthesis-file` and saving stdout under `LAST30DAYS_MEMORY_DIR`. Chat remains the primary surface; the `.html` file is an optional artifact for Slack, email, or Notion.
## When HTML briefs run
HTML save flow activates only when the user asks for it. `SKILL.md` gates the flow on either:
| Trigger | Examples |
|---|---|
| Explicit flag in `$ARGUMENTS` | `--emit=html`, `--emit:html`, `--html` |
| Natural language | “shareable HTML brief”, “for Slack”, “export as HTML”, “for Notion” |
If neither applies, skip HTML entirely and proceed to the normal wait-for-response step.
<Warning>
Slash commands in Agent Skills hosts do not pass shell pipes or redirects. `/last30days OpenClaw --emit=html | pbcopy` is invalid. The model must translate intent into engine flags and a real shell redirect (or use direct CLI invocation with full `python3 ...` syntax).
</Warning>
## Two save path conventions
The repo uses two related but distinct filenames under `LAST30DAYS_MEMORY_DIR` (default `~/Documents/Last30Days/`, overridable via env or `.claude/last30days.env`):
| Path pattern | Who writes it | Purpose |
|---|---|---|
| `{slug}-brief.html` | Harness agent (`references/save-html-brief.md`) | Canonical shareable brief from skill flow |
| `{slug}-raw-html[-suffix][-YYYY-MM-DD].html` | Engine `save_output()` when `--save-dir` is set | Engine-persisted HTML alongside other emit saves |
Slug rules: lowercase topic with non-alphanumeric runs collapsed to `-` (`slugify()` in `last30days.py`).
Collision handling in `save_output()`: if the target file already exists, append `-YYYY-MM-DD` before the extension. The skill reference documents the same behavior for agents saving briefs.
## End-to-end flow (skill / harness)
```mermaid
sequenceDiagram
participant User
participant Agent as Harness agent
participant Engine as last30days.py
participant Disk as LAST30DAYS_MEMORY_DIR
User->>Agent: /last30days topic (HTML intent)
Agent->>Engine: --emit=compact --save-dir ...
Engine-->>Agent: compact stdout (evidence + footer)
Agent-->>User: synthesis in chat (badge, What I learned, KEY PATTERNS, footer, invitation)
Agent->>Agent: Write synthesis verbatim to temp .md
Agent->>Engine: --emit=html --synthesis-file temp.md
Engine-->>Agent: full HTML document on stdout
Agent->>Disk: redirect stdout to {slug}-brief.html
Agent-->>User: 📎 Shareable brief saved to path
```
<Steps>
<Step title="Run research as usual">
Invoke the engine with `--emit=compact` and `--save-dir="${LAST30DAYS_MEMORY_DIR}"` (plus plan/targeting flags). Emit the full chat synthesis before any HTML step.
</Step>
<Step title="Write synthesis to a temp file">
Persist only the narrative the user saw: `What I learned:` paragraphs, inline `[name](url)` citations, and `KEY PATTERNS from the research:` list. Exclude the badge, engine footer, invitation block, evidence scratchpad, and debug headers.
</Step>
<Step title="Render HTML">
Re-invoke the engine with the same topic and the same plan/targeting flags as the compact run, plus `--emit=html` and `--synthesis-file`. Redirect stdout to `${LAST30DAYS_MEMORY_DIR}/${SLUG}-brief.html`.
</Step>
<Step title="Confirm in chat">
Append one line after the invitation: `📎 Shareable brief saved to <absolute or ~ path>`.
</Step>
</Steps>
Canonical shell pattern (from `references/save-html-brief.md`):
```bash
SYNTHESIS_FILE="/tmp/last30days-synthesis-${CLAUDE_SESSION_ID}.md"
# cat synthesis verbatim into SYNTHESIS_FILE (quoted heredoc)
SLUG=$(echo "$TOPIC" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-//;s/-$//')
HTML_PATH="${LAST30DAYS_MEMORY_DIR}/${SLUG}-brief.html"
"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" "${TOPIC}" \
--emit=html \
--synthesis-file "$SYNTHESIS_FILE" \
> "$HTML_PATH"
```
<Note>
`SKILL.md` requires reading `references/save-html-brief.md` before the wait-for-response step whenever HTML is requested. Do not improvise paths or synthesis scope from memory.
</Note>
### Follow-up turn without re-research
If the user already received a compact synthesis and later asks to “save as HTML”, reuse the synthesis from conversation history. Write it to the temp file and call `--emit=html --synthesis-file` only—do not restart WebSearch or rewrite the narrative.
## Direct CLI usage
For scripting, cron, or engine testing without the skill loop:
<ParamField body="--emit" type="string" required>
Must be `html`. Other values ignore `--synthesis-file` with a stderr warning.
</ParamField>
<ParamField body="--synthesis-file" type="path">
UTF-8 markdown file embedded as the HTML body. Only read when `--emit=html`. Missing or unreadable files exit with code 2.
</ParamField>
<ParamField body="--save-dir" type="path">
Optional. When set, the engine writes `{slug}-raw-html[-suffix][-date].html` via `save_output()` and still prints HTML to stdout.
</ParamField>
<CodeGroup>
```bash title="Stdout only"
python3 skills/last30days/scripts/last30days.py "OpenClaw" \
--emit=html \
--synthesis-file /tmp/synthesis.md
```
```bash title="Engine save + stdout"
python3 skills/last30days/scripts/last30days.py "OpenClaw" \
--emit=html \
--synthesis-file /tmp/synthesis.md \
--save-dir "$HOME/Documents/Last30Days" \
--save-suffix=v3
```
</CodeGroup>
Without `--synthesis-file`, `render_for_html()` still emits badge, date/source metadata, and the pass-through footer, but the body stays sparse—unsuitable as a shareable brief.
## What the HTML document contains
Rendering pipeline: `render_for_html()` → markdown intermediate → `html_render.render_html()` → single self-contained file.
| Section | Source | Notes |
|---|---|---|
| Badge | Engine `_render_badge()` | `🌐 last30days vX.Y.Z · synced YYYY-MM-DD` as styled pill |
| Metadata line | `<!-- META: range · sources -->` | Promoted to `<div class="meta">` after conversion |
| Synthesis body | `--synthesis-file` content | Verbatim; `What I learned:` / `KEY PATTERNS from the research:` promoted to `<h2>` |
| Citations | Inline markdown links | Converted to `<a href="...">` |
| Engine footer | Pass-through footer markers | Wrapped in `<div class="engine-footer"><pre>…</pre></div>`; tree preserved |
| Colophon | `_build_colophon()` | Generation date, skill version, topic, `/last30days {topic}` rerun hint |
Stripped before share (not in the artifact):
- `# last30days vX.Y.Z: TOPIC` debug file header (`<h1>` suppressed)
- `> Safety note:` blockquote
- `<!-- EVIDENCE FOR SYNTHESIS -->` blocks
- Invitation block (`I'm now an expert… Just ask.`)
- `---` / `# END OF last30days CANONICAL OUTPUT` boundary
- Data quality warnings (degraded run, thin evidence, quota messages)—by design these stay in stderr for the generator, not recipients
Presentation details:
- Inline CSS in `<style>`; no JavaScript (`test_self_containedness`)
- Google Fonts link for Inter and JetBrains Mono (offline-friendly content except font fetch)
- `prefers-color-scheme: light` and `@media print` rules (A4, link URLs in print)
## Comparison mode
For `X vs Y` topics, the engine routes to `render_for_html_comparison()` / `html_render.render_html_comparison()`. The agent uses the same save flow; the synthesis temp file should match the comparison template from chat (`## Quick Verdict`, per-entity sections, `## Head-to-Head`, `## The Bottom Line`, etc.). Saved comparison HTML uses topic slug `entity-a-vs-entity-b` when `--save-dir` persists via `comparison_topic()`.
## Synthesis file contract
| Include | Exclude |
|---|---|
| `What I learned:` bold-lead-in paragraphs with `[name](url)` | Badge line |
| `KEY PATTERNS from the research:` numbered list | `✅ All agents reported back!` footer tree |
| Comparison `##` sections when in comparison mode | Invitation block |
| Unicode and emoji as in chat | Evidence clusters, stats, source coverage |
| | Data quality warning text |
Use a quoted heredoc (`<<'SYNTHESIS_EOF'`) so shell metacharacters in the topic or synthesis do not expand. Match chat text exactly—no paraphrase or reorder.
## Configuration
| Variable / knob | Role |
|---|---|
| `LAST30DAYS_MEMORY_DIR` | Root for `{slug}-brief.html` and `{slug}-raw*.md` / `raw-html` saves; defaults per platform in `CONFIGURATION.md` |
| `--save-dir` | Per-run override passed on engine invocations |
| `--save-suffix` | Inserts `-{suffix}` before extension on engine saves (e.g. per-client `v3`) |
Project-scoped `.claude/last30days.env` can set `LAST30DAYS_MEMORY_DIR` per client without wrapper scripts.
## Troubleshooting
| Symptom | Likely cause | Mitigation |
|---|---|---|
| Thin HTML with no narrative | HTML run without `--synthesis-file` or empty temp file | Ensure synthesis file matches chat output scope |
| `[last30days] Warning: --synthesis-file is only used with --emit=html` | `--synthesis-file` on non-html emit | Add `--emit=html` |
| `Cannot read --synthesis-file` (exit 2) | Bad path or permissions | Fix path; use absolute paths in scripts |
| Warnings missing from file but in stderr | Expected—warnings excluded from artifact | Read stderr when debugging run quality |
| Slash command with pipe fails | Harness does not pass shell mechanics | Use agent redirect or direct CLI |
| Duplicate briefs | Same slug, same day | Engine adds date suffix on `--save-dir` saves; agent should report actual path after redirect |
## Related pages
<CardGroup>
<Card title="Output contract" href="/output-contract">
Badge, LAWs voice, all `--emit` modes, evidence blocks, and footer pass-through rules that define what goes into synthesis vs HTML.
</Card>
<Card title="Skill contract" href="/skill-contract">
SKILL.md HTML trigger section, `save-html-brief.md` reference load, and wait-for-response ordering.
</Card>
<Card title="CLI reference" href="/cli-reference">
Full `last30days.py` flags including `--emit` choices and direct CLI vs slash-command constraints.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
`LAST30DAYS_MEMORY_DIR`, `--save-dir`, `--save-suffix`, and per-client `.env` patterns.
</Card>
<Card title="Comparison mode" href="/comparison-mode">
Comparison synthesis shape and `render_for_html_comparison` behavior.
</Card>
</CardGroup>
---
## 12. Trend monitoring
> SQLite --store persistence, watchlist.py scheduling and delivery, briefing.py digests, and recommended baseline cadence.
- Page Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/12-trend-monitoring.md
- Generated: 2026-06-04T23:21:36.095Z
### Source Files
- `CONFIGURATION.md`
- `skills/last30days/scripts/store.py`
- `skills/last30days/scripts/watchlist.py`
- `skills/last30days/scripts/briefing.py`
- `tests/test_watchlist_commands.py`
- `skills/last30days/scripts/last30days.py`
---
title: "Trend monitoring"
description: "SQLite --store persistence, watchlist.py scheduling and delivery, briefing.py digests, and recommended baseline cadence."
---
Trend monitoring turns one-off `/last30days` research into a local time series: ranked findings land in SQLite at `~/.local/share/last30days/research.db`, `watchlist.py` drives recurring engine runs and optional webhooks, and `briefing.py` emits structured digest JSON for agent-side synthesis. Markdown snapshots under `LAST30DAYS_MEMORY_DIR` still save on every engine run; the database is the cross-run substrate for deltas, budgets, and digests.
## Snapshot mode vs accumulation
| Mode | Trigger | Primary artifact | Cross-run memory |
|------|---------|------------------|------------------|
| Snapshot (default) | `/last30days "<topic>"` or engine without persistence | `<slug>-raw.md` (or suffix) under `LAST30DAYS_MEMORY_DIR` | None — same topic + suffix overwrites or date-stamps per save rules |
| Accumulation | `--store`, `LAST30DAYS_STORE=1`, or watchlist `run-one` / `run-all` | `research.db` + optional brief JSON | URL-deduped `findings`, per-run `finding_sightings`, run history |
<Note>
Watchlist runs do **not** pass `--store` to the engine. They spawn `last30days.py` with `--emit=json`, then persist up to 25 findings via `store.findings_from_report()` and `store.store_findings()` inside `watchlist.py`.
</Note>
## Architecture
```mermaid
flowchart TB
subgraph user["Operator / scheduler"]
Cron["cron / Task Scheduler / CI"]
Slash["/last30days or direct CLI"]
end
subgraph engine["skills/last30days/scripts/last30days.py"]
Pipeline["v3 pipeline"]
end
subgraph persist["~/.local/share/last30days/"]
DB["research.db — store.py"]
Briefs["briefs/*.json — briefing.py"]
end
subgraph ops["Operational scripts"]
WL["watchlist.py"]
BR["briefing.py"]
end
subgraph delivery["Optional notifications"]
Slack["Slack incoming webhook"]
Hook["Generic HTTPS webhook"]
end
Slash --> Pipeline
Cron --> WL
WL -->|"subprocess: --emit=json --quick --lookback-days 90"| Pipeline
Pipeline -->|"--store or LAST30DAYS_STORE"| DB
WL --> DB
WL -->|"new findings only"| Slack
WL --> Hook
BR --> DB
BR --> Briefs
Pipeline --> Mem["LAST30DAYS_MEMORY_DIR markdown"]
```
## SQLite store (`store.py`)
Default path: `~/.local/share/last30days/research.db` (overridable in tests via `store._db_override`). Initialization uses WAL mode, `PRAGMA foreign_keys=ON`, and lightweight versioned migrations.
### Schema (v1 + migration v2)
```mermaid
erDiagram
topics ||--o{ research_runs : "topic_id"
topics ||--o{ findings : "topic_id"
research_runs ||--o{ findings : "run_id"
findings ||--o{ finding_sightings : "finding_id"
research_runs ||--o{ finding_sightings : "run_id"
topics {
int id PK
text name UK
text search_queries
text schedule
int enabled
}
research_runs {
int id PK
int topic_id FK
text run_date
text status
real token_cost
int findings_new
int findings_updated
}
findings {
int id PK
text source_url UK
int sighting_count
real engagement_score
}
finding_sightings {
int id PK
int run_id FK
int finding_id FK
}
```
| Table / object | Role |
|----------------|------|
| `topics` | Watchlist entries; `name` is unique; optional `search_queries` JSON |
| `research_runs` | Per-execution metadata, cost, `findings_new` / `findings_updated` |
| `findings` | One row per `source_url` (UNIQUE); re-sightings bump `sighting_count` and `last_seen` |
| `finding_sightings` | Per-run ledger for `compute_topic_delta()` (new / continued / dropped URLs) |
| `findings_fts` | FTS5 (porter + unicode61) over content, summary, title, author |
| `settings` | Global knobs: budget, delivery, briefing defaults |
Default `settings` rows on first init:
| Key | Default |
|-----|---------|
| `daily_budget` | `5.00` |
| `delivery_channel` | empty |
| `delivery_mode` | `announce` |
| `briefing_format` | `concise` |
| `default_schedule` | `0 8 * * *` |
### Dedup and persistence rules
- Only findings with a `source_url` (or `url` in the input dict) are inserted or updated; URL-less items are skipped silently.
- Re-sighting the same URL updates engagement to `max(new, existing)`, increments `sighting_count`, and refreshes `last_seen`.
- `findings_from_report()` prefers `ranked_candidates`; supplements Hacker News and Polymarket from `items_by_source` when ranked; if ranking is empty, supplements all sources from raw items.
### Store query CLI
Run from the scripts directory or with the full repo path:
```bash
python3 skills/last30days/scripts/store.py stats
python3 skills/last30days/scripts/store.py query "your topic" --since 7d
python3 skills/last30days/scripts/store.py search "keyword" --limit 20
python3 skills/last30days/scripts/store.py trending --days 7
```
## Engine persistence: `--store` and `LAST30DAYS_STORE`
<ParamField body="--store" type="flag">
When set (or when env/config enables storage), after a successful pipeline run the engine calls `persist_report()`: upsert topic, record run, convert report to findings, `store_findings()`, mark run completed. Stderr prints `[last30days] Stored N new, M updated findings`.
</ParamField>
<ParamField body="LAST30DAYS_STORE" type="env">
Read from `os.environ` and loaded config (`.env` is not always mirrored into `os.environ`). Truthy values: `1`, `true`, `yes` (case-insensitive). Additive with `--store` — either enables persistence.
</ParamField>
<CodeGroup>
```bash title="Per-run flag"
python3 skills/last30days/scripts/last30days.py "british airways middle east" \
--emit=compact --days=30 --store
```
```bash title="Always-on (.env)"
LAST30DAYS_STORE=1
```
</CodeGroup>
Manual `--store` runs still write markdown to `LAST30DAYS_MEMORY_DIR` when the save path is active; SQLite is additional, not a replacement.
## Watchlist (`watchlist.py`)
Manages scheduled topics and headless research loops. The `schedule` field on each topic is **metadata only** — you must invoke `run-one` or `run-all` from an external scheduler (cron, launchd, GitHub Actions, etc.).
### Commands
| Command | Purpose |
|---------|---------|
| `add <topic>` | Register topic; optional `--schedule`, `--weekly`, `--queries "q1,q2"` |
| `remove <topic>` | Delete topic and its findings / runs |
| `list` | JSON list of topics plus `budget_used` / `budget_limit` |
| `delta <topic>` | Compare latest two **completed** runs (new / continued / dropped URLs) |
| `run-one <topic>` | Single headless engine invocation + DB persist |
| `run-all` | All `enabled` topics until daily budget exceeded |
| `config delivery <url>` | Set `delivery_channel` (Slack or HTTPS) |
| `config budget <amount>` | Set `daily_budget` |
Schedule defaults:
| Flag | Stored cron | Description |
|------|-------------|-------------|
| (default) | `0 8 * * *` | Daily 8:00 |
| `--weekly` | `0 8 * * 1` | Mondays 8:00 |
| `--schedule "..."` | Custom | Stored as-is on the topic row |
### What `run-one` / `run-all` execute
Each enabled topic triggers:
```text
python3 last30days.py <search_term> --emit=json --quick --lookback-days 90
```
- `search_term` = first entry in `search_queries` JSON, else topic `name`
- Subprocess timeout: **300 seconds**
- On success: parse JSON → `findings_from_report(..., limit=25)` → `store_findings()`
- Failed runs set `research_runs.status = failed` with truncated stderr
`run-all` skips remaining topics when `store.get_daily_cost()` ≥ `daily_budget` (default **$5.00** per calendar day, UTC date on `run_date`).
### Webhook delivery
Fires only when `delivery_channel` is set **and** `counts["new"] > 0` after a successful run. Delivery failures are logged to stderr and do not fail the research run.
| Channel pattern | Payload |
|-----------------|---------|
| URL contains `hooks.slack.com` | `{"text": "<message>"}` |
| `https://...` otherwise | JSON: `message`, `source: "last30days"`, `timestamp` |
`delivery_mode` (DB default `announce`) controls message shape: `announce` (emoji + counts), `silent` (no emoji), or other (generic “complete” line). The `watchlist config` subcommand sets **delivery URL** and **budget** only — change `delivery_mode` via direct `store.set_setting()` if needed.
<Steps>
<Step title="Register a topic">
```bash
python3 skills/last30days/scripts/watchlist.py add "british airways middle east" --weekly
```
</Step>
<Step title="Configure delivery and budget (optional)">
```bash
python3 skills/last30days/scripts/watchlist.py config delivery "https://hooks.slack.com/services/..."
python3 skills/last30days/scripts/watchlist.py config budget 5.00
```
</Step>
<Step title="Schedule recurring execution">
Point cron (or equivalent) at:
```bash
python3 skills/last30days/scripts/watchlist.py run-all
```
Or one topic: `run-one "british airways middle east"`.
</Step>
<Step title="Inspect run-to-run change">
```bash
python3 skills/last30days/scripts/watchlist.py delta "british airways middle east"
```
Requires at least two completed runs; otherwise status is `insufficient_history`.
</Step>
</Steps>
<Info>
Headless watchlist runs use the engine’s internal planner path (`--emit=json` without `--plan`). Host-model reasoning keys are not required for cron-style execution; configure source API keys and `.env` the same as interactive runs.
</Info>
## Briefings (`briefing.py`)
Collects structured JSON for synthesis — the script does not render final prose; the skill/agent turns the payload into a readable briefing.
| Command | Output |
|---------|--------|
| `generate` | Daily digest data; default window: findings with `first_seen` since yesterday |
| `generate --weekly` | Week-over-week counts and engagement % change per topic |
| `generate --since YYYY-MM-DD` | Daily window override |
| `show [--date DATE]` | Load saved JSON from archive |
Archive directory: `~/.local/share/last30days/briefs/` as `{YYYY-MM-DD}.json` or `{YYYY-MM-DD}-weekly.json`.
Daily payload highlights:
- Per-topic `findings`, `new_count`, `last_run`, `last_status`
- `stale: true` when last completed run is **> 36 hours** ago
- `top_finding` per topic and cross-topic `top_finding` for TL;DR
- `cost.daily` vs `cost.budget`
- `failed_topics` list
Weekly payload adds `this_week_count`, `last_week_count`, `engagement_change_pct`, and top five findings per topic.
```bash
python3 skills/last30days/scripts/briefing.py generate
python3 skills/last30days/scripts/briefing.py generate --weekly
python3 skills/last30days/scripts/briefing.py show --date 2026-06-01
```
## Recommended baseline cadence
| Step | Cadence | Action |
|------|---------|--------|
| Baseline corpus | One-time per topic | `/last30days "<topic>"` with `--days=30` and `--store` (or `LAST30DAYS_STORE=1`) |
| Watchlist enrollment | One-time per topic | `watchlist.py add "<topic>"` (`--weekly` if Monday cadence fits) |
| Recurring research | Daily or weekly (your scheduler) | `watchlist.py run-all` or per-topic `run-one` |
| Digest | Weekly (typical) | `briefing.py generate --weekly`; use daily `generate` for operational triage |
<Warning>
The `schedule` column does not trigger runs by itself. Missing cron means topics go stale (`briefing.py` flags > 36h).
</Warning>
## Operational checks
| Symptom | Check |
|---------|-------|
| Empty brief | No enabled topics — `watchlist.py list`; add with `add` |
| Delta unavailable | Fewer than two completed runs for topic |
| Webhook never fires | `delivery_channel` unset, or zero **new** findings (updates alone do not notify) |
| `run-all` skips topics | Daily budget exhausted — `list` shows `budget_used` vs `budget_limit` |
| Thin watchlist results | Engine uses `--quick` and 90-day lookback; not full default-depth profile |
## Related pages
<CardGroup>
<Card title="Configuration reference" href="/configuration-reference">
Environment variables, `.env` lookup, and trend-monitoring toggles including `LAST30DAYS_STORE`.
</Card>
<Card title="CLI reference" href="/cli-reference">
`last30days.py` flags (`--store`, `--days`, `--emit`) and direct CLI vs slash-command constraints.
</Card>
<Card title="Research pipeline" href="/research-pipeline">
What `--quick` changes inside the v3 pipeline that watchlist runs hardcode.
</Card>
<Card title="Output contract" href="/output-contract">
Markdown save paths under `LAST30DAYS_MEMORY_DIR` alongside SQLite accumulation.
</Card>
<Card title="Quickstart" href="/quickstart">
First successful `/last30days` run before enabling `--store` or watchlist.
</Card>
</CardGroup>
---
## 13. Per-client setup
> Project-scoped .claude/last30days.env, save-dir and --save-suffix isolation, category peer subreddits, and recurring --competitors-plan templates.
- Page Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/13-per-client-setup.md
- Generated: 2026-06-04T23:22:17.441Z
### Source Files
- `CONFIGURATION.md`
- `skills/last30days/scripts/lib/env.py`
- `skills/last30days/scripts/lib/categories.py`
- `skills/last30days/SKILL.md`
- `skills/last30days/scripts/last30days.py`
---
title: "Per-client setup"
description: "Project-scoped .claude/last30days.env, save-dir and --save-suffix isolation, category peer subreddits, and recurring --competitors-plan templates."
---
Per-client isolation in last30days-skill is configuration-driven: a project-scoped `.claude/last30days.env` overrides global credentials, `LAST30DAYS_MEMORY_DIR` and `--save-suffix` partition saved research files, `scripts/lib/categories.py` supplies vertical-specific Reddit peers, and checked-in `--competitors-plan` JSON templates reuse Step 0.55 targeting for recurring comparison runs.
## Configuration layers
```text
Process environment (highest)
│
▼
.claude/last30days.env ← walk-up from cwd (stops at $HOME)
│
▼
~/.config/last30days/.env ← global default
│
▼
macOS Keychain (last30days-* prefix, Darwin only)
```
<ParamField body="LAST30DAYS_CONFIG_DIR" type="string">
Override the global config directory. Set to an empty string for no-config (clean) mode. When unset, global config lives at `~/.config/last30days/.env`.
</ParamField>
The engine loads API keys and source toggles from this stack via `get_config()` in `scripts/lib/env.py`. Project env discovery walks parent directories from `Path.cwd()` until it finds `.claude/last30days.env` or reaches `$HOME`.
<Warning>
POSIX secret files should be mode `600`. The engine warns on every run when group/other read bits are set; Claude Code’s `check-config.sh` hook applies the same check at session start.
</Warning>
## Pattern 1: Project-scoped `.claude/last30days.env`
Place one env file per client workspace:
```text
acme-corp/
.claude/
last30days.env
...
```
Typical contents isolate credentials, enabled sources, and the save root:
```bash
LAST30DAYS_MEMORY_DIR=/Users/you/Clients/acme/Research/Last30Days
SCRAPECREATORS_API_KEY=<client-or-shared-key>
INCLUDE_SOURCES=tiktok,instagram
BSKY_HANDLE=acme.bsky.social
BSKY_APP_PASSWORD=<app-password>
```
<Steps>
<Step title="Create the file">
Add `.claude/last30days.env` at the client repo root (or any ancestor of where you run research). Restrict permissions: `chmod 600 .claude/last30days.env`.
</Step>
<Step title="Work from the client directory">
`cd` into the client folder before `/last30days <topic>`. The engine resolves the nearest project env on each run; no wrapper script is required for the common case.
</Step>
<Step title="Verify keys loaded">
Run `python3 skills/last30days/scripts/last30days.py --diagnose` from the client directory. Confirm per-source availability matches the client’s `.env` contents.
</Step>
</Steps>
<Note>
Slash-command invocations pass `--save-dir="${LAST30DAYS_MEMORY_DIR}"` from the SKILL.md bash preamble. Export `LAST30DAYS_MEMORY_DIR` in the shell (or have the hosting model read it from `.claude/last30days.env` before the engine call) so saves land in the client folder. API keys in the project file are picked up by `get_config()` without extra export.
</Note>
## Pattern 2: Save-dir and suffix isolation
Saved artifacts use slug-based filenames under the active save directory.
| Mechanism | Scope | Effect |
|-----------|-------|--------|
| `LAST30DAYS_MEMORY_DIR` | Persistent per client | Root directory for all saves when passed as `--save-dir` |
| `--save-dir <path>` | Single run | One-off output location |
| `--save-suffix <name>` | Single run | Inserts `-<suffix>` before the extension |
**Filename rules** (`save_output()` in `scripts/last30days.py`):
- Default markdown: `<slug>-raw[-suffix].md`
- JSON: `<slug>-raw[-suffix].json`
- HTML: `<slug>-raw-html[-suffix].html`
- Same topic + suffix on the same calendar day overwrites; a collision on a later day appends `-YYYY-MM-DD` before the extension.
Default save root when unset: `~/Documents/Last30Days/` (Windows: `C:\Users\<you>\Documents\Last30Days\`).
### Wrapper for batch or multi-client hosts
When you do not `cd` into each client tree, set the memory dir and suffix in one shell function:
<CodeGroup>
```bash title="Bash"
l30d-client() {
local client=$1; shift
LAST30DAYS_MEMORY_DIR="$HOME/Clients/$client/Research/Last30Days" \
python3 skills/last30days/scripts/last30days.py "$@" \
--emit=compact \
--save-dir="$LAST30DAYS_MEMORY_DIR" \
--save-suffix="$client"
}
# l30d-client acme "british airways middle east"
```
```powershell title="PowerShell"
function Run-L30D-Client {
param([string]$ClientSlug, [Parameter(ValueFromRemainingArguments=$true)]$Args)
$env:LAST30DAYS_MEMORY_DIR = "C:\Users\$env:USERNAME\Clients\$ClientSlug\Research\Last30Days"
python3 skills/last30days/scripts/last30days.py @Args `
--emit=compact `
--save-dir=$env:LAST30DAYS_MEMORY_DIR `
--save-suffix=$ClientSlug
}
# Run-L30D-Client acme "british airways middle east"
```
</CodeGroup>
<Info>
Combine Pattern 1 and Pattern 2: project env sets `LAST30DAYS_MEMORY_DIR` for the client tree; add `--save-suffix=<client-slug>` when multiple clients share one save root and you need distinct filenames for the same topic slug.
</Info>
### Comparison mode saves
Vs-mode (`--competitors-plan` or `--competitors`) writes one file per entity plus merged stdout output. Peer entities save as `{peer-slug}-raw[-suffix].md`; the main topic uses the leading entity slug. All paths honor the same `--save-dir` and `--save-suffix`.
## Pattern 3: Category peer subreddits
Brand-specific subreddits from WebSearch miss cross-product technique threads (the documented `GPT Image 2` failure mode). Step 0.55 Section 2a requires appending category peers for product topics.
**Hosting-model path (Claude Code, Codex, Gemini, etc.):** SKILL.md carries the canonical category table and merging rule—start with WebSearch subs, append 2–3 peers in priority order, dedupe case-insensitively, cap at 10 (WebSearch subs win when trimming). Emit `(+ {category_id} peers)` on the Resolved Reddit line when peers were added.
**Engine `--auto-resolve` path:** `scripts/lib/resolve.py` calls `categories.detect_category()` and `categories.peer_subs_for()` with the same cap and preservation rules.
### Extending the map
`scripts/lib/categories.py` defines `CATEGORY_PEERS`: each entry has `patterns` (compound terms, first-match-wins in declaration order) and `peer_subs` (priority-ordered subreddit names without `r/` prefix).
```python
# Illustrative new row — add inside CATEGORY_PEERS in categories.py
"legal_tech": {
"patterns": ["legal tech", "contract automation", "clm platform"],
"peer_subs": ["legaltech", "LawFirm", "SaaS"],
},
```
<Warning>
There is no runtime override file for categories. New verticals require a code change (or a beta/private fork). Keep patterns specific—bare tokens like `ai` or `model` are intentionally avoided to limit false positives.
</Warning>
| Category id | Example triggers | Peer subs (first entries) |
|-------------|------------------|---------------------------|
| `ai_image_generation` | gpt image, midjourney, stable diffusion | StableDiffusion, midjourney, dalle2, aiArt |
| `ai_coding_agent` | claude code, cursor ide, openclaw | ChatGPTCoding, LocalLLaMA, singularity |
| `saas_screen_recording` | loom video, tella screen | SaaS, screenrecording, productivity |
| `prediction_markets` | polymarket, kalshi | Polymarket, Kalshi, predictionmarkets |
Person topics, news stories, and topics outside any category skip peer expansion.
## Pattern 4: Recurring `--competitors-plan` templates
Store per-client competitor targeting as JSON beside the client workspace. Reuse across scheduled or ad hoc comparison runs instead of re-running full Step 0.55 discovery every time.
### Schema
Top-level object: entity name → targeting dict. Keys are normalized to lowercase. Inline JSON or a file path is accepted (same behavior as `--plan`).
<ParamField body="x_handle" type="string">
Official X/Twitter handle for the entity (`@` optional).
</ParamField>
<ParamField body="x_related" type="string[]">
Related handles searched at lower weight.
</ParamField>
<ParamField body="subreddits" type="string[]">
Subreddit names without `r/` prefix.
</ParamField>
<ParamField body="github_user" type="string">
GitHub username for person-mode search.
</ParamField>
<ParamField body="github_repos" type="string[]">
`owner/repo` strings for project-mode search.
</ParamField>
<ParamField body="context" type="string">
Free-text news/founding context passed into sub-runs.
</ParamField>
Unknown fields log a stderr warning and are ignored. Malformed JSON or a non-object top level exits with code 2.
### Example template
```json
{
"Anthropic": {
"x_handle": "AnthropicAI",
"subreddits": ["ClaudeAI", "LocalLLaMA", "artificial"],
"github_user": "anthropics",
"context": "Founded 2021, Claude family of models"
},
"xAI": {
"x_handle": "xai",
"subreddits": ["singularity", "LocalLLaMA"],
"github_user": "xai-org",
"context": "Grok models, X integration"
}
}
```
### Invocation
Main topic (first in the `vs` string) uses outer flags (`--x-handle`, `--subreddits`, `--github-user`, …). Peers take targeting from the plan via `subrun_kwargs_for()`—plan values beat engine `auto_resolve`, and main-topic flags cannot leak into peer sub-runs.
<RequestExample>
```bash
# Write JSON to a tmpfile when context strings contain apostrophes (SKILL.md heredoc pattern)
COMPETITORS_PLAN_FILE=$(mktemp "${TMPDIR:-/tmp}/last30days-competitors.XXXXXX")
trap 'rm -f "$COMPETITORS_PLAN_FILE"' EXIT
cat > "$COMPETITORS_PLAN_FILE" <<'PLAN_EOF'
{"Anthropic":{"x_handle":"AnthropicAI","subreddits":["ClaudeAI","LocalLLaMA"]}}
PLAN_EOF
python3 skills/last30days/scripts/last30days.py "OpenAI vs Anthropic vs xAI" \
--emit=compact \
--save-dir="${LAST30DAYS_MEMORY_DIR:-$HOME/Documents/Last30Days}" \
--save-suffix=acme \
--x-handle=OpenAI \
--subreddits=OpenAI,ChatGPT,singularity \
--competitors-plan "$COMPETITORS_PLAN_FILE"
```
</RequestExample>
<Check>
Prefer `--competitors-plan` over `--competitors-list` when peers need handles and subreddits. List-only mode falls back to thin planner defaults; empty fields in the Resolved Entities block indicate skipped Step 0.55 for that entity.
</Check>
File-path form for checked-in templates:
```bash
--competitors-plan clients/acme/competitors-plan.json
```
Refresh templates when handles, primary repos, or category peers change—peer sub-runs do not inherit the main topic’s subreddit list.
## Composing patterns
```text
┌─────────────────────────────────────────────────────────────┐
│ Client workspace │
│ .claude/last30days.env → keys, INCLUDE_SOURCES, save root │
│ competitors-plan.json → recurring vs-mode targeting │
│ (optional) categories.py fork row → vertical peer subs │
└───────────────────────────┬─────────────────────────────────┘
│
cd + /last30days │ wrapper: LAST30DAYS_MEMORY_DIR
│ + --save-suffix
▼
Engine: get_config() + --save-dir + plan JSON
│
▼
~/Clients/<slug>/Research/Last30Days/
<topic>-raw-<suffix>.md
<peer>-raw-<suffix>.md (comparison)
```
| Client need | Primary pattern | Secondary |
|-------------|-----------------|-----------|
| Separate API keys per account | `.claude/last30days.env` | — |
| Separate output folders | `LAST30DAYS_MEMORY_DIR` in project env | `--save-dir` override |
| Same folder, same topic, different clients | `--save-suffix` | wrapper export |
| Niche SaaS vertical | `categories.py` row | SKILL.md 2a for model path |
| Monthly competitor brief | `competitors-plan.json` | `--competitors` for discovery refresh |
## Beta and private forks
Client-specific category rows, internal subreddit lists, and industry plan templates that should not ship on the public marketplace belong on the beta/private companion install (`/last30days-beta` from `mvanhorn/last30days-skill-private`). Promote upstream only through review against the public repo.
<Tip>
After editing `categories.py` or plan templates in a dev checkout, re-run `npx skills add . -g -y` or symlink `~/.agents/skills/last30days` to the working tree so harnesses load the updated skill copy.
</Tip>
## Verification
| Check | Command / signal |
|-------|------------------|
| Project env found | `--diagnose` shows expected keys; `_CONFIG_SOURCE` in debug paths may read `project:.claude/last30days.env` |
| Saves isolated | Footer line `📎 Raw results saved to …/<slug>-raw-<suffix>.md` points under the client `LAST30DAYS_MEMORY_DIR` |
| Category peers applied | Resolved block includes `(+ ai_image_generation peers)` or engine log `[Resolve] Matched category=…` |
| Plan threaded | No dashes in per-entity Resolved block; peer raw files exist for each competitor |
## Related pages
<CardGroup>
<Card title="Configure sources" href="/configure-sources">
Credential layers, `INCLUDE_SOURCES`, setup wizard, and `--diagnose` availability checks.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
Full environment variable and `.env` lookup order.
</Card>
<Card title="Comparison mode" href="/comparison-mode">
`--competitors` fan-out, plan schema details, and synthesis scaffold.
</Card>
<Card title="Comparison recipes" href="/comparison-recipes">
Copy-paste vs-topic workflows and Head-to-Head table expectations.
</Card>
<Card title="Model clients" href="/model-clients">
Per-harness install patterns and stale-install avoidance.
</Card>
<Card title="Beta channel" href="/beta-channel">
Private-repo customizations that stay off the public marketplace.
</Card>
</CardGroup>
---
## 14. Configuration reference
> Environment variables, .env lookup order, reasoning provider priority, web-backend priority, output paths, and trend-monitoring toggles.
- Page Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/14-configuration-reference.md
- Generated: 2026-06-04T23:22:46.853Z
### Source Files
- `CONFIGURATION.md`
- `skills/last30days/scripts/lib/env.py`
- `skills/last30days/scripts/lib/providers.py`
- `skills/last30days/scripts/last30days.py`
- `skills/last30days/SKILL.md`
---
title: "Configuration reference"
description: "Environment variables, .env lookup order, reasoning provider priority, web-backend priority, output paths, and trend-monitoring toggles."
---
Runtime configuration for `/last30days` resolves through `skills/last30days/scripts/lib/env.py` (`get_config()`), then per-run CLI flags in `last30days.py` override or extend those defaults. The slash-command contract in `skills/last30days/SKILL.md` governs voice and orchestration; this page lists every knob the engine reads.
## Configuration layers
Three layers stack from most transient to most persistent:
| Layer | Mechanism | Typical use |
|---|---|---|
| Per-run flags | `python3 skills/last30days/scripts/last30days.py ...` | `--save-dir`, `--web-backend`, `--store`, `--quick` / `--deep` |
| Process environment | `export VAR=value` before invocation | CI secrets, one-off overrides |
| Files + Keychain | `.claude/last30days.env`, `~/.config/last30days/.env`, macOS Keychain | Long-lived API keys and defaults |
Per-key resolution inside `get_config()` (highest wins):
```mermaid
flowchart TB
subgraph highest["Highest priority"]
OS["os.environ"]
end
subgraph files["File sources"]
PROJ[".claude/last30days.env\n(walk up from cwd)"]
GLOB["~/.config/last30days/.env\nor LAST30DAYS_CONFIG_DIR/.env"]
end
subgraph lowest["Lowest priority"]
KC["macOS Keychain\nlast30days-{KEY}"]
end
KC --> MERGE["merged_env"]
GLOB --> MERGE
PROJ --> MERGE
MERGE --> RESOLVE["config[key] = os.environ[key] or merged_env[key]"]
OS --> RESOLVE
```
<Note>
`LAST30DAYS_CONFIG_DIR=""` disables global file loading (clean/no-config mode). `LAST30DAYS_CONFIG_DIR=/path` relocates the global `.env` to `/path/.env`.
</Note>
<Warning>
POSIX hosts: the engine warns on stderr when a secrets file is group- or world-readable. Set mode `600` on `.env` files.
</Warning>
## `.env` lookup paths
| Path | Scope | Precedence |
|---|---|---|
| `.claude/last30days.env` | Project tree (walk parents from `cwd` until home or root) | Overrides global file |
| `~/.config/last30days/.env` | User default | Fallback when no project file |
| `LAST30DAYS_CONFIG_DIR/.env` | Custom global directory | Replaces default `~/.config/last30days` |
Project-scoped env is the preferred pattern for per-client folders: `cd` into the client directory and run `/last30days` without wrapper scripts.
### macOS Keychain (additive, lowest priority)
On Darwin, `security find-generic-password` loads items with service name `last30days-{KEY}` (for example `last30days-BRAVE_API_KEY`). Keys mirrored in `scripts/setup-keychain.sh` include all major API credentials in `env.KEYCHAIN_KEYS`. Keychain never overrides values already present in `.env` or `os.environ`.
## Output paths
### `LAST30DAYS_MEMORY_DIR`
| Platform | Default when unset |
|---|---|
| Linux / macOS | `~/Documents/Last30Days/` |
| Windows | `C:\Users\<you>\Documents\Last30Days\` |
The skill passes `--save-dir="${LAST30DAYS_MEMORY_DIR}"` on agent invocations. Override with the env var or per-run `--save-dir <path>`.
### Filename rules
Each saved run uses a slug derived from the topic:
| Pattern | When |
|---|---|
| `<slug>-raw[-suffix].md` | Default markdown artifact |
| `<slug>-raw-html[-suffix].html` | `--emit=html` |
| `<slug>-raw[-suffix].json` | `--emit=json` |
- **`--save-suffix <name>`** — isolates variants (for example per client: `--save-suffix=acme`).
- **Collision** — same slug + suffix on the same calendar day overwrites; on a different day the engine appends `-YYYY-MM-DD` before the extension.
The footer line `📎 Raw results saved to …/<slug>-raw.md` is the canonical pointer in synthesized output.
### Trend-monitoring storage (separate from markdown saves)
| Artifact | Default path |
|---|---|
| SQLite research store | `~/.local/share/last30days/research.db` |
| Briefing JSON snapshots | `~/.local/share/last30days/briefs/` |
| Last-run metadata | `{LAST30DAYS_CONFIG_DIR or ~/.config/last30days}/last-run.json` |
Markdown under `LAST30DAYS_MEMORY_DIR` and SQLite under `~/.local/share/last30days/` are independent; `--store` / `LAST30DAYS_STORE` only affects SQLite.
## Reasoning provider priority
Headless runs (cron, watchlist, direct CLI without a hosting model supplying `--plan`) need one provider for planner + rerank. Pin explicitly or rely on auto-detect.
<ParamField body="LAST30DAYS_REASONING_PROVIDER" type="string" default="auto">
`auto` | `gemini` | `openai` | `xai` | `openrouter`. Resolved in `lib/providers.py` → `resolve_runtime()`.
</ParamField>
**Auto-detect order** (first match wins):
| Priority | Provider | Credential signal |
|---|---|---|
| 1 | Gemini | `GOOGLE_API_KEY`, `GEMINI_API_KEY`, or `GOOGLE_GENAI_API_KEY` |
| 2 | OpenAI | `OPENAI_API_KEY` with `OPENAI_AUTH_STATUS=ok` |
| 3 | xAI | `XAI_API_KEY` |
| 4 | OpenRouter | `OPENROUTER_API_KEY` |
| 5 | Local | No keys — deterministic planner, `local-score` rerank |
<Info>
When `/last30days` runs inside Claude Code, Codex, Gemini CLI, or similar, the **host model** is the reasoning provider for planning and synthesis. API keys above matter for headless `last30days.py` runs and for engine-side rerank when the host does not pass `--plan`.
</Info>
### Model pins
| Variable | Role |
|---|---|
| `LAST30DAYS_PLANNER_MODEL` | Override planner model for the resolved provider |
| `LAST30DAYS_RERANK_MODEL` | Override rerank model; Gemini `deep` profile defaults rerank to `gemini-3.1-pro-preview` |
| `OPENAI_MODEL_PIN` | Legacy pin alias consumed alongside provider defaults |
| `XAI_MODEL_PIN` | Legacy pin alias for X search and xAI defaults |
| `LAST30DAYS_X_MODEL` | X search model when using xAI backend |
Provider defaults (when pins unset): Gemini `gemini-3.1-flash-lite`, OpenAI `gpt-5.4-nano`, xAI `grok-4-1-fast`, OpenRouter `google/gemini-3.1-flash-lite-preview`. Gemini planner/rerank models must use the `gemini-3.1-*` prefix or the engine raises at startup.
### X search backend pin
<ParamField body="LAST30DAYS_X_BACKEND" type="string">
Optional pin: `xai` or `bird`. When unset, resolution order is `XAI_API_KEY` → explicit `AUTH_TOKEN` + `CT0` (Bird) → `xurl` CLI (OAuth2).
</ParamField>
## Web search backend priority
Grounding supplements (`grounding` source) power `--auto-resolve` and Step 2 web supplements when the host has no native WebSearch.
<ParamField body="--web-backend" type="choice" default="auto">
CLI choices: `auto`, `brave`, `exa`, `serper`, `parallel`, `none`. Implemented in `lib/grounding.py` → `web_search()`.
</ParamField>
**Auto-detect order** (in `grounding.web_search` and `pipeline.diagnose`):
| Priority | Backend | Key |
|---|---|---|
| 1 | Brave | `BRAVE_API_KEY` |
| 2 | Exa | `EXA_API_KEY` |
| 3 | Serper | `SERPER_API_KEY` |
| 4 | Parallel | `PARALLEL_API_KEY` |
| 5 | None / host | Empty key set — rely on harness native WebSearch (Claude Code, Codex, Gemini) |
`--web-backend=none` drops `grounding` from the active source list. Pinning `brave` without `BRAVE_API_KEY` raises a runtime error.
<Warning>
Thin cross-host results often trace to missing web keys on headless runs. Run `--diagnose` and compare `native_web_backend` against a machine that has Brave (or another key) configured.
</Warning>
## Source credentials and toggles
### API keys by source
| Source | Key(s) | Notes |
|---|---|---|
| Reddit, HN, Polymarket | none | Always on |
| GitHub | `gh` CLI auth or `GITHUB_TOKEN` | CLI preferred |
| YouTube | `yt-dlp` on PATH; optional `LAST30DAYS_YOUTUBE_SSH_HOST` | SSH-routes yt-dlp for datacenter bot-wall |
| X / Twitter | `XAI_API_KEY` **or** `AUTH_TOKEN`+`CT0` **or** `FROM_BROWSER` **or** xurl OAuth | See X backend table above |
| TikTok / Instagram / Threads | `SCRAPECREATORS_API_KEY` | Comma-separated keys rotate per run; legacy alias `SCRAPE_CREATORS_API_KEY` accepted |
| Pinterest | `SCRAPECREATORS_API_KEY` + request `pinterest` via `--search` | Opt-in at query time |
| Bluesky | `BSKY_HANDLE`, `BSKY_APP_PASSWORD` | App password `xxxx-xxxx-xxxx-xxxx` |
| Bluesky host | `BSKY_SEARCH_HOST` | Default `api.bsky.app` |
| Truth Social | `TRUTHSOCIAL_TOKEN` | Bearer from browser session |
| Web grounding | `BRAVE_API_KEY` / `EXA_API_KEY` / `SERPER_API_KEY` / `PARALLEL_API_KEY` | One is enough for auto mode |
| Perplexity Sonar | `OPENROUTER_API_KEY` + `INCLUDE_SOURCES=perplexity` | Additive source |
| Deep Research | `OPENROUTER_API_KEY` + `--deep-research` | ~$0.90/query |
| Apify (legacy) | `APIFY_API_TOKEN` | Fallback when ScrapeCreators exhausted |
| Xiaohongshu | `XIAOHONGSHU_API_BASE` | Default `http://host.docker.internal:18060` |
| Xquik | `XQUIK_API_KEY` | Alternate X search |
### `INCLUDE_SOURCES` and `EXCLUDE_SOURCES`
Comma-separated, case-insensitive lists in `.env` or environment.
| Variable | Behavior |
|---|---|
| `INCLUDE_SOURCES` | **Required** for `perplexity`, `youtube_comments`, `tiktok_comments`. When non-empty, quality scoring treats sources not listed as intentionally disabled. |
| `EXCLUDE_SOURCES` | Removes matching sources from `pipeline.available_sources()` (for example `EXCLUDE_SOURCES=threads,pinterest`). |
ScrapeCreators-backed TikTok, Instagram, and Threads enter the available set when `SCRAPECREATORS_API_KEY` is set; use `EXCLUDE_SOURCES` or `--search` to narrow the fan-out.
### Browser cookie extraction
<ParamField body="FROM_BROWSER" type="string">
`off` — disable extraction. `firefox` / `chrome` / `safari` — single browser. `auto` — try Firefox, Safari, then Chrome (Chrome may trigger Keychain prompts on macOS). **Default (unset):** Firefox and Safari only (silent).
</ParamField>
Extracted cookies map to `AUTH_TOKEN`/`CT0` (X) and `TRUTHSOCIAL_TOKEN` without overwriting explicit env values.
### Other engine tunables
| Variable | Default | Purpose |
|---|---|---|
| `LAST30DAYS_TRANSCRIPT_TIMEOUT` | `30` (seconds) | Instagram/SC transcript fetch timeout |
| `LAST30DAYS_DEBUG` | off | HTTP debug logging to stderr (`1` / `true` / `yes`) |
| `LAST30DAYS_SKIP_PREFLIGHT` | off | Bypass Class-1 keyword-trap refuse gate (exit 2) |
| `SETUP_COMPLETE` | — | Set by setup wizard after first configuration |
| `CODEX_AUTH_FILE` | `~/.codex/auth.json` | Codex auth path (OpenAI via API key is preferred; Codex JWT path is not used for planner auth in current `get_openai_auth`) |
## Trend monitoring toggles
Snapshot mode (default) writes slug-named markdown only. Continuous monitoring adds SQLite persistence plus optional scheduling scripts.
### Persistence toggle
| Mechanism | Effect |
|---|---|
| `--store` | Persist ranked findings to SQLite after the run |
| `LAST30DAYS_STORE=1` (or `true` / `yes`) | Same as `--store` on every run; flag and env are additive |
Findings dedupe on `source_url` (UNIQUE). Tables: `topics`, `research_runs`, `findings`, `settings` (`scripts/store.py`).
### Watchlist and briefings
| Script | Role |
|---|---|
| `scripts/watchlist.py` | `add`, `remove`, `list`, `run-one`, `run-all`, `config` — schedule metadata + Slack/webhook delivery on new findings |
| `scripts/briefing.py` | `generate`, `generate --weekly`, `show [--date DATE]` — structured digest data under `briefs/` |
Watchlist spawns the engine with `--quick` and `--lookback-days 90`. The stored `schedule` field is metadata; **cron / Task Scheduler / GitHub Actions** must call `run-one` or `run-all`.
<Steps>
<Step title="Baseline snapshot">
Run once with persistence: `/last30days "<topic>" --days=30 --store` (or `LAST30DAYS_STORE=1` in `.env`).
</Step>
<Step title="Register topic">
`python3 skills/last30days/scripts/watchlist.py add "<topic>" --weekly`
</Step>
<Step title="Schedule runs">
External scheduler: `python3 skills/last30days/scripts/watchlist.py run-all`
</Step>
<Step title="Weekly digest">
`python3 skills/last30days/scripts/briefing.py generate --weekly`
</Step>
</Steps>
## Verification
```bash
python3 skills/last30days/scripts/last30days.py --diagnose
```
Emits JSON with `providers`, `reasoning_provider`, `x_backend`, `native_web_backend`, `has_scrapecreators`, `has_github`, and `available_sources` — no full search executed.
<Check>
After editing `.env`, re-run `--diagnose` before a production topic. Confirm `native_web_backend` and expected sources appear under `available_sources`.
</Check>
## Example `.env` skeleton
```bash
# Reasoning (one provider; auto picks first match)
GOOGLE_API_KEY=<gemini-key>
# Web grounding (one key is enough)
BRAVE_API_KEY=<brave-key>
# Optional ScrapeCreators family
SCRAPECREATORS_API_KEY=<sc-key>
INCLUDE_SOURCES=tiktok,instagram,threads
# Always-on persistence
LAST30DAYS_STORE=1
# Output location
LAST30DAYS_MEMORY_DIR=$HOME/Clients/acme/Research/Last30Days
```
```bash
chmod 600 ~/.config/last30days/.env
# or: chmod 600 .claude/last30days.env
```
## Related pages
<CardGroup>
<Card title="Configure sources" href="/configure-sources">
Credential layers, setup wizard, and per-source availability checks.
</Card>
<Card title="Per-client setup" href="/per-client-setup">
Project-scoped `.claude/last30days.env`, save-dir isolation, and client wrappers.
</Card>
<Card title="Trend monitoring" href="/trend-monitoring">
SQLite schema, watchlist delivery, briefing synthesis, and cadence patterns.
</Card>
<Card title="CLI reference" href="/cli-reference">
Full `last30days.py` flag surface including `--emit` and targeting flags.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
`--diagnose` recovery, thin results, and stale marketplace installs.
</Card>
</CardGroup>
---
## 15. CLI reference
> last30days.py positional topic, all argparse flags, --emit choices, targeting flags, and direct CLI vs slash-command constraints.
- Page Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/15-cli-reference.md
- Generated: 2026-06-04T23:22:55.940Z
### Source Files
- `skills/last30days/scripts/last30days.py`
- `CONFIGURATION.md`
- `tests/test_regression.py`
- `skills/last30days/scripts/verify_v3.py`
- `AGENTS.md`
- `pyproject.toml`
---
title: "CLI reference"
description: "last30days.py positional topic, all argparse flags, --emit choices, targeting flags, and direct CLI vs slash-command constraints."
---
`skills/last30days/scripts/last30days.py` is the v3 research engine: a Python 3.12+ `argparse` program that accepts a multi-word positional topic, fans out retrieval through `lib.pipeline`, and prints a rendered report on stdout while progress, promos, saves, and store writes go to stderr. The Agent Skills slash command (`/last30days`) is the primary product surface; direct CLI invocation is the supported path for scripting, cron, watchlist, verification, and engine testing.
## Invocation surfaces
```text
Harness (Claude Code, Codex, Cursor, Gemini, …)
│
│ User: /last30days <topic> (no shell pipes; model maps intent → flags)
▼
SKILL.md contract ──► Bash: ${LAST30DAYS_PYTHON} ${SKILL_DIR}/scripts/last30days.py …
│
│ Operator / cron / watchlist / pytest
▼
python3 skills/last30days/scripts/last30days.py "<topic>" [flags…]
│
▼
stdout: rendered --emit payload
stderr: banner, promos, saves, store counts, quality nudges
```
<Warning>
Slash commands do not pass shell mechanics through. `/last30days OpenClaw --emit=html | pbcopy` is invalid in any harness. Use natural language in the slash form and let the hosting model translate intent into engine flags, or run the full `python3 …` command in a real shell when you need pipes, redirection, or explicit flags.
</Warning>
| Surface | When to use | Typical command shape |
| --- | --- | --- |
| Slash `/last30days` | Interactive research in an agent harness | Topic only; model adds `--emit=compact`, `--plan`, targeting, `--save-dir` per SKILL.md |
| Direct CLI | Cron, CI, watchlist, local debugging | Full flag list; stdout captured or redirected |
| Topic `setup` | First-run credential wizard | `last30days.py setup` plus unknown args (see Setup) |
From a repo checkout (dev/fallback):
```bash
python3 skills/last30days/scripts/last30days.py "openclaw skills" --emit=compact
uv run pytest tests/test_cli_v3.py # CLI contract tests
```
Installed skill layout (production):
```bash
"${LAST30DAYS_PYTHON}" "${SKILL_DIR}/scripts/last30days.py" "$TOPIC" --emit=compact --save-dir="${LAST30DAYS_MEMORY_DIR}" --save-suffix=v3
```
`SKILL_DIR` must be the directory containing the `SKILL.md` the harness loaded. `LAST30DAYS_PYTHON` must be Python 3.12+ (resolved once per SKILL.md session).
## Positional topic
<ParamField body="topic" type="string (0+ tokens)" required>
Multi-word research query. Tokens are joined with spaces: `biosecurity ai agents` → `"biosecurity ai agents"`.
</ParamField>
| Case | Behavior |
| --- | --- |
| Normal topic | Required unless `--diagnose` alone (see Diagnostics) |
| Empty topic | Usage printed to stderr; exit code `2` |
| `setup` (case-insensitive) | Routes to setup wizard instead of research |
| `… vs …` / `… versus …` | Auto-enters comparison fan-out when the planner splits ≥2 entities (see Comparison) |
## `--emit` modes
Default: `compact`. All modes write human-oriented output to **stdout**; operational messages use **stderr**.
| Value | Renderer | Primary use |
| --- | --- | --- |
| `compact` | `render.render_compact` | Default skill path; badge + synthesis-oriented envelope |
| `md` | Same as `compact` | Debug/inspection; SKILL.md disallows as primary user-facing emit in harness runs |
| `json` | `schema.to_dict(report)` | Automation, tests, watchlist, `verify_v3.py` |
| `context` | `render.render_context` | Trimmed agent context (cluster-limited) |
| `html` | `html_render.render_html` | Shareable offline brief; pairs with `--synthesis-file` |
Comparison runs (`--competitors`, explicit list, or vs-topic) use parallel emit helpers: JSON wraps `{comparison, entities, reports[]}`; compact/md/context/html use multi-entity renderers.
<Note>
`--synthesis-file` is only honored when `--emit=html`. Otherwise the engine logs a warning and ignores the file.
</Note>
## Depth and source selection
| Flag | Effect |
| --- | --- |
| `--quick` | `depth="quick"` — lower latency (`per_stream_limit` 6, `pool_limit` 15, `rerank_limit` 12) |
| *(none)* | `depth="default"` |
| `--deep` | `depth="deep"` — higher recall (limits 20 / 60 / 60) |
<ParamField body="--search" type="comma-separated sources">
Restricts retrieval to named sources. Aliases normalized via `SEARCH_ALIAS` (`hn`→`hackernews`, `web`→`grounding`, `bsky`→`bluesky`, `truth`→`truthsocial`, `xhs`→`xiaohongshu`). Unknown or empty lists exit with an error.
</ParamField>
Valid source keys (also used by `--mock`): `reddit`, `x`, `youtube`, `tiktok`, `instagram`, `hackernews`, `bluesky`, `truthsocial`, `polymarket`, `grounding`, `xiaohongshu`, `github`, `perplexity`, `threads`, `pinterest`, `xquik`, `digg`.
## Targeting flags
Per-run targeting merges into `pipeline.run()` and is recorded in `report.artifacts["resolved"]`. When any of these are set, the renderer can emit a pre-research status warning if Step 0.55 was skipped in harness runs.
| Flag | Purpose |
| --- | --- |
| `--x-handle` | Primary X handle (leading `@` stripped) |
| `--x-related` | Comma-separated related handles (lower weight) |
| `--subreddits` | Comma-separated subs (`r/` prefix stripped) |
| `--tiktok-hashtags` | Hashtags without `#` |
| `--tiktok-creators` | Creator handles |
| `--ig-creators` | Instagram handles |
| `--github-user` | GitHub username for person-mode |
| `--github-repo` | Comma-separated `owner/repo` (canonicalized unless from `--auto-resolve`) |
| `--polymarket-keywords` | Comma-separated title filters for Polymarket disambiguation |
| `--days` / `--lookback-days` | Lookback window (default `30`; watchlist uses `90`) |
### Plan and auto-resolve
<ParamField body="--plan" type="JSON string or file path">
External query plan; skips the internal LLM planner when valid JSON loads. Invalid JSON logs to stderr and leaves planning unset.
</ParamField>
<ParamField body="--auto-resolve" type="boolean">
Engine-side Step 0.55/0.75 for hosts without WebSearch: discovers subreddits, X handle, GitHub user/repos before planning. Skipped when `--plan` is already provided.
</ParamField>
Harness models with WebSearch are expected to resolve targeting themselves and pass `--plan` or per-entity `--competitors-plan` instead of relying on `--auto-resolve`.
## Web backends and deep research
<ParamField body="--web-backend" type="enum" default="auto">
Choices: `auto`, `brave`, `exa`, `serper`, `parallel`, `none`. `auto` tries Brave → Exa → Serper → Parallel per config.
</ParamField>
| Flag | Requirement |
| --- | --- |
| `--deep-research` | Sets `OPENROUTER_API_KEY`; auto-appends `perplexity` to `INCLUDE_SOURCES`; exits `1` if key missing |
## Comparison mode
Comparison activates when any of these is true:
1. `--competitors` or `--competitors-list`
2. Topic contains ` vs ` or ` versus ` and splits into ≥2 entities
<ParamField body="--competitors" type="optional int (1–6)" default="2 when bare">
`--competitors` alone → discover **2** peers (3-way: topic + 2). `--competitors 4` → four discovered peers. Counts < 1 exit `2`; counts > 6 clamp with stderr warning.
</ParamField>
<ParamField body="--competitors-list" type="comma-separated names">
Skips discovery; list length wins over numeric `--competitors`. Empty list exits `2`. More than six entries clamp.
</ParamField>
<ParamField body="--competitors-plan" type="JSON object or file path">
Per-entity Step 0.55 targeting when comparison mode is already active. Top-level map: `{entity_name: {x_handle?, x_related?, subreddits?, github_user?, github_repos?, context?}}`. Keys normalized to lowercase; unknown fields warned and dropped. Invalid JSON or non-dict top-level exits `2`.
</ParamField>
Discovery requires a web backend or `--mock`; otherwise stderr prints the recommended WebSearch + `--competitors-plan` path and exits `2`. Fewer than two surviving sub-runs exits `1`.
Vs-topic routing sets the first entity as the main topic and remaining segments as the explicit competitor list. With `--save-dir`, comparison HTML saves one combined file plus per-entity raw files for peers.
## Output persistence
| Flag | Behavior |
| --- | --- |
| `--save-dir` | Writes slug-named file under directory; collision same day → date-stamped name |
| `--save-suffix` | Inserts suffix into filename (`topic-raw-SUFFIX.md`) |
| `--store` | Persists findings to SQLite (`scripts/store.py`) |
`LAST30DAYS_STORE=1` in environment or `~/.config/last30days/.env` enables store writes without the flag (same hybrid pattern as `LAST30DAYS_DEBUG`).
Save extensions follow emit: `.json`, `.html` (`raw-html` infix), else `.md`. Footer display uses `~/…` posix paths under the user home.
## Diagnostics, mock, and debug
| Flag | Behavior |
| --- | --- |
| `--diagnose` | Prints JSON availability report (`pipeline.diagnose`); **no topic required**; exit `0` |
| `--mock` | Fixture retrieval; bypasses live APIs |
| `--debug` | Sets `LAST30DAYS_DEBUG=1` for HTTP logging |
Preflight refusals (class-1 traps) honor `LAST30DAYS_SKIP_PREFLIGHT` and return exit `2` with message on stderr.
## Setup subcommand
`parse_known_args` leaves trailing argv available for setup-only flags:
| Invocation | Extra argv | Result |
| --- | --- | --- |
| `last30days.py setup` | *(none)* | `run_auto_setup`; writes config; human-readable status on stderr |
| `last30days.py setup` | `--device-auth` | JSON device-auth results on stdout |
| `last30days.py setup` | `--github` | JSON GitHub auth results |
| `last30days.py setup` | `--openclaw` | JSON OpenClaw setup results |
## Exit codes
| Code | Typical cause |
| --- | --- |
| `0` | Success; diagnose-only; setup completed |
| `1` | Comparison fan-out < 2 entities; `--deep-research` without `OPENROUTER_API_KEY` |
| `2` | Missing topic; preflight refusal; competitor parse/validation errors; unreadable `--synthesis-file` |
Unsupported `--emit` values raise `SystemExit` inside emit helpers (parser choices prevent this in normal use).
## Flag inventory
### Output and persistence
| Flag | Type | Default |
| --- | --- | --- |
| `--emit` | `compact\|json\|context\|md\|html` | `compact` |
| `--save-dir` | path | — |
| `--save-suffix` | string | — |
| `--synthesis-file` | path | — (html only) |
| `--store` | boolean | off (unless env) |
### Retrieval profile
| Flag | Type |
| --- | --- |
| `--quick` | flag |
| `--deep` | flag |
| `--search` | string |
| `--mock` | flag |
| `--lookback-days` / `--days` | int (30) |
| `--web-backend` | enum (`auto`) |
| `--deep-research` | flag |
### Targeting and planning
| Flag | Type |
| --- | --- |
| `--plan` | JSON or file |
| `--auto-resolve` | flag |
| `--x-handle`, `--x-related` | string |
| `--subreddits` | string |
| `--tiktok-hashtags`, `--tiktok-creators` | string |
| `--ig-creators` | string |
| `--github-user`, `--github-repo` | string |
| `--polymarket-keywords` | string |
### Comparison
| Flag | Type |
| --- | --- |
| `--competitors` | optional int (const 2) |
| `--competitors-list` | string |
| `--competitors-plan` | JSON or file |
### Operations
| Flag | Type |
| --- | --- |
| `--diagnose` | flag |
| `--debug` | flag |
## Examples
<CodeGroup>
```bash title="Smoke test (repo root)"
python3 skills/last30days/scripts/last30days.py "test topic" --mock --emit=json
```
```bash title="Availability probe"
python3 skills/last30days/scripts/last30days.py --diagnose
```
```bash title="Headless comparison with explicit peers"
python3 skills/last30days/scripts/last30days.py "OpenAI" \
--competitors-list "Anthropic,xAI" \
--emit=json --mock
```
```bash title="HTML brief with saved synthesis"
python3 skills/last30days/scripts/last30days.py "OpenClaw" \
--emit=html \
--synthesis-file /tmp/synthesis.md \
--save-dir ~/Documents/Last30Days
```
```bash title="Cron-friendly quick pass"
python3 skills/last30days/scripts/last30days.py "british airways middle east" \
--quick --search=grounding,hackernews --emit=json
```
</CodeGroup>
<RequestExample>
```bash
python3 skills/last30days/scripts/last30days.py "openclaw vs nanoclaw" --mock --emit=json
```
</RequestExample>
<ResponseExample>
```json
{
"comparison": true,
"entities": ["openclaw", "nanoclaw"],
"reports": [
{ "entity": "openclaw", "report": { "topic": "openclaw", "query_plan": { } } },
{ "entity": "nanoclaw", "report": { "topic": "nanoclaw", "query_plan": { } } }
]
}
```
</ResponseExample>
## Harness vs CLI constraints
<Steps>
<Step title="Slash command (default)">
User supplies topic and intent in natural language. The model reads SKILL.md, resolves `SKILL_DIR` and `LAST30DAYS_PYTHON`, runs Step 0.55 / plan generation when required, and invokes the engine with `--emit=compact` (not `md` as the primary path), plus `--save-dir` / `--save-suffix=v3` and targeting flags. Output is passed through verbatim (badge, evidence envelope, footer).
</Step>
<Step title="Direct CLI (scripting)">
Operator supplies every flag explicitly. Shell pipes, env exports, and cron wrappers are valid. Use `--emit=json` or `--mock` for automation; use `--diagnose` before debugging missing sources.
</Step>
</Steps>
<Tip>
New engine flags are incomplete until SKILL.md documents them — harness models only learn flags present in the skill contract. For env-backed defaults and save paths, see the configuration reference rather than duplicating every variable here.
</Tip>
## Related pages
<CardGroup>
<Card title="Skill, engine, and harness" href="/skill-engine-harness">
Boundaries between the Agent Skills package, Python engine, and multi-harness runtimes.
</Card>
<Card title="Output contract" href="/output-contract">
Badge line, LAWs voice rules, evidence envelopes, and emit semantics the harness must pass through.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
Environment variables, `.env` lookup order, reasoning and web-backend priority, and store toggles.
</Card>
<Card title="Comparison mode" href="/comparison-mode">
Discovery fan-out, `--competitors-plan` schema, and multi-entity synthesis expectations.
</Card>
<Card title="Develop and test" href="/develop-and-test">
`uv run pytest`, `verify_v3.py`, and regression fixtures for the CLI surface.
</Card>
</CardGroup>
---
## 16. Data sources
> Per-platform retrieval modules, keyless vs keyed sources, --search alias map, and engagement scoring inputs by source.
- Page Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/16-data-sources.md
- Generated: 2026-06-04T23:23:29.213Z
### Source Files
- `skills/last30days/scripts/lib/pipeline.py`
- `CONFIGURATION.md`
- `skills/last30days/scripts/lib/reddit_public.py`
- `skills/last30days/scripts/lib/bird_x.py`
- `skills/last30days/scripts/lib/github.py`
- `skills/last30days/scripts/lib/polymarket.py`
- `skills/last30days/scripts/lib/youtube_yt.py`
---
title: "Data sources"
description: "Per-platform retrieval modules, keyless vs keyed sources, --search alias map, and engagement scoring inputs by source."
---
Each research run fans out through `skills/last30days/scripts/lib/pipeline.py`, which resolves which platforms are callable (`available_sources`), normalizes CLI `--search` tokens, dispatches per-source retrieval in `_retrieve_stream`, and passes normalized `SourceItem` records through `signals.annotate_stream` for engagement-aware local ranking before fusion and LLM rerank.
## Source inventory
Canonical source names match `pipeline.MOCK_AVAILABLE_SOURCES` and `last30days.parse_search_flag` validation. Retrieval modules live under `skills/last30days/scripts/lib/`; `_retrieve_stream` is the single dispatch table.
| Source | Module | Auth / prerequisites | Default availability |
|--------|--------|----------------------|----------------------|
| `reddit` | `reddit_public.py` → `reddit_keyless.py`; backup `reddit.py` (ScrapeCreators) | Keyless tiers first; `SCRAPECREATORS_API_KEY` for paid backup | Always |
| `hackernews` | `hackernews.py` | None (Algolia) | Always |
| `polymarket` | `polymarket.py` | None (Gamma API) | Always |
| `github` | `github.py` | `GITHUB_TOKEN` or `gh` CLI | When token/CLI present |
| `youtube` | `youtube_yt.py` | `yt-dlp` on PATH; optional `SCRAPECREATORS_API_KEY` fallback + comment enrichment | When `yt-dlp` or SC YouTube path available |
| `grounding` | `grounding.py` | One of `BRAVE_API_KEY`, `EXA_API_KEY`, `SERPER_API_KEY`, `PARALLEL_API_KEY` (or forced via `--web-backend`) | When a web-search key is set |
| `x` | `bird_x.py`, `xai_x.py`, or `xurl_x.py` | Bird: `AUTH_TOKEN`+`CT0`+Node; xAI: `XAI_API_KEY`; xurl: authenticated CLI | When `env.get_x_source()` resolves a backend |
| `tiktok` | `tiktok.py` | `SCRAPECREATORS_API_KEY` or `APIFY_API_TOKEN` | When SC/Apify key set (see opt-out below) |
| `instagram` | `instagram.py` | `SCRAPECREATORS_API_KEY` | When SC key set |
| `threads` | `threads.py` | `SCRAPECREATORS_API_KEY` | When SC key set |
| `bluesky` | `bluesky.py` | `BSKY_HANDLE` + `BSKY_APP_PASSWORD` | When credentials set |
| `truthsocial` | `truthsocial.py` | `TRUTHSOCIAL_TOKEN` | When token set |
| `digg` | `digg.py` | `digg-pp-cli` on PATH | When CLI installed |
| `perplexity` | `perplexity.py` | `OPENROUTER_API_KEY` + `INCLUDE_SOURCES` contains `perplexity` (or explicit `--search=perplexity`) | Opt-in |
| `pinterest` | `pinterest.py` | `SCRAPECREATORS_API_KEY` | Only when `--search` (or planner) explicitly requests `pinterest` |
| `xiaohongshu` | `xiaohongshu_api.py` | Reachable `XIAOHONGSHU_API_BASE` with logged-in session | Only when explicitly requested and health probe passes |
| `xquik` | `xquik.py` | `XQUIK_API_KEY` | When key set |
<Note>
`available_sources()` is the runtime gate. User-facing docs sometimes mention `INCLUDE_SOURCES=tiktok`; in code, TikTok and Instagram are added whenever `SCRAPECREATORS_API_KEY` is set. Use `EXCLUDE_SOURCES=tiktok,instagram` to deny them without removing the key.
</Note>
## Keyless vs keyed retrieval
**Keyless (no API key in config):** Reddit (public/keyless pipeline), Hacker News, Polymarket, and GitHub when only the unauthenticated search API is used (higher rate limits with `GITHUB_TOKEN` or `gh`).
**Keyed or tool-backed:** Everything else depends on env vars, CLI tools, or session cookies as in the table above.
### Reddit tiering
Reddit is the most layered source:
1. **Tier 0** — legacy `reddit.com/search.json` via `reddit_public` (often 403; cheap one-shot).
2. **Tier 1** — RSS discovery (`reddit_rss.py`) inside `reddit_keyless.py`.
3. **Tier 2** — Shreddit comment enrichment (`reddit_shreddit.py`).
4. **Backup** — `reddit.search_and_enrich` via ScrapeCreators when keyless tiers return empty and `SCRAPECREATORS_API_KEY` is set.
Pipeline passes `raw_topic` (not only the planner sub-query) into Reddit/YouTube/TikTok/Instagram so query expansion stays anchored to the user topic.
### X backend selection
`env.get_x_source()` picks the active X path: `LAST30DAYS_X_BACKEND` pin, else `xai` if `XAI_API_KEY`, else Bird when explicit `AUTH_TOKEN`/`CT0` and vendored `bird-search.mjs`+Node, else `xurl`. Browser cookie probing is disabled in normal runs (`BIRD_DISABLE_BROWSER_COOKIES=1`).
### YouTube dual path
`_retrieve_stream` tries `youtube_yt.search_and_transcribe` (yt-dlp) first, then `search_youtube_sc` when SC is configured. Optional `youtube_comments` in `INCLUDE_SOURCES` triggers `enrich_with_comments` on top-N videos by engagement.
### Web grounding (`grounding`)
Registered as source `grounding`. CLI alias `web` maps to it. Backends: Brave, Exa, Serper, Parallel — selected from keys or `--web-backend`. Planner may append `grounding` to every sub-query when available; `--web-backend none` removes it.
## `--search` alias map
The `--search` flag accepts a comma-separated list. Tokens are lowercased, aliased, deduped, and validated against `MOCK_AVAILABLE_SOURCES`.
| CLI token | Canonical source |
|-----------|------------------|
| `hn` | `hackernews` |
| `bsky` | `bluesky` |
| `truth` | `truthsocial` |
| `web` | `grounding` |
| `xhs` | `xiaohongshu` |
| `xquik` | `xquik` |
| *(any other valid name)* | unchanged (e.g. `reddit`, `youtube`) |
```python
# pipeline.SEARCH_ALIAS (representative)
SEARCH_ALIAS = {
"hn": "hackernews",
"bsky": "bluesky",
"truth": "truthsocial",
"web": "grounding",
"xhs": "xiaohongshu",
"xquik": "xquik",
}
```
After normalization, `run()` intersects the list with `available_sources()`. Unknown tokens exit with `Unknown search source`. An empty list exits with `--search requires at least one source.`
<Warning>
Slash-command invocations do not pass shell flags. Restricting sources belongs in engine flags the hosting model translates, or in direct CLI: `python3 skills/last30days/scripts/last30days.py "topic" --search=reddit,youtube`.
</Warning>
## INCLUDE_SOURCES, EXCLUDE_SOURCES, and per-run opt-in
| Mechanism | Effect |
|-----------|--------|
| `EXCLUDE_SOURCES` | Removed from `available_sources()` (comma-separated, case-insensitive). Primary way to suppress TikTok/Instagram while keeping `SCRAPECREATORS_API_KEY`. |
| `INCLUDE_SOURCES=perplexity` | Enables Perplexity Sonar when `OPENROUTER_API_KEY` is set (unless `--search=perplexity` forces it). |
| `INCLUDE_SOURCES=youtube_comments` | Top-video comment enrichment via ScrapeCreators (requires SC key). |
| `INCLUDE_SOURCES=tiktok_comments` | Top-post TikTok comment enrichment (requires SC key). |
| Explicit `--search=pinterest` / `xiaohongshu` | Required for those sources even when keys/health checks pass. |
`quality_nudge.py` treats a **non-empty** `INCLUDE_SOURCES` as an allowlist when judging silent Instagram failures (zero items may be expected if `instagram` was omitted intentionally).
## Retrieval dispatch flow
```mermaid
flowchart TB
subgraph cli [CLI / harness]
L30[last30days.py]
SF["--search comma list"]
end
subgraph pipe [pipeline.py]
NS[normalize_requested_sources]
AV[available_sources]
PL[planner.plan_query]
RS["_retrieve_stream per subquery × source"]
NORM["_normalize_score_dedupe"]
end
subgraph mods [lib/*.py source modules]
R[reddit_public / reddit]
X[bird_x / xai_x / xurl_x]
YT[youtube_yt]
PM[polymarket]
GH[github]
GR[grounding]
OTH[tiktok instagram hn bluesky ...]
end
L30 --> SF --> NS
NS --> AV --> PL --> RS
RS --> R & X & YT & PM & GH & GR & OTH
RS --> NORM
```
Parallel fan-out uses a `ThreadPoolExecutor` sized from sub-query × source count. `MAX_SOURCE_FETCHES` caps **X** at two fetches per run to limit duplicate API cost.
## Normalization contract
`normalize.normalize_source_items()` maps each source’s raw dicts into `schema.SourceItem` with a shared `engagement` dict. Per-source normalizers are registered by source name (`reddit`, `x`, `youtube`, `polymarket`, etc.). Comment threads from Reddit, YouTube, and TikTok are remapped to `{score, excerpt}` in metadata `top_comments` for downstream scoring and rendering.
## Engagement scoring
Engagement is computed in two stages: **raw fields** from retrieval modules, then **pipeline scores** in `signals.py`.
### Stage 1 — Raw `engagement` dict by source
| Source | Typical `engagement` keys (from retrieval / normalize) |
|--------|--------------------------------------------------------|
| `reddit` | `score`, `num_comments`, `upvote_ratio`; top comment via metadata |
| `x` / `xquik` | `likes`, `reposts`, `replies`, `quotes` |
| `youtube` | `views`, `likes`, `comments` |
| `tiktok` / `instagram` | `views`, `likes`, `comments` (short-form video normalizer) |
| `hackernews` | `points`, `comments` |
| `bluesky` / `truthsocial` / `threads` | `likes`, `reposts`, `replies`, `quotes` (microblog shape) |
| `polymarket` | `volume` (from `volume1mo` or `volume24hr`), `liquidity` |
| `digg` | `postCount`, `uniqueAuthors`, `rank_score` |
| `pinterest` | `saves`, `comments` |
| `github` | `reactions`, `comments` (issue/PR search); profile modes use `reactions` as star-like signals |
| `xiaohongshu` | `likes`, `comments`, `favorites` |
| `grounding` / `perplexity` | Usually empty — ranking leans on text relevance and freshness |
### Stage 2 — `engagement_raw` and `local_rank_score`
`signals.engagement_raw()` applies per-source formulas (all use `log1p` on numeric inputs):
| Source | Formula class |
|--------|----------------|
| `reddit` | Custom: 50% score + 35% comments + 5% upvote_ratio + 10% top comment |
| `youtube` | Custom: 45% views + 32% likes + 13% comments + 10% top comment |
| `tiktok` | Custom: 45% views + 27% likes + 18% comments + 10% top comment |
| `x` | Weighted: likes 0.55, reposts 0.25, replies 0.15, quotes 0.05 |
| `instagram` | Weighted: views 0.50, likes 0.30, comments 0.20 |
| `hackernews` | Weighted: points 0.55, comments 0.45 |
| `bluesky` | Weighted: likes 0.40, reposts 0.30, replies 0.20, quotes 0.10 |
| `truthsocial` | Weighted: likes 0.45, reposts 0.30, replies 0.25 |
| `polymarket` | Weighted: volume 0.60, liquidity 0.40 |
| `digg` | Weighted: postCount 0.40, uniqueAuthors 0.30, rank_score 0.30 |
| Others with sparse metrics | `_generic_engagement`: mean of log1p across all numeric engagement values |
Scores are **min–max normalized to 0–100** within each stream batch (`signals.normalize`), stored on `SourceItem.engagement_score`.
**Local rank** (pre-fusion sort within a stream):
```
local_rank_score = 0.65 × local_relevance
+ 0.25 × (freshness / 100)
+ 0.10 × (engagement_score / 100)
```
`local_relevance` uses token overlap (`relevance.token_overlap_relevance`) with source-specific floors (e.g. YouTube views > 100k → min 0.3; GitHub `project-mode` label → min 0.8).
**Source quality** (`SOURCE_QUALITY` in `signals.py`) is a separate editorial multiplier used later in fusion weighting — not the same as engagement fields.
### Pruning rules tied to engagement
- `prune_low_relevance`: social sources with zero `engagement_score` need ~1.5× the relevance floor to survive.
- TikTok/Instagram: items with **< 1000 views** drop when other sources contributed items (unless that source is the only one in the batch).
## Planner capabilities (source roles)
`planner.SOURCE_CAPABILITIES` tags each source with roles (`discussion`, `social`, `video`, `web`, `market`, …). The planner (LLM or deterministic fallback) picks sub-queries and source sets from **available** sources and query intent. `INTENT_SOURCE_EXCLUSIONS` drops `polymarket` for `concept` and `how_to` intents.
## Verification
<Steps>
<Step title="List resolved sources">
```bash
python3 skills/last30days/scripts/last30days.py --diagnose
```
Confirms keys, X backend, native web backend, and `available_sources` for the current config.
</Step>
<Step title="Run a source-scoped smoke test">
```bash
python3 skills/last30days/scripts/last30days.py "test topic" --quick --search=grounding,hackernews --emit=compact
```
Uses aliases (`hn` → `hackernews`, `web` → `grounding`) the same way as full names.
</Step>
</Steps>
## Related pages
<CardGroup>
<Card title="Configure sources" href="/configure-sources">
Credential layers, INCLUDE_SOURCES, setup wizard, and --diagnose availability checks.
</Card>
<Card title="Research pipeline" href="/research-pipeline">
Planner sub-queries, parallel fan-out, fusion, dedupe, and depth profiles after retrieval.
</Card>
<Card title="CLI reference" href="/cli-reference">
--search, targeting flags, and direct CLI vs slash-command constraints.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
Environment variables and web-backend priority that gate grounding and reasoning.
</Card>
</CardGroup>
---
## 17. Skill contract
> SKILL.md runtime authority: STEP 0 stale-clone guard, pre-flight protocol, engine invocation rules, and SKILL_DIR substitution.
- Page Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/17-skill-contract.md
- Generated: 2026-06-04T23:23:05.702Z
### Source Files
- `skills/last30days/SKILL.md`
- `skills/last30days/scripts/lib/skill_meta.py`
- `AGENTS.md`
- `.claude-plugin/plugin.json`
- `skills/last30days/scripts/last30days.py`
---
title: "Skill contract"
description: "SKILL.md runtime authority: STEP 0 stale-clone guard, pre-flight protocol, engine invocation rules, and SKILL_DIR substitution."
---
`skills/last30days/SKILL.md` is the runtime authority for `/last30days`: the harness loads it when the slash command fires, the model must follow its step order before any engine Bash call, and `scripts/last30days.py` is invoked only through the `SKILL_DIR`-anchored path of the SKILL.md instance that was actually read. The contract is not optional metadata — it defines stale-install guards, pre-flight resolution, plan JSON handoff, and output-shape LAWs that the engine and `lib/render.py` enforce on stdout.
## Skill, engine, and harness boundaries
| Layer | Artifact | Role |
|-------|----------|------|
| **Skill** | `SKILL.md` + sibling `scripts/` | Agent-facing prose contract: steps, flags, synthesis LAWs, failure-mode guards |
| **Engine** | `scripts/last30days.py` + `scripts/lib/*` | Retrieval, planning intake, clustering, `--emit=compact` rendering |
| **Harness** | Claude Code, Codex, Cursor, `~/.agents/skills/`, etc. | Loads SKILL.md via Read; does not pass shell flags through slash invocations |
<Note>
Slash commands cannot carry pipes or flags (`/last30days topic --emit=html` is invalid in harnesses). The model translates user intent into engine flags per SKILL.md. Direct CLI (`python3 …/last30days.py`) is for scripting, cron, and engine testing only.
</Note>
```mermaid
flowchart TB
subgraph harness["Harness"]
U["User: /last30days topic"]
R["Read SKILL.md"]
end
subgraph skill["Skill contract — SKILL.md"]
S0["STEP 0 stale-clone guard"]
PF["Pre-flight 0.45 → 0.5 → 0.55 → 0.75"]
INV["SKILL_DIR engine invocation"]
LAW["OUTPUT CONTRACT / LAWs 1–8"]
end
subgraph engine["Engine — last30days.py"]
PRE["lib/preflight.py Class 1 gate"]
PIPE["lib/pipeline.py"]
REN["lib/render.py compact + warnings"]
end
U --> R --> S0 --> PF --> INV --> PRE --> PIPE --> REN
REN --> LAW
```
Version strings stay aligned across three surfaces: SKILL.md frontmatter (`version: "3.3.1"`), `.claude-plugin/plugin.json`, and `lib/skill_meta.read_skill_version()` used by `lib/render.py` for badge emission.
## STEP 0: Stale-clone self-check
STEP 0 runs **before** the rest of SKILL.md. It targets one known bad load path: Claude Code’s **marketplaces** git clone at `~/.claude/plugins/marketplaces/last30days-skill/`, which auto-restores to `origin/main` and can lag the versioned plugin cache.
<Steps>
<Step title="Detect cache SKILL.md">
Run the bash probe in STEP 0 to resolve `CLAUDE_CACHE_SKILL_MD` from `~/.claude/plugins/cache/last30days-skill/last30days/{version}/` (nested `skills/last30days/SKILL.md` or flat `SKILL.md`).
</Step>
<Step title="Compare load path">
If the SKILL.md you Read contains `/.claude/plugins/marketplaces/` **and** the cache path is non-empty, **stop** and re-read `$CLAUDE_CACHE_SKILL_MD` before continuing.
</Step>
<Step title="Otherwise continue">
Codex (`~/.codex/skills/`), `~/.agents/skills/`, `npx skills add` installs, and repo checkouts are valid — do not hop installs on those paths.
</Step>
</Steps>
<Warning>
A stale marketplace clone caused real regressions: models ran `--help` from the stale tree, missed flags such as `--competitors`, and improvised comparison flows. STEP 0 exists only for that Claude Code layout mismatch.
</Warning>
## Skill contract preface (three structural anchors)
Immediately after STEP 0, the **SKILL CONTRACT** block states that `/last30days` is a fixed research tool, not a generic “last 30 days” prompt. Three anchors prevent the documented v3.0.6-style improvisations:
1. **Mandatory badge** — first line `🌐 last30days v{VERSION} · synced {YYYY-MM-DD}` (engine emits it in `--emit=compact`; model passes through or builds it from `SKILL_DIR` + `plugin.json` / SKILL frontmatter).
2. **SKILL_DIR substitution** — engine path is the directory of the SKILL.md just Read; no install-path resolver walk.
3. **Read top-to-bottom** — no WebSearch-only answers, no `for dir in …` path discovery, no bare `python3 scripts/last30days.py` without pre-flight flags on named entities.
The **OUTPUT CONTRACT** (LAWs 1–8) was moved high in the file so long SKILL.md reads still load synthesis rules before the model stops reading (~line 1000). LAWs cover: no trailing `Sources:` (LAW 1), `What I learned:` vs comparison title (LAW 2), hyphen not em-dash (LAW 3), no `##` in GENERAL bodies (LAW 4), emoji-tree footer pass-through (LAW 5), no raw evidence clusters in user output (LAW 6), `--plan` on named entities (LAW 7), inline `[name](url)` citations (LAW 8).
## Invocation order (after STEP 0)
| Step | Name | Requirement |
|------|------|-------------|
| Runtime preflight | `LAST30DAYS_PYTHON` | Python 3.12+ (`python3.14` → `python3.12` → `python3`) |
| First-run | Step 0 wizard | If `~/.config/last30days/.env` lacks `SETUP_COMPLETE=true`, run `nux-wizard.md` |
| WebSearch load | STEP 0 (invoke section) | `ToolSearch select:WebSearch` first on Claude Code (deferred tool) |
| Intent | CRITICAL: Parse User Intent | `TOPIC`, `QUERY_TYPE`, optional `TARGET_TOOL`; branded one-liner, then 0.45 |
| 0.45 | Query quality pre-flight | Keyword-trap classes 1–4; ask or reframe before engine |
| 0.5 | Handle / repo resolution | `--x-handle`, `--x-related`, `--github-user`, `--github-repo` per checklist |
| 0.55 | Pre-research intelligence | Subreddits, TikTok/IG targets; mandatory when WebSearch exists |
| 0.75 | Query plan | Model-authored JSON → tmpfile → `--plan` |
| 1 | Research execution | `SKILL_DIR/scripts/last30days.py` foreground, 300s timeout, `--emit=compact` |
| 2 | WebSearch supplements | 2–3 post-engine searches (separate budget from 0.55) |
<Info>
Without a user topic, ask once for a topic — do not run WebSearch or the engine. With a topic, the Python engine is mandatory; missing the `✅ All agents reported back!` footer means the skill was not run.
</Info>
## Pre-flight protocol
### Step 0.45 — Topic quality (model-side)
Four keyword-trap classes drive clarify-or-reframe behavior **before** Bash:
| Class | Pattern | Action |
|-------|---------|--------|
| 1 | Demographic shopping (`gift for 42 year old man`) | One clarifying question, or reframe + gift subreddits |
| 2 | Numeric collisions (`42`, `100`) | Strip number from engine query when not semantically load-bearing |
| 3 | Tutorial phrasing (`how to use Docker`) | Reframe to discussion vocabulary |
| 4 | Generic single noun (`sneakers`) | Ask for facet before running |
Engine mirror: `lib/preflight.check_class_1_trap()` refuses Class 1 on stderr (exit 2) unless qualifiers (budget, hobbies, `who loves …`) are present. Bypass: `LAST30DAYS_SKIP_PREFLIGHT=1`.
### Step 0.5 — Targeting checklist (model-side)
The checklist is the **full** contract — not only X handles:
| Flag | When |
|------|------|
| `--x-handle` | Person, brand, product, creator with X presence |
| `--x-related` | Founders, collaborators, commentators |
| `--github-user` | Person who ships code (peer to X resolution) |
| `--github-repo` | Product / OSS project |
| `--subreddits` | Nearly always |
| `--tiktok-hashtags` / `--tiktok-creators` / `--ig-creators` | Per topic class |
| `--auto-resolve` | Fallback when WebSearch ran but 0.55 incomplete |
<Warning>
Person topics require **minimum** `--x-handle` + `--github-user` + `--subreddits` (typically `--x-related` too). X-handle-only invocations are a documented regression shape.
</Warning>
### Step 0.55 and 0.75 — WebSearch platform gate
When WebSearch is available, both steps are mandatory before engine invocation. When unavailable (OpenClaw, headless CLI), skip 0.55/0.75 and pass `--auto-resolve` so the engine discovers subreddits/handles via configured web backends.
Step 0.75 produces `QUERY_PLAN_JSON` (intent, freshness_mode, cluster_mode, 1–4 subqueries with sources/weights). Primary subquery must include reddit, x, youtube, tiktok, instagram, hackernews, polymarket.
### Engine-visible warnings
If pre-flight flags or `--plan` are missing on eligible named-entity topics, `lib/render.py` injects:
- `## Pre-Research Status` — Step 0.55 skip (keyword-only retrieval)
- `## DEGRADED RUN WARNING` — deterministic planner + bare invocation (LAW 7 backstop on stdout, not stderr)
Pass these blocks through verbatim (LAW 4 exception for engine-emitted status blocks; LAW 5/7 for degraded banner).
## SKILL_DIR substitution and engine invocation
`SKILL_DIR` is the absolute directory containing the SKILL.md the harness Read. `scripts/last30days.py` is always `${SKILL_DIR}/scripts/last30days.py`. Substitute the real path from the Read result — do not walk `~/.openclaw/skills/`, marketplace trees, or multi-install precedence lists.
```bash
SKILL_DIR="<absolute path of the directory containing the SKILL.md you Read>"
if [ ! -f "$SKILL_DIR/scripts/last30days.py" ]; then
echo "ERROR: scripts/last30days.py not found under SKILL_DIR=$SKILL_DIR" >&2
exit 1
fi
"${LAST30DAYS_PYTHON}" "${SKILL_DIR}/scripts/last30days.py" "$TOPIC" \
--emit=compact \
--save-dir="${LAST30DAYS_MEMORY_DIR}" \
--save-suffix=v3 \
--plan "$QUERY_PLAN_FILE" \
--x-handle=... \
--subreddits=...
```
<ParamField body="QUERY_PLAN_FILE" type="path" required>
Tmpfile from quoted heredoc (`mktemp …/last30days-plan.XXXXXX` + `<<'PLAN_EOF'`). Engine `parse_plan` reads file paths. **Never** inline `--plan '$JSON'` — apostrophes in plan strings break shell parsing.
</ParamField>
<ParamField body="COMPETITORS_PLAN_FILE" type="path">
Same tmpfile pattern for comparison `--competitors-plan` JSON (entity → handles, subreddits, github_user, context).
</ParamField>
<ParamField body="LAST30DAYS_MEMORY_DIR" type="path">
Defaults to `~/Documents/Last30Days` for raw saves; overridable per run.
</ParamField>
### Research execution precondition gate
Before Bash, confirm:
1. Platform branch chosen (WebSearch vs `--auto-resolve`).
2. WebSearch path: 0.55 + 0.75 completed.
3. `--emit=compact` (not `md` as primary user flow).
4. Command includes `--plan` plus every resolved flag from the checklist.
Run in the **foreground** with **300000 ms** timeout. Read full stdout (evidence envelope + pass-through footer). Post-engine: 2–3 WebSearch supplements (Step 2), distinct from 0.55 budget.
### Comparison and agent modes
- **COMPARISON** (`vs` / `versus`): per-entity 0.55 resolution, single engine call with `--competitors-plan` file + main entity outer flags; synthesis uses required `##` headers per template.
- **`--agent`**: skip intro, AskUserQuestion, wait states, and invitation; emit structured research report; still runs engine + supplements.
## Version and metadata helpers
`lib/skill_meta.read_skill_version(path)` parses SKILL.md YAML `version:` (quoted or unquoted). Used by render badge logic and release consistency tests — one regex, shared with `tests/test_plugin_contract.py` and `tests/test_version_consistency.py`.
Plugin manifest `.claude-plugin/plugin.json` carries the same semantic version for marketplace installs; badge resolution tries `jq` on `"$SKILL_DIR/../../.claude-plugin/plugin.json"` then SKILL frontmatter.
## Contract vs direct CLI
| Surface | Who sets flags | SKILL.md authority |
|---------|----------------|-------------------|
| `/last30days topic` | Model after reading SKILL.md | Full step + LAW contract |
| `python3 …/last30days.py …` | Human/script | Same flags exist, but no model pre-flight unless scripted |
<Check>
A successful harness run: STEP 0 satisfied → WebSearch loaded (if Claude Code) → 0.45/0.5/0.55/0.75 as applicable → engine from `SKILL_DIR` with `--emit=compact` + plan + targeting → synthesis obeys LAWs → engine footer present.
</Check>
## Related pages
<CardGroup>
<Card title="Skill, engine, and harness" href="/skill-engine-harness">
Boundaries between the Agent Skills package, Python engine, and multi-harness runtimes.
</Card>
<Card title="Query types" href="/query-types">
QUERY_TYPE classification, intent parsing, and engine refusal paths.
</Card>
<Card title="Output contract" href="/output-contract">
Badge line, LAWs voice contract, `--emit` modes, and footer pass-through.
</Card>
<Card title="CLI reference" href="/cli-reference">
Direct `last30days.py` flags vs slash-command constraints.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Stale marketplace clones, missing sources, and `--diagnose`.
</Card>
</CardGroup>
---
## 18. Comparison recipes
> Copy-paste comparison workflows: vs-topic phrasing, --competitors fan-out, and multi-entity Head-to-Head table expectations.
- Page Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/18-comparison-recipes.md
- Generated: 2026-06-04T23:23:48.379Z
### Source Files
- `README.md`
- `skills/last30days/SKILL.md`
- `skills/last30days/scripts/lib/competitors.py`
- `tests/test_render_comparison_multi.py`
- `fixtures/eval_topics.json`
---
title: "Comparison recipes"
description: "Copy-paste comparison workflows: vs-topic phrasing, --competitors fan-out, and multi-entity Head-to-Head table expectations."
---
Comparison runs classify as `QUERY_TYPE=COMPARISON`, fan out one full `pipeline.run()` per entity in parallel (via `lib/fanout.py`), merge stdout through `render.render_comparison_multi()`, and require a hosting-model synthesis that fills the engine-emitted `## Head-to-Head` scaffold—not the general-query `What I learned:` shape.
## When to use which recipe
| User intent | Slash-command phrasing | Engine path |
| --- | --- | --- |
| Named entities already known | `/last30days OpenAI vs Anthropic` or `/last30days OpenClaw vs Hermes vs Paperclip` | vs-string auto-routes to N-pass fanout |
| Main topic + discover peers | `/last30days OpenAI --competitors` or `/last30days Cursor --competitors=3` | Model discovers peers → builds vs-string → `--competitors-plan` |
| Headless / cron with API keys | `python3 .../last30days.py "OpenAI" --competitors` | Engine `discover_competitors()` via Brave/Exa/Serper/Parallel |
| Peers known, targeting thin | `--competitors-list "Anthropic,xAI"` | Names only; peers use planner defaults |
| Full per-entity depth | `--competitors-plan` JSON or file path | Preferred; drives Step 0.55 targeting per peer |
<Note>
Slash commands do not pass shell flags. The hosting model translates user phrasing (`--competitors`, `vs` topics) into engine flags and a tmpfile-backed `--competitors-plan`. Direct CLI invocation is the fallback for scripting and verification.
</Note>
## Recipe 1: Explicit vs-topic (2-way or 3-way)
**Trigger phrases** (Step 0.5 intent parse): `X vs Y`, `X versus Y`, `compare X and Y`, `X or Y which is better`. Topics split on spaced ` vs ` or ` versus ` (also accepts `vs.`).
<Steps>
<Step title="Confirm comparison intent">
Display the branded one-liner (not a multi-line parsed-intent block):
```
/last30days - comparing {TOPIC_A} vs {TOPIC_B} across {ACTIVE_SOURCES_LIST}.
```
For three entities, extend the vs-list in the confirmation message.
</Step>
<Step title="Run Step 0.55 per entity">
For each entity, resolve X handle, subreddits (include category-peer subs for products), GitHub user/repos, and news context. Batch WebSearch across entities (e.g. three entities × four lookup types → 3–4 batched queries, not twelve serial searches).
</Step>
<Step title="Invoke the engine once">
Write per-peer targeting to a tmpfile and pass the path (avoids apostrophe breakage in inline JSON):
```bash
COMPETITORS_PLAN_FILE=$(mktemp "${TMPDIR:-/tmp}/last30days-competitors.XXXXXX")
trap 'rm -f "$COMPETITORS_PLAN_FILE"' EXIT
cat > "$COMPETITORS_PLAN_FILE" <<'PLAN_EOF'
{
"Anthropic": {"x_handle":"AnthropicAI","subreddits":["ClaudeAI","MachineLearning"],"github_user":"anthropics","context":"Claude 4.x ship cycle"},
"xAI": {"x_handle":"xai","subreddits":["LocalLLaMA"],"github_user":"","context":"Grok release chatter"}
}
PLAN_EOF
python3 skills/last30days/scripts/last30days.py "OpenAI vs Anthropic vs xAI" \
--emit=compact \
--save-dir="${LAST30DAYS_MEMORY_DIR}" \
--x-handle=OpenAI \
--subreddits=OpenAI,MachineLearning \
--competitors-plan "$COMPETITORS_PLAN_FILE"
```
Topic A (first in the vs-string) uses outer `--x-handle`, `--subreddits`, `--github-user`, `--github-repo`, `--tiktok-*`, `--ig-creators`. Peers read overrides from `--competitors-plan` keys (case-insensitive).
</Step>
<Step title="WebSearch supplements (skill path)">
After the engine returns, supplement with rivalry queries the per-entity passes may miss:
- `{TOPIC_A} vs {TOPIC_B} comparison {YEAR}`
- `{TOPIC_A} vs {TOPIC_B} which is better`
</Step>
<Step title="Synthesize with the comparison template">
Skip general-query synthesis. Use the comparison sections below and pass through the engine footer (LAW 5).
</Step>
</Steps>
**Engine routing:** If the positional topic contains ` vs ` / ` versus ` and splits into ≥2 entities (`planner._comparison_entities`), `last30days.py` rewrites the main topic to the first entity, sets competitor list to the remainder, and runs the same fanout path as `--competitors` (stderr: `[Competitors] vs-mode: routing to N-pass fanout`).
**Eval fixture example:** `OpenClaw vs NanoClaw vs ZeroClaw` is tagged `comparison` in `fixtures/eval_topics.json` for multi-entity extraction tests.
## Recipe 2: `--competitors` (discover peers)
Use when the user names one product or company and wants peers auto-selected.
<Steps>
<Step title="Discover peers (hosting model)">
WebSearch:
- `{topic} competitors`
- `{topic} alternatives`
Default peer count: **2** (3-way total: main + 2 peers). User may pass `--competitors=N` where N is 1..6 (out-of-range values clamp with stderr warning).
</Step>
<Step title="Step 0.55 for main and every peer">
Same resolution stack as Recipe 1—for each discovered peer, not only the main topic.
</Step>
<Step title="Build vs-string and invoke">
Construct `"{main} vs {peer1} vs {peer2}"` and call the engine with `--competitors-plan` covering peers (and optionally the main entity if overriding outer flags).
</Step>
</Steps>
**Headless discovery:** With `BRAVE_API_KEY`, `EXA_API_KEY`, `SERPER_API_KEY`, or `PARALLEL_API_KEY`, `lib/competitors.discover_competitors()` runs three parallel SERP queries (`{topic} competitors`, `{topic} alternatives`, `{topic} vs`), scores capitalized brand phrases, filters topic overlap and listicle stopwords, and returns up to N names. Without a web backend and without `--competitors-list`, the engine exits with stderr instructions to use the hosting-model path.
<Warning>
`--competitors-list "A,B,C"` is the minimum escape hatch. Without `--competitors-plan`, peer sub-runs use thin deterministic planner queries. The `## Resolved Entities` block shows dashes for skipped Step 0.55—treat that as a failed recipe, not a finished comparison.
</Warning>
## Recipe 3: Direct CLI quick checks
For engine testing without the skill loop:
<CodeGroup>
```bash title="2-way vs-string"
python3 skills/last30days/scripts/last30days.py "OpenAI vs Anthropic" --emit=compact --mock
```
```bash title="Explicit peer list"
python3 skills/last30days/scripts/last30days.py "OpenAI" \
--competitors-list "Anthropic,xAI" \
--emit=compact --mock
```
```bash title="JSON plan file"
python3 skills/last30days/scripts/last30days.py "OpenAI vs Anthropic" \
--competitors-plan /tmp/plan.json \
--emit=compact --mock
```
</CodeGroup>
`--emit=compact` and `--emit=md` both route comparison output through `render_comparison_multi`. `--emit=json` nests `comparison: true`, `entities: [...]`, and per-entity `reports`.
## `--competitors-plan` schema
Top-level object: entity name → targeting dict. Parsed by `parse_competitors_plan()`; accepts inline JSON or a file path.
| Field | Type | Role |
| --- | --- | --- |
| `x_handle` | string | Primary X handle for the entity sub-run |
| `x_related` | string | Related handles (comma-separated) |
| `subreddits` | array of strings | Subreddit names without `r/` prefix |
| `github_user` | string | GitHub person-mode username |
| `github_repos` | array of strings | `owner/repo` project targets |
| `context` | string | News/context snippet for planner subqueries |
Unknown fields log a warning and are ignored. Missing entries fall back to engine `auto_resolve()` when a web backend exists.
<ParamField body="--competitors" type="integer (optional)" default="2 when flag present without value">
Signals competitor mode. Bare `--competitors` → discover 2 peers. `--competitors=3` → main + 3 peers (4-way). Range 1..6.
</ParamField>
<ParamField body="--competitors-list" type="string">
Comma-separated peer names; implies `--competitors`. Count overrides `--competitors=N` when both are set.
</ParamField>
<ParamField body="--competitors-plan" type="JSON or file path">
Per-entity Step 0.55 targeting; implies `--competitors`. Preferred over `--competitors-list`.
</ParamField>
## Engine stdout shape (before synthesis)
Merged compact output includes:
```text
🌐 last30days v{VERSION} · synced {YYYY-MM-DD}
# last30days v{VERSION}: {Entity1} vs {Entity2} vs ...
- Comparison mode: N entities (Entity1, Entity2, ...)
<!-- EVIDENCE FOR SYNTHESIS --> ← read, do not emit verbatim
## Resolved Entities ← when any sub-run has artifacts.resolved
- **Entity**: X @handle | Subs r/a, r/b | GitHub @user (repos) | Context: ...
## {Entity} ← per-entity evidence (clusters or placeholder)
<!-- END EVIDENCE FOR SYNTHESIS -->
## Head-to-Head ← empty table scaffold for the model to fill
| Dimension | Entity1 | Entity2 | ...
<!-- PASS-THROUGH FOOTER --> ← emit verbatim (LAW 5)
```
**Thin peer signal:** An entity section containing `(no significant discussion this month)` means that sub-run returned no clusters—still synthesize, but do not invent volume.
**Per-entity artifacts:** `--save-dir` receives `{slug}-raw.md` per entity (main + each peer).
## Head-to-Head table expectations
The engine emits the scaffold; the hosting model **fills cells** in the final user-facing response (5–15 words per cell; use ` - ` not em-dashes; `N/A` when an axis does not apply).
**Default axes** (from `_render_comparison_scaffold` in `lib/render.py`):
| Dimension |
| --- |
| What it is |
| GitHub stars |
| Philosophy |
| Skills |
| Memory |
| Models |
| Security |
| Best for |
| Install |
Column headers match entity labels in order (`| Dimension | OpenAI | Anthropic | xAI |` for three-way runs). For non-tool topics (geopolitics, people, sports), substitute topic-appropriate row meaning or `N/A` rather than inventing GitHub-star cells.
<Check>
Verification checklist after a comparison run:
- Stderr shows `[Competitors] Comparing: …` or vs-mode fanout line
- Compact stdout includes `## Head-to-Head` with one column per entity
- `## Resolved Entities` has handles/subs for every peer (not all dashes)
- One `*-raw.md` per entity under `--save-dir`
- Final synthesis uses comparison headers only—not `What I learned:`
</Check>
## Required synthesis shape (hosting model)
Comparison output **replaces** the general template. Required `##` headers (LAW 4 exception):
1. `## Quick Verdict` — one paragraph; competitors vs layers; inline scale stats; one quotable community line
2. `## {Entity}` — per entity: **Community Sentiment**, **Strengths**, **Weaknesses** with `per <source>` attribution
3. `## Head-to-Head` — filled table (pass through scaffold structure; fill cells)
4. `## The Bottom Line` — `**Choose {Entity} if**` per entity
5. `## The emerging stack` — one paragraph on combination patterns, or explicit “No emerging stack pattern has crystallized…”
**Title line** (after badge): `# {TOPIC_A} vs {TOPIC_B} [vs {TOPIC_C}]: What the Community Says (/Last30Days)`
**Do not use:** `What I learned:`, bold-lead-in general paragraphs, `KEY PATTERNS from the research:`, fabricated `## Notable Stats`, or a trailing `Sources:` block.
**Invitation** (comparison-specific):
```
I've compared {TOPIC_A} vs {TOPIC_B} using the latest community data. Some things you could ask:
- Deep dive into {Entity} alone with /last30days {Entity}
- …
```
Reference exemplar path (skill contract): `$LAST30DAYS_MEMORY_DIR/openclaw-vs-hermes-vs-paperclip-LAUNCH-VIDEO-april9-exemplar.md`.
## Comparison vs RECOMMENDATIONS
| Signal | COMPARISON | RECOMMENDATIONS |
| --- | --- | --- |
| User phrasing | `X vs Y`, `compare`, `which is better` | `best X`, `top X`, `what X should I use` |
| Ranking logic | Side-by-side entities + Head-to-Head axes | Signal-weighted picks, not mention counts |
| Body structure | Per-entity sections + table | `🏆 Top recommendations` list |
| Engine fanout | N parallel pipelines | Single pipeline |
## Failure modes and fixes
| Symptom | Likely cause | Fix |
| --- | --- | --- |
| No `--competitors` in `--help` | Stale Claude marketplace clone (STEP 0) | Re-read cache SKILL.md per STEP 0 guard |
| `[Competitors] Cannot auto-discover peers` | No web API keys on headless CLI | Use slash-command WebSearch + `--competitors-plan`, or set Brave/Exa/Serper |
| Resolved Entities all `-` for a peer | Skipped Step 0.55 for that entity | Re-run with plan entry for that peer |
| Raw `### 1. (score N, M items…)` in chat | LAW 6 violation—emitted evidence verbatim | Regenerate synthesis from evidence block only |
| `What I learned:` on a vs query | Wrong template | Switch to comparison synthesis sections |
| Fewer than 2 sub-runs survived | Fanout dropped failed entities | Check stderr warnings; widen sources or fix targeting |
## Related pages
<CardGroup>
<Card title="Comparison mode" href="/comparison-mode">
Engine flags, discovery fan-out, `--competitors-plan` wiring, and comparison synthesis contract in SKILL.md.
</Card>
<Card title="Query types" href="/query-types">
`QUERY_TYPE=COMPARISON` classification, intent parsing, and pre-flight paths.
</Card>
<Card title="Output contract" href="/output-contract">
Badge line, LAWs voice rules, `--emit` modes, and footer pass-through.
</Card>
<Card title="CLI reference" href="/cli-reference">
Full `last30days.py` flags including `--competitors`, `--competitors-list`, and `--competitors-plan`.
</Card>
<Card title="Per-client setup" href="/per-client-setup">
Recurring `--competitors-plan` templates and project-scoped env isolation.
</Card>
</CardGroup>
---
## 19. Prompting recipes
> PROMPTING query workflows: community technique extraction, KEY PATTERNS output shape, and copy-paste prompt generation patterns.
- Page Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/19-prompting-recipes.md
- Generated: 2026-06-04T23:24:25.637Z
### Source Files
- `README.md`
- `skills/last30days/SKILL.md`
- `skills/last30days/scripts/lib/planner.py`
- `tests/test_pipeline_v3.py`
- `skills/last30days/scripts/lib/render.py`
---
title: "Prompting recipes"
description: "PROMPTING query workflows: community technique extraction, KEY PATTERNS output shape, and copy-paste prompt generation patterns."
---
`/last30days` treats **PROMPTING** as a skill-level `QUERY_TYPE` in `SKILL.md`: the hosting model classifies the user topic, runs the v3 engine with resolved communities and a JSON `--plan`, synthesizes community techniques into the canonical `What I learned:` + `KEY PATTERNS from the research:` shape, then—only after the user describes what to create—writes one paste-ready prompt that matches the format the research surfaced. The Python engine does not expose a `PROMPTING` enum; it plans with intents such as `how_to`, `product`, and `concept` via `scripts/lib/planner.py`.
## PROMPTING vs engine intent
| Layer | Identifier | Where it lives | Role for prompting runs |
|-------|------------|----------------|-------------------------|
| Skill | `QUERY_TYPE = PROMPTING` | `skills/last30days/SKILL.md` | Voice contract, WebSearch supplements, invitation, follow-up prompt rules |
| Engine | `intent` on `QueryPlan` | `scripts/lib/planner.py`, `--plan` JSON | Source priority, `freshness_mode`, `cluster_mode` (often `how_to` → `workflow`, `evergreen_ok`) |
| Search modules | Local `_infer_query_intent` | e.g. `scripts/lib/reddit.py` | Reddit query expansion only (`how_to`, `product`, …)—not the skill `QUERY_TYPE` |
<Note>
Eval fixtures label topics like `nano banana pro prompting` as engine `product` or `how_to`, while the skill still classifies the same phrasing as **PROMPTING** for synthesis and the post-research invitation. Plan for both: skill workflow + engine `how_to`/`product` subqueries.
</Note>
## Classify a PROMPTING query
The model parses **TOPIC**, optional **TARGET_TOOL**, and **QUERY_TYPE** before any engine call.
| User phrasing | `QUERY_TYPE` | `TARGET_TOOL` |
|---------------|--------------|---------------|
| `X prompts`, `prompting for X`, `X best practices` | PROMPTING | From `for [tool]` if present; else `unknown` until after research |
| `UI design prompts for Midjourney` | PROMPTING | `Midjourney` (specified in query) |
| `iOS design mockups` (no tool) | PROMPTING or GENERAL | `unknown`—do **not** ask for tool before research |
Confirmation line (same shape as GENERAL/NEWS/RECOMMENDATIONS):
```
/last30days - searching {ACTIVE_SOURCES_LIST} for what people are saying about {TOPIC}.
```
Do not emit a multi-line `Parsed intent:` block with `TOPIC=` / `QUERY_TYPE=` variables.
## End-to-end workflow
```mermaid
sequenceDiagram
participant User
participant Model as Hosting model (SKILL.md)
participant Engine as last30days.py
participant Sources as Reddit X YouTube TikTok …
User->>Model: /last30days {topic} [for {tool}]
Model->>Model: QUERY_TYPE=PROMPTING, Step 0.45 pre-flight
Model->>Model: Step 0.55 resolve + category peers
Model->>Model: Step 0.75 JSON --plan
Model->>Engine: --emit=compact --plan --x-handle --subreddits …
Engine->>Sources: parallel fan-out
Sources-->>Engine: ranked clusters + footer
Engine-->>Model: evidence envelope + PASS-THROUGH FOOTER
Model->>Model: WebSearch supplements (prompting queries)
Model->>User: badge, What I learned, KEY PATTERNS, footer, invitation
User->>Model: describe what to create
Model->>User: one prompt in research-recommended format
```
<Steps>
<Step title="Pre-flight and reframe (Step 0.45)">
Tutorial-shaped topics (`how to use X`, `tutorial for Z`) are **Class 3 keyword traps**. Reframe to discussion vocabulary (`Docker tips tricks workflows`) before the engine runs. Wait for user confirmation if you asked a clarifying question.
</Step>
<Step title="Resolve communities (Step 0.55)">
Run 2–3 WebSearches for subreddits and news context. For product topics, **mandatory category-peer expansion**: brand subs alone miss cross-product technique threads (documented `GPT Image 2` failure). Merge WebSearch subs with peers from `scripts/lib/categories.py`; cap at 10; annotate `(+ ai_image_generation peers)` on the Resolved line when peers were added.
</Step>
<Step title="Plan and execute (Steps 0.75 + Research Execution)">
Generate 1–4 subqueries as JSON; primary subquery must include `reddit`, `x`, `youtube`, `tiktok`, `instagram`, `hackernews`, `polymarket`. Pass `--plan` via a tmpfile (never inline JSON with apostrophes). Use `--emit=compact`, resolved `--x-handle`, `--subreddits`, hashtags, creators. Named-entity topics require `--plan` (LAW 7).
</Step>
<Step title="Supplement and synthesize (Steps 2 + Judge)">
Run 2–3 WebSearches: `{TOPIC} prompts examples 2026`, `{TOPIC} techniques tips`. Append bullets to the saved raw file under `## WebSearch Supplemental Results`. Transform engine clusters into prose—do not dump `## Ranked Evidence Clusters` (LAW 6).
</Step>
<Step title="Invite, then write one prompt">
After synthesis, use the PROMPTING invitation. When the user describes something to **create** or asks for a **prompt**, deliver **one** paste-ready prompt in the format the research recommends (JSON, structured params, etc.).
</Step>
</Steps>
## Community technique extraction
### Query stripping (`scripts/lib/query.py`)
Platform search uses `extract_core_subject()` to remove prompting meta-words (`prompt`, `prompts`, `prompting`, `techniques`, `tips`, …) and prefixes like `how to use`, `tips for`, `best practices for`. Multi-word suffixes `prompting techniques` / `prompting tips` strip when `strip_suffixes=True` (used on some modules). Compound terms (`Claude Code`, `multi-agent`) stay quoted in expansion.
### Category-peer subreddits (`scripts/lib/categories.py`)
`detect_category(topic)` returns IDs such as `ai_image_generation`, `ai_coding_agent`, `ai_video_generation`. `peer_subs_for()` returns priority-ordered subs—for image gen: `StableDiffusion`, `midjourney`, `dalle2`, `aiArt`, `PromptEngineering`, `MediaSynthesis`. Tests in `tests/test_categories.py` lock the **Prompting GPT Image 2** regression: OpenAI-brand subs only → thin technique signal; peers required.
| Category ID | Example trigger terms | Peer subs (first entries) |
|-------------|----------------------|---------------------------|
| `ai_image_generation` | GPT Image, Nano Banana, Midjourney, Stable Diffusion | `StableDiffusion`, `midjourney`, `dalle2` |
| `ai_coding_agent` | Claude Code, Cursor, OpenClaw, Hermes Agent | `ChatGPTCoding`, `LocalLLaMA`, `singularity` |
| `ai_video_generation` | Sora, Veo, Runway Gen, Kling | `aivideo`, `StableDiffusion`, `runwayml` |
### Planner hints for technique-heavy topics
When building `--plan` JSON for prompting runs, align with Step 0.75 rules:
- **Product-style topics**: route subqueries to YouTube (reviews/demos), Reddit (discussions), TikTok (demos).
- **How-to-shaped retrieval**: `intent: "how_to"`, `cluster_mode: "workflow"`, `freshness_mode: "evergreen_ok"`.
- **Search queries**: keyword-heavy, match how posts are **titled**; no `last 30 days`, `recent`, or year literals in `search_query`.
- Strip intent modifiers (`tutorial`, `workflow`, `examples`) from `search_query`; keep meaning in `ranking_query`; prefer 4–5 paraphrased subqueries for broad retrieval.
### WebSearch supplements (Step 2)
| Query type | Supplement searches | Goal |
|------------|---------------------|------|
| PROMPTING | `{TOPIC} prompts examples 2026`, `{TOPIC} techniques tips` | Examples and structures for copy-paste prompts |
| All types | Exclude `reddit.com`, `x.com`, `twitter.com` (engine already covers) | Blogs, tutorials, docs, GitHub |
Use the user's **exact terminology** in supplements—do not substitute model-known aliases.
### Evidence → synthesis fields
From **Judge Agent** instructions (all query types; critical for PROMPTING):
- **PROMPT FORMAT** — JSON vs natural language vs tag lists, as stated in sources
- **Top 3–5 patterns** across platforms
- **Keywords/structures** and **pitfalls** cited by sources—not invented
## KEY PATTERNS output shape
PROMPTING uses the same voice contract as GENERAL, NEWS, and RECOMMENDATIONS (not the COMPARISON template).
### Required sequence
1. **Line 1**: Badge pass-through — `🌐 last30days v{VERSION} · synced {YYYY-MM-DD}` (engine emits on `--emit=compact`).
2. **Line 3**: Prose label `What I learned:` (no invented title, no `#` heading).
3. **Body**: 3+ paragraphs, each starting `**Headline phrase** - ` then 1–2 sentences with inline `[name](url)` citations (LAW 8).
4. **Prose label**: `KEY PATTERNS from the research:` then a **numbered** list (typically 3–5 items).
5. **Engine footer**: `<!-- PASS-THROUGH FOOTER -->` block verbatim (LAW 5).
6. **Invitation**: PROMPTING-specific block (below).
7. **End**: No trailing `Sources:` (LAW 1).
### KEY PATTERNS list rules
| Rule | Requirement |
|------|-------------|
| Structure | Numbered list only; no `## Key patterns` header (LAW 4) |
| Bold | Skill template uses `**bold**` for pattern lead-ins; do not strip for global "no bold" prefs |
| Citations | One source per pattern: `per [@handle](url)` or `per [r/sub](url)` |
| Separators | Use ` - ` (spaced hyphen), never em-dash or en-dash (LAW 3) |
| Density | Do not chain `per @a, @b, @c`; pick the strongest source |
<RequestExample>
```text
🌐 last30days v3.3.1 · synced 2026-06-04
What I learned:
**JSON blocks are replacing tag soup** - Creators on X describe nested JSON fields to stop concept bleed between subjects, per [@pictsbyai](https://x.com/pictsbyai).
**Edit-first beats full regeneration** - r/StableDiffusion threads treat img2img and mask edits as the default loop for iteration, per [r/StableDiffusion](https://reddit.com/r/StableDiffusion).
KEY PATTERNS from the research:
1. **Nested JSON with explicit device/scene keys** - per [@pictsbyai](https://x.com/pictsbyai)
2. **Separate negative prompt channel** - per [r/midjourney](https://reddit.com/r/midjourney)
3. **Reference image + strength slider for style lock** - per [r/dalle2](https://reddit.com/r/dalle2)
---
✅ All agents reported back!
…
---
---
I'm now an expert on Nano Banana Pro prompting for Nano Banana Pro. What do you want to make? For example:
- …
Just describe your vision and I'll write a prompt you can paste straight into Nano Banana Pro.
```
</RequestExample>
HTML briefs (`--emit=html`) map labels via `scripts/lib/html_render.py`: `KEY PATTERNS from the research:` → display title "Key patterns from the research". Synthesis temp files include `What I learned:` + KEY PATTERNS only—not the badge or footer (`references/save-html-brief.md`).
## Copy-paste prompt generation
Research completes in one turn; **prompt delivery is a second turn** after the invitation.
### PROMPTING invitation (after synthesis)
```
---
I'm now an expert on {TOPIC} for {TARGET_TOOL}. What do you want to make? For example:
- [specific idea from popular technique in research]
- [specific idea from trending style in research]
- [specific idea from what people are actually creating]
Just describe your vision and I'll write a prompt you can paste straight into {TARGET_TOOL}.
```
If `TARGET_TOOL` was `unknown`, infer or ask **after** showing results—not before the engine run.
### When the user responds
| User intent | Action |
|-------------|--------|
| Question about the topic | Answer from research only—no new WebSearch, no prompt |
| Go deeper on a subtopic | Elaborate from existing findings |
| Describe something to **create** or asks for a **prompt** | Write **one** tailored prompt |
| Different topic | New `/last30days` run |
### Prompt quality contract
<Warning>
**Anti-pattern:** Research recommends JSON or structured params but the model outputs plain prose. That voids the research pass.
</Warning>
<Check>
**Pre-delivery checklist**
- Format matches research (JSON / structured / natural language)
- Addresses the user's stated creation goal
- Uses patterns and keywords from KEY PATTERNS and narrative
- Paste-ready with clear `[PLACEHOLDERS]` only where needed
- Length and style fit `TARGET_TOOL`
</Check>
**Delivery shape:**
```
Here's your prompt for {TARGET_TOOL}:
---
[Prompt in the format the research recommends]
---
This uses [one-line research insight applied].
```
Provide 2–3 variations **only** if the user asks for more options. After each prompt, optional expert footer with source counts; then: `Want another prompt? Just tell me what you're creating next.`
**Context memory** for the session: retain `TOPIC`, `TARGET_TOOL`, KEY PATTERNS list, and findings; answer follow-ups without re-searching unless the topic changes.
## Recipe catalog
### Slash command (primary)
| Goal | Example invocation | Notes |
|------|-------------------|-------|
| Tool-specific prompting | `/last30days Nano Banana Pro prompting` | README exemplar: JSON prompts, edit-first workflow |
| Image model techniques | `/last30days Prompting GPT Image 2` | Requires `ai_image_generation` peer subs in Resolved block |
| Coding agent prompts | `/last30days Claude Code prompting` | Category `ai_coding_agent`; plan with `how_to`/`product` subqueries |
| Video model | `/last30days Seedance prompting` | `ai_video_generation` peers + prompting supplements |
### Direct CLI (dev/scripting only)
```bash
python3 skills/last30days/scripts/last30days.py "Nano Banana Pro prompting" \
--emit=compact \
--plan /tmp/query-plan.json \
--subreddits=StableDiffusion,midjourney,dalle2,aiArt \
--save-dir="${LAST30DAYS_MEMORY_DIR}"
```
The hosting model must still perform Step 0.55 resolution and Step 0.75 planning when using the skill; bare CLI omits synthesis, KEY PATTERNS, invitation, and prompt generation unless a separate agent applies `SKILL.md`.
### Depth and timing
| Flag | Effect on prompting runs |
|------|--------------------------|
| `--quick` | Fewer items per source (faster, thinner technique coverage) |
| (default) | Balanced counts |
| `--deep` | More Reddit/X items—use when peer subs are broad |
| `--days=N` | Lookback window (default 30) |
## Failure modes and fixes
| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| Brand-only subreddits, weak patterns | Skipped category-peer expansion | Re-run Step 0.55; add `(+ {category_id} peers)` subs from `categories.py` |
| Literal `how to use X` retrieval noise | Class 3 keyword trap | Reframe to tips/workflows phrasing in Resolved block |
| Raw `## Ranked Evidence Clusters` in chat | LAW 6 violation | Regenerate synthesis from evidence envelope only |
| Plain `@handle` without links | LAW 8 violation | Regenerate with `[name](url)` from raw dump |
| Prose prompt after JSON-heavy research | Format anti-pattern | Re-read PROMPT FORMAT from Judge section; output structured prompt |
| `Sources:` after invitation | LAW 1 / WebSearch override | Delete trailing list; keep engine `🌐 Web:` footer only |
## Related pages
<CardGroup>
<Card title="Query types" href="/query-types">
GENERAL, NEWS, PROMPTING, RECOMMENDATIONS, COMPARISON classification and pre-flight refusal paths.
</Card>
<Card title="Output contract" href="/output-contract">
Badge, LAWs voice contract, `--emit` modes, evidence blocks, footer pass-through.
</Card>
<Card title="Research pipeline" href="/research-pipeline">
Planner sub-queries, fan-out, fusion, clustering, dedupe, rerank, depth profiles.
</Card>
<Card title="Skill contract" href="/skill-contract">
SKILL.md authority: Step 0, pre-flight, engine invocation, `SKILL_DIR` substitution.
</Card>
<Card title="Configure sources" href="/configure-sources">
Credentials and `--diagnose` for Reddit, X, YouTube, ScrapeCreators, and web backends.
</Card>
<Card title="Per-client setup" href="/per-client-setup">
Project-scoped `.claude/last30days.env` and save-dir isolation for recurring prompt research.
</Card>
</CardGroup>
---
## 20. Troubleshooting
> --diagnose probes, missing-source recovery, stale Claude marketplace clones, thin results, and documented failure modes.
- Page Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/20-troubleshooting.md
- Generated: 2026-06-04T23:25:32.691Z
### Source Files
- `CONFIGURATION.md`
- `skills/last30days/scripts/last30days.py`
- `skills/last30days/SKILL.md`
- `skills/last30days/scripts/lib/setup_wizard.py`
- `docs/solutions/workflow-issues/release-consistency-test-cascade-2026-05-16.md`
- `docs/solutions/architecture/search-quality-eval-manual-by-default-2026-05-10.md`
---
title: "Troubleshooting"
description: "--diagnose probes, missing-source recovery, stale Claude marketplace clones, thin results, and documented failure modes."
---
The engine exposes a non-destructive availability probe via `--diagnose`: `last30days.py` loads config from `.claude/last30days.env` or `~/.config/last30days/.env`, calls `pipeline.diagnose()`, prints JSON, and exits before any retrieval. Full runs add post-research quality nudges, pipeline thin-source retries, and structured warnings in the report footer.
```mermaid
flowchart TD
subgraph probe [Probe path]
CLI["last30days.py --diagnose"]
ENV["env.get_config()"]
DIAG["pipeline.diagnose()"]
JSON["stdout: JSON + exit 0"]
CLI --> ENV --> DIAG --> JSON
end
subgraph run [Research path]
RUN["last30days.py TOPIC"]
PF["preflight.check_class_1_trap()"]
PIPE["pipeline.run()"]
RETRY["_retry_thin_sources()"]
NUDGE["quality_nudge → stderr"]
RUN --> PF
PF -->|pass| PIPE
PIPE --> RETRY
PIPE --> NUDGE
end
```
## Run `--diagnose` first
Use the direct CLI form from the skill directory (not a slash command with pipes):
<CodeGroup>
```bash title="From repo checkout"
cd skills/last30days
python3 scripts/last30days.py --diagnose
```
```bash title="Scoped sources"
python3 scripts/last30days.py --diagnose --search reddit,x,youtube
```
</CodeGroup>
<Check>
Exit code `0` and JSON on stdout means config loaded and probes completed. No topic argument is required.
</Check>
Hermes and other hosts that pin Python 3.12+ should use the same entrypoint documented in `HERMES_SETUP.md`: `python3.12 scripts/last30days.py --diagnose`.
Maintainers can fold diagnose into release verification via `scripts/verify_v3.py`, which parses `--diagnose` stdout as a smoke gate.
## Diagnose JSON fields
`pipeline.diagnose(config, requested_sources)` returns a single object. Use it to separate “key missing” from “source errored mid-run.”
| Field | Meaning |
| --- | --- |
| `providers` | `google`, `openai`, `xai`, `openrouter` booleans (OpenAI requires `OPENAI_AUTH_STATUS == ok`) |
| `local_mode` | `true` when no cloud reasoning keys are configured |
| `reasoning_provider` | `LAST30DAYS_REASONING_PROVIDER` or `"auto"` |
| `available_sources` | Sources `pipeline.run()` would fan out to (after `EXCLUDE_SOURCES`, `INCLUDE_SOURCES`, CLI `--search`) |
| `x_backend` | Resolved X path: Bird cookies, xAI, ScrapeCreators, etc. |
| `bird_installed` / `bird_authenticated` / `bird_username` | Bundled Bird CLI state |
| `native_web_backend` | First configured web key: `brave`, `exa`, `serper`, or `parallel` |
| `has_scrapecreators` | `SCRAPECREATORS_API_KEY` present |
| `has_github` | `GITHUB_TOKEN` or `gh` on `PATH` |
<Warning>
`available_sources` empty for a normal topic (no `--search` narrowing) leads to `RuntimeError: No sources are available for this run.` Fix credentials before researching.
</Warning>
## Missing-source recovery
Default availability is computed in `pipeline.available_sources()` from env keys and CLI presence. The table maps common gaps to fixes.
| Symptom in diagnose / footer | Typical cause | Recovery |
| --- | --- | --- |
| `x` not in `available_sources` | No X auth | Log into x.com in a supported browser and re-run, or set `AUTH_TOKEN`+`CT0`, `XAI_API_KEY`, `SCRAPECREATORS_API_KEY`, or `FROM_BROWSER=firefox` |
| `youtube` missing | `yt-dlp` not on `PATH` | `brew install yt-dlp` (setup wizard can auto-install via Homebrew) |
| `grounding` missing | No web API key | Add `BRAVE_API_KEY`, `EXA_API_KEY`, `SERPER_API_KEY`, or `PARALLEL_API_KEY`; on Claude Code/Codex/Gemini the host WebSearch may suffice without keys |
| `tiktok` / `instagram` missing | No SC key or not in `INCLUDE_SOURCES` | Set `SCRAPECREATORS_API_KEY` and `INCLUDE_SOURCES=tiktok,instagram` |
| `bluesky` missing | Credentials absent | `BSKY_HANDLE` + `BSKY_APP_PASSWORD` (19-char app password); default search host is `api.bsky.app` — override with `BSKY_SEARCH_HOST` if infrastructure moves |
| Reddit “public only” | No `SCRAPECREATORS_API_KEY` | Reddit still works keyless; SC unlocks full threads with comments |
| `.env` ignored | Wrong path | Project `.claude/last30days.env` wins over `~/.config/last30days/.env`; set `LAST30DAYS_CONFIG_DIR` to relocate |
<Steps>
<Step title="Run first-time setup">
```bash
python3 scripts/last30days.py setup
```
Extracts browser cookies for configured domains, checks or installs `yt-dlp`, writes `SETUP_COMPLETE=true` and `FROM_BROWSER` to the active config file.
</Step>
<Step title="Re-probe">
```bash
python3 scripts/last30days.py --diagnose
```
Confirm `available_sources` includes the platforms you expect.
</Step>
<Step title="Run a smoke topic">
```bash
python3 scripts/last30days.py "your topic" --emit=compact --quick
```
</Step>
</Steps>
Optional setup subcommands: `setup --openclaw`, `setup --github`, `setup --device-auth` (JSON on stdout).
<Tip>
On POSIX hosts, secrets files should be mode `600`. The engine warns every run if group/other can read `.env` — run `chmod 600` on the path shown in the warning.
</Tip>
After a full run, missing or degraded core sources also surface through `quality_nudge.compute_quality_score()` on stderr (advisory, not a hard failure).
## Stale Claude marketplace clone
Claude Code can load `SKILL.md` from `~/.claude/plugins/marketplaces/last30days-skill/`, a git clone that auto-restores to `origin/main` and may lag the versioned plugin cache by one or more releases. Documented impact: `--help` and engine flags from the stale tree (e.g. missing `--competitors`) while the cache already had them.
**Model-side guard (STEP 0 in SKILL.md):** If the loaded path contains `/.claude/plugins/marketplaces/` and a newer cache exists under `~/.claude/plugins/cache/last30days-skill/last30days/{version}/`, re-read `SKILL.md` from the cache before any tool calls.
**Operator-side fixes:**
| Action | Command / note |
| --- | --- |
| Force plugin refresh | `claude plugin update last30days@last30days-skill` |
| Avoid duplicate installs | Use marketplace **or** `npx skills add … -a claude-code`, not both — duplicate `/last30days` entries mean two skill copies |
| Prefer versioned cache | Let marketplace update populate `~/.claude/plugins/cache/…` before relying on marketplaces path |
<Info>
`~/.codex/skills/`, `~/.agents/skills/`, `npx skills` global installs, and repo checkouts are valid; STEP 0 does not redirect those paths. Engine invocation uses `SKILL_DIR` from the `SKILL.md` the harness actually loaded.
</Info>
## Stale Agent Skills / dev-tree install
For Codex, Cursor, Gemini, and other Agent Skills hosts, `npx skills add` copies into `~/.agents/skills/<name>/` **frozen at install time**. Working-tree edits do not propagate until you re-sync:
```bash
npx skills add mvanhorn/last30days-skill -g -y
# or from a local repo:
npx skills add . -g -y
```
Live development symlink (repo root):
```bash
ln -sfn "$PWD/skills/last30days" ~/.agents/skills/last30days
```
Update published installs: `npx skills update last30days -g`.
## Thin results and low evidence
Thin output can come from query traps, sparse platforms, missing web backend, or per-source retrieval limits — not only “broken install.”
### Engine behaviors
| Mechanism | When it runs | Effect |
| --- | --- | --- |
| `_retry_thin_sources()` | Default/deep depth only; skipped in `--quick` | Re-queries sources with fewer than 3 items using `query.extract_core_subject(topic, max_words=3)`; skips sources in `errors_by_source` and rate-limited sources |
| `_warnings()` | End of `pipeline.run()` | Adds `"Evidence is thin for this topic."` when fewer than 5 ranked candidates; lists `errors_by_source` |
| Cluster `uncertainty` | `cluster._cluster_uncertainty()` | `thin-evidence` when max cluster score < 55; `single-source` when one platform dominates |
| `--quick` | User flag | Disables thin-source retry — faster but sparser |
### Operator levers
- Add or fix web search keys (`BRAVE_API_KEY`, etc.) — CONFIGURATION.md notes client setups often look thinner than a host with native WebSearch.
- Use `--deep` for higher-recall depth settings (more pool/rerank budget).
- Narrow with `--subreddits`, `--x-handle`, `--github-repo` when the topic is broad.
- Avoid demographic-shopping phrasing without hobbies/relationship/budget (see preflight below).
### Post-run quality nudge (stderr)
After retrieval, `quality_nudge` scores five core sources (`hn`, `polymarket`, `x`, `youtube`, `reddit`) and may emit actionable text:
| Signal | Detection | Typical fix |
| --- | --- | --- |
| Missing X | No creds or `x_error` | Browser login or `XAI_API_KEY` |
| Missing YouTube | No `yt-dlp` | `brew install yt-dlp` |
| Degraded YouTube | Videos found, transcript ratio below `DEGRADED_TRANSCRIPT_THRESHOLD` (default 0.5) | `brew upgrade yt-dlp` / `pip install -U yt-dlp` — stale binary is the canonical failure mode |
| Silent Instagram | SC configured, zero items, not excluded | Topic may lack reel coverage; SC v2 endpoint flakiness; skill retries hashtag form automatically |
Captions-disabled YouTube videos are excluded from the degraded ratio so one uploader-disabled track does not false-trigger a stale-`yt-dlp` nudge.
## Preflight refuse gates (exit code 2)
Before the pipeline starts, `preflight.check_class_1_trap(topic)` blocks Class 1 demographic-shopping patterns unless qualifiers (budget, hobbies, relationship, activity noun) are present. Stderr prints a structured `REFUSE:` message; the model should ask for context and re-run with an enriched query.
Bypass only when the user insists:
```bash
LAST30DAYS_SKIP_PREFLIGHT=1 python3 scripts/last30days.py "birthday gift for 40 year old man"
```
SKILL.md Step 0.45 documents additional trap classes (numeric keyword collision, tutorial phrasing, generic single nouns) handled at the harness layer before engine invocation.
## Documented failure modes
| Failure mode | Surface | Mitigation |
| --- | --- | --- |
| Stale marketplace SKILL.md | Missing flags, wrong Step 0 behavior | STEP 0 cache redirect; `claude plugin update` |
| v3.0.6 “generic research” regression | Invented titles, `Sources:` footer, skipped Step 0.55 | Mandatory badge, LAWs block at top of SKILL.md, `SKILL_DIR`-scoped engine |
| Class 1 keyword trap | Irrelevant Reddit/HN noise | Engine refuse + Step 0.45 reframe/`--subreddits` |
| Stale `yt-dlp` | Videos without transcripts | Quality nudge + upgrade binary |
| Instagram SC silent zero | No section, no error | Documented in `quality_nudge`; check `INCLUDE_SOURCES` / topic shape |
| No sources available | Immediate `RuntimeError` | `--diagnose`; add keys per table above |
| Open PR CI cascade on version pins | Unrelated PRs red after release | Resolved by removing `sync.sh` pin test (see `docs/solutions/workflow-issues/release-consistency-test-cascade-2026-05-16.md`) |
| Search-quality regressions | Not caught in default CI | `evaluate_search_quality.py` is manual / `workflow_dispatch` by design (`docs/solutions/architecture/search-quality-eval-manual-by-default-2026-05-10.md`) |
<Note>
Search-quality eval intentionally stays out of default PR CI: live APIs, cost, and LLM-judge non-determinism. Request manual eval on retrieval/ranking changes; do not expect pytest alone to catch relevance regressions.
</Note>
## Runtime UI and errors in output
During a run, `ProgressDisplay` shows per-source counts at completion. If Reddit and web grounding are both unavailable, `show_promo()` nudges configuration (suppressed when `--plan` or `--competitors-plan` indicates a hosting model with native WebSearch).
Rendered `--emit=md` / compact output includes **Source Coverage** and **Source Errors** sections when `report.errors_by_source` is non-empty. Warnings from `_warnings()` pass through in the report payload for synthesis.
For SSH/datacenter YouTube blocks, set `LAST30DAYS_YOUTUBE_SSH_HOST` in config so `yt-dlp` routes through `ssh` (see `youtube_yt.py`).
## Debug and verification flags
<ParamField body="LAST30DAYS_DEBUG" type="string">
Set `1` in environment or config (or pass `--debug`) to enable HTTP debug logging.
</ParamField>
<ParamField body="LAST30DAYS_SKIP_PREFLIGHT" type="string">
Bypass Class 1 refuse gate when set before invocation.
</ParamField>
<ParamField body="--mock" type="flag">
Use fixture retrieval; useful for tests without live API keys.
</ParamField>
## Related pages
<CardGroup>
<Card title="Configure sources" href="/configure-sources">
Credential layers, `INCLUDE_SOURCES`, and per-source API keys tied to diagnose output.
</Card>
<Card title="Quickstart" href="/quickstart">
First invocation, setup wizard, and `--diagnose` verification in the happy path.
</Card>
<Card title="CLI reference" href="/cli-reference">
Full flag list including `--diagnose`, `--search`, depth profiles, and direct CLI constraints.
</Card>
<Card title="Skill contract" href="/skill-contract">
STEP 0 stale-clone guard, pre-flight protocol, and `SKILL_DIR` engine substitution.
</Card>
<Card title="Model clients" href="/model-clients">
Per-harness install, marketplace vs `npx skills`, and stale-install avoidance.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
Env var catalog, `.env` lookup order, and web-backend priority.
</Card>
</CardGroup>
---
## 21. Develop and test
> uv/pytest workflow, coverage omit rules, validate and security CI, optional search-quality eval, and contributor constraints from AGENTS.md.
- Page Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/21-develop-and-test.md
- Generated: 2026-06-04T23:26:24.252Z
### Source Files
- `AGENTS.md`
- `pyproject.toml`
- `tests/test_pipeline_v3.py`
- `.github/workflows/validate.yml`
- `.github/workflows/security.yml`
- `docs/search-quality-eval.md`
- `skills/last30days/scripts/evaluate_search_quality.py`
---
title: "Develop and test"
description: "uv/pytest workflow, coverage omit rules, validate and security CI, optional search-quality eval, and contributor constraints from AGENTS.md."
---
Local development centers on **Python 3.12+**, the **uv** toolchain (`pyproject.toml`, `uv.lock`), and a **pytest** suite under `tests/` that imports engine modules from `skills/last30days/scripts` via `tests/conftest.py`. Default CI on pull requests and `main` runs `uv run pytest` only; advisory security scans and optional retrieval-quality evaluation sit outside that blocking path.
## Prerequisites
| Requirement | Source |
|-------------|--------|
| Python ≥ 3.12 | `pyproject.toml` `requires-python` |
| [uv](https://docs.astral.sh/uv/) | `AGENTS.md`, `.github/workflows/validate.yml` |
| Virtualenv at `.venv/` | Created by `uv` on first `uv run` |
Runtime dependencies for the skill engine are stdlib-first (`[project] dependencies = []`). Dev tools live in `[dependency-groups] dev`: `pytest` and `pytest-cov`.
<Steps>
<Step title="Install toolchain and sync dev deps">
```bash
uv python install 3.12
uv sync --group dev
```
</Step>
<Step title="Run the full test suite">
```bash
uv run pytest
```
Default pytest options (`pyproject.toml`): quiet mode (`-q`), short tracebacks (`--tb=short`), `testpaths = ["tests"]`.
</Step>
<Step title="Optional: coverage report">
```bash
uv run pytest --cov
```
Vendor code under `skills/last30days/scripts/lib/vendor/` is omitted from coverage (see below).
</Step>
</Steps>
## Test layout and imports
`tests/conftest.py` prepends `skills/last30days/scripts` to `sys.path`, so tests import engine modules as `from lib import pipeline` (same layout as the engine at runtime).
The suite spans **94** `test_*.py` modules and **1600+** collected cases (pipeline, sources, CLI, rendering, env, competitors, watchlist, evaluator, and workflow contract tests). Representative offline pipeline coverage lives in `tests/test_pipeline_v3.py`, which exercises `pipeline.run(..., mock=True)` without live credentials.
### Targeted runs
```bash
uv run pytest tests/test_pipeline_v3.py
uv run pytest tests/test_dedupe_v3.py -k test_some_case
```
### Direct engine smoke test (dev only)
`AGENTS.md` treats the slash command as the product path; direct CLI invocation is for scripting, cron, and engine debugging:
```bash
python3 skills/last30days/scripts/last30days.py "test query" --emit=compact
```
## Coverage configuration
Coverage is configured under `[tool.coverage.run]` and `[tool.coverage.report]` in `pyproject.toml`:
| Setting | Value |
|---------|--------|
| `branch` | `true` |
| `source` | `skills/last30days/scripts`, `tests` |
| `omit` | `skills/last30days/scripts/lib/vendor/*`, `dist/*` |
| `skip_empty` | `true` (report) |
| `show_missing` | `true` (report) |
Vendored X client code (`lib/vendor/bird-search/`) is excluded so coverage reflects project-owned logic, not third-party trees.
## CI workflows
```mermaid
flowchart LR
subgraph pr_main["PR / push to main"]
V[validate.yml\nuv run pytest]
S[security.yml\nadvisory scans]
end
subgraph tag["Tag push v*"]
R[release.yml\nbuild-skill.sh → .skill]
end
pr_main --> V
pr_main --> S
tag --> R
```
### Validate (blocking)
`.github/workflows/validate.yml` runs on every pull request and push to `main`:
1. Checkout
2. `astral-sh/setup-uv@v5`
3. `uv python install 3.12`
4. `uv run pytest`
No search-quality eval, lint gate, or release build in this workflow.
### Security (advisory)
`.github/workflows/security.yml` runs on PR, push to `main`, and `workflow_dispatch`. Both jobs use **`continue-on-error: true`** until maintainers confirm a clean baseline; comments in the workflow describe flipping to blocking enforcement.
| Job | Tool | Purpose |
|-----|------|---------|
| `dependency-audit` | `uv export --locked --all-groups` → `pip-audit` | CVE visibility on locked dev deps |
| `secret-scan` | TruffleHog OSS `--only-verified` | Verified secrets in PR/push ranges |
`tests/test_security_workflow.py` locks the advisory policy (pip-audit and TruffleHog present, `continue-on-error: true`, contributor guidance in workflow comments). `AGENTS.md` requires not weakening this workflow without an explained PR rationale.
### Release (tags only)
`.github/workflows/release.yml` triggers on tags `v*`, runs `skills/last30days/scripts/build-skill.sh`, uploads `dist/last30days.skill` via `softprops/action-gh-release@v2`.
## Contract and policy tests
Beyond behavioral unit tests, several modules enforce repository invariants:
| Module | Enforces |
|--------|----------|
| `tests/test_plugin_contract.py` | Version alignment across `pyproject.toml`, `SKILL.md`, `.claude-plugin/plugin.json`, `gemini-extension.json`, marketplace shape; no removed `.codex-plugin/`; workflows must not reference legacy root `scripts/` paths |
| `tests/test_version_consistency.py` | `SKILL.md` double-quoted version, header badge, memory-dir env conventions |
| `tests/test_security_workflow.py` | Security workflow structure and `AGENTS.md` secret-hygiene mentions |
<Warning>
Avoid cross-file version pin tests that compare a bumped `SKILL.md` to a stale hardcoded path in another artifact — that pattern caused cascade CI failures across open PRs after releases (`docs/solutions/workflow-issues/release-consistency-test-cascade-2026-05-16.md`). Prefer single-source version reads or generated pins.
</Warning>
## Optional search-quality evaluation
`skills/last30days/scripts/evaluate_search_quality.py` compares two git revisions on ranked-candidate output. It is **not** part of default CI (`docs/solutions/architecture/search-quality-eval-manual-by-default-2026-05-10.md`): live API cost, latency, and judge non-determinism make it a maintainer-triggered check, not a per-PR gate.
### What it measures
**Deterministic stability** (baseline vs candidate):
- Jaccard overlap on ranked item keys
- Retention vs baseline
- Per-source counts and overlap
**Optional LLM judge** (when a Google/Gemini API key is available):
- `Precision@5`, `nDCG@5`, source-coverage recall over a judged union pool
- Judgments cached under `--output-dir` (default `tmp/search-quality`)
### Default topics
`fixtures/eval_topics.json` supplies eight reviewer topics (comparison, how_to, breaking_news, product, opinion, prediction, concept, factual). Override with `--topics-file`.
### CLI flags (script)
| Flag | Default | Role |
|------|---------|------|
| `--baseline` | `HEAD~1` | Git ref or label; materialized via `git worktree` unless `WORKTREE` |
| `--candidate` | `WORKTREE` | Same; `WORKTREE` uses the current checkout |
| `--search` | `""` | Passed to engine `--search` when set |
| `--output-dir` | `tmp/search-quality` | Metrics and judgment cache |
| `--judge-model` | `gemini-3.1-flash-lite` | Gemini judge model (`lib.providers.GEMINI_FLASH_LITE`) |
| `--timeout` | `240` | Per-topic subprocess timeout (seconds) |
| `--limit` | `20` | Ranked items considered per report |
| `--mock` | off | Forwards `--mock` to engine |
| `--quick` | off | Forwards `--quick` to engine |
| `--topics-file` | — | JSON list of `{topic, query_type}` |
Example:
```bash
uv run python skills/last30days/scripts/evaluate_search_quality.py \
--baseline origin/main \
--candidate WORKTREE \
--quick
```
`create_eval_env()` clears `LAST30DAYS_CONFIG_DIR` and passes selected API keys from env/config (`GOOGLE_API_KEY`, `OPENAI_API_KEY`, `XAI_API_KEY`, `SCRAPECREATORS_API_KEY`, Bluesky/Truth Social tokens, etc.) so eval runs use a controlled config surface. See `docs/search-quality-eval.md` for judge key names and operational notes.
<Info>
Jaccard and retention are regression guards, not ground-truth quality. Judge metrics help compare revisions but do not replace a larger labeled benchmark.
</Info>
Unit tests in `tests/test_evaluator_v3.py` cover ranked-item extraction, worktree resolution, env construction, and summary writers without requiring a full live eval pass.
## Contributor constraints (`AGENTS.md`)
| Area | Rule |
|------|------|
| Product boundary | Agent Skills package; new engine flags need `SKILL.md` integration so harness models see them |
| `lib/__init__.py` | Bare package marker only — no eager imports |
| Install sync | `npx skills add . -g -y` copies a frozen skill tree; re-run after edits, or symlink `skills/last30days` → `~/.agents/skills/last30days` for live dev |
| Slash vs shell | Harness slash commands cannot pipe flags; use direct `python3 .../last30days.py` for shell mechanics |
| Secrets | Never commit real keys, cookies, or `.env`; use `lib/env.py` patterns; dummy values in tests/fixtures |
| Security CI | Do not disable `.github/workflows/security.yml` without PR explanation |
| `CONFIGURATION.md` | Update when adding env vars, CLI config flags, install patterns, or config precedence |
| Beta | Experimental work on private repo → `/last30days-beta`; public promotion via review PR |
## Skill install during development
To test harness behavior after skill edits:
```bash
npx skills add . -g -y
```
For iterative skill work without reinstalling:
```bash
ln -sfn "$PWD/skills/last30days" ~/.agents/skills/last30days
```
(run from repo root)
## Related pages
<CardGroup>
<Card title="Skill, engine, and harness" href="/skill-engine-harness">
Boundaries between SKILL.md, `last30days.py`, and multi-harness runtimes — context for why tests import `lib` from `scripts/`.
</Card>
<Card title="Research pipeline" href="/research-pipeline">
v3 orchestration exercised heavily by `test_pipeline_v3.py` and mock runs.
</Card>
<Card title="CLI reference" href="/cli-reference">
Direct engine flags (`--emit`, `--quick`, `--mock`) used in dev smoke tests and eval subprocesses.
</Card>
<Card title="Beta channel" href="/beta-channel">
Parallel `/last30days-beta` install path for pre-public experiments.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
`--diagnose` and runtime failure modes outside pytest.
</Card>
</CardGroup>
---
## 22. Beta channel
> Parallel /last30days-beta install from the private repo, promotion workflow, and where client-specific customizations belong.
- Page Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/pages/22-beta-channel.md
- Generated: 2026-06-04T23:24:57.467Z
### Source Files
- `CONCEPTS.md`
- `CONFIGURATION.md`
- `AGENTS.md`
- `CHANGELOG.md`
- `docs/releases/v3.0.9.md`
---
title: "Beta channel"
description: "Parallel /last30days-beta install from the private repo, promotion workflow, and where client-specific customizations belong."
---
The beta channel is a second Agent Skills install sourced from the private companion repository `mvanhorn/last30days-skill-private`, exposed to harnesses as `/last30days-beta` alongside the public `/last30days` skill from `mvanhorn/last30days-skill`. Experimental SKILL.md and engine changes land in the private repo first; promotion to the public marketplace requires an explicit review pull request against the public repository—beta-only changes never ship on the public channel without that step.
## Public vs beta
| Dimension | Public channel | Beta channel |
|---|---|---|
| Repository | `mvanhorn/last30days-skill` (this repo) | `mvanhorn/last30days-skill-private` |
| Slash command | `/last30days` | `/last30days-beta` |
| Distribution | Claude Code marketplace, `npx skills add`, per-harness skill dirs | Private-repo install (see `BETA.md` in the private repo) |
| Badge line (first output line) | `🌐 last30days v{VERSION} · synced {YYYY-MM-DD}` | `🧪 last30days-beta · branch <name> · synced {YYYY-MM-DD}` |
| Promotion path | Default release target | Review PR → merge into public `main` |
Both channels share the same architectural split documented in [Skill, engine, and harness](/skill-engine-harness): `SKILL.md` is the runtime contract; `scripts/last30days.py` is the engine. The beta install is a parallel copy of that package with its own harness registration, not a flag on the public skill.
```mermaid
flowchart LR
subgraph public["mvanhorn/last30days-skill"]
P_SKILL["skills/last30days/SKILL.md"]
P_ENGINE["scripts/last30days.py"]
end
subgraph private["mvanhorn/last30days-skill-private"]
B_SKILL["SKILL.md + scripts/"]
B_BETA["BETA.md workflow"]
end
subgraph harness["Harness runtime"]
CMD_P["/last30days"]
CMD_B["/last30days-beta"]
end
P_SKILL --> CMD_P
P_ENGINE --> CMD_P
B_SKILL --> CMD_B
B_BETA -.->|promotion via review PR| public
```
<Note>
Detailed install, sync, and day-to-day beta operations live in `BETA.md` inside the private repository. That file is not vendored into the public tree; treat it as the operator runbook for beta maintainers.
</Note>
## Why the beta channel exists
Real-user validation on experimental SKILL.md and engine changes must not block the stable public marketplace. The beta channel lets maintainers iterate on contract changes (LAWs, badge enforcement, pre-flight flags) and client-specific data (category peers, subreddit lists, competitors-plan templates) before those changes appear in `mvanhorn/last30days-skill`.
The public `SKILL.md` documents why that separation matters in practice: on 2026-04-18 the same model achieved **10/10** compliance on the beta channel and **0/8** on public v3.0.6 with similar SKILL.md content—the delta was structural anchors (mandatory badge, `SKILL_DIR` substitution, preface ordering), not model capability. Beta is the environment where those anchors are proven before promotion.
## What belongs in beta vs public
Use the beta channel for customizations you do **not** intend to upstream—client-vertical category rows in `scripts/lib/categories.py`, internal subreddit lists, recurring `--competitors-plan` JSON skeletons, and other fork-specific SKILL.md tweaks.
Use the public repo’s [per-client setup](/per-client-setup) patterns when the customization is portable or may eventually ship to all users:
| Pattern | Public repo support | Typical beta use |
|---|---|---|
| Project-scoped `.claude/last30days.env` | Yes — [Configure sources](/configure-sources) | Rarely; env files work on either install |
| `--save-suffix` / `LAST30DAYS_MEMORY_DIR` wrappers | Yes | Beta runs often use `-raw-beta` suffix (see A/B testing) |
| Custom `categories.py` rows | Yes (upstreamable) | Vertical-specific peers you will not PR publicly |
| Pre-built `--competitors-plan` JSON | Yes (upstreamable) | Client-industry templates kept private |
<Warning>
Never publish beta-only changes to the public Claude Code marketplace or `main` without a review PR against `mvanhorn/last30days-skill`. The public marketplace plugin (`last30days` in `.claude-plugin/marketplace.json`) tracks only the public repository.
</Warning>
## Promotion workflow
Promotion is a deliberate merge from private experimentation into the public release line—not a separate release channel inside one repo.
<Steps>
<Step title="Develop and validate on beta">
Install or sync from `mvanhorn/last30days-skill-private` so `/last30days-beta` loads the private `SKILL.md` and engine. Run representative topics; confirm the beta badge line appears and output passes the LAWs contract.
</Step>
<Step title="Open a review PR on the public repo">
Cherry-pick or re-implement the validated changes on a branch of `mvanhorn/last30days-skill`. Scope the PR to what should ship to all users; leave client-only forks in the private repo.
</Step>
<Step title="Merge and release publicly">
After review, merge to public `main`, bump plugin version metadata, and let users pick up the release via marketplace update or `npx skills add` re-sync. Beta-only artifacts stay in the private repo.
</Step>
</Steps>
```mermaid
stateDiagram-v2
[*] --> BetaDev: change in last30days-skill-private
BetaDev --> BetaTest: /last30days-beta validation
BetaTest --> BetaDev: fix failures
BetaTest --> ReviewPR: open PR on last30days-skill
ReviewPR --> PublicMain: merge after review
PublicMain --> [*]: /last30days marketplace release
BetaDev --> BetaOnly: client fork stays private
BetaOnly --> [*]
```
The setup plan referenced in contributor docs (`docs/plans/2026-04-17-005-feat-beta-skill-from-private-repo-plan.md`) established this two-repo layout; that plan file is not present in the public tree—use `BETA.md` in the private repo for operational detail.
## A/B testing public vs beta
`skills/last30days/scripts/compare.sh` is a maintainer-side runner that invokes both slash commands sequentially (30s gap for rate limits) and prints saved raw file paths for manual diff review.
<CodeGroup>
```bash title="compare.sh invocation"
bash skills/last30days/scripts/compare.sh "Kevin Rose"
```
```text title="Expected output files"
$LAST30DAYS_MEMORY_DIR/<slug>-raw.md # public /last30days
$LAST30DAYS_MEMORY_DIR/<slug>-raw-beta.md # private /last30days-beta
```
</CodeGroup>
The script drives Claude Code headlessly:
```bash
claude -p --dangerously-skip-permissions "/last30days $TOPIC"
claude -p --dangerously-skip-permissions "/last30days-beta $TOPIC"
```
Verification signals after a run:
| Check | Public | Beta |
|---|---|---|
| First-line badge | `🌐 last30days v…` | `🧪 last30days-beta · branch …` |
| Raw filename suffix | `-raw.md` | `-raw-beta.md` |
| Regression signal | Missing public badge → stale install | Missing beta badge → beta badge regression |
If the beta badge is absent, treat it as a contract regression in the private `SKILL.md` synthesis section (the public repo’s `compare.sh` footer points maintainers at the beta setup plan naming pattern).
## Install and sync boundaries
**Public install (end users):** Claude Code marketplace (`/plugin install last30days@last30days-skill`), `npx skills add mvanhorn/last30days-skill -g -y`, or harness-specific paths documented in [Installation](/installation) and [Model clients](/model-clients).
**Beta install (maintainers / private forks):** Follow `BETA.md` in `mvanhorn/last30days-skill-private`. The public repo does not ship beta marketplace metadata.
Historical note: an older maintainer `sync.sh` script once pinned Claude plugin-cache paths; [#402](https://github.com/mvanhorn/last30days-skill/pull/402) fixed it pointing at the public cache instead of the private repo, and [#405](https://github.com/mvanhorn/last30days-skill/pull/405) removed `sync.sh` entirely in favor of `npx skills add . -g -y` (live symlink) for public dev deploys. Beta deploy semantics remain private-repo concerns.
<Info>
Public `SKILL.md` STEP 0 guards against a different stale-install bug: Claude Code’s `~/.claude/plugins/marketplaces/last30days-skill/` git clone lagging the versioned cache. That check applies to `/last30days`, not the beta install path.
</Info>
## Contributor rules (public repo)
From `AGENTS.md` and `CONFIGURATION.md`:
- Mirror new **public** configuration knobs in `CONFIGURATION.md` when they land in `SKILL.md` or the engine.
- Keep beta-only experiments out of public `main` until the review PR lands.
- When a config concept is beta-only today but becomes universal, promote code **and** docs in the same public PR.
## Related pages
<CardGroup>
<Card title="Per-client setup" href="/per-client-setup">
Project-scoped `.claude/last30days.env`, save-dir isolation, and portable wrapper patterns on the public skill.
</Card>
<Card title="Installation" href="/installation">
Marketplace and `npx skills add` flows for the public `/last30days` channel.
</Card>
<Card title="Skill contract" href="/skill-contract">
SKILL.md runtime authority, STEP 0 stale-clone guard, and engine invocation rules shared by both channels.
</Card>
<Card title="Develop and test" href="/develop-and-test">
`uv run pytest`, contributor constraints, and validation before promotion.
</Card>
</CardGroup>
---