Competitive Positioning Skill

SkillDocs & knowledge

Maps a startup's competitive landscape, scores moat strength across 6+ dimensions, and generates an investor-ready competition narrative with positioning map. Run the verified scoring rather than assessing positioning from memory. Also covers plain-language questions with no brief attached — 'who else is doing this?', 'who are my competitors?', 'is that a real moat?' — which run verified research instead of recalled competitor names.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Competitive Positioning Skill skill

What this skill tells your AI

The instructions your AI receives, as published by lool-ventures/founder-skills in founder-skills/skills/competitive-positioning/SKILL.md and read by ahel’s review.

Help startup founders see their competitive landscape clearly — who the real competitors are, where they're differentiated, how defensible that differentiation is, and how to present it to investors. Produce a competitive analysis with positioning maps, moat scorecards, and an investor-ready narrative. The tone is founder-first: a coaching tool for preparation, not a judgment.

Skill Metadata

  • Author: lool-ventures
  • Version: managed in founder-skills/.claude-plugin/plugin.json
  • Compatibility: Python 3.10+ and uv for script execution.
  • Imports (optional):
    • deck-review:checklist.json — competition slide claims for cross-validation
    • market-sizing:sizing.json — validate market claims in positioning
  • Exports:
    • landscape.jsondeck-review, fundraise-readiness
    • report.jsonic-sim, fundraise-readiness, cross-document-consistency

Skill Execution Model (READ FIRST)

See founder-skills/references/skill-execution-model.md for the full inline-skill execution model (3 dispatch contexts, Mitigation 1+2, producer contract, Cowork quirks, per-symptom triage).

This skill runs inline in the main thread, not as a sub-agent — see the reference above ("Why Inline (Not Forked Sub-Agent)") for the rationale. Sub-agents are deliberately shell-free, so orchestration (producer scripts, artifact persistence) stays in the main thread. Network note: this skill's Context A sub-agent declares WebSearch in its own tool allowlist and performs its own live competitor research (LANDSCAPE_RESEARCH, MOAT_SCORING, POSITIONING_SCORING) — the main thread does NOT need a research-before-dispatch pass; pass founder-provided context inline and let the sub-agent research.

Two dispatch contexts for the sub-agent:

  • Context A — Per-step analytical dispatch (Mitigation 1): Steps 4 (LANDSCAPE_RESEARCH), 5 (MOAT_SCORING + POSITIONING_SCORING), and 6 (CHECKLIST) dispatch the competitive-positioning agent via the Task tool. The agent does deep analysis (including its own WebSearch research), WRITES its output JSON to the OUTPUT_PATH given in its prompt (the handoff/ dir), and returns a small receipt. The main thread gates the file with check_handoff.py, then pipes it through the producer script (validate_landscape.py, score_moats.py, score_positioning.py, or checklist.py). The sub-agent never writes canonical artifacts — only its hand-off file.
  • Context B — Post-compose coaching dispatch: Step 7 dispatches the sub-agent after compose_report.py writes report.md. The sub-agent Reads the staged coaching_payload.json from the hand-off dir (Mitigation 2) — it does NOT read the full report.md — composes the coaching commentary, WRITES it to the OUTPUT_PATH hand-off file, and returns a small receipt. The main thread gates the file (check_handoff.py) and inserts it via the shared insert_coaching.py script (idempotency matrix, uuid-marker replacement, run_id-parity verification — all deterministic). See the reference above for the full Context B contract.

Tolerant JSON extraction protocol (Context B returns; also the Context A message-channel fallback): capture the sub-agent's final assistant message. It should be raw JSON, but may be wrapped in ```json ... ``` fences or carry a prose preamble. Extract tolerantly:

  1. If the message is wrapped in a ```json ... ``` (or plain ``` ... ```) fence, strip the fence first.
  2. Try to parse the stripped text directly as JSON.
  3. If that fails, walk through the text looking for the first { character and try json.JSONDecoder().raw_decode(text[i:]) — this is brace-aware and handles nested objects correctly (unlike regex, which truncates on the first }).
  4. If extraction fails entirely, re-prompt the sub-agent with: "Your previous reply could not be parsed as JSON. Return ONLY the JSON object — no markdown fences, no prose preamble."

Context A receipts don't need this protocol by hand — check_handoff.py --receipt-json - applies the same tolerant extraction internally; pass the final message verbatim.

Input Formats

Accept any combination: pitch deck (PDF), competitive analysis document, text description of the product and market, prior deck-review or market-sizing artifacts, or conversational input. If a pitch deck is provided, extract competitor claims from the competition slide for validation.

Available Scripts

All scripts are at ${CLAUDE_PLUGIN_ROOT}/skills/competitive-positioning/scripts/:

  • validate_landscape.py — Validates and normalizes competitor landscape; checks slug uniqueness, category distribution, research depth; emits warnings for quality issues
  • verify_competitors.py — Validates the COMPETITOR_VERIFICATION sub-agent's per-competitor verdicts (genuine/adjacent/not_a_competitor); enforces the show-your-work gate (a flag must carry reasoning + independent buyer/job characterization), cross-checks landscape slug coverage, computes summary. Validator, not detector. --blind-set additionally diffs the COMPETITOR_RECALL agent's independently-derived set against the draft and emits recall_gaps (deterministic slug comparison; unsourced candidates dropped)
  • score_moats.py — Validates per-company moat assessments, computes aggregates (moat_count, strongest_moat, overall_defensibility), produces cross-company comparison by moat dimension
  • score_positioning.py — Scores positioning views with rank-based differentiation, detects vanity axes, passes through stress-test results
  • checklist.py — Scores 25 criteria across 6 categories (pass/fail/warn/not_applicable) with mode-based gating by input_mode
  • compose_report.py — Assembles report with cross-artifact validation; --strict exits 1 on high-severity warnings
  • visualize.py — Generates self-contained HTML with SVG charts (not JSON)
  • explore.py — Generates interactive HTML explorer with Chart.js scatter plot, view switching, bubble encoding controls, and company detail panels (not JSON)
  • gate3_triggers.py — Evaluates the four Gate 3 positioning-reality-check triggers from positioning_scores.json and returns founder-ready descriptions. Thresholds pinned and exhaustively tested; reports not_evaluated separately from "did not fire". Reports only — Gate 3 is a founder decision, so it never exits non-zero
  • verify_positioning.py — Delivery gate (Step 7f). Checks that the deliverable SHOWS what the artifacts contain (axis rationales, claim verdicts, the adversarial competitor verdicts, the explorer's scored layer) and that no internal token reached the founder (raw enums, field names, slugs, criterion IDs in the coaching commentary), plus cross-artifact consistency. --gate 1 mid-pipeline, --gate 2 pre-delivery. Exit 0 = publishable, exit 1 = gaps

Also available from ${CLAUDE_PLUGIN_ROOT}/scripts/ (shared):

  • founder_context.py — Per-company context management (init/read/merge/validate)
  • find_artifact.py — Resolves artifact paths by skill name and filename (for cross-skill lookups)

Run with: python3 ${CLAUDE_PLUGIN_ROOT}/skills/competitive-positioning/scripts/<script>.py --pretty [args]

Available References

Read each when first needed — do NOT load all upfront. At ${CLAUDE_PLUGIN_ROOT}/skills/competitive-positioning/references/:

  • competitive-analysis-methodology.md — Read before Step 3. Axis selection, competitor categorization, stress-testing, investor expectations
  • moat-definitions.md — Read before Step 5. Six canonical moat dimensions with scoring rubrics and stage-calibrated expectations
  • checklist-criteria.md — Read before Step 6. All 25 checklist criteria with category definitions and mode-based gating rules
  • artifact-schemas.md — Consult as needed when depositing agent-written artifacts

From ${CLAUDE_PLUGIN_ROOT}/references/ (shared): stage-expectations.md, benchmarks.md, israel-guidance.md

Artifact Pipeline

Every analysis deposits structured JSON artifacts into a working directory. The final step assembles all artifacts into a report and validates consistency. This is not optional.

StepArtifactProducer
2product_profile.jsonAgent (main, staged) → persist_agent_artifact.py
3landscape_draft.jsonAgent (main, staged) → persist_agent_artifact.py
3.5 + 3.6competitor_verification.jsonParallel Context A dispatches: COMPETITOR_VERIFICATION (precision) + COMPETITOR_RECALL (recall) → one verify_competitors.py call
4landscape.jsonContext A dispatch: LANDSCAPE_RESEARCH → validate_landscape.py
5apositioning.jsonAgent (main, staged — views, claims) → persist_agent_artifact.py
5bmoat_scores.jsonContext A dispatch: MOAT_SCORING → score_moats.py
5cpositioning_scores.jsonContext A dispatch: POSITIONING_SCORING → score_positioning.py
6checklist.jsonContext A dispatch: CHECKLIST → checklist.py
7report.jsoncompose_report.py reads all
7dreport.htmlvisualize.py
7eexplore.htmlexplore.py

Rules:

  • Deposit each artifact before proceeding to the next step
  • For agent-authored artifacts, consult references/artifact-schemas.md for the JSON schema, and write them through persist_agent_artifact.py (stage in $STAGING_DIR, pipe to $ANALYSIS_DIR) rather than by heredoc. It checks required keys and stamps _produced_by; without the stamp compose_report.py raises UNVALIDATED_ARTIFACT at high severity. What that buys is presence-of-keys and provenance, not shape — the model still authors the content
  • If a step is not applicable, deposit a stub: {"skipped": true, "reason": "..."}
  • Do NOT use isolation: "worktree" for sub-agents — files written in a worktree won't appear in the main $ANALYSIS_DIR

Keep the founder informed with brief, plain-language updates at each step. Narrate the founder-visible OUTCOME, never the internal step. That is the test to apply, and it catches more than a word list can: the forbidden thing is not a syntax, it is talking about the machinery. Bad — "Gating and piping the extraction through the producer, then staging the coaching hand-off"; good — "I've checked your numbers and I'm writing up what stood out." Bad — "schema-drift warning on coaching_payload"; good — nothing, because the founder has no stake in it. Never name an internal artifact, field, or token (a payload key, a marker name, an artifact filename, a hand-off dir) even in plain prose with no backticks — a detector keyed on syntax cannot see "gated", "hand-off" or "canonical artifacts", but the founder still reads them and they still mean nothing to them. The between-step progress lines are the primary leak vector, not the final summary. They feel internal — you are narrating what you are about to do — but the founder reads every one of them, and this is where the leaks actually appear: "Now gating the hand-off before piping through the checklist producer", "Gate 1 passes", "Running the final verification gate". Rewrite each pipeline transition as the founder-visible outcome: "Checking your numbers against the 46-point review", "Your inputs look consistent — moving on to unit economics", "Finishing up and putting the report together". If a progress line would mean nothing to someone who has never seen this skill's internals, it does not belong in the channel. Also excluded, as before: file/script names, paths, *.py, --flags, $vars, exit codes ("Exit N", "not found"), W_/E_ codes, JSON, and step/route labels ("Lane N", "Context A/B", "Phase N", "structure detection", "the grid", any ALL_CAPS_TOKEN). After each analytical step (4-6), share a one-sentence finding before moving on. Do not "fix" a leak here by adding more of this text, co-located or not — that has now been measured three times and does not work. Same probe, same detector, task turn only: this rule alone left 9 of 27 founder-visible blocks carrying a leak; six reminders placed beside each dispatch gave 6 of 32; widening those reminders to name the exact offending token (positioning.json) gave 7 of 34 — and that token still appeared four times in the run whose reminder named it. The three numbers are noise around a fifth of blocks, with no trend. The artifact side is enforced instead, by verify_positioning.py at Step 7f, which is why a leak that reaches a DELIVERABLE cannot ship; what remains is chat text, and it is bounded. If you want to change this outcome, change the mechanism, not the wording. The task tracker is founder-visible too — the same rule governs its labels. "Gate the inputs review handoff", "Validate inputs.json", "resolve agent namespace paths", "Initialize founder context" are leaks even though each names a real step, and even when the prose around them is clean. Label each task by the founder-visible outcome — "Check your inputs", "Score against the review", "Write up what I found" — never by a file, directory, script, or pipeline stage. The Coaching Commentary section appended to the report is founder-visible too — the same rule governs its text. A checklist criterion ID (NARR_03) or an internal field name (moat_count) means exactly as little to a founder there as it does in a progress line, backticked or not — see the post-compose coaching dispatch template (Step 7c) for the specific instruction.

Workflow

Step 0: Path Setup

Every Bash tool call runs in a fresh shell — variables do not persist. Run the block below exactly once: it resolves $PLUGIN_ROOT deterministically, and every later block must substitute the printed value as a literal rather than re-running the resolution — repeating the self-heal search can land on a different mount than Step 0 picked when more than one is present (see why in the block's comments).

Optional, best-effort, and via the Read tool (not a shell command): before the block below, Read ${CLAUDE_PLUGIN_ROOT}/.claude-plugin/plugin.json and note its version field as EXPECT_VERSION. Passing it to select_plugin_root.py below lets an exact version match win over an arbitrary first hit. If the Read fails, skip it and omit --expect-version — selection is still deterministic without it.

SCRIPTS="${CLAUDE_PLUGIN_ROOT}/skills/competitive-positioning/scripts"
if [ ! -d "$SCRIPTS" ]; then
  # In Cowork, CLAUDE_PLUGIN_ROOT substitutes to a host-side path absent inside
  # the session VM — self-heal by collecting EVERY candidate mount (a session can
  # have more than one at once: a stale host-side cache, a test marketplace, even
  # a symlink into a different session's tree) and handing them to
  # select_plugin_root.py, which picks ONE deterministically and names the
  # rejects — never trust `find`'s arbitrary first hit, which can silently mix
  # scripts across plugin versions mid-pipeline.
  CANDIDATES="$(find /sessions -type d -path '*/skills/competitive-positioning/scripts' 2>/dev/null)"
  [ -n "$CANDIDATES" ] || CANDIDATES="$(find / -type d -path '*/skills/competitive-positioning/scripts' 2>/dev/null)"
  PROVISIONAL_ROOT="$(printf '%s\n' "$CANDIDATES" | head -1)"
  PROVISIONAL_ROOT="${PROVISIONAL_ROOT%/skills/*}"
  # Bootstrap order: $SHARED_SCRIPTS isn't known until a root is chosen, so use the
  # provisional root's OWN copy of the selector; an older plugin copy without one
  # falls back to the provisional root unchanged.
  SELECTOR="$PROVISIONAL_ROOT/scripts/select_plugin_root.py"
  if [ -f "$SELECTOR" ]; then
    if [ -n "$EXPECT_VERSION" ]; then
      PLUGIN_ROOT="$(printf '%s\n' "$CANDIDATES" | python3 "$SELECTOR" --expect-version "$EXPECT_VERSION")"
    else
      PLUGIN_ROOT="$(printf '%s\n' "$CANDIDATES" | python3 "$SELECTOR")"
    fi
  else
    PLUGIN_ROOT="$PROVISIONAL_ROOT"
  fi
  SCRIPTS="$PLUGIN_ROOT/skills/competitive-positioning/scripts"
fi
PLUGIN_ROOT="${SCRIPTS%/skills/*}"
echo "PLUGIN_ROOT=$PLUGIN_ROOT"   # resolved ONCE, here — paste this literal into every later block; never re-run this resolution
REFS="$PLUGIN_ROOT/skills/competitive-positioning/references"
SHARED_SCRIPTS="$PLUGIN_ROOT/scripts"
SHARED_REFS="$PLUGIN_ROOT/references"
# Resolve the canonical artifacts root via a SCRIPT, not inline bash (the agent paraphrases inline
# path computations → outputs/ vs outputs/artifacts/ drift across runs). Deterministic + creates it.
python3 "$SHARED_SCRIPTS/resolve_artifacts_root.py"   # prints ARTIFACTS_ROOT — use the printed path verbatim as ARTIFACTS_ROOT in every later block (a captured var dies in the next fresh shell)

Reaching the self-heal branch is normal in Cowork — ${CLAUDE_PLUGIN_ROOT} resolves to a HOST path that does not exist inside the VM, so the [ ! -d "$SCRIPTS" ] test fails by design rather than by misconfiguration. It is not a sign anything is wrong, and it is not worth narrating to the founder — say nothing about this step at all, including the version you read and the path you resolved. A live run announced "EXPECT_VERSION = 0.6.0. Now running the Step 0 path resolution block": three internal tokens and a step label in one sentence, and the founder's first line should be about their company, not about locating files.

Outputs mount is append-only. Everything under the promoted outputs mount (.../mnt/outputs/, not just $ANALYSIS_DIR) is write-allowed and delete-denied by the platform: never rm, move away, or empty anything under it — including files you created yourself. Never create ad-hoc scratch anywhere under the outputs mount (no _src/ copies, no run-state note files); scratch belongs in $STAGING_DIR (a /tmp dir, defined below). Do not "clean up" the outputs folder before delivering — extra working files there are expected and harmless.

If ARTIFACTS_ROOT resolves to $(pwd)/artifacts but no artifacts/ directory exists at $(pwd): The workspace may not be mounted yet. Use Glob with pattern **/artifacts/founder_context.json to locate existing artifacts, and derive ARTIFACTS_ROOT from the result. If nothing is found, mkdir -p "$ARTIFACTS_ROOT" and proceed — never a relative ./artifacts, which resolves against the shell's cwd (the session root) and lands outside the outputs mount, undelivered.

After Step 1 (when the slug is known), derive ANALYSIS_DIR. Two modes — pick exactly one:

  • Full analysis (default — the founder asked for a competitive analysis, a positioning map, a moat assessment, or a report, OR there is no existing full analysis for this slug): run Steps 2–10. ANALYSIS_DIR="$ARTIFACTS_ROOT/competitive-positioning-${SLUG}".
  • Quick-check mode — a single directional question in conversation with no request for an analysis ("who else is doing this?", "is 'we have better UX' a real moat?"). Run Step 5-quick instead of Steps 2–10. ANALYSIS_DIR="$ARTIFACTS_ROOT/competitive-positioning-${SLUG}-quickcheck".

Tie-breaker when both bullets seem to fit. Decide on the verb, not the inputs: a request for the work product ("map our competitive landscape, score our moats, build the competition slide") is a full run even when every number is already in hand, while a request for a read ("who else is doing this, is that a real moat, ballpark") is a quick check even when materials are attached. Complete inputs make the full run faster, not less wanted. When the verb is genuinely absent, default to the full run and say you did — an unwanted full run costs time, an unwanted quick check costs the founder the analysis they came for.

Never answer from your own recollection of the market. Quick-check exists because the alternative a model reaches for — listing competitors from memory and offering the real analysis as an opt-in — produces an unverified competitor set under this skill's name, and a wrong competitor set is the one error this skill exists to prevent. Running fewer producers is fine; running none is not.

Step 5-quick: the quick-check path

Run only the producer the question needs, on a landscape you actually researched:

# "Who competes with us?" -> research the landscape, then validate it.
printf '%s' "$QUICK_JSON" | python3 "$SCRIPTS/validate_landscape.py" --pretty \
  --run-id "$RUN_ID" -o "$ANALYSIS_DIR/landscape.json"
# "Is X a real moat?" -> score_moats.py on the single dimension in question.

Producers deliberately NOT run: verify_competitors.py (both the adversarial competitor-set check and the blind recall diff), score_positioning.py, checklist.py, compose_report.py, visualize.py, explore.py, and the Context-B coaching dispatch. No report.md is written.

Same-numbers guarantee. Whatever is scored is scored by the same producer the full analysis uses, so the grades match — only the production weight is dropped, never the accuracy. What you do not get is what the skipped producers add, and here the omission is unusually load-bearing: the competitor set has NOT been adversarially verified, so a surface-level match that doesn't genuinely compete can survive. Say so explicitly rather than letting the list read as vetted.

Presenting it. Label it a quick check, not an analysis. Then close with a statement, never a question: "The full analysis adversarially verifies each competitor, scores six moat dimensions, and produces a positioning map — say the word and I'll run it." A question invites a "no" to something the founder would have wanted.

ANALYSIS_DIR="${ANALYSIS_DIR:-$ARTIFACTS_ROOT/competitive-positioning-${SLUG}}"              # full analysis
# ANALYSIS_DIR="${ANALYSIS_DIR:-$ARTIFACTS_ROOT/competitive-positioning-${SLUG}-quickcheck}"  # quick check
mkdir -p "$ANALYSIS_DIR"
RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)"
# Context A hand-off dir — PER RUN: sub-agents WRITE their raw output JSON here (the audit trail —
# raw sub-agent output as returned, before producer validation). Permanent by platform design
# (outputs/ mounts are write-allowed / delete-denied); nothing in it is ever a canonical artifact.
# The $RUN_ID segment is load-bearing: it prevents a stale prior-run file from silently passing
# the hand-off gate when a dispatch fails to write.
HANDOFF_DIR="$ANALYSIS_DIR/handoff/$RUN_ID"
mkdir -p "$HANDOFF_DIR"
# Sub-agents address the SAME dir by a different path (their file tools are rooted at the outputs
# mount in Cowork). Resolve the FULL agent-namespace paths via the script — never hand-splice the
# printed root with a literal skill-name/slug/run-id string yourself (that string-splicing is
# exactly the non-determinism the resolver script exists to remove):
python3 "$SHARED_SCRIPTS/resolve_artifacts_root.py" --handoff-dir-agent \
  --dir-name "competitive-positioning-${SLUG}" --run-id "$RUN_ID"   # prints HANDOFF_AGENT verbatim
HANDOFF_AGENT="<printed value>"   # use verbatim in OUTPUT_PATH lines
# Sub-agent READ paths for under-outputs artifacts use the SAME agent namespace (relative — the
# sub-agent's file-tool cwd IS the outputs mount on host-loop; an absolute /sessions/... read is denied):
python3 "$SHARED_SCRIPTS/resolve_artifacts_root.py" --analysis-dir-agent \
  --dir-name "competitive-positioning-${SLUG}"   # prints ANALYSIS_DIR_AGENT verbatim
ANALYSIS_DIR_AGENT="<printed value>"   # e.g. landscape_draft.json, positioning.json reads
# Ad-hoc scratch (NOT sub-agent hand-off) lives OUTSIDE the promoted outputs/ tree, in a temp dir
# that is safe to both create and reclaim. Use the printed path verbatim in later steps.
STAGING_DIR="$(mktemp -d "${TMPDIR:-/tmp}/competitive-positioning-${SLUG:-co}.staging.XXXXXX")"

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
34
Forks
3
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
competitive-positioning-lool-ventures
Source
github.com/lool-ventures/founder-skills