Files
last30days-skill/docs/plans/2026-04-22-005-feat-vs-mode-n-passes-competitors-auto-discovery-plan.md
T
Matt Van Horn 949bcf8942 feat: vs mode N full passes + --competitors auto-discovery + (/Last30Days) title (#312)
* feat: vs mode runs N full passes; --competitors wraps vs with auto-discovery

Unifies vs-mode and --competitors onto one fanout architecture. A topic
containing "vs" / "versus" now runs N full pipeline.run() calls in parallel
(reverting the one-pass latency optimization that removed per-entity
depth); --competitors becomes a SKILL.md-level shortcut where the hosting
reasoning model (Claude Code, Codex, Hermes, Gemini) discovers N peers via
its own WebSearch, runs Step 0.55 per entity, and invokes the engine with
a vs-topic + --competitors-plan JSON.

Changed:
- vs-mode: N full passes in parallel via fanout (was 1 merged pass).
- --competitors: SKILL.md shortcut for vs-mode-with-discovery. Engine flag
  kept for headless/cron use. LAW 7-style stderr reframed to lead with the
  hosting-model path (use WebSearch + --competitors-plan) instead of
  BRAVE_API_KEY. Footer BRAVE/SERPER nudge suppressed when --plan or
  --competitors-plan present (hosting model already has WebSearch).

Added:
- --competitors-plan JSON flag: per-entity {x_handle, x_related, subreddits,
  github_user, github_repos, context}. Accepts inline JSON or file path.
  subrun_kwargs_for helper is the single source of truth for per-entity
  kwargs — no closure-default fallthrough from main scope.
- Per-entity save files: each entity's sub-run produces its own
  {slug}-raw.md with a single-row Resolved Entities block.
- --polymarket-keywords filter for ambiguous single-token topics.

Fixed:
- test_competitor_subrun_isolation regression suite locks in 3.0.12's
  no-leak invariant (main flags do not inherit into peer sub-runs).
- Updates test_regression.py for the new comparison-mode payload shape.

Bumps plugin.json to 3.0.13. 1,219 tests passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: comparison title attribution — (Last 30 Days) → (/Last30Days)

User feedback on 3.0.13 dogfood runs (Kanye vs Drake, Mercer Island,
Figma): the comparison-mode synthesis title should attribute to the
slash command rather than restate the date range.

Three SKILL.md occurrences updated. Pure documentation change. Bumps to
3.0.14.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 21:31:00 -07:00

32 KiB
Raw Blame History

title, type, status, date, origin
title type status date origin
feat: vs mode runs N full passes and --competitors is vs with auto-discovery feat active 2026-04-22 docs/plans/2026-04-22-004-fix-competitors-hosting-model-resolve-and-leak-plan.md.superseded

feat: vs mode runs N full passes and --competitors is vs with auto-discovery

Overview

Architectural unification driven by user correction 2026-04-22: vs mode and --competitors are the same thing. A user typing /last30days OpenAI vs Anthropic vs xAI should get a full single-entity last30days pass for each of the three entities — three full pipelines, three saved *-raw.md files, merged into one comparison output. A user typing /last30days OpenAI --competitors should get the same output after the hosting model auto-picks 2 peers; i.e., --competitors is a thin shortcut that expands "topic + --competitors" into "topic vs peer1 vs peer2" and then runs the unified vs pipeline.

Current state diverges from this:

  • vs mode today: one pipeline.run() with a comparison-optimized plan that merges all entities' targeting into a single retrieval pool. Lower-weight --x-related for peers, merged subreddits, cross-entity keyword noise. One saved file.
  • --competitors today (3.0.12): N parallel pipeline.run() calls via scripts/lib/fanout.py, but per-entity Step 0.55 depends on an engine-side web backend key Matt doesn't have. Silently degrades to planner defaults for peers. One saved file (main topic only). Override-leak from main into peers.

After this plan:

  • vs mode: N parallel pipeline.run() calls, one per entity, each with its own full Step 0.55-grade targeting, each saving its own *-raw.md. Merged into one comparison output.
  • --competitors: SKILL.md shortcut. Hosting model discovers N peers, builds "topic vs peer1 vs peer2", and invokes the same vs pipeline. No separate orchestration path.
  • Same fanout machinery (scripts/lib/fanout.py) serves both. One fix, both behaviors improve.

Problem Frame

The product insight from 2026-04-22 test runs is simple: the user wants three full last30days reports plus a comparison merge. Not one comparison pass with N-way targeting merged into a single retrieval pool. Not one save file. Not "main gets Step 0.55, peers get planner defaults." Three full passes. Three save files. Merged output.

The historical vs mode did that (it ran as 3 passes, saving 3 files). SKILL.md §551 currently says:

"When the user asks 'X vs Y', run ONE research pass with a comparison-optimized plan that covers both entities AND their rivalry. This replaces the old 3-pass approach (which took 13+ minutes and produced tangential content)."

That change was a latency optimization that removed the user-visible behavior the user wants. The fix is to revert the architectural direction: N passes per entity, in parallel rather than serial (parallelism lowers wall-clock to ~1× a single pass, not N×), with per-entity save files.

The 3.0.11 --competitors flag already introduced parallel N-pass machinery (fanout.run_competitor_fanout). The 3.0.12 follow-up tried to wire per-entity Step 0.55 into it but failed when no web backend was configured. The elegant move: stop maintaining two architectures. vs-mode and --competitors both use fanout.py. --competitors becomes a SKILL.md-level shortcut that discovers 2 peers and hands off to vs-mode.

Four 2026-04-22 test receipts (Warriors, Seattle, Arizona Wildcats, Kanye West) all confirmed the user's pain points:

  • Peers thin because they ran without per-entity handle/sub targeting.
  • Only one *-raw.md per run — no per-entity audit.
  • Kanye peers leaked main topic's --subreddits.
  • Engine footer nudging BRAVE_API_KEY to Claude Code users who already have WebSearch.
  • Polymarket noise on ambiguous topics (Warriors → Glasgow rugby; Arizona → Diamondbacks).

This plan closes all of them by unifying the architecture and making hosting-model-driven Step 0.55 per entity the canonical path.

Requirements Trace

  • R1. vs mode (any topic containing vs / versus) runs N full pipeline.run() calls in parallel, one per entity. Each sub-run uses its entity's own Step 0.55 targeting (from the hosting model's pre-resolution, passed via a new --competitors-plan JSON).
  • R2. --competitors (and --competitors=N) becomes a SKILL.md-level shortcut: the hosting model (a) discovers N peers via WebSearch, (b) runs Step 0.55 per entity (main + peers), (c) rewrites the topic to "main vs peer1 vs peer2", (d) invokes the engine with --competitors-plan containing each entity's targeting.
  • R3. New --competitors-plan JSON flag. Schema: {entity_name: {x_handle, x_related, subreddits, github_user, github_repos, context}}. Implies vs mode when present with a single-entity topic. Applies per-entity targeting to each sub-run. Accepts inline JSON or a file path (matches --plan).
  • R4. Each entity's sub-run saves its own *-raw.md file when --save-dir is in use. Example: /last30days "Kanye West vs Drake vs Kendrick Lamar" --save-dir=~/Documents/Last30Days produces kanye-west-raw.md, drake-raw.md, kendrick-lamar-raw.md. Same filenames a single-entity run of each topic would produce. Matches historical vs-mode behavior.
  • R5. Each per-entity saved file includes its own single-row ## Resolved Entities block so the audit survives. The merged comparison stdout still shows the full 3-row block.
  • R6. Override-leak fix: no main-topic flags (--subreddits, --x-handle, --x-related, --tiktok-*, --ig-creators, --github-*) leak into peer sub-runs. Every per-entity kwarg is scrubbed at the sub-run call site.
  • R7. LAW 7-style stderr for --competitors invocations with no list, no plan, no backend is reframed for hosting-model context: leads with "use your WebSearch to discover peers, resolve Step 0.55 per entity, re-invoke with topic vs peer1 vs peer2 --competitors-plan '...'." Does not lead with BRAVE_API_KEY.
  • R8. Footer nudge 💡 You can unlock native grounded web search with BRAVE_API_KEY... is suppressed when --plan or --competitors-plan was passed.
  • R9. Polymarket disambiguation: support --polymarket-keywords "kw1,kw2" to filter market matches; auto-skip Polymarket when topic is single-token-ambiguous and no override is provided.
  • R10. Default --competitors count stays 2 peers (3-way comparison). Unchanged from 3.0.12.

Scope Boundaries

  • No changes to single-entity pipeline.run() semantics. Each sub-run in vs mode behaves identically to a bare /last30days {entity} invocation.
  • No changes to the planner's comparison-intent logic for single-entity-containing topics. The _should_force_deterministic_plan shortcut for vs-topics routes to fanout, not to its current single-pipeline path.
  • No new emit modes. Comparison output format unchanged.
  • No removal of --competitors-list. Stays as a minimum escape hatch (names-only, no per-entity targeting) for scripted headless use.
  • No removal of engine-internal resolve.auto_resolve() in fanout. Remains as headless / cron fallback for users with BRAVE/EXA/SERPER/PARALLEL/OPENROUTER keys. The dominant Claude Code path bypasses it via --competitors-plan.

Deferred to Separate Tasks

  • Explicit "head-to-head" rivalry pass in vs-mode (a supplemental subquery like "A vs B" that catches rivalry articles missing from pure entity-scoped passes). Start with N independent passes; add a head-to-head supplemental pass if the rivalry-content gap shows up in dogfood.
  • Cache layer for hosting-model pre-resolution.
  • Cross-source disambiguation (not just Polymarket).
  • Latency knob for users who want the old one-pass vs behavior (probably not needed; parallel N-pass is ~1× wall clock).

Context & Research

Relevant Code and Patterns

  • scripts/last30days.py — main(), _main_runner, _competitor_runner, the competitor enable/discovery branch. Primary file.
  • scripts/lib/fanout.py — existing orchestrator (3.0.11). Reused as-is; competitor_runner closure is where per-entity kwargs apply.
  • scripts/lib/planner.py_should_force_deterministic_plan detects vs-topics via regex. Current path synthesizes ONE comparison plan; new path routes to fanout.
  • scripts/lib/render.pyrender_comparison_multi (3.0.12) + _render_resolved_entities_block. Both reused. render_full needs a per-entity variant when saving sub-run files.
  • scripts/last30days.py save_output — where raw files are written. Needs to iterate per entity when competitor_reports artifact present.
  • scripts/lib/quality_nudge.py — BRAVE/SERPER nudge emission.
  • scripts/lib/polymarket.py — source adapter for --polymarket-keywords and ambiguous-topic auto-skip.
  • SKILL.md §551 "If QUERY_TYPE = COMPARISON" and §679 per-entity Step 0.55 protocol — the hosting-model contract that drives per-entity pre-resolution for both vs mode and --competitors.

Institutional Learnings

  • 3.0.11 plan (2026-04-22-002): built fanout.
  • 3.0.12 plan (2026-04-22-003): tried engine-internal per-entity auto_resolve; failed without backend keys.
  • 3.0.13 plan draft (2026-04-22-004-...superseded): proposed --competitors-plan JSON + vs-mode-shortcut path but kept them separate. User's 2026-04-22 correction unifies them.
  • 2026-04-22 test receipts: Warriors, Seattle, Arizona Wildcats, Kanye West runs all reproduced the per-entity resolve gap.
  • User's architectural steer: "vs mode should work that way too" + "--competitors is just vs mode with auto-discovery." This plan encodes that.

External References

  • None. All patterns in-repo.

Key Technical Decisions

  • Unify vs-mode and --competitors on one orchestrator. fanout.run_competitor_fanout serves both. vs-mode is "topic contains ' vs '" detection → fanout. --competitors is "SKILL.md shortcut → hosting model rewrites topic to vs form → fanout." One code path.
  • Per-entity targeting via --competitors-plan JSON. Schema {entity_name: {x_handle, x_related, subreddits, github_user, github_repos, context}}. Mirrors --plan. Applies to both vs-mode and --competitors paths. Hosting model passes it after running Step 0.55 per entity.
  • N save files, one per entity. Each sub-run writes a {entity-slug}-raw.md file when --save-dir is set. Matches historical vs-mode behavior. Single-entity runs unchanged.
  • Revert the "one pass for latency" optimization that removed per-entity passes. Parallel execution via ThreadPoolExecutor means wall-clock is ~max(per-entity-latency), not sum. The old latency concern (13+ minutes for 3 serial passes) does not apply to a parallel fan-out.
  • Override-leak fix at the call site. _subrun_kwargs(entity, plan_entry) helper returns fully explicit per-entity kwargs; no closure-default fallthrough from main scope.
  • LAW 7 stderr reframed, not just updated. Current message treats BRAVE_API_KEY as the solution. New message treats hosting-model Step 0.55 as the solution, with backend keys listed only as the headless fallback.
  • Polymarket disambiguation is additive and conservative. --polymarket-keywords is explicit; auto-skip only fires for a known-ambiguous single-token list.

Open Questions

Resolved During Planning

  • vs mode N passes or single-pass? N passes. User's architectural correction.
  • Should --competitors still be an engine flag at all? Yes, kept for headless / cron contexts with backend keys. Dominant Claude Code path is SKILL.md shortcut → vs-mode fanout. Engine flag stays as compatibility surface.
  • --competitors-plan JSON or multi-flag? JSON. Matches --plan.
  • Default count? 2 peers → 3-way comparison. Unchanged.
  • Saved-file naming? {entity-slug}-raw.md per entity, same as single-entity runs would produce.

Deferred to Implementation

  • Exact trace of override-leak path (closure capture vs shared config vs Reddit adapter fallback). Test-first per Unit 2; patch at the right layer.
  • Heuristic for single-token-ambiguous Polymarket auto-skip. Start with a short hard-coded list; iterate.
  • Whether to include a head-to-head rivalry supplemental pass in vs-mode. Ship N-independent passes first; revisit after dogfood if rivalry content is missing.
  • Exact filename convention when the comparison merged output is saved (if saved at all). Not blocking — per-entity files are the primary save artifact.

High-Level Technical Design

This illustrates the intended approach and is directional guidance for review, not implementation specification. The implementing agent should treat it as context, not code to reproduce.

User invokes:
   /last30days "OpenAI vs Anthropic vs xAI"
   OR
   /last30days OpenAI --competitors     (hosting model rewrites to vs form)
   OR
   /last30days OpenAI --competitors-list "Anthropic,xAI"
   OR
   /last30days "OpenAI vs Anthropic vs xAI" --competitors-plan '{...per-entity...}'

           ↓

scripts/last30days.py main():
   - Detect: topic has " vs " OR --competitors enabled
   - If --competitors and no list/plan: emit LAW 7-style stderr with hosting-model instruction
   - If --competitors with list or discovery: rewrite topic to vs form, continue
   - Parse --competitors-plan JSON, map to entities

           ↓

fanout.run_competitor_fanout (shared path):
   - For each entity (main + peers):
       - entity_config = dict(config)  [deep copy to prevent leak]
       - kwargs = _subrun_kwargs(entity, plan_entry)  [explicit; no main-topic leak]
       - If plan_entry missing a field AND backend available: auto_resolve() fill
       - pipeline.run(topic=entity, **kwargs, internal_subrun=True)
   - Parallel ThreadPoolExecutor
   - Collect per-entity Reports
   - Attach resolved targeting to each Report.artifacts["resolved"]

           ↓

scripts/last30days.py after fanout:
   - If --save-dir: save each entity's Report as {entity-slug}-raw.md
     Each file includes its own single-row Resolved Entities block
   - emit_comparison_output → render_comparison_multi (merged stdout)
     Includes full N-row Resolved Entities block

Implementation Units

  • Unit 1: vs-topic detection routes to fanout (not single-pipeline)

Goal: A topic containing vs / versus triggers fanout.run_competitor_fanout with the parsed entities. Each entity runs a full pipeline.run(). Replace the current single-pipeline-with-comparison-plan behavior.

Requirements: R1

Dependencies: None

Files:

  • Modify: scripts/last30days.py (main() — detect vs-topic, route to fanout)
  • Modify: scripts/lib/planner.py (remove / bypass the _should_force_deterministic_plan special case for vs topics; vs topics no longer go through plan_query as a single comparison plan)
  • Test: tests/test_vs_mode_fanout.py (new)

Approach:

  • Parse the incoming topic: if it contains vs or versus (case-insensitive), split into entities (reuse planner._comparison_entities-style logic or move that utility into main()).
  • When vs-entities are detected, route to the same fanout branch --competitors uses today. The entity list comes from the topic string; no discovery step needed.
  • Each entity runs pipeline.run() with its own plan (either from --competitors-plan[entity] or from the engine's per-entity fallback path).
  • For back-compat, if the user passes both a vs-topic AND --plan, honor --plan for the main (first) entity and use per-entity defaults for peers unless --competitors-plan is also provided.

Execution note: Start with an integration test that runs "A vs B" via mock mode and asserts fanout was called with two entities + two pipeline.run calls.

Patterns to follow:

  • 3.0.11 fanout wiring in scripts/last30days.py's --competitors branch.
  • planner._comparison_entities for the split logic.

Test scenarios:

  • Happy path: topic "A vs B" → two pipeline.run calls, two Reports returned, merged render.
  • Happy path: topic "A vs B vs C" → three pipeline.run calls.
  • Happy path: topic "A versus B" → matches the same regex, two pipelines.
  • Edge case: topic "OpenAI vs" (trailing empty entity) → treated as single-entity "OpenAI", not vs mode.
  • Edge case: topic contains "vs." (dot, no trailing space) → existing regex tolerates it; verify.
  • Edge case: topic "A vs B" plus --plan → plan applies to first entity only, peers use per-entity defaults.
  • Integration: full vs-mode run end-to-end in mock mode; verify rendered output, stderr has one [Competitors] Comparing: A vs B vs ... line.

Verification:

  • Test assertions pass.

  • Mock-mode smoke of /last30days "OpenAI vs Anthropic" shows fanout invocation, per-entity Reports, merged comparison output.

  • Unit 2: --competitors-plan JSON flag + _subrun_kwargs helper + override-leak fix

Goal: New JSON flag threads per-entity targeting into each sub-run's pipeline.run(). A _subrun_kwargs(entity, plan_entry) helper is the single source of truth for per-entity kwargs, eliminating override-leak.

Requirements: R3, R6

Dependencies: None (can land alongside or before Unit 1)

Files:

  • Modify: scripts/last30days.py (argparse + parse + _competitor_runner + _subrun_kwargs helper)
  • Possibly modify: scripts/lib/fanout.py (no signature change expected; the competitor_runner contract is unchanged)
  • Test: tests/test_cli_competitors.py (extend)
  • Test: tests/test_competitors_plan_threading.py (new)
  • Test: tests/test_competitor_subrun_isolation.py (new, regression)

Approach:

  • Add --competitors-plan argparse flag. Accepts inline JSON or file path (mirror --plan).
  • Validation: top-level dict; each value is a dict; unknown fields log warnings; malformed input exits 2. Case-insensitive entity matching.
  • Schema: {entity_name: {x_handle?, x_related?, subreddits?, github_user?, github_repos?, context?}}.
  • Build _subrun_kwargs(entity, plan_entry) — returns an explicit dict with every per-entity flag. No closure-default fallthrough. This is the leak fix.
  • _competitor_runner(entity):
    1. Get plan_entry from --competitors-plan if present.
    2. Build base kwargs with _subrun_kwargs(entity, plan_entry).
    3. Fill missing fields via resolve.auto_resolve(entity, entity_config) only if backend is configured (3.0.12 fallback path).
    4. Call pipeline.run(topic=entity, internal_subrun=True, **kwargs).
    5. Attach resolved dict to report.artifacts.
  • Verify no per-entity flag from main() leaks via closure. The helper is the only source of per-entity values.

Execution note: Test-first for the override-leak regression. Use the Kanye 2026-04-22 receipt as the failing test input (main --subreddits=Kanye,hiphopheads + --competitors-list "Drake" → assert Drake's pipeline.run receives subreddits=None).

Patterns to follow:

  • --plan parsing block in scripts/last30days.py.
  • 3.0.12's entity_config = dict(config) deep-copy pattern.

Test scenarios:

  • Happy path: --competitors-plan '{"Drake":{"x_handle":"Drake","subreddits":["Drizzy"]}}' → Drake's pipeline.run receives x_handle="Drake", subreddits=["Drizzy"]. No auto_resolve call for Drake.
  • Happy path: plan covers 2 of 3 entities, backend configured → covered skip auto_resolve; third falls back.
  • Happy path: plan file path accepted like --plan.
  • Happy path: case-insensitive entity match.
  • Edge case: unknown fields → warn, ignore.
  • Edge case: plan entry for entity not in list → warn, ignore.
  • Error path: malformed JSON → exit 2.
  • Error path: top-level JSON is list → exit 2.
  • Regression (leak): main --subreddits=A,B + --competitors-list "X" + no plan → X's pipeline.run gets subreddits=None.
  • Regression (leak): same for --x-handle, --x-related, --tiktok-hashtags, --tiktok-creators, --ig-creators, --github-user, --github-repo.
  • Regression (leak): main --x-handle=kanye + plan {"Drake":{"x_handle":"Drake"}} → Drake's sub-run gets x_handle="Drake", NOT "kanye".

Verification:

  • All regression tests pass.

  • Smoke run (mock mode + plan): stderr shows per-entity [Competitors] {entity}: x=... subs=... line; no leak from main topic's flags.

  • Unit 3: Per-entity save files

Goal: When --save-dir is set in a vs-mode or --competitors run, each entity's sub-run saves its own {entity-slug}-raw.md file — same format as a single-entity run would produce.

Requirements: R4, R5

Dependencies: Unit 1, Unit 2

Files:

  • Modify: scripts/last30days.py (save_output iteration after fanout)
  • Modify: scripts/lib/render.py (render_full includes single-row Resolved Entities block when that entity's artifacts["resolved"] is present)
  • Test: tests/test_save_raw_per_entity.py (new)

Approach:

  • After fanout completes, iterate report.artifacts["competitor_reports"] (or equivalent). For each (entity, entity_report):
    • Call save_output(entity_report, emit="md", save_dir=args.save_dir, suffix=args.save_suffix).
    • Uses entity's slugify(entity) for the filename. Same pattern a single-entity run uses.
  • Each saved file invokes render_full (or the save-variant). render_full now checks for report.artifacts["resolved"] and prepends a single-row Resolved Entities block.
  • Stderr logs one [last30days] Saved output to <path> line per entity.
  • Single-entity runs unchanged (no extra files, render_full unchanged for them).

Patterns to follow:

  • Existing save_output invocation in main() for single-entity runs.
  • slugify(topic) for filename.
  • 3.0.12's _render_resolved_entities_block (reused, single-row mode).

Test scenarios:

  • Happy path: /last30days "A vs B vs C" --save-dir=/tmp/x/tmp/x/a-raw.md, /tmp/x/b-raw.md, /tmp/x/c-raw.md exist.
  • Happy path: --competitors-list "Drake,Kendrick" --save-dir=/tmp/x on topic Kanye → three files: kanye-west-raw.md, drake-raw.md, kendrick-lamar-raw.md.
  • Happy path: each file includes a single-row Resolved Entities block for its entity.
  • Happy path: single-entity run with --save-dir → one file, no Resolved block (unchanged).
  • Edge case: --save-suffix=v3 → all N files get the suffix.
  • Edge case: one entity sub-run failed → its file is NOT saved; the others are.
  • Integration: ls {save-dir}/*-raw.md returns N files after a vs-mode run.

Verification:

  • Test assertions pass.

  • Manual vs-mode smoke saves N files.

  • Unit 4: LAW 7-style stderr reframe + footer-nudge suppression

Goal: The --competitors-with-no-backend stderr tells the hosting model to do Step 0.55 per entity and pass --competitors-plan. The BRAVE/SERPER footer nudge is suppressed when --plan or --competitors-plan is present.

Requirements: R7, R8

Dependencies: Unit 2 (flag must exist)

Files:

  • Modify: scripts/last30days.py (the [Competitors] --competitors requires... stderr block)
  • Modify: scripts/lib/quality_nudge.py (or wherever footer nudge emits; verify during implementation)
  • Test: tests/test_competitors_no_backend_message.py (new)
  • Test: tests/test_footer_nudge_suppression.py (new)

Approach:

  • Rewrite stderr in this order:
    1. "If you are the hosting reasoning model (Claude Code, Codex, Hermes, Gemini, or any agent with WebSearch), the recommended path: (a) discover N peers via WebSearch, (b) run Step 0.55 for main + each peer, (c) re-invoke as /last30days 'topic vs peer1 vs peer2' --competitors-plan '{...}'. See SKILL.md 'Competitor mode'."
    2. "Headless / cron path: set BRAVE_API_KEY / EXA_API_KEY / SERPER_API_KEY / PARALLEL_API_KEY / OPENROUTER_API_KEY and re-run."
    3. "Minimum escape hatch: --competitors-list 'A,B,C' skips discovery but does not pre-resolve peers."
  • Suppress footer nudge when external_plan OR competitors_plan was passed.

Test scenarios:

  • Happy path: --competitors with no backend, no list, no plan → stderr leads with "If you are the hosting reasoning model" and references --competitors-plan before naming API keys.
  • Happy path: --plan passed → footer nudge does NOT fire.
  • Happy path: --competitors-plan passed → footer nudge does NOT fire.
  • Happy path: --competitors-list only (no plan, no backend) → footer nudge still fires (hosting model didn't fully engage).
  • Happy path: no --competitors, no --plan → footer nudge unchanged.

Verification:

  • Tests pass.

  • Unit 5: Polymarket disambiguation guard

Goal: --polymarket-keywords "kw1,kw2" filters market matches; auto-skip Polymarket on single-token-ambiguous topics without override.

Requirements: R9

Dependencies: None

Files:

  • Modify: scripts/last30days.py (argparse)
  • Modify: scripts/lib/polymarket.py
  • Test: tests/test_polymarket_disambiguation.py (new)

Approach:

  • Add --polymarket-keywords "kw1,kw2". When provided, Polymarket adapter filters market titles to those whose normalized text contains at least one keyword.
  • Auto-skip: if topic is one token AND matches a known-ambiguous list (US state names, US city names, common sports/color/animal words) AND no --polymarket-keywords, skip Polymarket with stderr note.
  • SKILL.md update (small): mention --polymarket-keywords in Step 0.55 instructions for ambiguous topics.

Test scenarios:

  • Happy path: topic "Warriors", no override → Polymarket skipped; stderr note.
  • Happy path: topic "Warriors", --polymarket-keywords "nba,gsw" → Polymarket runs, filtered.
  • Happy path: topic "OpenAI" → Polymarket runs as before.
  • Happy path: topic "Arizona Wildcats" (multi-token) → Polymarket runs as before.
  • Edge case: --polymarket-keywords "" → treated as empty, no filter.

Verification:

  • Warriors smoke → Polymarket footer absent or filtered.

  • Unit 6: SKILL.md rewrite — vs mode is the canonical path, --competitors is a shortcut

Goal: SKILL.md documents the unified architecture. vs mode runs N full passes. --competitors is a SKILL.md-level shortcut that discovers 2 peers and invokes vs mode with --competitors-plan.

Requirements: R1, R2, R10 (surfaces them)

Dependencies: Units 1-4

Files:

  • Modify: SKILL.md (§551 "If QUERY_TYPE = COMPARISON" rewrite; Competitor mode subsection rewrite)
  • Modify: README.md (one-line example)

Approach:

  • Rewrite §551 to describe the N-pass architecture: "When the user asks 'X vs Y' (or 'X vs Y vs Z'), run Step 0.55 per entity, then invoke the engine. The engine fans out N full pipelines in parallel. Each entity gets its own single-entity-grade coverage. Wall clock is close to a single run."
  • Remove the "ONE research pass with a comparison-optimized plan that replaces the old 3-pass approach" language.
  • Add a --competitors-plan JSON example.
  • Rewrite the Competitor mode subsection: "--competitors is a shortcut. The hosting model: (1) runs WebSearch to discover N=2 peers, (2) runs Step 0.55 for main + each peer, (3) rewrites topic to 'main vs peer1 vs peer2', (4) invokes engine with --competitors-plan '{...}'. Engine flag --competitors and --competitors-list remain for headless fallback."
  • Cross-reference §679 (per-entity Step 0.55 protocol).
  • Warning: a thin ## Resolved Entities block (dashes for any entity) means the hosting model skipped Step 0.55 for that one.

Patterns to follow:

  • Existing §679 per-entity Step 0.55 protocol for tone.
  • 3.0.12 Competitor mode prose for terseness.

Test scenarios:

  • Test expectation: none — documentation. Verification is dogfood.

Verification:

  • /last30days "OpenAI vs Anthropic vs xAI" in a fresh Claude Code window produces 3 save files with populated Resolved blocks and non-dash per-entity targeting.

  • /last30days OpenAI --competitors produces same after discovery step.

  • Unit 7: Version 3.0.13, CHANGELOG, sync, hot-copy

Goal: Ship 3.0.13 to all local targets.

Requirements: Closes R1-R10

Dependencies: Units 1-6

Files:

  • Modify: .claude-plugin/plugin.json
  • Modify: CHANGELOG.md
  • Run: bash scripts/sync.sh
  • Hot-copy: ~/.claude/plugins/cache/last30days-skill/last30days/3.0.13/

Approach:

  • CHANGELOG: group the changes. "Changed: vs mode now runs N full passes in parallel, one per entity — reverting the one-pass optimization to restore per-entity depth. Added: --competitors-plan JSON for per-entity Step 0.55 targeting (applies to vs mode and --competitors). Changed: --competitors is now a SKILL.md shortcut for vs-with-discovery. Added: per-entity *-raw.md save files. Fixed: override-leak from main to peer sub-runs. Changed: LAW 7 stderr framing for hosting-model context. Changed: BRAVE/SERPER footer nudge suppressed when --plan / --competitors-plan present. Added: --polymarket-keywords + auto-skip for ambiguous topics."
  • Beta channel first per CLAUDE.md.
  • Hot-copy so public /last30days picks up 3.0.13.

Test scenarios:

  • Test expectation: none — packaging.

Verification:

  • grep version .claude-plugin/plugin.json → 3.0.13.
  • sync.sh exits 0.
  • Hot-copy contains the new files.

System-Wide Impact

  • Interaction graph: vs-mode and --competitors share one orchestrator (fanout.run_competitor_fanout). _subrun_kwargs is the single source of per-entity kwargs. Save loop iterates per entity.
  • Error propagation: Per-entity sub-run failure → logged, dropped, continue (3.0.11 behavior unchanged). --competitors-plan JSON parse errors exit 2 (same shape as --plan).
  • State lifecycle risks: entity_config = dict(config) deep-copy pattern extends to every per-entity flag (Unit 2 fix). No cross-entity context leak.
  • API surface parity: --competitors-plan is additive. --competitors, --competitors-list, --plan unchanged. --polymarket-keywords additive. vs-mode keeps its topic-string surface.
  • Integration coverage: New vs-mode-fanout integration test. New override-leak regression test. New plan-threading test. New nudge-suppression test. New per-entity-save test. New Polymarket disambiguation test.
  • Unchanged invariants: pipeline.run() signature unchanged. Single-entity render path unchanged. LAW 7 on the default path unchanged (still fires when a single-entity run lacks --plan).

Risks & Dependencies

Risk Mitigation
vs-mode N-pass latency feels slower for users who remember the one-pass shortcut. Parallel execution keeps wall-clock ~= max(per-entity-latency), not sum. --quick on a vs-topic still applies to each sub-run. CHANGELOG calls out the revert + parallelism.
API cost scales linearly with N (per source). Default count 2 caps it. Hard max 6 on --competitors. vs-mode users opted into N entities explicitly.
Rivalry content ("A vs B" articles) missed in N-independent passes. Deferred to separate task (head-to-head supplemental pass). Start shipping and observe whether this is actually a gap.
Hosting model skips --competitors-plan and uses --competitors-list only. Unit 4 stderr reframe steers explicitly. SKILL.md Unit 6 makes the plan-path canonical. Thin Resolved block in output makes skipped-Step-0.55 visible.
Override-leak fix misses a subtle closure path. Unit 2 is test-first with the Kanye receipt as the failing input. Regression test asserts every per-entity flag is None unless plan provides it.

Documentation / Operational Notes

  • Beta channel first per CLAUDE.md.
  • After merge: hot-copy to ~/.claude/plugins/cache/last30days-skill/last30days/3.0.13/.
  • CHANGELOG explicitly frames the vs-mode change as an architectural revert-with-parallelism, not a regression to the old serial N-pass.

Sources & References

  • Superseded plan: docs/plans/2026-04-22-004-fix-competitors-hosting-model-resolve-and-leak-plan.md.superseded
  • Previous plan (3.0.12): docs/plans/2026-04-22-003-fix-competitors-per-entity-resolution-plan.md
  • Initial plan (3.0.11): docs/plans/2026-04-22-002-feat-competitors-flag-comparison-fanout-plan.md
  • 2026-04-22 test session receipts (Warriors, Seattle, Arizona Wildcats, Kanye West)
  • SKILL.md §551 + §679 — the per-entity Step 0.55 protocol the hosting model uses for both paths
  • Related code: scripts/lib/fanout.py, scripts/last30days.py _competitor_runner, scripts/lib/planner.py vs-topic special-case, scripts/lib/render.py _render_resolved_entities_block, scripts/lib/polymarket.py, scripts/lib/quality_nudge.py
  • Related PRs: #308 (3.0.11), #309 (3.0.12)