# Comparison recipes

> Copy-paste comparison workflows: vs-topic phrasing, --competitors fan-out, and multi-entity Head-to-Head table expectations.

- Repository: mvanhorn/last30days-skill
- GitHub: https://github.com/mvanhorn/last30days-skill
- Human docs: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca
- Complete Markdown: https://grok-wiki.com/public/docs/mvanhorn-last30days-skill-50b5421a8cca/llms-full.txt

## 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>
