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).
26 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: "fix: per-entity resolution, default-2, and stale-path guard for --competitors" type: fix status: active date: 2026-04-22 origin: docs/plans/2026-04-22-002-feat-competitors-flag-comparison-fanout-plan.md
fix: per-entity resolution, default-2, and stale-path guard for --competitors
Overview
Three test runs of v3.0.11 --competitors surfaced four real bugs plus one product tweak. This plan fixes all of them in a single follow-up:
- Competitor sub-runs get no Step 0.55 resolution (no X handle, no subreddits, no GitHub repo). Drake / Kendrick / Travis ran with deterministic-fallback single-word queries while Kanye had the full targeting package. User called it "lazy" and was right.
- Two of three test windows (Linear, Coinbase) never invoked the new flag at all. They loaded SKILL.md from
plugins/marketplaces/last30days-skill/(a Claude-Code-managed git clone pinned to origin/main, which predates PR #308) instead ofplugins/cache/last30days-skill/last30days/3.0.11/, so--helpshowed no--competitorsflag and the model fell back to the manual comparison path. - Each competitor sub-run emits a scary
[Planner] No --plan passed... deterministic fallbackstderr line because LAW 7 targets the hosting-model path, not internal fan-out sub-runs. - Default competitor count is 3 (→ 4-way comparison). User wants default 2 (→ 3-way: original + 2 peers). Flag keeps
--competitors=Nto customize.
Problem Frame
The 3 test runs (Kanye, Linear, Coinbase) showed a pattern:
| Window | Loaded SKILL.md from | Invoked --competitors? | Per-entity resolution? | Outcome |
|---|---|---|---|---|
| Kanye | cache/3.0.11/ (correct) | Yes | Only for main topic (Kanye) | Drake/Kendrick/Travis thin; Reddit 403 fallbacks |
| Linear | marketplaces/ (stale) | No — fell back to manual comparison | No | Thin run with noisy subreddits |
| Coinbase | marketplaces/ (stale) | No — fell back to manual comparison | Main only; keyword-search poisoned pool | Top subs: r/survivor, r/Airpodsmax (noise) |
Root causes:
- Per-entity resolution gap:
scripts/lib/fanout.pycallspipeline.run()with topic + depth + web_backend + lookback_days only. It does not callresolve.auto_resolve()per entity, so sub-runs have no X handle, subreddit, or GitHub targeting. The original plan (2026-04-22-002) acknowledged this as a deliberate v1 simplification ("competitor sub-runs use planner defaults"). In practice this produces visibly asymmetric output and triggers downstream retrieval issues (403 fallbacks, keyword-search noise). - Stale-path loading: Claude Code's skill loader alphabetizes
findresults withmarketplaces/beforecache/, and the model reads the first plausible SKILL.md it sees. SKILL.md line 823'sSKILL_ROOTresolver is the correct path but only fires in engine-invocation blocks, not in the skill-load step. - LAW 7 in sub-runs: LAW 7 exists because the hosting reasoning model is supposed to pass
--plan. For competitor sub-runs, there is no hosting-model planning — it's an engine-internal fan-out. The warning is a false positive there.
Requirements Trace
- R1. Default
--competitorscount is 2 peers (3-way comparison: original + 2). - R2. Each competitor sub-run performs Step 0.55 resolution (X handle, subreddits, GitHub user/repos, news context) before its pipeline runs — not just the main topic.
- R3. Sub-runs do not emit the LAW 7
No --plan passedwarning; they are internal fan-out, not hosting-model calls. - R4. The rendered comparison output includes a visible "Resolved entities" block showing per-entity handles/subs/github for debug transparency (answers "did it resolve everyone?" without the user having to read stderr).
- R5. SKILL.md has a canonical-path self-check at the top: if the reader loaded it from anywhere other than
plugins/cache/last30days-skill/last30days/{VERSION}/, re-read from the versioned path before proceeding. - R6. Version bumps to 3.0.12; CHANGELOG entry;
scripts/sync.shdeploys.
Scope Boundaries
- No new discovery strategy. The web-search + regex extraction in
scripts/lib/competitors.pystays as-is. - No new CLI flags beyond the behavior changes above. Specifically: no per-entity override flags like
--competitor-handles. The hosting-model escape hatch remains--competitors-list. - No changes to the explicit
A vs Bcomparison path (topic-string parsing inplanner._comparison_entities). - No marketplace-clone auto-restore fix — that's Claude Code harness behavior. This plan only guards against the symptom on the skill side.
Deferred to Separate Tasks
- Caching of per-entity resolution results: separate follow-up once hit rate justifies it.
- Fan-out rate-limiting tuning (currently
max_workers=len(entities)+1, capped at 6): defer until we see real-world quota exhaustion. - Pre-flight cost hint when N ≥ 4 (noted in
2026-04-22-002risks): defer.
Context & Research
Relevant Code and Patterns
scripts/last30days.py:205-219—--competitors/--competitors-listargparse definition (const=3 today; changing to 2).scripts/last30days.py:220-290—resolve_competitors_args()validator; updateCOMPETITORS_DEFAULT.scripts/last30days.py:438-520— main() fan-out orchestration; currently passes only topic/depth to each_competitor_runner.scripts/lib/fanout.py:40-95—run_competitor_fanout()signature. Thecompetitor_runnercallable is where per-entity resolution needs to happen.scripts/lib/resolve.py:179-258—auto_resolve()is the exact per-entity resolver to reuse. Already does X handle + subreddits + GitHub user/repos + news context in parallel via ThreadPoolExecutor.scripts/lib/planner.py:80-135—plan_query()emits the LAW 7 stderr. Aquiet: boolkeyword orinternal_subrun: boolflag will suppress it.scripts/lib/pipeline.py:162-220—pipeline.run()signature. Needs a new keyword to propagate quiet-mode down to the planner.scripts/lib/render.py:render_comparison_multi— where the "Resolved entities" block is inserted.SKILL.mdline 823 — canonicalSKILL_ROOTresolver already exists but fires in engine bash, not at skill-load time.
Institutional Learnings
docs/plans/2026-04-22-002-feat-competitors-flag-comparison-fanout-plan.mdacknowledged the per-entity-resolution gap as a v1 tradeoff. This plan closes that gap.- Kanye run stderr:
[Planner] No --plan passed... deterministic fallback× 3 (once per competitor sub-run). That's the LAW 7 noise R3 targets. - Linear / Coinbase runs loaded
plugins/marketplaces/last30days-skill/CLAUDE.mdas the first hit. That's the stale-path issue R5 targets.
External References
- None. All patterns are in-repo.
Key Technical Decisions
- Per-entity resolve happens inside fanout, not in SKILL.md. The user-facing promise of
--competitorsis "one flag, engine does the work." Pushing resolution onto the hosting model creates another path-of-least-resistance trap (model skips it, output looks lazy). Auto-resolve inside each sub-run when a web backend is available makes the feature self-contained. - Stale-path guard is a SKILL.md self-check, not a code change. We cannot stop Claude Code from auto-restoring the marketplace clone. But we can put a 3-line banner at the top of SKILL.md that forces any path-mismatched read to re-read from the versioned cache. Both the marketplace copy (once main catches up) and the cache copy carry the guard.
- LAW 7 suppression is opt-in via
internal_subrun=Truekeyword. Do not remove the warning from the default path — it's load-bearing for the hosting-model contract. Add an explicit bypass for engine-internal fan-out only. - Default 2, hard max 6 unchanged. "Original + 2" matches the Kanye/Drake/Kendrick mental model from the feature description. Still allow
--competitors=Nfrom 1 to 6. - Resolved block is inside the EVIDENCE envelope, not above it. Keeps the rendered output structure stable for the synthesis contract (LAW 1–8). The block is context, not output.
- Skip auto-resolve when
--mockor no web backend. Mirrors the existingresolve.auto_resolve()fast-fail and keeps the mock test path deterministic.
Open Questions
Resolved During Planning
- Where does per-entity resolve live? Inside
fanout.run_competitor_fanout, not inmain(). Each sub-run callsauto_resolve()just beforepipeline.run(). - Should the hosting model still be able to override? Yes —
--competitors-listremains the escape hatch. When an explicit list is passed, the engine still does auto-resolve per entity; the user's list just skips discovery. - Should sub-runs run auto-resolve in parallel with each other? Yes. The existing
ThreadPoolExecutorin fanout already parallelizes sub-runs; auto-resolve happens inside each sub-run's thread, so resolve calls for different entities run concurrently. - Default count: 2 peers (3-way). Confirmed.
Deferred to Implementation
- Whether to expose a
--no-auto-resolve-competitorsflag for power users who want the fast, shallow behavior. Probably not needed v2; ship auto-resolve always-on and revisit if someone complains about cost. - Whether to surface the per-entity resolution context back into the main topic's planner (cross-entity context sharing). Stays deferred.
- Whether the Resolved block should be collapsible or always inline. Start inline; revisit based on output length feedback.
Implementation Units
- Unit 1: Default
--competitorsto 2 peers
Goal: Change the bare --competitors default from 3 to 2 per user feedback. --competitors=N still overrides; range 1..6 unchanged.
Requirements: R1
Dependencies: None
Files:
- Modify:
scripts/last30days.py(COMPETITORS_DEFAULT,--competitorsconst, stderr messages if any reference 3) - Modify:
SKILL.mdCompetitor mode section ("discovered 2-6" wording, bare-flag default line) - Modify:
README.mdauto-discovered example line (if it references count) - Test:
tests/test_cli_competitors.py
Approach:
- Change
COMPETITORS_DEFAULT = 3→2inscripts/last30days.py. - Change argparse
--competitorsconst=3→const=2. - Update any SKILL.md / README copy referencing "3 peers" to "2 peers" (default) or "2-6 peers" (range).
Patterns to follow:
- Existing default constants in
scripts/last30days.pyargparse block.
Test scenarios:
- Happy path: bare
--competitorsyields count=2, enabled=True, empty explicit_list. - Edge case:
--competitors=3still works (explicit override). - Edge case: existing
test_bare_flag_defaults_to_threetest is updated totest_bare_flag_defaults_to_twoand asserts count=2. - Edge case:
--competitors=5with a--competitors-listof length 2 still logs the mismatch warning and uses the list.
Verification:
-
pytest tests/test_cli_competitors.py -vpasses with the updated default. -
Unit 2: Per-entity Step 0.55 resolution inside fanout
Goal: Each competitor sub-run auto-resolves its own X handle, subreddits, GitHub user/repos, and news context via resolve.auto_resolve() before its pipeline.run() call — just like the main topic.
Requirements: R2
Dependencies: None (but Unit 3 should land together so sub-runs don't emit LAW 7 stderr while the resolution context is being passed)
Files:
- Modify:
scripts/lib/fanout.py - Modify:
scripts/last30days.py(_competitor_runnerclosure builds the resolved args) - Test:
tests/test_competitor_fanout.py - Test:
tests/test_competitors_resolve_integration.py(new; covers the auto-resolve path)
Approach:
_competitor_runner(entity)in main() does:- Call
resolve.auto_resolve(entity, config)whennot args.mockand a web backend is configured (reuse_has_backend). - Extract resolved x_handle, subreddits, github_user, github_repos, context.
- Pass them to
pipeline.run()for that sub-run. - Inject resolved context into a per-entity config copy (so
_auto_resolve_contextdoes not leak across sub-runs — deep-copy the config or use a local dict). - Store the resolved block on the Report's
artifactsso the renderer can surface it (Unit 4).
- Call
- When
args.mockis True or no backend is available, skip auto-resolve (fall through to planner defaults, matching the existingauto_resolve()early-return contract). - Update
fanout.run_competitor_fanoutdocstring to note that auto-resolve happens inside the caller-provided runner.
Execution note: Start with a failing integration test that exercises two-entity fanout + auto-resolve via a mocked resolve.auto_resolve and asserts that pipeline.run receives the resolved x_handle/subreddits for each entity.
Patterns to follow:
scripts/last30days.pymain topic branch (if args.auto_resolve and not external_plan) already callsresolve.auto_resolveand propagates results — mirror the shape for competitors.- Config isolation:
scripts/lib/pipeline.py:162-220reads config as-is; usedict(config)to avoid cross-sub-run mutation of_auto_resolve_context.
Test scenarios:
- Happy path: 3 entities, mocked
auto_resolvereturns distinct handles per entity;pipeline.runreceivesx_handle=@drakefor Drake,x_handle=@kendricklamarfor Kendrick, etc. - Happy path: the main topic still uses the user-supplied
--x-handle/--subredditsoverrides (not overwritten by auto-resolve for the main). Competitors use their own auto-resolved values. - Edge case:
--mockskips auto-resolve entirely for all sub-runs (noresolve.auto_resolvecalls). - Edge case:
resolve.auto_resolvereturns empty dicts for one entity (low-signal topic) — the sub-run still executes with planner defaults; doesn't crash. - Edge case: no web backend configured — auto-resolve returns empty for every entity, sub-runs fall through to planner defaults, no stack trace.
- Error path:
resolve.auto_resolveraises — the sub-run logs a warning and continues with planner defaults (does not fail the whole comparison). - Integration: config
_auto_resolve_contextfrom entity A does not leak into entity B'spipeline.run. Assert each sub-run gets its own context string.
Verification:
-
New integration test passes.
-
End-to-end smoke (mock mode + explicit list): each sub-run's stderr shows
[AutoResolve]lines per entity with distinct values. -
Unit 3: Suppress LAW 7 warning for engine-internal sub-runs
Goal: The [Planner] No --plan passed... deterministic fallback warning does not fire during competitor sub-runs. LAW 7 is load-bearing for hosting-model contracts and must stay on the default path; this is an opt-in bypass for internal fan-out only.
Requirements: R3
Dependencies: Unit 2 (so the sub-run call site is already being modified)
Files:
- Modify:
scripts/lib/planner.py(plan_querysignature + conditional stderr) - Modify:
scripts/lib/pipeline.py(runsignature + propagation) - Modify:
scripts/last30days.pyorscripts/lib/fanout.py(passinternal_subrun=Truefor competitor runners) - Test:
tests/test_planner_v3.py(or newtests/test_planner_quiet_mode.py) - Test:
tests/test_competitor_fanout.py(assert sub-runs don't emit LAW 7 stderr)
Approach:
- Add a keyword
internal_subrun: bool = Falsetoplanner.plan_query. When True, skip the twoprint(..., file=sys.stderr)blocks that emit the LAW 7 banner and the[Planner] No --plan passedcapability message. - Add the same keyword to
pipeline.run(); pass through toplan_query. - In main()/fanout, set
internal_subrun=Truefor every competitor sub-run's pipeline.run call. The main topic's pipeline.run keeps the default (LAW 7 stays on for the hosting-model path). - Also suppress the LAW 7-triggered degraded-run warning block in the render layer for sub-reports when the envelope is going to be merged into a comparison output (or accept that the block is per-entity and surfaces once per entity).
Patterns to follow:
- Existing keyword-only parameters on
pipeline.run(mock,x_handle, etc.). planner.plan_querysignature is already keyword-only.
Test scenarios:
- Happy path:
plan_query(..., internal_subrun=True, provider=None, model=None)returns the deterministic fallback plan WITHOUT writing the LAW 7 stderr block. - Happy path:
plan_query(...)with defaultinternal_subrun=Falsestill writes the LAW 7 warning (unchanged behavior). - Integration: end-to-end competitor fanout; assert captured stderr contains zero occurrences of
No --plan passedand zero ofYOU ARE the planner. - Integration: main topic is not part of competitor mode; if the user invokes bare
/last30days OpenAIwithout--plan, LAW 7 stderr fires exactly once (regression test).
Verification:
-
Running the Kanye-style smoke test shows zero
[Planner] No --plan passedlines for Drake / Kendrick / Travis sub-runs. -
Unit 4: "Resolved entities" block in comparison output
Goal: The rendered comparison output includes a visible block listing per-entity handles, subreddits, GitHub user, and resolved context. Answers "did it resolve everyone?" at a glance without reading stderr.
Requirements: R4
Dependencies: Unit 2 (needs resolved data on report artifacts)
Files:
- Modify:
scripts/lib/render.py(render_comparison_multiandrender_comparison_multi_context) - Test:
tests/test_render_comparison_multi.py
Approach:
- When each entity's
Report.artifactscontains aresolveddict (populated by Unit 2),render_comparison_multiemits a## Resolved Entitiesblock early in the EVIDENCE envelope:## Resolved Entities - **Kanye West**: X @kanyewest | Subs r/Kanye, r/hiphopheads | GitHub: — | Context: BULLY released, UK ban… - **Drake**: X @Drake | Subs r/DrakeTheType, r/hiphopheads | GitHub: — | Context: ICEMAN rollout… - **Kendrick Lamar**: X @kendricklamar | Subs r/KendrickLamar | GitHub: — | Context: Grammy wins, dormant… - Missing fields render as
—not empty. - When no entity has a
resolvedpayload (mock mode, no web backend), omit the block entirely rather than emit an empty section. - Context strings are truncated at 120 chars to keep the block scannable.
Patterns to follow:
- Existing
render_comparison_multienvelope structure (lines ~395-480 in render.py). - Existing per-entity evidence block format (
## {label}) for consistency.
Test scenarios:
- Happy path: 3 entities each with a
resolvedartifact → block lists all 3 with their fields. - Happy path: 2 entities, one with full resolution, one with partial (x_handle only) → missing fields render as
—. - Edge case: no entity has a resolved artifact → block is omitted entirely.
- Edge case: context string > 120 chars → truncated with ellipsis.
- Integration: rendered output passes through the same EVIDENCE envelope comments and synthesis contract (LAW 1–8 unchanged).
Verification:
-
Snapshot tests confirm the block appears in the right spot with the right formatting.
-
End-to-end smoke shows a realistic 3-entity Resolved block in the rendered output.
-
Unit 5: SKILL.md canonical-path self-check
Goal: A top-of-file SKILL.md directive forces any reader (Claude Code, Codex, Hermes, Gemini) to verify they loaded from plugins/cache/last30days-skill/last30days/{VERSION}/SKILL.md before proceeding. If loaded from marketplaces/ or any other path, re-read from the pinned versioned cache.
Requirements: R5
Dependencies: None
Files:
- Modify:
SKILL.md(prepend a STEP 0 block before the existing STEP 0 / LAW list)
Approach:
- Add a numbered first step at the top (before or bundled with existing "STEP 0: ToolSearch preload"):
## STEP 0: Canonical Path Self-Check (must run first) Before reading anything else below, verify you loaded this SKILL.md from the versioned cache, not the marketplace clone: CANONICAL=$HOME/.claude/plugins/cache/last30days-skill/last30days/ CANONICAL_LATEST=$(ls -d "$CANONICAL"*/ 2>/dev/null | sort -V | tail -1) If the SKILL.md you just read is not under $CANONICAL_LATEST, STOP. Re-read $CANONICAL_LATEST/SKILL.md and restart from here. Marketplace clones (`plugins/marketplaces/last30days-skill/`) are pinned to origin/main and can be stale; the versioned cache is the ground truth. - Reinforce in the existing LAW 7 block that
--helpoutput must be read from the same pinnedSKILL_ROOTto avoid flag-list skew.
Patterns to follow:
- Existing STEP 0 ToolSearch preload (top of SKILL.md) for tone / imperative voice.
- Existing
SKILL_ROOTresolver snippet (line ~823).
Test scenarios:
- Test expectation: none — SKILL.md is documentation; no unit test, verified by follow-up user invocation.
Verification:
-
In a fresh Claude Code window,
/last30days Test --competitorsloads SKILL.md, the model executes the STEP 0 self-check, and (if it had loaded from marketplaces/) switches to the cache path before running--helpor the engine. Observable via the model's announced reasoning / task list. -
Unit 6: Version bump, CHANGELOG, sync
Goal: Ship 3.0.12 and deploy to all local targets.
Requirements: R6
Dependencies: Units 1-5
Files:
- Modify:
.claude-plugin/plugin.json(version 3.0.11 → 3.0.12) - Modify:
CHANGELOG.md - Run:
bash scripts/sync.sh
Approach:
- CHANGELOG entry under
## [3.0.12]dated 2026-04-22 covering the four fixes (Fixed: per-entity resolution; Fixed: LAW 7 sub-run noise; Changed: default count 3→2; Added: Resolved entities block; Added: canonical-path self-check in SKILL.md). sync.shdeploys to~/.claude/plugins/cache/last30days-skill-private/...,~/.agents/,~/.codex/, Hermes.- Manual hot-copy to
~/.claude/plugins/cache/last30days-skill/last30days/3.0.12/so the public/last30daysslash command picks up the new version before PR merge (matches the 3.0.11 testing pattern).
Test scenarios:
- Test expectation: none — packaging only. Verification is by inspection.
Verification:
grep version .claude-plugin/plugin.jsonreturns3.0.12.sync.shexits 0 with "Import check: OK" for each target.- Hot-copied 3.0.12 directory contains the new files and
/last30dayspicks up the new version (highest-version resolver).
System-Wide Impact
- Interaction graph: Fanout sub-runs now call
resolve.auto_resolveper entity. Each sub-run is independent; no shared mutable state with other sub-runs or with the main topic. - Error propagation:
auto_resolvefailures inside a sub-run log a warning and degrade to planner defaults; do not propagate up to abort the comparison. Same contract as today for the main topic. - State lifecycle risks: Config dict is mutated by
auto_resolve(viaconfig["_auto_resolve_context"]). Must deep-copy per sub-run or scope context to a local mapping — otherwise two sub-runs' context strings race. - API surface parity:
pipeline.rungains a keyword (internal_subrun); callers that don't pass it get the existing behavior.planner.plan_querygains the same. Backward compatible. - Integration coverage: New integration test for the fanout + auto-resolve + render chain. Existing snapshot tests update to include the Resolved block.
- Unchanged invariants: Single-entity
/last30daysinvocations (no--competitors) behave identically. ExplicitA vs Bcomparison topics behave identically. LAW 7 still fires on the default hosting-model path.render_compactpath is untouched.
Risks & Dependencies
| Risk | Mitigation |
|---|---|
| Auto-resolving per competitor triples the WebSearch call volume (4 queries × 3 competitors = 12 extra web searches). | Fast-fail when no backend; user can pass --competitors-list to skip discovery but still get auto-resolve. Cost note in CHANGELOG. |
Config mutation across sub-runs via _auto_resolve_context. |
Unit 2 deep-copies config per sub-run before each auto_resolve + pipeline.run call. Integration test asserts no cross-entity leak. |
| LAW 7 suppression leaks onto the hosting-model path via a wrong default. | Default internal_subrun=False. Only fanout's competitor sub-runs set True. Unit test asserts bare-topic invocation still emits LAW 7. |
| SKILL.md STEP 0 banner gets ignored by the model (same failure mode as line 823 today). | Put it in the guaranteed-read top band (before LAW 1, above all other content), imperative voice, concrete STOP verb. Still not bulletproof but strictly better than current. |
| Default count change breaks assumptions in downstream tools or existing user muscle memory. | Changelog calls it out as Changed; --competitors=3 still works for users who want the old default. |
Documentation / Operational Notes
- Beta channel first: merge behind
/last30days-betavia the private repo before cherry-picking to public. Follows the same process as 3.0.11. - Version 3.0.12 is a fix release; no marketing post required.
- After merge, add a line to the PR description pointing at this plan.
Sources & References
- Origin plan:
docs/plans/2026-04-22-002-feat-competitors-flag-comparison-fanout-plan.md - Related PR: #308 (v3.0.11 shipping --competitors)
- Test windows that surfaced the bugs: Kanye, Linear, Coinbase (2026-04-22 session)
- Related code:
scripts/lib/fanout.py,scripts/lib/resolve.py(auto_resolve),scripts/lib/planner.py(plan_query),scripts/lib/render.py(render_comparison_multi)