Documents the cascade pattern surfaced during this session's install-modernization arc: a `test_sync_cache_path_uses_skill_version` test asserted that a hardcoded version pin in `sync.sh` matched the version frontmatter in SKILL.md. When a release bumped SKILL.md, every open PR's CI failed simultaneously on the unrelated stale-pin assertion. Affected at least 5 PRs across the 2026-05-13 to 2026-05-15 window (#400, #390, #392, and two others) plus required hotfix PR #397 to unblock the queue. The permanent fix shipped in PR #405 (deleted sync.sh + the test). This doc captures the design lesson so the pattern doesn't reappear: don't write consistency tests that read two files and assert one matches a value derived from the other. Either derive at runtime from a single source of truth, or self-skip / merge-base-scope the test so deletion is a non-event. Created via /ce-compound. Includes: - docs/solutions/workflow-issues/release-consistency-test-cascade-2026-05-16.md (the new learning — first entry under docs/solutions/) - CONCEPTS.md (new — 4 entries: Skill, Engine, Harness, Beta channel, capturing project-specific vocabulary that surfaced across the session) - AGENTS.md (added one-line Structure entries surfacing docs/solutions/ and CONCEPTS.md so fresh agents discover them) - docs/plans/2026-04-22-{002,003,005,006}-*-plan.md (added deprecation banner to each, pointing readers at PR #405 and the new docs/solutions entry — these 4 historical plans still reference the deleted sync.sh inline) Also: closed PR #379 (j-sperling's workaround for the same cascade, superseded by PR #405's permanent fix).
32 KiB
NOTE (added 2026-05-16): This plan references
bash scripts/sync.sh. That script was deleted in PR #405; the install workflow is nownpx skills add . -g -y(symlinks the working tree across every detected harness). For context on why sync.sh went away, see docs/solutions/workflow-issues/release-consistency-test-cascade-2026-05-16.md. The decisions captured in this plan remain accurate; only the deploy mechanism changed.
title: "feat: vs mode runs N full passes and --competitors is vs with auto-discovery" type: feat status: active date: 2026-04-22 origin: 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-relatedfor peers, merged subreddits, cross-entity keyword noise. One saved file. --competitorstoday (3.0.12): N parallelpipeline.run()calls viascripts/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.mdper run — no per-entity audit. - Kanye peers leaked main topic's
--subreddits. - Engine footer nudging
BRAVE_API_KEYto 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 fullpipeline.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-planJSON). - 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-plancontaining each entity's targeting. - R3. New
--competitors-planJSON 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.mdfile when--save-diris in use. Example:/last30days "Kanye West vs Drake vs Kendrick Lamar" --save-dir=~/Documents/Last30Daysproduceskanye-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 Entitiesblock 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
--competitorsinvocations 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 withtopic 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--planor--competitors-planwas 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
--competitorscount 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_planshortcut 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_runnerclosure is where per-entity kwargs apply.scripts/lib/planner.py—_should_force_deterministic_plandetects vs-topics via regex. Current path synthesizes ONE comparison plan; new path routes to fanout.scripts/lib/render.py—render_comparison_multi(3.0.12) +_render_resolved_entities_block. Both reused.render_fullneeds a per-entity variant when saving sub-run files.scripts/last30days.pysave_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-keywordsand 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-planJSON + 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_fanoutserves both. vs-mode is "topic contains ' vs '" detection → fanout.--competitorsis "SKILL.md shortcut → hosting model rewrites topic to vs form → fanout." One code path. - Per-entity targeting via
--competitors-planJSON. Schema{entity_name: {x_handle, x_related, subreddits, github_user, github_repos, context}}. Mirrors--plan. Applies to both vs-mode and--competitorspaths. 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.mdfile when--save-diris 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
ThreadPoolExecutormeans 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-keywordsis 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-planJSON or multi-flag? JSON. Matches--plan.- Default count? 2 peers → 3-way comparison. Unchanged.
- Saved-file naming?
{entity-slug}-raw.mdper 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_planspecial case for vs topics; vs topics no longer go throughplan_queryas a single comparison plan) - Test:
tests/test_vs_mode_fanout.py(new)
Approach:
- Parse the incoming topic: if it contains
vsorversus(case-insensitive), split into entities (reuseplanner._comparison_entities-style logic or move that utility into main()). - When vs-entities are detected, route to the same fanout branch
--competitorsuses 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--planfor the main (first) entity and use per-entity defaults for peers unless--competitors-planis 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--competitorsbranch. planner._comparison_entitiesfor 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-planJSON flag +_subrun_kwargshelper + 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_kwargshelper) - 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-planargparse 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):- Get
plan_entryfrom--competitors-planif present. - Build base kwargs with
_subrun_kwargs(entity, plan_entry). - Fill missing fields via
resolve.auto_resolve(entity, entity_config)only if backend is configured (3.0.12 fallback path). - Call
pipeline.run(topic=entity, internal_subrun=True, **kwargs). - Attach
resolveddict toreport.artifacts.
- Get
- 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:
--planparsing block inscripts/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 receivesx_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 getssubreddits=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 getsx_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_outputiteration after fanout) - Modify:
scripts/lib/render.py(render_fullincludes single-row Resolved Entities block when that entity'sartifacts["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.
- Call
- Each saved file invokes
render_full(or the save-variant).render_fullnow checks forreport.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_outputinvocation 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.mdexist. - Happy path:
--competitors-list "Drake,Kendrick" --save-dir=/tmp/xon 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.mdreturns 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:
- "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'." - "Headless / cron path: set BRAVE_API_KEY / EXA_API_KEY / SERPER_API_KEY / PARALLEL_API_KEY / OPENROUTER_API_KEY and re-run."
- "Minimum escape hatch:
--competitors-list 'A,B,C'skips discovery but does not pre-resolve peers."
- "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
- Suppress footer nudge when
external_planORcompetitors_planwas passed.
Test scenarios:
- Happy path:
--competitorswith no backend, no list, no plan → stderr leads with "If you are the hosting reasoning model" and references--competitors-planbefore naming API keys. - Happy path:
--planpassed → footer nudge does NOT fire. - Happy path:
--competitors-planpassed → footer nudge does NOT fire. - Happy path:
--competitors-listonly (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-keywordsin 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,
--competitorsis 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-planJSON example. - Rewrite the Competitor mode subsection: "
--competitorsis 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--competitorsand--competitors-listremain for headless fallback." - Cross-reference §679 (per-entity Step 0.55 protocol).
- Warning: a thin
## Resolved Entitiesblock (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 --competitorsproduces 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
/last30dayspicks up 3.0.13.
Test scenarios:
- Test expectation: none — packaging.
Verification:
grep version .claude-plugin/plugin.json→ 3.0.13.sync.shexits 0.- Hot-copy contains the new files.
System-Wide Impact
- Interaction graph: vs-mode and
--competitorsshare one orchestrator (fanout.run_competitor_fanout)._subrun_kwargsis 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-planJSON 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-planis additive.--competitors,--competitors-list,--planunchanged.--polymarket-keywordsadditive. 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.pyvs-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)