competitor-watch
Use when an already-named set of rivals is watched on a cadence — pricing, features, positioning and changelog diffed into a maintained tracker plus an append-only, classified change log. NOT sizing the market or choosing who the rivals are (that is `market-research`), NOT one-of
Install
npx skills add https://github.com/ericrisco/rsc-harness/tree/main/skills/competitor-watch
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install ericrisco-rsc-harness@llmmart
git clone https://github.com/ericrisco/rsc-harness.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole ericrisco/rsc-harness collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
competitor-watch
You run a standing watch, not a one-shot study. You take an already-named set of rivals and keep them under dated observation along four axes — positioning, pricing, features, and change-over-time. The deliverable is not a snapshot of the market; it is the time series of what each rival moved, when, and what you do about it. Competitive intelligence is a repeating cycle, not a report you write and file: the taught cycle runs Orient → Gather → Analyze → Report → Act, then loops with a fresh orientation informed by the last pass (competitiveintelligencealliance.io, accessed 2026-06-02).
Two near-misses decide the routing (the rest are in Handoffs, below). ../market-research/SKILL.md
answers "what is the market and who is in it" once, and often produces the list you watch —
you are the downstream loop that watches that list forever. ../data-scraper/SKILL.md owns the
generic mechanics of pulling data off a page on demand; you use change detection as a means, but
your identity is the maintained tracker + classified change log + cadence. "Extract this one table
once" → data-scraper. "Keep watching these five companies" → you.
Ethics gate — runs FIRST, before any capture
Legitimate CI is legal + ethical collection from public, observable sources with your identity disclosed. SCIP's Code of Ethics is the industry line (scip.org, accessed 2026-06-02). The practical gate is the front-page test: would you be comfortable if your collection method were reported on the front page of the news? If not, don't do it.
- Public/observable sources only — their own site, public filings, trade-show material, published reviews (G2/Capterra), public social. Why: anything else is not CI, it's a legal risk.
- Never pose as a customer to extract non-public info (no fake demo requests, no false pretenses, no misrepresenting who you are). Why: it's a Code-of-Ethics violation and it taints the data.
- Never bypass access controls, paywalls, or rate limits / "no-scrape" terms. Why: circumventing access is the bright line between intelligence and intrusion.
If a request needs any of those, refuse and reframe to the legal equivalent: instead of "get their internal pricing," watch their public pricing page on a cadence and log the moves.
Ground & scope before you watch anything
- Get the competitor list. If there is none, or it's unvalidated guesswork → STOP and
route to
../market-research/SKILL.md. Why: watching the wrong rivals forever is worse than not watching. You are not the one who decides who the competitors are. - Pick the vital few, then the watch-axes. The 3–7 rivals that move your roadmap, and only the surfaces (below) that change your decisions. Why: watching 30 companies on every axis produces noise nobody reads; depth on the few beats breadth on the many.
- Persist the tracker of record under
02-DOCS/wiki/competitors/(one profile per rival- a shared change log); keep raw captures under
02-DOCS/raw/competitors/. Why: the tracker is a maintained artifact, not a chat answer — it has to live somewhere re-runnable.
- a shared change log); keep raw captures under
- Every price/feature cell carries a
source_url+date, or it stays blank. Why: this is the single highest-value guard against inventing a rival's number. If you didn't see it on a dated public page, you don't know it — leave the cell empty and say so.
Bad: Acme Pro tier — $79/mo (no source, no date — invented)
Good: Acme Pro tier — $79/mo [acme.com/pricing, 2026-05-28] (seen, sourced, dated)
Watch-list → config: map each surface
This is the real branch — different surfaces want different cadences and different selector types, so the table earns its place. Cadence is tiered by how fast the surface moves: time- sensitive surfaces want 5–15 min checks, general competitor surfaces hourly–daily, slow/compliance surfaces daily (visualping.io / scrapx.io cadence guidance, accessed 2026-06-02). Over-frequent on a slow page is just cost and noise; under-frequent on pricing is a missed move.
| Axis (surface) | What you watch | Cadence | Selector type |
|---|---|---|---|
| Pricing / packaging | the price node, tier names, promo banner | 5–15 min | CSS on the price element, or JSONPath if pricing comes from an API |
| Features / changelog | release-notes / "what's new" list | daily | CSS on the release list (first N items) |
| Positioning / homepage | hero headline + subhead copy | daily–weekly | CSS on the hero text block |
| Launches / partnerships | press / blog index | daily | CSS on the post list |
| Careers / hiring | open-roles count + titles | weekly | CSS on the jobs list (signals strategy) |
| Customer reviews | G2 / Capterra recent reviews | weekly | CSS on the review feed |
| Social | public profile / posts | daily–weekly | platform-dependent; public only |
That mapping — URL + selector + cadence per surface — is the monitoring config. Write it down as config, don't hold it in your head.
The change loop
Each pass on a watched URL: capture → diff vs last → classify → score → log → flag.
- Capture the watched node (not the whole page — the selector keeps the diff signal-clean).
- Diff against the last stored capture for that URL.
- Classify the change into exactly one axis:
pricing | feature | positioning | messaging | team | other. - Score materiality:
high(changes our roadmap or pricing),medium(worth knowing),low(cosmetic / noise). Why: a diff with no classification and no materiality is noise — it tells you something moved but not whether to care. - Append a dated change-log row. Never overwrite; the log is the time series.
- Flag the
highrows for action and route them — a price move to whoever owns our pricing decision, a feature ship to product. You log and flag; you don't make those calls.
Bad: "They changed their website."
(no date, no axis, no old/new, no materiality, no action — unactionable)
Good: 2026-05-20 · pricing · Acme · acme.com/pricing
Pro tier $49→$59/mo · high · revisit our mid-tier vs theirs
(dated, classified, old→new, scored, with a next step)
The tracker artifacts
Three structured files. Required fields named here; full schema + a filled end-to-end example
competitor live in references/tracker-schema.md. The profile is a .md page under the
02-DOCS/wiki/ OKF v0.1 bundle, so its YAML frontmatter carries a non-empty type: competitor
(plus the OKF-recommended title/description/tags/timestamp) alongside the domain keys;
its body uses standard markdown links, never wikilinks. The two CSVs are data files, not OKF
documents.
- Competitor profile (one per rival): OKF frontmatter (
type: competitor, …) + the domain keysname,positioning_line,segment, pricing tiers (each withamount,currency,source_url,date), feature-matrix rows, watched URLs. - Feature matrix (CSV): rows = features, columns = competitors, each cell sourced + dated.
- Change log (CSV, append-only):
date,competitor,axis,url,old_value,new_value,materiality,action.
date,competitor,axis,url,old_value,new_value,materiality,action
2026-05-20,Acme,pricing,https://acme.com/pricing,$49/mo,$59/mo,high,revisit our mid-tier
2026-05-22,Acme,feature,https://acme.com/changelog,,SSO on Team plan,medium,note for product
Tooling — what you can actually run
The runnable default is changedetection.io (open-source, self-hosted): it does text/
visual / XPath/CSS-selector and JSON-API (JSONPath / jq) change detection, checks as
often as ~1 minute, notifies via Slack/Discord/Telegram/email/API, and ships AI change
summaries like "Price dropped from $89.99 to $67.00" (github.com/dgtlmoon/changedetection.io,
accessed 2026-06-02). Prefer this when you must produce a config you can run rather than
recommend a SaaS. Full docker-compose + per-axis watch recipe is in references/monitoring-config.md.
- The Wayback Machine is an archive, not a monitor. It captures some snapshots (a pricing page may be archived once in months, or never) and does not tell you when something changed; its "Changes" diff (added=blue, deleted=yellow) only compares two existing captures (archive.org "Compare two versions", accessed 2026-06-02). Use it to reconstruct historical positioning, never as the live alerting layer.
- The paid CI-suite tier exists and sets the feature bar — quote real numbers, don't
over-prescribe: Crayon median ≈$28.7K/yr, Klue ~$16K–$42.7K/yr (priced by seats), Kompyte
~$20K avg ARR (entry from ~$300/yr), lightweight page-monitors Visualping from ~$10/mo,
ChangeTower from ~$9/mo (vendr.com, autobound.ai, kompyte.com, accessed 2026-06-02). These
auto-update battlecards — but the battlecard is a sales artifact owned by
../sales-pipeline/SKILL.md, not you. A self-host config covers most teams; the suite is overkill until you're tracking many rivals across social + filings 24/7 with a dedicated CI owner. - The recurring run (cron / webhook scheduling) is wiring, not watching →
../automation-flows/SKILL.md.
Handoffs
| Request | Route to |
|---|---|
| Size the market, produce/validate the competitor list, TAM/SAM/SOM, buyer/JTBD | ../market-research/SKILL.md |
| Set OUR price / packaging / tiers (a decision, not an observation) | ../pricing/SKILL.md |
| Build the sales battlecard / objection handling / "why we win" | ../sales-pipeline/SKILL.md |
| Define OUR positioning / value prop / messaging | ../brand-voice/SKILL.md |
| One-off "extract this page/table once" with no cadence or tracker | ../data-scraper/SKILL.md |
| Wire the recurring run as a cron/webhook automation | ../automation-flows/SKILL.md |
| Track OUR own SEO / AI-search visibility vs rivals on one page | ../seo-geo/SKILL.md |
Anti-patterns
| Anti-pattern | Why it's wrong | Do instead |
|---|---|---|
| Treating it as a one-shot study | The value is the delta over time; a snapshot is stale on arrival | Set a cadence, append to a change log every pass |
| Inventing a rival's price/feature | Unsourced "facts" pollute the tracker and mislead decisions | Every cell gets source_url + date, or stays blank |
| Posing as a customer / bypassing a paywall | Fails the front-page test; not CI, it's a legal risk | Public observable sources only; refuse and reframe |
| Using the Wayback Machine as the live monitor | It doesn't tell you when something changed and may never capture the page | Wayback for historical reconstruction only; changedetection.io for live alerts |
| 5-min cadence on a careers page / weekly on pricing | Over-frequent = noise + cost; under-frequent = a missed move | Tier the cadence by axis volatility (see the table) |
| Logging a raw diff with no axis/materiality | "Something changed" is unactionable | Classify the axis, score materiality, add a next step |
| Watching 30 competitors | Breadth produces noise nobody reads | Watch the vital 3–7 that move your roadmap |
| Building the battlecard inside this skill | That's sales enablement, a different owner | Hand off to ../sales-pipeline/SKILL.md; you supply the tracker |
Verify
After you emit a tracker / change log / config, run scripts/verify.sh against your project
docs. Read-only lint: required tracker columns present; every pricing/feature row has a
non-empty source_url and date (the anti-invention guard); change-log axis and
materiality in the allowed sets, with a url + date on every row; and a warning when a
monitoring-config entry pairs a slow axis with a sub-15-min cadence, or pricing with a
slower-than-daily one. It exits 0 on an empty/clean target — no false failures.
Files (rsc-harness)
-
evals
-
cases.yaml 4 KB
skill: competitor-watch # Prompts that MUST load `competitor-watch`. The skill owns the STANDING WATCH over a # named set of rivals: a maintained tracker (pricing/features/positioning), a classified # change log over time, and the monitoring config (URL + selector + cadence). It does NOT # size the market or build the list (market-research), set our price (pricing), build the # sales battlecard (sales-pipeline), or do a one-off extraction (data-scraper). should_trigger: - prompt: "Set up monitoring on our top 3 competitors' pricing and feature pages." why: "Core watch: named set, watched surfaces, ongoing cadence — the skill's whole job." - prompt: "Did Competitor X change their pricing or ship anything new this month?" why: "Change-loop ask — capture, diff, classify the move, report what changed over a period." - prompt: "Build a feature comparison matrix versus our rivals and keep it updated." why: "Non-obvious: reads like a one-off table, but 'keep it updated' makes it the maintained matrix + change log, not a snapshot." - prompt: "monitoritza els competidors i avisa'm quan canviïn de preu." why: "Catalan: monitor competitors and alert me on price changes — the watch loop with notification, phrased without the skill name." - prompt: "Track how their positioning has shifted over the last 6 months." why: "Non-obvious time-series framing — no 'monitor/competitor' keyword in the obvious slot, but it's the dated change-over-time the skill produces (Wayback reconstruction + change log)." should_not_trigger: - prompt: "How big is this market and who are the main players we should worry about?" route_to: market-research why: "Sizing the market and producing the landscape/list is upstream; competitor-watch watches an already-named set, it doesn't build the list." - prompt: "What price should we charge for our new mid-tier plan?" route_to: pricing why: "Setting OUR price is a decision, not an observation of a rival; pricing owns it." - prompt: "Write the sales battlecard for beating Competitor X on calls." route_to: sales-pipeline why: "The sales-enablement battlecard / objection handling is a sales artifact; competitor-watch only supplies the tracker behind it." - prompt: "Scrape this one page and give me the table once." route_to: data-scraper why: "A one-off extraction with no cadence, tracker, or change log — that's the generic scraping primitive, not the standing watch." - prompt: "Schedule the competitor monitor to run every morning and post to Slack via webhook." route_to: automation-flows why: "Cron/webhook scheduling and reliable delivery is automation wiring; competitor-watch defines the watches, automation-flows makes them run." capability: - scenario: "We have a validated list of 3 competitors. Set up a competitor watch: the tracker of record, the watch config, and a repeatable change-loop process." must_include: - "Applies the ethics gate first: public/observable sources only, transparent identity, the front-page test; never pose as a customer or bypass access controls/paywalls." - "Persists the tracker under 02-DOCS/ (wiki/competitors + raw/competitors) as a maintained artifact, not a chat answer — or STOPs and routes to market-research if the list were not validated." - "Requires a source_url + date on every price/feature cell and refuses to invent a rival's number (blank cell when unconfirmed)." - "Maps each watched surface to URL + selector + tiered cadence: pricing 5-15 min, features/changelog daily, positioning/careers daily-weekly." - "Defines the change-log schema with axis classification (pricing|feature|positioning|messaging|team|other) and materiality (high|medium|low), append-only over time." - "Recommends changedetection.io (CSS/JSONPath selectors, ~1-min capable, notify) as the runnable default and flags the Wayback Machine as archive-only, not a live monitor." - "Routes the recurring run to automation-flows and the battlecard to sales-pipeline; does not build either inside this skill." -
README.md 1.5 KB
# Evals — competitor-watch `cases.yaml` has three blocks. **should_trigger** and **should_not_trigger** are routing checks: feed each `prompt` to the router and confirm it selects `competitor-watch` for the triggers (including the non-obvious "keep the matrix updated" and "how their positioning shifted over 6 months" framings and the Catalan phrasing) and the named real sibling for each near-miss — `market-research` (list/sizing), `pricing` (our price), `sales-pipeline` (the battlecard), `data-scraper` (one-off extraction), `automation-flows` (scheduling). A near-miss passes only when the router prefers the named sibling over `competitor-watch`. The **capability** block is an LLM- or human-graded rubric: run the scenario with the skill loaded and check the produced plan hits every `must_include` line — ethics gate first, tracker persisted under `02-DOCS/` with a `source_url`+`date` on every price/feature cell (no invented numbers), each surface mapped to URL+selector+tiered cadence, the change-log axis+materiality schema, `changedetection.io` as the runnable default with Wayback flagged as archive-only, and the handoffs to automation-flows and sales-pipeline. There is no automated runner and no live network call — the agent produces the tracker, watch config, and change-loop process as artifacts; grade by reading the output against the list, or wire it into your eval harness of choice. `scripts/verify.sh` separately lints an emitted tracker/change-log for sourcing and schema (see its header).
-
-
references
-
monitoring-config.md 5.1 KB
# Monitoring config: the runnable stack The default you can actually run is **`changedetection.io`** — open-source, self-hosted, no vendor lock-in. It does text/visual, **XPath/CSS-selector**, and **JSON-API (JSONPath / jq)** change detection; checks as often as ~1 minute; notifies via Slack/Discord/Telegram/email/API; and ships AI change summaries like "Price dropped from \$89.99 to \$67.00" (github.com/dgtlmoon/changedetection.io, accessed 2026-06-02). ## Self-host: docker-compose ```yaml # docker-compose.yml — changedetection.io + a Playwright fetcher for JS-heavy pages services: changedetection: image: ghcr.io/dgtlmoon/changedetection.io container_name: changedetection ports: - "5000:5000" volumes: - ./datastore:/datastore environment: - PLAYWRIGHT_DRIVER_URL=ws://playwright-chrome:3000 - BASE_URL=https://watch.internal.example.com restart: unless-stopped playwright-chrome: image: dgtlmoon/sockpuppetbrowser:latest container_name: playwright-chrome restart: unless-stopped ``` Bring it up with `docker compose up -d`, open `http://localhost:5000`, and add one watch per watched URL from the competitor profile. ## Per-axis watch definitions Each watch = URL + selector + cadence. Map straight from the watch-list table in `SKILL.md`. ```text # Pricing (time-sensitive → 5–15 min) URL: https://acme.com/pricing Filter: css:.price-amount # CSS selector for the price node only Cadence: 00:10:00 # 10 minutes Notify: tgram://<token>/<chat_id> # Telegram on change # Features / changelog (daily) URL: https://acme.com/changelog Filter: css:.changelog li:nth-child(-n+5) # first 5 release items Cadence: 1 day # Positioning / homepage (weekly) URL: https://acme.com Filter: css:h1.hero-title, css:.hero-subtitle Cadence: 7 days ``` ### JSON-API pricing (when price comes from an endpoint, not HTML) If the pricing page hydrates from an API, watch the API and pin the exact field with JSONPath — far more stable than scraping rendered HTML. ```text URL: https://acme.com/api/v2/plans Filter: json:$.plans[?(@.id=='pro')].monthly_price Cadence: 00:10:00 ``` ### AI change summary (optional) changedetection.io can attach an LLM that turns a raw diff into one line ("Pro monthly went from \$49 to \$59"). Enable it per-watch when the page is noisy and you want the classification pre-chewed — but you still own the axis + materiality call; the summary is an input, not the log. ## Notification wiring Point the notify URL at where your team already lives. Common targets: ```text tgram://<bot_token>/<chat_id> # Telegram slack://<token_a>/<token_b>/<token_c> # Slack discord://<webhook_id>/<webhook_token> # Discord post://watch.internal.example.com/hook # your own webhook → automation-flows ``` The *scheduling and downstream routing* of those webhooks (cron, retries, fan-out into a tracker write) is automation wiring → `../automation-flows/SKILL.md`. This skill defines the watches; that skill makes them run reliably on a schedule. ## Wayback Machine — historical reconstruction ONLY The Wayback Machine is a **passive archive, not a monitor**. It captures *some* snapshots (a pricing page may be archived once in months, or never) and **does not tell you when something changed**. Use it to reconstruct what a rival's page said in the past — never as the live alerting layer. Its "Changes" diff compares two existing captures (added = blue, deleted = yellow): ```text # Compare two captured timestamps of the same URL https://web.archive.org/web/diff/<TS1>/<TS2>/https://acme.com/pricing # e.g. TS = 20260101000000 (YYYYMMDDhhmmss) https://web.archive.org/web/diff/20260101000000/20260501000000/https://acme.com/pricing ``` (archive.org "Compare two versions", accessed 2026-06-02.) Good for "how did their positioning read 6 months ago"; useless for "alert me when it changes." ## Self-host vs paid SaaS — when each is the right call | Option | Price (accessed 2026-06-02) | Right call when | |---|---|---| | **changedetection.io** (self-host) | free / infra cost only | Default. You want a config you run, full selector control, no vendor. | | Visualping | from ~\$10/mo | A few pages, want zero ops, visual diffs. | | ChangeTower | from ~\$9/mo | Same — lightweight page monitor, hosted. | | Kompyte | ~\$20K avg ARR (entry ~\$300/yr, 1–2wk setup) | You need site+social+filings 24/7 and a dedicated CI owner. | | Crayon | median ≈\$28.7K/yr (~\$12K–\$47K, 7–8wk setup) | Many rivals, auto-updated battlecards, priced by # competitors. | | Klue | ~\$16K–\$42.7K/yr (Basic/Standard/Premium, by seats) | Large CI team splitting "curators" vs "consumers." | (vendr.com/marketplace/crayon, autobound.ai "Top 15 CI tools 2026", kompyte.com comparison, visualping.io — accessed 2026-06-02.) The paid suites also auto-update **battlecards** — but the battlecard itself is a sales artifact owned by `../sales-pipeline/SKILL.md`, not this skill. A self-host config covers most teams; reach for a suite only when breadth (many rivals, many surfaces, 24/7) outgrows what one config + one owner can maintain. -
tracker-schema.md 5.2 KB
# Tracker schema + a filled example The tracker of record is three structured files under `02-DOCS/wiki/competitors/`. Raw captures (the diffed HTML/JSON snapshots) go under `02-DOCS/raw/competitors/<rival>/`. The rule that overrides everything: **a price or feature cell without a `source_url` + `date` is not knowledge — leave it blank and say so.** `02-DOCS/wiki/` is an OKF v0.1 bundle, so the one `.md` file here (the competitor profile) carries YAML frontmatter with a non-empty `type:`; the two CSVs are data files, not OKF documents. Any cross-references in the profile body use **standard markdown links** (e.g. `[Beta](./beta.md)`), never wikilinks. See `../../harness/references/wiki-protocol.md` "## Conventions". ## 1. Competitor profile (one file per rival) Markdown front-matter + body. One per rival, named `02-DOCS/wiki/competitors/<rival>.md`. The frontmatter is OKF v0.1 conformant: `type:` is the only required field (non-empty); `title`/`description`/`tags`/`timestamp` are the recommended standard surface. All the domain keys below (`name`, `positioning_source.date`, every `pricing_tiers[].date`, …) are kept verbatim — OKF allows any extra key and `verify.sh` reads them, so adding the OKF fields is purely additive. ```yaml --- type: competitor title: Acme description: "Tracker of record for Acme — positioning, pricing tiers, and watched surfaces." tags: [competitor, pricing-watch, feature-watch] timestamp: 2026-05-28T00:00:00Z name: Acme positioning_line: "The all-in-one workspace for remote-first teams" positioning_source: { url: "https://acme.com", date: "2026-05-28" } segment: "SMB / mid-market SaaS" watched_urls: - { axis: pricing, url: "https://acme.com/pricing", selector: ".price-amount", cadence: "10m" } - { axis: feature, url: "https://acme.com/changelog", selector: ".changelog li:first-child", cadence: "daily" } - { axis: positioning, url: "https://acme.com", selector: "h1.hero-title", cadence: "weekly" } - { axis: team, url: "https://acme.com/careers", selector: ".jobs-count", cadence: "weekly" } pricing_tiers: - { tier: "Starter", amount: 0, currency: USD, period: mo, source_url: "https://acme.com/pricing", date: "2026-05-28" } - { tier: "Pro", amount: 59, currency: USD, period: mo, source_url: "https://acme.com/pricing", date: "2026-05-28" } - { tier: "Team", amount: 99, currency: USD, period: mo, source_url: "https://acme.com/pricing", date: "2026-05-28" } --- ## Notes Repositioned from "project tool" to "workspace" in Q2 2026 — see change log 2026-05-12. ## Related - [Beta](./beta.md) — closest competing positioning; cross-watch their pricing moves. ``` Every `pricing_tiers` row and any feature claim must carry `source_url` + `date`. If you only have the tier name but not a confirmed price, omit `amount` rather than guess. ## 2. Feature matrix (CSV) Rows = features, columns = competitors, plus a sourcing column pair per competitor so `verify.sh` can confirm each asserted cell is dated. Keep it at `02-DOCS/wiki/competitors/feature-matrix.csv`. ```csv feature,acme_value,acme_source_url,acme_date,beta_value,beta_source_url,beta_date SSO (SAML),Team plan,https://acme.com/pricing,2026-05-28,Enterprise only,https://beta.io/security,2026-05-29 API rate limit,1000 req/min,https://acme.com/docs/limits,2026-05-28,,, Audit log,yes,https://acme.com/security,2026-05-28,yes,https://beta.io/security,2026-05-29 ``` A blank `*_value` with blank source is honest ("we haven't confirmed it"). A non-blank value with a blank source is a violation — that's an invented fact and `verify.sh` fails it. ## 3. Change log (CSV, append-only) The time series. Never overwrite a row; each pass appends. Keep it at `02-DOCS/wiki/competitors/change-log.csv`. ```csv date,competitor,axis,url,old_value,new_value,materiality,action 2026-05-12,Acme,positioning,https://acme.com,"project tool","remote-first workspace",high,"flag to brand-voice — they moved into our lane" 2026-05-20,Acme,pricing,https://acme.com/pricing,$49/mo,$59/mo,high,"revisit our mid-tier vs theirs (route to pricing)" 2026-05-22,Acme,feature,https://acme.com/changelog,,"SSO on Team plan",medium,"note for product" 2026-05-29,Beta,team,https://beta.io/careers,12 roles,19 roles,low,"hiring sales — watch for go-to-market push" ``` Field rules (these are what `verify.sh` enforces): - `axis` ∈ `{pricing, feature, positioning, messaging, team, other}` — exactly one. - `materiality` ∈ `{high, medium, low}`. - `url` and `date` are required on every row (the change is unverifiable without them). - `old_value` may be blank for a *new* surface (first capture); `new_value` should not be blank. - `action` is the next step or the route — empty action on a `high` row is a smell. ## End-to-end: one rival, one pass 1. Orient: Acme is in our vital-few list. Watch pricing (10m), changelog (daily), hero (weekly), careers (weekly). 2. Gather: capture `.price-amount` → reads `$59/mo` on the Pro tier. 3. Analyze: diff vs last stored capture (`$49/mo`) → changed. 4. Classify: `pricing`. Score: `high` (it's our competing tier). 5. Log: append the `2026-05-20` pricing row above. 6. Flag + route: high → route the pricing implication to `../pricing/SKILL.md`; you don't set our price.
-
-
scripts
-
verify.sh 10.3 KB
#!/usr/bin/env bash # # verify.sh — competitor-tracker / change-log / monitoring-config linter for the # `competitor-watch` skill. # # WHAT IT DOES (read-only; never edits a file) # Lints the structured artifacts this skill emits against the rules in SKILL.md. # It keys off filenames and CSV headers, so any unrelated file is simply skipped # and an empty/clean target never false-fails. # # Checks: # 1. CHANGE LOG — any CSV whose header contains both `axis` and `materiality` # (e.g. change-log.csv). Per data row: # - `axis` must be one of pricing|feature|positioning|messaging|team|other (FAIL) # - `materiality` must be one of high|medium|low (FAIL) # - `url` must be non-empty (FAIL) # - `date` must be non-empty (FAIL) # 2. FEATURE MATRIX — any CSV with paired `<x>_value` + `<x>_source_url` + # `<x>_date` columns. A non-empty value with an empty source_url OR empty # date is an invented fact (FAIL) # 3. MONITORING CONFIG — cadence sanity (WARN, never fail). In any *.md/*.yaml/ # *.yml/*.txt file, a line that names an axis and a cadence where: # - a slow axis (positioning|careers|team|reviews|social) is paired with a # sub-15-minute cadence -> noise/cost (WARN) # - the pricing axis is paired with a slower-than-daily cadence (weekly/ # monthly) -> a missed move (WARN) # # HOW TO RUN (inside YOUR project, not the skills repo) # ./verify.sh # scan ./ for tracker / change-log / config files # ./verify.sh --path 02-DOCS # scan a subdirectory # ./verify.sh --strict # treat any warning as a failure (exit 1) # # EXIT CODES # 0 clean, or warnings only without --strict (also: nothing to check) # 1 a structural violation (bad axis/materiality, missing url/date, unsourced # value), or --strict with a warning # 2 bad usage # # Runs on stock macOS bash 3.2 — no mapfile, no associative arrays. set -euo pipefail if [ -t 1 ]; then RED=$'\033[31m'; GREEN=$'\033[32m'; YELLOW=$'\033[33m'; NC=$'\033[0m' else RED=''; GREEN=''; YELLOW=''; NC='' fi ok_count=0; skip_count=0; warn_count=0; fail_count=0 ok() { printf '%s[ ok ]%s %s\n' "$GREEN" "$NC" "$*"; ok_count=$((ok_count + 1)); } skip() { printf '%s[skip]%s %s\n' "$YELLOW" "$NC" "$*"; skip_count=$((skip_count + 1)); } warn() { printf '%s[warn]%s %s\n' "$YELLOW" "$NC" "$*"; warn_count=$((warn_count + 1)); } fail() { printf '%s[fail]%s %s\n' "$RED" "$NC" "$*"; fail_count=$((fail_count + 1)); } usage() { sed -n '2,46p' "$0" | sed 's/^# \{0,1\}//'; } SCAN_PATH="." STRICT=0 while [ $# -gt 0 ]; do case "$1" in --path) SCAN_PATH="${2:?--path needs a value}"; shift 2 ;; --strict) STRICT=1; shift ;; -h|--help) usage; exit 0 ;; *) printf '%sUnknown argument: %s%s\n\n' "$RED" "$1" "$NC"; usage; exit 2 ;; esac done if [ ! -e "$SCAN_PATH" ]; then printf '%sPath not found: %s%s\n' "$RED" "$SCAN_PATH" "$NC"; exit 2 fi TMPDIR_V="$(mktemp -d 2>/dev/null || printf '/tmp/cw-verify.%s' "$$")" mkdir -p "$TMPDIR_V" 2>/dev/null || true cleanup() { rm -rf "$TMPDIR_V" 2>/dev/null || true; } trap cleanup EXIT CSV_FILES="$TMPDIR_V/csv" CFG_FILES="$TMPDIR_V/cfg" find "$SCAN_PATH" -type f -name '*.csv' 2>/dev/null > "$CSV_FILES" || true find "$SCAN_PATH" -type f \ \( -name '*.md' -o -name '*.yaml' -o -name '*.yml' -o -name '*.txt' \) \ 2>/dev/null > "$CFG_FILES" || true # Allowed sets (whitespace-padded for safe substring match). AXES=" pricing feature positioning messaging team other " MATS=" high medium low " # Find the 0-based index of a column name in a comma-separated header line. # Echoes -1 if absent. col_index() { header="$1"; want="$2" idx=-1; i=0 oldifs="$IFS"; IFS=',' for c in $header; do # trim spaces and a trailing CR c="$(printf '%s' "$c" | tr -d '\r' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')" if [ "$c" = "$want" ]; then idx=$i; fi i=$((i + 1)) done IFS="$oldifs" printf '%s' "$idx" } # Echo the Nth (0-based) field of a CSV row. Simple split on comma — values in this # schema do not contain commas; quoted commas would need a real parser, which is out # of scope for a lint (we only read low-structure tracker cells). field_at() { row="$1"; n="$2" i=0 oldifs="$IFS"; IFS=',' for c in $row; do if [ "$i" -eq "$n" ]; then printf '%s' "$c" | tr -d '\r' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' IFS="$oldifs"; return 0 fi i=$((i + 1)) done IFS="$oldifs" printf '' } # --------------------------------------------------------------------------- # 1 + 2. CSV checks # --------------------------------------------------------------------------- csv_change_log_seen=0 csv_matrix_seen=0 while IFS= read -r f; do [ -z "$f" ] && continue header="$(head -n 1 "$f" 2>/dev/null | tr -d '\r')" [ -z "$header" ] && continue # --- change log: header has both axis and materiality --- ai="$(col_index "$header" axis)" mi="$(col_index "$header" materiality)" ui="$(col_index "$header" url)" di="$(col_index "$header" date)" if [ "$ai" -ge 0 ] && [ "$mi" -ge 0 ]; then csv_change_log_seen=1 rownum=0; bad=0 while IFS= read -r row; do rownum=$((rownum + 1)) [ "$rownum" -le 1 ] && continue # skip header # skip blank lines [ -z "$(printf '%s' "$row" | tr -d ', \r')" ] && continue axv="$(field_at "$row" "$ai")" mav="$(field_at "$row" "$mi")" case "$AXES" in *" $axv "*) : ;; *) fail "$f row $rownum: axis '$axv' not in {pricing,feature,positioning,messaging,team,other}"; bad=1 ;; esac case "$MATS" in *" $mav "*) : ;; *) fail "$f row $rownum: materiality '$mav' not in {high,medium,low}"; bad=1 ;; esac if [ "$ui" -ge 0 ]; then urv="$(field_at "$row" "$ui")" [ -z "$urv" ] && { fail "$f row $rownum: empty url"; bad=1; } fi if [ "$di" -ge 0 ]; then dtv="$(field_at "$row" "$di")" [ -z "$dtv" ] && { fail "$f row $rownum: empty date"; bad=1; } fi done < "$f" [ "$bad" -eq 0 ] && ok "change log clean: $f" fi # --- feature matrix: any <x>_value with a PAIRED <x>_source_url column --- # discover candidate prefixes that have a *_value column, then keep only those # that also declare a *_source_url column (so change-log old_value/new_value, # which have no paired source column, are not mistaken for matrix cells). cand="$(printf '%s' "$header" | tr ',' '\n' | sed 's/\r//' | sed -n 's/^[[:space:]]*\([A-Za-z0-9_]*\)_value[[:space:]]*$/\1/p')" prefixes="" for pre in $cand; do if [ "$(col_index "$header" "${pre}_source_url")" -ge 0 ]; then prefixes="$prefixes $pre" fi done if [ -n "$(printf '%s' "$prefixes" | tr -d ' ')" ]; then csv_matrix_seen=1 bad=0 for pre in $prefixes; do vi="$(col_index "$header" "${pre}_value")" si="$(col_index "$header" "${pre}_source_url")" pi="$(col_index "$header" "${pre}_date")" rownum=0 while IFS= read -r row; do rownum=$((rownum + 1)) [ "$rownum" -le 1 ] && continue # skip header [ -z "$(printf '%s' "$row" | tr -d ', \r')" ] && continue val="$(field_at "$row" "$vi")" [ -z "$val" ] && continue # blank value is honest; nothing to source src=""; dat="" [ "$si" -ge 0 ] && src="$(field_at "$row" "$si")" [ "$pi" -ge 0 ] && dat="$(field_at "$row" "$pi")" if [ -z "$src" ] || [ -z "$dat" ]; then fail "$f row $rownum: '${pre}' value '$val' has no source_url+date (invented fact)" bad=1 fi done < "$f" done [ "$bad" -eq 0 ] && ok "feature-matrix cells all sourced+dated: $f" fi done < "$CSV_FILES" [ "$csv_change_log_seen" -eq 0 ] && skip "no change-log CSV (header with axis+materiality) found" [ "$csv_matrix_seen" -eq 0 ] && skip "no feature-matrix CSV (<x>_value/_source_url/_date) found" # --------------------------------------------------------------------------- # 3. Monitoring-config cadence sanity (WARN only) # --------------------------------------------------------------------------- SLOW_AXES="positioning careers team reviews review social" cfg_seen=0 while IFS= read -r f; do [ -z "$f" ] && continue # only consider files that look like config (mention a cadence keyword) grep -iqE 'cadence|every|min|hour|daily|weekly|monthly' "$f" 2>/dev/null || continue while IFS= read -r ln; do low="$(printf '%s' "$ln" | tr 'A-Z' 'a-z')" case "$low" in *axis*|*cadence*|*pricing*|*positioning*|*careers*) : ;; *) continue ;; esac # sub-15-minute cadence present on the line? sub15=0 # patterns like 5m, 10 min, 00:10:00, "5 minutes", "every 10 min" if printf '%s' "$low" | grep -Eq '(^|[^0-9])(0?[0-9]|1[0-4])[[:space:]]*m(in)?([^a-z]|$)'; then sub15=1; fi if printf '%s' "$low" | grep -Eq '00:0[0-9]:00|00:1[0-4]:00'; then sub15=1; fi if printf '%s' "$low" | grep -Eq '(0?[0-9]|1[0-4])[[:space:]]*minute'; then sub15=1; fi slower_than_daily=0 if printf '%s' "$low" | grep -Eq 'weekly|monthly|[0-9]+[[:space:]]*(day|days)'; then # >1 day or weekly/monthly if printf '%s' "$low" | grep -Eq 'weekly|monthly|([2-9]|[1-9][0-9]+)[[:space:]]*day'; then slower_than_daily=1; fi fi # pricing axis with a slower-than-daily cadence -> missed move if printf '%s' "$low" | grep -q 'pricing' && [ "$slower_than_daily" -eq 1 ]; then cfg_seen=1 warn "$f: pricing axis on a slower-than-daily cadence — risks a missed move :: $ln" fi # slow axis with a sub-15-min cadence -> noise/cost if [ "$sub15" -eq 1 ]; then for sa in $SLOW_AXES; do if printf '%s' "$low" | grep -q "$sa"; then cfg_seen=1 warn "$f: '$sa' axis on a sub-15-min cadence — noise and cost :: $ln" break fi done fi done < "$f" done < "$CFG_FILES" [ "$cfg_seen" -eq 0 ] && skip "no monitoring-config cadence mismatches found" printf '\nok=%d skip=%d warn=%d fail=%d\n' "$ok_count" "$skip_count" "$warn_count" "$fail_count" if [ "$fail_count" -gt 0 ]; then exit 1; fi if [ "$STRICT" -eq 1 ] && [ "$warn_count" -gt 0 ]; then exit 1; fi exit 0
-
-
SKILL.md 12.3 KB
--- name: competitor-watch description: "Use when an already-named set of rivals is watched on a cadence — pricing, features, positioning and changelog diffed into a maintained tracker plus an append-only, classified change log. NOT sizing the market or choosing who the rivals are (that is `market-research`), NOT one-off page extraction (that is `data-scraper`)." tags: [competitive-intelligence, monitoring, pricing-tracking, feature-tracking, marketing-ops] recommends: [market-research, pricing, sales-pipeline, brand-voice, data-scraper, automation-flows, seo-geo] origin: risco --- # competitor-watch You run a **standing watch**, not a one-shot study. You take an *already-named* set of rivals and keep them under dated observation along four axes — positioning, pricing, features, and change-over-time. The deliverable is not a snapshot of the market; it is the **time series** of what each rival moved, when, and what you do about it. Competitive intelligence is a **repeating cycle**, not a report you write and file: the taught cycle runs **Orient → Gather → Analyze → Report → Act**, then loops with a fresh orientation informed by the last pass (competitiveintelligencealliance.io, accessed 2026-06-02). Two near-misses decide the routing (the rest are in **Handoffs**, below). `../market-research/SKILL.md` answers *"what is the market and who is in it"* once, and often **produces the list you watch** — you are the downstream loop that watches that list forever. `../data-scraper/SKILL.md` owns the generic mechanics of pulling data off a page on demand; you *use* change detection as a means, but your identity is the maintained tracker + classified change log + cadence. "Extract this one table once" → data-scraper. "Keep watching these five companies" → you. ## Ethics gate — runs FIRST, before any capture Legitimate CI is **legal + ethical collection from public, observable sources** with your identity disclosed. SCIP's Code of Ethics is the industry line (scip.org, accessed 2026-06-02). The practical gate is the **front-page test**: would you be comfortable if your collection method were reported on the front page of the news? If not, don't do it. - **Public/observable sources only** — their own site, public filings, trade-show material, published reviews (G2/Capterra), public social. *Why:* anything else is not CI, it's a legal risk. - **Never pose as a customer** to extract non-public info (no fake demo requests, no false pretenses, no misrepresenting who you are). *Why:* it's a Code-of-Ethics violation and it taints the data. - **Never bypass access controls, paywalls, or rate limits / "no-scrape" terms.** *Why:* circumventing access is the bright line between intelligence and intrusion. If a request needs any of those, **refuse and reframe to the legal equivalent**: instead of "get their internal pricing," watch their *public* pricing page on a cadence and log the moves. ## Ground & scope before you watch anything 1. **Get the competitor list.** If there is none, or it's unvalidated guesswork → STOP and route to `../market-research/SKILL.md`. *Why:* watching the wrong rivals forever is worse than not watching. You are not the one who decides who the competitors are. 2. **Pick the vital few, then the watch-axes.** The 3–7 rivals that move your roadmap, and only the surfaces (below) that change your decisions. *Why:* watching 30 companies on every axis produces noise nobody reads; depth on the few beats breadth on the many. 3. **Persist the tracker of record** under `02-DOCS/wiki/competitors/` (one profile per rival + a shared change log); keep raw captures under `02-DOCS/raw/competitors/`. *Why:* the tracker is a maintained artifact, not a chat answer — it has to live somewhere re-runnable. 4. **Every price/feature cell carries a `source_url` + `date`, or it stays blank.** *Why:* this is the single highest-value guard against inventing a rival's number. If you didn't see it on a dated public page, you don't know it — leave the cell empty and say so. ```text Bad: Acme Pro tier — $79/mo (no source, no date — invented) Good: Acme Pro tier — $79/mo [acme.com/pricing, 2026-05-28] (seen, sourced, dated) ``` ## Watch-list → config: map each surface This is the real branch — different surfaces want different cadences and different selector types, so the table earns its place. Cadence is tiered by how fast the surface moves: time- sensitive surfaces want **5–15 min** checks, general competitor surfaces **hourly–daily**, slow/compliance surfaces **daily** (visualping.io / scrapx.io cadence guidance, accessed 2026-06-02). Over-frequent on a slow page is just cost and noise; under-frequent on pricing is a missed move. | Axis (surface) | What you watch | Cadence | Selector type | |---|---|---|---| | Pricing / packaging | the price node, tier names, promo banner | **5–15 min** | CSS on the price element, or JSONPath if pricing comes from an API | | Features / changelog | release-notes / "what's new" list | daily | CSS on the release list (first N items) | | Positioning / homepage | hero headline + subhead copy | daily–weekly | CSS on the hero text block | | Launches / partnerships | press / blog index | daily | CSS on the post list | | Careers / hiring | open-roles count + titles | weekly | CSS on the jobs list (signals strategy) | | Customer reviews | G2 / Capterra recent reviews | weekly | CSS on the review feed | | Social | public profile / posts | daily–weekly | platform-dependent; public only | That mapping — **URL + selector + cadence per surface** — *is* the monitoring config. Write it down as config, don't hold it in your head. ## The change loop Each pass on a watched URL: **capture → diff vs last → classify → score → log → flag.** 1. **Capture** the watched node (not the whole page — the selector keeps the diff signal-clean). 2. **Diff** against the last stored capture for that URL. 3. **Classify** the change into exactly one axis: `pricing | feature | positioning | messaging | team | other`. 4. **Score materiality**: `high` (changes our roadmap or pricing), `medium` (worth knowing), `low` (cosmetic / noise). *Why:* a diff with no classification and no materiality is noise — it tells you something moved but not whether to care. 5. **Append a dated change-log row.** Never overwrite; the log is the time series. 6. **Flag the `high` rows** for action and route them — a price move to whoever owns *our* pricing decision, a feature ship to product. You log and flag; you don't make those calls. ```text Bad: "They changed their website." (no date, no axis, no old/new, no materiality, no action — unactionable) Good: 2026-05-20 · pricing · Acme · acme.com/pricing Pro tier $49→$59/mo · high · revisit our mid-tier vs theirs (dated, classified, old→new, scored, with a next step) ``` ## The tracker artifacts Three structured files. Required fields named here; full schema + a filled end-to-end example competitor live in `references/tracker-schema.md`. The profile is a `.md` page under the `02-DOCS/wiki/` OKF v0.1 bundle, so its YAML frontmatter carries a non-empty `type: competitor` (plus the OKF-recommended `title`/`description`/`tags`/`timestamp`) alongside the domain keys; its body uses standard markdown links, never wikilinks. The two CSVs are data files, not OKF documents. - **Competitor profile** (one per rival): OKF frontmatter (`type: competitor`, …) + the domain keys `name`, `positioning_line`, `segment`, pricing tiers (each with `amount`, `currency`, `source_url`, `date`), feature-matrix rows, watched URLs. - **Feature matrix** (CSV): rows = features, columns = competitors, each cell sourced + dated. - **Change log** (CSV, append-only): `date,competitor,axis,url,old_value,new_value,materiality,action`. ```csv date,competitor,axis,url,old_value,new_value,materiality,action 2026-05-20,Acme,pricing,https://acme.com/pricing,$49/mo,$59/mo,high,revisit our mid-tier 2026-05-22,Acme,feature,https://acme.com/changelog,,SSO on Team plan,medium,note for product ``` ## Tooling — what you can actually run The runnable default is **`changedetection.io`** (open-source, self-hosted): it does text/ visual / **XPath/CSS-selector** and **JSON-API (JSONPath / jq)** change detection, checks as often as ~1 minute, notifies via Slack/Discord/Telegram/email/API, and ships AI change summaries like "Price dropped from \$89.99 to \$67.00" (github.com/dgtlmoon/changedetection.io, accessed 2026-06-02). Prefer this when you must *produce a config you can run* rather than recommend a SaaS. Full docker-compose + per-axis watch recipe is in `references/monitoring-config.md`. - **The Wayback Machine is an archive, not a monitor.** It captures *some* snapshots (a pricing page may be archived once in months, or never) and **does not tell you when something changed**; its "Changes" diff (added=blue, deleted=yellow) only compares two existing captures (archive.org "Compare two versions", accessed 2026-06-02). Use it to **reconstruct historical positioning**, never as the live alerting layer. - **The paid CI-suite tier** exists and sets the feature bar — quote real numbers, don't over-prescribe: Crayon median ≈\$28.7K/yr, Klue ~\$16K–\$42.7K/yr (priced by seats), Kompyte ~\$20K avg ARR (entry from ~\$300/yr), lightweight page-monitors Visualping from ~\$10/mo, ChangeTower from ~\$9/mo (vendr.com, autobound.ai, kompyte.com, accessed 2026-06-02). These auto-update battlecards — but the **battlecard is a sales artifact owned by `../sales-pipeline/SKILL.md`**, not you. A self-host config covers most teams; the suite is overkill until you're tracking many rivals across social + filings 24/7 with a dedicated CI owner. - **The recurring run** (cron / webhook scheduling) is wiring, not watching → `../automation-flows/SKILL.md`. ## Handoffs | Request | Route to | |---|---| | Size the market, produce/validate the competitor list, TAM/SAM/SOM, buyer/JTBD | `../market-research/SKILL.md` | | Set OUR price / packaging / tiers (a decision, not an observation) | `../pricing/SKILL.md` | | Build the sales battlecard / objection handling / "why we win" | `../sales-pipeline/SKILL.md` | | Define OUR positioning / value prop / messaging | `../brand-voice/SKILL.md` | | One-off "extract this page/table once" with no cadence or tracker | `../data-scraper/SKILL.md` | | Wire the recurring run as a cron/webhook automation | `../automation-flows/SKILL.md` | | Track OUR own SEO / AI-search visibility vs rivals on one page | `../seo-geo/SKILL.md` | ## Anti-patterns | Anti-pattern | Why it's wrong | Do instead | |---|---|---| | Treating it as a one-shot study | The value is the delta over time; a snapshot is stale on arrival | Set a cadence, append to a change log every pass | | Inventing a rival's price/feature | Unsourced "facts" pollute the tracker and mislead decisions | Every cell gets `source_url` + `date`, or stays blank | | Posing as a customer / bypassing a paywall | Fails the front-page test; not CI, it's a legal risk | Public observable sources only; refuse and reframe | | Using the Wayback Machine as the live monitor | It doesn't tell you *when* something changed and may never capture the page | Wayback for historical reconstruction only; changedetection.io for live alerts | | 5-min cadence on a careers page / weekly on pricing | Over-frequent = noise + cost; under-frequent = a missed move | Tier the cadence by axis volatility (see the table) | | Logging a raw diff with no axis/materiality | "Something changed" is unactionable | Classify the axis, score materiality, add a next step | | Watching 30 competitors | Breadth produces noise nobody reads | Watch the vital 3–7 that move your roadmap | | Building the battlecard inside this skill | That's sales enablement, a different owner | Hand off to `../sales-pipeline/SKILL.md`; you supply the tracker | ## Verify After you emit a tracker / change log / config, run `scripts/verify.sh` against your project docs. Read-only lint: required tracker columns present; **every pricing/feature row has a non-empty `source_url` and `date`** (the anti-invention guard); change-log `axis` and `materiality` in the allowed sets, with a `url` + `date` on every row; and a **warning** when a monitoring-config entry pairs a slow axis with a sub-15-min cadence, or pricing with a slower-than-daily one. It exits 0 on an empty/clean target — no false failures.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.