Skip to content

Competitor analysis skill#76

Open
jay-sahnan wants to merge 23 commits intomainfrom
competitor-analysis
Open

Competitor analysis skill#76
jay-sahnan wants to merge 23 commits intomainfrom
competitor-analysis

Conversation

@jay-sahnan
Copy link
Copy Markdown
Contributor

@jay-sahnan jay-sahnan commented Apr 24, 2026

Adds competitor-analysis — a skill that researches a company's competitors and surfaces the output as a browsable HTML report with screenshot


Note

Medium Risk
Adds a brand-new skill with multiple Node scripts that orchestrate Browserbase CLI calls, parse/merge research outputs, and generate HTML/CSV artifacts; while mostly additive, the gating/merging/report compilation logic is non-trivial and could misclassify competitors or produce incorrect reports if edge cases aren’t handled.

Overview
Introduces a new competitor-analysis skill that discovers competitor domains via bb search, gates candidates by category-fit, runs multi-lane enrichment (including an optional post-fact-check Battle Card synthesis lane), and compiles results into an offline report.

Adds Node-based tooling to support the pipeline: URL dedup + “X vs Y” name extraction, a concurrency-capable homepage gate, partial merging/normalization, optional screenshot capture via browse, and compile_report.mjs to generate index.html, per-competitor pages, a matrix view (optionally driven by matrix.json with win/loss summaries), a mentions feed, and results.csv.

Reviewed by Cursor Bugbot for commit 37087d2. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread skills/competitor-analysis/scripts/gate_candidates.mjs
Comment thread skills/competitor-analysis/scripts/extract_vs_names.mjs
…t set with user

The gate has known blind spots that silently drop real competitors:

- JS-heavy homepages (Tavily, Firecrawl) — bb fetch returns near-empty
  hero text, keyword matcher has nothing to match on
- Cloudflare challenge pages (Perplexity) — title becomes "Just a
  moment...", no category signal
- Semantic variants — "search foundation" (Jina AI), "retrieval
  backbone", etc. don't lexically match a list centered on "search API"
- Apex-vs-product domain confusion — brave.com (browser) vs
  api-dashboard.search.brave.com (actual API)

Auto-promoting the PASS list to enrichment is the wrong default because
enrichment is expensive (5 competitors × 5 lane-subagents = 25 subagents,
~10-15 min wall time, ~300 bb calls). Running that on a partly wrong set
wastes all of it.

Insert a mandatory Step 4.5 between Gate and Deep Enrichment:

1. Main agent groups /tmp/competitor_gated.jsonl into three buckets —
   PASS, UNKNOWN (fetch failed — surfaced separately, these are the
   silent misses), and rejected-brand-matches (top ~10 REJECTs whose
   title contains a seed token or shows up in Wave C "X vs Y" graph).
2. AskUserQuestion with a checkbox list + free-text "add more" field.
3. Write the confirmed set to /tmp/competitor_enrichment_set.txt — this
   is the input for Step 5, not /tmp/competitor_passed.txt.

Surfaced while testing the skill on Exa (exa.ai): the gate passed
22/101 candidates but silently rejected Tavily, Jina AI, Firecrawl, and
Perplexity — all real direct competitors. Step 4.5 catches them.

SKILL.md pipeline overview is now 8 steps (was 7). Step 5 input path is
updated. workflow.md gets a User-confirm phase section with the three
buckets and the list of known gate blind spots.
Lane subagents don't consistently emit the canonical Mentions bullet
format specified in workflow.md — they drift into variants per lane:

- Discussion lane:  `- **HN** — [Title](url) — snippet`
- News lane:        `- **2025-08-06** — [News] Outlet — "title" — url`
- Technical lane:   `- **[Benchmark]** ...` (canonical)

compile_report.mjs' parseMentions only matches the canonical
`- **[SourceType]** Title | Snippet (source: URL, YYYY-MM-DD)` shape,
so non-canonical variants silently dropped from the mentions feed.
On the Exa end-to-end test, merge reported 404 total mentions across
5 competitors but the rendered feed showed 0.

Rather than fight prompt drift across 25 subagents, normalize at merge
time. New normalizeMentionBullet() rewrites the three observed variants
into canonical form before dedup, so downstream (CSV, per-competitor
pages, mentions.html feed) stays clean.

After fix on the Exa run: 294 mentions render, 81% with dates,
distribution: 42 LinkedIn / 34 HN / 25 Blog / 24 YouTube / 20 Reddit /
12 Comparison / 11 DevTo / 8 News / 4 Substack.
Pricing-page screenshots were adding ~300KB per competitor (Browsaur's
was 580KB) and doubling the per-run browse-CLI cost, but the per-tier
text already lives in the frontmatter (pricing_tiers, pricing_model)
and renders in the Pricing section of the per-competitor page. The
visual didn't add signal over the structured data — it was redundant.

Homepage hero stays. That one is worth keeping: the tagline, visual
brand identity, and positioning screenshot-vs-text diff surface things
the fields can't (logo treatment, animation cues, hero copy voice).

Changes:
- capture_screenshots.mjs: drop pricingCandidates() + pricing capture loop,
  simplify result shape to {slug, hero, errors}, halve per-competitor
  wall time (~10-20s vs ~15-20s, no pricing fallback chain).
- compile_report.mjs: remove 2-column .shots grid + .shot-pricing CSS,
  render single .shot-hero card per page.
- SKILL.md Step 6 + references/workflow.md: doc sync. Also clarify that
  `browse` is a separate package from `bb` (@browserbasehq/browse-cli
  vs @browserbasehq/cli) — came up as a user question during test runs.

Existing runs re-rendered without pricing shots; ~1.5MB of PNGs removed
from the two test output dirs on Desktop.
…onomy

The old matrix view was broken on real runs. Subagents write key_features
and integrations as prose (comma-separated or full sentences), not as
pipe-separated atomic labels the matrix expected. Pipe-splitting gave one
unique blob per competitor, so the matrix trivially rendered a diagonal —
zero actual comparison across competitors.

Fix is to synthesize a shared taxonomy after enrichment and render the
matrix from that. New flow:

- After merge, the main agent reads all per-competitor .md files, distills
  a canonical list of 12-20 atomic features and 10-20 integrations that
  apply across the category, and writes {OUTPUT_DIR}/matrix.json with a
  per-competitor yes/no mapping.
- compile_report.mjs auto-detects matrix.json and renders the Features +
  Integrations axes from it. Falls back to the old pipe-split behavior
  when matrix.json is missing.

Verified on the Exa test: before fix, Features axis was 5 one-off blobs
with a diagonal of ●s. After: 19 atomic feature rows × 5 competitors
with 36 ● cells showing real overlap (Web Search API, MCP server, Free
tier, Structured JSON are universal; only Jina has Reranker+Embeddings;
only Tavily has Site crawler; SerpAPI alone has CAPTCHA solving and
hourly throughput SLA; etc.).

SKILL.md Step 5 gets a new "Synthesize the comparison matrix" substep
with the matrix.json schema and the rule "do not skip — without this the
matrix view is trivially diagonal".
Rotated competitor-name headers were misaligned with their data columns
and the rightmost labels (Tavily, You.com in the Exa test) got cut off.

Root cause: `transform-origin: left top` combined with a fixed
`width: 160px` on the label made each label's horizontal extent run
~131px to the right of the column, so labels visually floated several
columns to the right of their target and the last N labels overflowed
off-screen.

Fix: anchor the rotated label to the BOTTOM-RIGHT of its column cell
(position:absolute right:4px bottom:8px, transform-origin:right bottom)
and steepen rotation from -35° to -55° so horizontal extent is
reduced. Drop the fixed width — label is now only as wide as its text,
which shrinks short names (Jina AI, Serper) and tightens layout.
Cell width 44→52px and header height 130→150px give rotated labels
room to live inside the cell rather than overflowing.

Result on the Exa run: all 5 competitor names visible, each label's
bottom-right sits at the top-right of its column, leaning up-left
toward the column — the shingled "hanging label" pattern.
Rotated/diagonal competitor names (35° then 55°) kept producing awkward
alignment: the rotation anchor vs the column's visual center never quite
matched, and long names (You.com, LlamaIndex) overflowed off the right.

Simpler fix: just make the headers horizontal. With 5 competitors × 110px
each = 550px, plus the 240px feature column, the table is 790px wide —
fits inside the 1200px container without scrolling. For >10 competitors
the .mx-scroll wrapper already provides horizontal scroll.

Drops the .mx-comp-h-inner rotation wrapper, bumps cell width from 52→110px
and data font from 0.9→0.95rem for readability. Feature column grows
220→240px to fit longer taxonomy labels like "Hourly throughput SLA".
…rview

The overview page showed a list of competitors but no explicit view of
the user's own strategic position. Hard to answer at a glance: what do
I uniquely have, what are the table-stakes features I'm missing?

Extend matrix.json with a `userCompany` entry (same shape as each
competitor — features + integrations yes/no flags), and compute two
buckets on the overview page:

- Winning: features the user has where 0–1 competitors also have
  them. Ordered by rarity (unique features first).
- Losing: features the user lacks where 3+ competitors have them.
  Ordered by gap size (most common features first).

Each item shows who else has it ("only you" / "Tavily, SerpAPI" /
"4 competitors"), so users can assess the strategic weight at a glance.

Rendered as two cards (green-bordered "win", brand-red-bordered "loss")
between the summary stats and the results table on index.html. Cards
gracefully degrade to nothing if matrix.json lacks userCompany — a
skill run that skipped Step 5b's matrix synthesis gets an overview
without the strategic summary rather than an error.

On the Exa test: 4 wins (Site crawler · Embeddings · 3+ SDK languages
· CrewAI integration) and 3 losses (Image/visual search · Dedicated
news endpoint · Hourly throughput SLA). Clear strategic picture in
one screen.

SKILL.md Step 5b "Synthesize the comparison matrix" now documents
userCompany as a required field with the explicit note that without it
the strategic summary doesn't render.
Missed in the strategic-summary commit (652a9a4) — the SKILL.md block
that defines the matrix.json schema was on an older revision of the
file that didn't get re-staged. Re-add the userCompany field and flag
it as required, with the explicit note that skipping it means the
"Where you're winning / losing" cards don't render.
Step 5b (matrix synthesis) produces LLM inference from heterogeneous
subagent prose. On the Browserbase run 2026-04-23 that inference
confidently marked SOC 2 as a Browserbase moat — except Hyperbrowser,
Kernel, AND Anchor Browser all have SOC 2 Type II (verified via their
own trust portals and compliance blog posts). Shipping that to a GTM
team would have made the whole report un-trustable.

Add Step 5c: a mandatory fact-check subagent that runs after the
taxonomy synthesis and before compile. For every true/false cell in
matrix.json, it:

- If true: finds a concrete source URL (docs, trust portal, changelog,
  GitHub license) or flips to false.
- If false: runs one targeted bb search to guard against misses.
- Outputs a verified matrix.json with a per-cell `sources` field plus
  a matrix_fact_check.md delta log of every flip.

The "Where you're winning / losing" cards are strategic claims. Without
verification they hallucinate moats. The SKILL now labels this step
MANDATORY with the Browserbase-SOC 2 example as proof of what skipping
it costs.
Bulleted lists of winning/losing features read like a spreadsheet, not
the analyst-briefing the overview page is supposed to be. Extend
matrix.json's userCompany with optional winningSummary / losingSummary
prose fields (2-4 sentences each) and render them as paragraphs when
present. Falls back to the existing bulleted list when absent so a
partial run still shows the boolean comparison.

SKILL.md flags these as strongly preferred and tells the main agent to
write them AFTER the fact-check step so the prose is grounded in
verified cells — otherwise the paragraph will state fluent but false
moats. Updated the Exa example in the schema block to include the two
summary fields.

On the Browserbase run: two paragraphs replace the previous 12 bullets.
Winning reads as enterprise moats (SLA, Stagehand, EU/APAC, Selenium,
OpenAI Agents + n8n integrations). Losing reads as transparency +
openness gaps — concrete competitor names cited (Anchor's Halluminate
win, Steel's leaderboard, Browsaur MIT + Kernel + Steel AGPL).
…ompetitors

Step 1 was doing light self-research on the user's company while Step 5
did deep 5-lane enrichment on every competitor. That asymmetry meant the
userCompany row in matrix.json was filled from the main agent's memory
rather than from verified partials, and the strategic summary printed
fabricated moats about the user's OWN product.

Concrete examples from the Browserbase run 2026-04-23, caught only when
the user pushed back:
- Claimed a "published uptime SLA" — no numeric SLA exists on
  browserbase.com, only a status page.
- Marked open-source as false — Stagehand is MIT-licensed at
  github.com/browserbase/stagehand, plus Browserbase ships 10+ other OSS
  repos (sdk-node, sdk-python, create-browser-app, Arena, open-operator,
  mcp-server-browserbase, etc). The correct framing is "OSS at the SDK
  layer, cloud-only at the infra layer" — a split the skill wasn't
  capturing.

Systemic fix:

- Step 1 now mandates the same 5-lane partial enrichment on the user's
  company that Step 5 runs on competitors. Partials go to
  partials/{user-slug}.{lane}.md. merge_partials.mjs consolidates to
  {OUTPUT_DIR}/{user-slug}.md.
- Step 5b (matrix synthesis) now explicitly reads {user-slug}.md as
  the source for userCompany flags. Every flag must be traceable to
  a Research Findings bullet with a cited URL — the rule applies
  identically to the user's company and every competitor.
- Added the Browserbase-SLA + Browserbase-Stagehand errors to SKILL.md
  as the cautionary tale for why this parity matters.
Adds sales-enablement output grounded in the fact-checked matrix.
Closes the single biggest gap surfaced by the v0.2 framework research:
the skill was a Competitor Profiling Matrix but not a Battle Card tool
— Klue/Crayon's most-requested artifact had no equivalent in our pipe.

Design: synthesis-only lane (no new bb calls), runs AFTER Step 5c
fact-check so battle cards are grounded in verified cells, not fresh
inference. Eliminates the failure mode where the skill's sales output
would contradict the matrix it publishes.

Changes:
- scripts/merge_partials.mjs: add 'battle' to LANES; union the
  `## Battle Card` section into the merged {slug}.md between the
  Comparison and Mentions sections.
- scripts/compile_report.mjs: parse the Battle Card section from
  c.sections, render as a brand-accented `.research.battle` card on
  the per-competitor HTML page (left border in brand orange,
  uppercase small-caps subheadings for Landmines / Objection Handlers
  / Talk Tracks).
- references/battle-card.md (new): format spec — three sections,
  citation rules, adversarial self-check checklist.
- references/battle-card-subagent.md (new): standalone prompt template
  with placeholder list. Main agent substitutes per competitor and
  launches one subagent per competitor in parallel.
- references/example-research.md: add a worked Battle Card section to
  the Rival Co example.
- SKILL.md: new Step 5d (Battle synthesis) with explicit dependency
  on Step 5c fact-check; Pipeline Overview updated to mention the
  6th lane in deep/deeper modes.

Scope deliberately tight — this is Phase E of the approved v0.2 plan
(/Users/jay/.claude/plans/you-can-figure-out-jaunty-pelican.md).
Phases A/B/C/D/F/G deferred. Existing Browserbase + Exa compile runs
verified unchanged (no battle partials → battle card card omitted).
First end-to-end run of the battle lane on Browserbase data: 4 of 5
subagents emitted their Battle Card content with format drift that
parseSections() couldn't resolve — some led with `# Battle Card: X vs Y`
(h1, not h2 and so invisible to the `## `-only section splitter), some
skipped the wrapper heading entirely and led with `## 1. Landmines`.
Only 1 of 5 battle cards made it into the merged {slug}.md.

Same root cause as the earlier mention-bullet-format fix (commit 953f078):
subagents will drift from any prompt-level format spec.

Treat the entire battle partial body as the Battle Card content
regardless of heading style. Strip any leading `# Battle Card …` h1 or
`## Battle Card` h2 wrapper so we don't double-wrap, then emit the rest
under our canonical `## Battle Card` heading in the merged file.

After the fix: 5 of 5 battle cards rendered in per-competitor HTML on the
Browserbase run, each with 5-6 cited landmines, 5 objection handlers with
source links, and 2-3 talk tracks. Content quality spot-checked on Anchor
(counters the Halluminate stealth benchmark loss with the Advanced Stealth
update link) and Steel (flags US-only regions + self-benchmark bias).
Second end-to-end run on Browserbase (2026-04-24-1955) exposed two
small-but-real bugs not caught on the 2026-04-23 run:

1) merge_partials.mjs — the Battle Card heading-stripper's regex
   required the first line to be exactly `## Battle Card\s*\n` or
   `# Battle Card[^\n]*\n`, so an h2-with-suffix line like
   `## Battle Card — Hyperbrowser` slipped through. The merged
   hyperbrowser.md got a duplicate `## Battle Card` heading and the
   HTML rendered the section twice.

   Generalize to strip any leading heading line (h1-h3) mentioning
   "Battle Card" with any suffix. One regex handles all observed
   drift patterns from the 5 subagents.

2) capture_screenshots.mjs — the --help template literal contained
   unescaped backticks around `website`, breaking the enclosing
   `\`...\`` literal and yielding a SyntaxError at load time. Never
   caught before because prior runs skipped --help. Replaced the
   inner backticks with double quotes.

Verified on the fresh Browserbase run: all 5 battle cards merge with
exactly one `## Battle Card` header each; 5/5 hero screenshots captured
(anchor / browserbase / hyperbrowser / kernel / steel).
…cate cells

Three concrete bugs on the refreshed Browserbase run:

1) The user's own company leaked into the competitor table as the
   first row with '—' pricing. Filter it out by matching
   competitor_name AND slug (case-insensitive) against
   matrix.json userCompany.name (falling back to --user-company).
   Also rebuild metaLine + {{TOTAL}} + mentions-header count off the
   filtered list so "N competitors" is accurate.

2) Overview table cells rendered full 650-char pricing_tiers strings
   when subagents drifted into prose instead of pipe-separated tiers.
   Add truncate() helper (~140-160ch with word-boundary ellipsis) on
   tagline, pricing, and strategic_diff cells.

3) featurePills dropped all pills when key_features had no pipes —
   because splitPipes returned a single giant blob. Fall back to
   splitting on semicolons/commas, and cap each pill to 40 chars
   with a word-boundary ellipsis. Prevents wall-of-text pills.

Also lifted the curatedMatrix load above the first use site to avoid
a temporal dead zone (the filter needs userCompany.name; the matrix
was previously loaded farther down for the renderer functions).

After fix: 5-row table instead of 6, pricing/tagline/diff cells fit
the intended max-widths, feature pills show as short capsules.
…ze pills

Two bugs on the fresh Browserbase run that both traced to subagent
format drift:

1) Browsaur missing entirely from mentions feed chips
   Root cause: browsaur.marketing.md wrote `competitor: Browsaur`
   instead of canonical `competitor_name: Browsaur`. merge_partials'
   CANONICAL_FIELDS whitelist dropped the field silently, leaving
   Browsaur's merged .md with an empty competitor_name. The overview
   table still rendered (by slug) but the mentions feed keys on
   competitor name for the chip label — blank chips filtered out.

   Fix: FIELD_ALIASES map in merge_partials — `competitor` and
   `name` and `company` all map to `competitor_name`; `homepage` and
   `url` to `website`; `price_tiers` and `pricing` to `pricing_tiers`.
   canonicalValue(fm, key) walks the alias table when the canonical
   key is absent. Silent fallback: subagents can drift on field names
   without us losing data.

2) Unstyled mention pills with invented source types
   Subagents emitted `[VendorBlog]`, `[HackerNews]`, `[GitHubIssue]`,
   `[CompetitorBlog]` — none matching the CSS classes. Rendered as
   unstyled spans.

   Fix: normalizeSourceType() in parseMentions. Canonical set
   (Benchmark/Comparison/News/Reddit/HN/LinkedIn/YouTube/Review/
   Podcast/X/DevTo/Hashnode/Substack/Blog) stays. Aliases map
   HackerNews→HN, Twitter→X, VendorBlog/CompetitorBlog/GitHubIssue/
   Medium/Docs→Blog. Unknown types keyword-scan for a canonical
   token; else fall back to Blog. Guarantees every pill gets styled.

Also filter competitorRows (not deduped) when building allMentions, so
the user's own company doesn't leak into the feed even if it has
mentions. Fallback chip label is c.slug if competitor_name is blank.

After fix on the Browserbase run: 5 competitor chips (Anchor 21,
Browsaur 12, Hyperbrowser 22, Kernel 28, Steel 23), all source pills
mapped to canonical palette.
@jay-sahnan jay-sahnan force-pushed the competitor-analysis branch from 70817ed to d8702df Compare April 24, 2026 21:51
Comment thread skills/competitor-analysis/scripts/compile_report.mjs Outdated
Comment thread skills/competitor-analysis/references/report-template.html
…or nits

A real 10-competitor run on Browserbase clocked 40 minutes and never
reached fact-check or screenshots before interrupt. Trace attribution:
Step 5 enrichment alone burned 25 minutes by self-throttling to 10
agents per message (5 sequential rounds of 10), when the Agent tool
happily runs 50+ in parallel. Wall clock collapses to the slowest
single agent (~5 min) once we stop batching.

Three classes of fix in this commit:

1. Parallelism guidance — workflow.md + SKILL.md
   - Drop the "up to ~6 per message" cap. Replaced with the explicit
     rule: launch ALL subagents needed for a phase in ONE Agent
     message. For 10 × 5 lanes = 50 parallel agents in one message.
   - Document the measured cost: splitting cost 20 minutes vs
     unsplit on the Apr 2026 Browserbase run.
   - Update Step 5 + Wave Management + the lane-fan-out section to
     match. No remaining contradictions in the docs.

2. Discovery is parallel Bash, not subagents
   Discovery is 6-12 `bb search` calls. Wrapping each wave in an
   Agent subagent costs more in cold-start + tool-reasoning overhead
   than the work itself (~1-2 min wasted). New "Discovery — parallel
   Bash, not subagents" section in workflow.md gives the exact
   3-Bash-call recipe (Wave A/B/C). SKILL.md Step 3 points at it.

3. Skill-creator audit nits (rules from the skill-creator skill)
   - Add Tables of Contents to all 4 reference docs >100 lines
     (workflow.md, research-patterns.md, example-research.md,
     battle-card-subagent.md). battle-card.md is 91 lines so
     skipped per the rule.
   - Bump version "0.1.0" → "0.2.0". The skill picked up battle
     cards (df62374), fact-check (8502f71), prose summaries
     (9fd482f), user-company parity (845422d), matrix taxonomy
     (c74d229), Step 4.5 user-confirm (ae58982), and 7 more fixes
     since 0.1.0 — well past a minor bump.
   - Kept `allowed-tools` frontmatter field. Not in skill-creator's
     spec but harness-consumed in some Claude Code setups; harmless
     if ignored, useful if respected.

Estimated next-run impact: 40 min → ~12-15 min through compile,
dominated by per-subagent ceiling (3-5 min) + matrix synthesis
(4 min) + fact-check (5-10 min if you want it).
…budget

The Apr 25 Browserbase run got stuck at 111+ bb tool calls in
fact-check before user interrupt. Root cause: the previous Step 5c
mandated verifying EVERY cell of matrix.json — for a 7-company ×
33-axis matrix that's 231 cells. Most of those cells are universal
table-stakes (Playwright, Puppeteer, CDP, Python SDK) where any cloud
browser has them; verifying all of those is redundant work that blocks
the pipeline from reaching battle cards / screenshots / compile.

The original problem fact-check was solving (the SOC 2 hallucination
on the Apr 23 run) was about a HANDFUL of high-stakes cells: the ones
that drive the "Where you're winning" summary, plus compliance + license
+ pricing. Rest doesn't need verification.

Switch the default to spot-check with a hard 25-call budget. Priority
order is explicit and ranked:

  1. Every cell that appears in userCompany.winningSummary/losingSummary
  2. Compliance cells (SOC 2, HIPAA, ISO 27001) across all competitors
  3. Open-source license cells (Steel was wrong as AGPL — actually Apache 2.0)
  4. Pricing tiers + funding numbers cited in summaries

Explicit skip list:
  - Universal cells (Playwright, Puppeteer, CDP, Python SDK, etc.)
  - `false` cells with no claim
  - Integration cells unless cited in summaries

The subagent counts its own bb calls and STOPS at 25 — partial
fact-check beats blocking the pipeline. Full-sweep mode (~80 calls,
verifies every non-universal cell) is opt-in for board-deck-level
deliverables.

Estimated impact on next run: fact-check phase 15+ min → 3-5 min,
no more pipeline stalls before battle cards. The summary stays
trustworthy because we verify the cells that actually feed it.
…discovery results

User reported research phase still takes ~25min before fact-check even after
prior parallelism fixes. Trace showed 2 lanes hitting 29-30 bb calls each
against an 8-call advisory budget, dragging the 30-agent fan-out from 5→12min.

- references/workflow.md: replace soft "BUDGETS (respect strictly)" with
  HARD CAP + per-call self-counter ("# bb call N/8") and explicit "stop and
  write what you have" instruction. Cite Apr 25 incident.
- references/workflow.md: drop discovery searches from 25→12 results per
  query. Gate already filters most noise; 25 just inflated the candidate
  list and downstream gate calls.
- profiles/example.json: drop redundant template (browserbase.json is the
  reference profile).
1. matrix.html leaked user company as a column with all-false features.
   Move competitorRows definition above aggregates and replace `deduped`
   with `competitorRows` in matrix headers/cells, axis counts, pricing
   table, strategic-summary inner loop, per-competitor page generation,
   and CSV. Now a single filter applies consistently across all views.

2. report-template.html referenced undefined --high / --low CSS vars on
   strategic win/loss card border-lefts (and the loss badge text color),
   so the colored borders silently didn't render. Define both in :root
   (high=#5a8a1a green, low=#F03603 brand) so they match the existing
   palette tokens.

3. gate_candidates.mjs used spawnSync inside async gateOne, blocking the
   event loop and reducing the documented --concurrency 6 to N=1 in
   practice. Switch to promisified execFile so the worker pool actually
   parallelizes.

4. extract_vs_names.mjs used bidirectional startsWith for domain resolution,
   which mapped "steel" -> steelhead.com and "browse" -> browserbase.com.
   Restrict prefix matches to known branding suffixes (browser/ai/io/app/
   labs/etc.), break ties by shortest suffix, and exclude seeds from the
   host map so the user's own domain can't shadow shorter extracted names.

5. capture_screenshots.mjs (also flagged): the underlying `browse` CLI
   shares a single session, so true async parallelism would race on the
   same tab. Clamp --concurrency to 1 with a stderr note rather than
   silently corrupting output.
Comment thread skills/competitor-analysis/scripts/compile_report.mjs Outdated
Comment thread skills/competitor-analysis/scripts/compile_report.mjs Outdated
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 37087d2. Configure here.


if (shouldOpen) {
const { execSync } = await import('child_process');
try { execSync(`open "${join(dir, 'index.html')}"`); } catch {}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shell injection via string interpolation in execSync

Medium Severity

The execSync call interpolates the user-provided directory path (dir from args[0]) into a shell command string. If the output directory path contains shell metacharacters like ", $, or backticks, this allows arbitrary command execution. Using execFileSync('open', [join(dir, 'index.html')]) would avoid shell interpretation entirely.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 37087d2. Configure here.

}
}
return fields;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated parseFrontmatter and parseSections across three scripts

Low Severity

parseFrontmatter is independently implemented in compile_report.mjs, capture_screenshots.mjs, and merge_partials.mjs. parseSections is duplicated between compile_report.mjs and merge_partials.mjs. These are near-identical implementations in the same scripts/ directory. A shared utility module would reduce maintenance burden and ensure consistent parsing behavior across all scripts.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 37087d2. Configure here.

else if (incEarly.length > 0 && excEarly.length === 0) { status = 'PASS'; reason = `hero200→include(${incEarly[0]})`; }
else if (excEarly.length > 0) { status = 'REJECT'; reason = `hero200→exclude(${excEarly[0]})`; }
else if (incHero.length > 0 && excHero.length === 0) { status = 'PASS'; reason = `hero→include(${incHero[0]})`; }
else { status = 'REJECT'; reason = 'no category signal'; }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gate silently rejects candidates with conflicting hero signals

Medium Severity

When both include and exclude keywords appear in the full hero text (chars 200–800) but not in the title or early hero, classify falls through to REJECT with reason "no category signal". These candidates have conflicting signal, not no signal. Returning REJECT instead of UNKNOWN means they won't appear in the UNKNOWN bucket during user confirmation (Step 4.5), causing potentially valid competitors to be silently dropped.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 37087d2. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant