Skip to content
Skillv1.0.0

competitive-positioning

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 asse

by yaniv-golan(0) 0 installs
Free
Sign in to install

Free account. Installing gives you the manifest plus copy-paste snippets.

See reviews

About

Imported from yaniv-golan/founderskills-test (founder-skills/skills/competitive-positioning/SKILL.md). Install upstream with npx skills add yaniv-golan/founderskills-test --skill competitive-positioning. Copyright stays with the author.

Competitive Positioning Skill

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.

Step Artifact Producer
2 product_profile.json Agent (main)
3 landscape_draft.json Agent (main)
3.5 + 3.6 competitor_verification.json Parallel Context A dispatches: COMPETITOR_VERIFICATION (precision) + COMPETITOR_RECALL (recall) → one verify_competitors.py call
4 landscape.json Context A dispatch: LANDSCAPE_RESEARCH → validate_landscape.py
5a positioning.json Agent (main — views, moats, stress-tests)
5b moat_scores.json Context A dispatch: MOAT_SCORING → score_moats.py
5c positioning_scores.json Context A dispatch: POSITIONING_SCORING → score_positioning.py
6 checklist.json Context A dispatch: CHECKLIST → checklist.py
7 report.json compose_report.py reads all
7d report.html visualize.py
7e explore.html explore.py

Rules:

  • Deposit each artifact before proceeding to the next step
  • For agent-written artifacts, consult references/artifact-schemas.md for the JSON schema
  • 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 ./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 and proceed.

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")"

Pass RUN_ID to all sub-agents. Every artifact must include "metadata": {"run_id": "$RUN_ID"}. compose_report.py checks run_id consistency — a mismatch triggers STALE_ARTIFACT. Its sibling integrity checks emit CORRUPT_ARTIFACT (artifact file is not valid JSON) and UNVALIDATED_ARTIFACT (artifact exists but was written directly instead of through its producer script — the _produced_by stamp is missing or wrong). All three are high-severity: fix the artifact by re-running the producer; never hand-edit it to silence the warning.

Overwrite-in-place — do NOT delete prior artifacts under $ANALYSIS_DIR. It is the promoted outputs/ tree in Cowork, where deleting a user-visible path is unsafe (Cowork can deny it; the parity gate flags it). Each producer writes its artifact fresh via -o every run, and RUN_ID is minted fresh per run — so if a prior run left an artifact a later step doesn't regenerate, compose_report.py's STALE_ARTIFACT check (run_ids must match) catches the mismatch. No bulk rm is needed or wanted.

Step 1: Read or Create Founder Context

python3 "$SHARED_SCRIPTS/founder_context.py" read --artifacts-root "$ARTIFACTS_ROOT" --pretty

Exit 0 (found): Use the company slug and pre-filled fields. Proceed to Step 2.

Exit 1 (not found): Expected on a first run — do NOT mention this check or its exit status to the founder; if you narrate anything first, say only "Let me grab a few basics about the company." Deck/materials carve-out — derive field-by-field, never all-or-nothing (do not ask for what you were already given): if the founder provided materials (a deck, or a sufficiently detailed description), derive each of the four basics — company name, stage, sector, geography — that the materials state, and skip the gate entirely when all four are in hand. Treat the four independently: deriving three and missing one does NOT send you back to asking for all four. Before gating on a still-missing field, try to infer it from a clear signal in the materials and proceed (noting it as inferred, not founder-stated, so it isn't presented as confirmed): geography from a phone country code, office address, or currency (e.g. a +972 number → Israel); stage from an ambiguous fundraise signal (a named round, round size, or "raising our seed" language → the matching --stage value); sector from the product category and ICP. Use AskUserQuestion (NOT plain chat) only for the specific field(s) that genuinely have no derivable or inferable signal — and ask for only those, stating the values you already derived so the founder confirms or corrects rather than re-supplying everything. If AskUserQuestion is genuinely unavailable in the host, do NOT skip the ask and do NOT assume the answer: ask the same question in plain chat, state the options explicitly, and wait for an answer before continuing. The ban above is on asking casually WHILE the tool is available — it is not a reason to stall a host that lacks it. (If none of the four can be derived at all, that reduces to asking for all four.)

Stage is the one field with a real fixed label set — use it verbatim if asking. Options: Pre-seed / Seed / Series A / Series B+pre-seed | seed | series-a | series-b (founder_context.py's VALID_STAGES has 7 values including series-c/series-d/later; on a Series B+ pick, ask a plain-text follow-up for the specific stage rather than defaulting to series-b). Company name, sector and geography cannot take fixed labels — shape each as an affirmative option carrying the derived value plus a stated-value fallback. Provide at least 2 options. Note in the report metadata that no cross-skill validation was performed. Then create:

--stage is enum-validated (hyphenated, lowercase) — one of: pre-seed, seed, series-a, series-b, series-c, series-d, later. Passing a non-canonical token (e.g. seriesa, pre_seed) is an argparse error and forces a retry — map the founder's answer to one of these 7 values before calling init.

--sector-type is an optional override (also enum-validated, hyphenated): one of saas, ai-native, marketplace, hardware, hardware-subscription, consumer-subscription, usage-based, transactional-fintech, retail. When omitted, founder_context.py auto-derives it from --sector via a small alias table (e.g. "B2B SaaS" -> saas); if the sector doesn't match a known alias, the script emits a runtime warning asking you to set --sector-type explicitly — pick the closest value from the enum above rather than waiting for that warning.

python3 "$SHARED_SCRIPTS/founder_context.py" init \
  --company-name "Acme Corp" --stage seed --sector "B2B SaaS" \
  --geography "US" --artifacts-root "$ARTIFACTS_ROOT"
  # Add --sector-type <value> if the auto-derivation warning fires or the sector
  # doesn't map cleanly to one of the 9 canonical sector-type values above.

Exit 2 (multiple): Present the list, ask which company, re-read with --slug.

Execution checkpoint — END OF STEP 1, READ BEFORE CONTINUING

You now have enough to run. Invoking this skill is not the same as running it. From here, every number that reaches the founder must come out of a producer script. Concretely:

  • Never compute a figure in chat. Not TAM, not runway, not a ratio, not a benchmark comparison — not even one you are confident about. An in-chat number has no provenance, no range, no artifact, and nothing downstream can contradict it. That is worse than a slow answer and worse than no answer.
  • Never benchmark against a figure you recalled. Benchmarks live in the reference files and the producers read them. If you find yourself writing "typically around X for this stage", stop: either a producer sourced it or it does not go in front of the founder.
  • A what-if, a sensitivity illustration, or "roughly what would X give" is NOT an exemption. This is the exemption a live run invented: having correctly produced the real figure, it then wrote "using the current count would shave TAM to roughly €249M rather than €270M" — a second number, computed in chat, from an input the founder never gave. An illustrative figure is read exactly as confidently as a computed one, and the founder cannot tell which came from the pipeline. Two ways to answer a what-if: re-run the producer with the alternate input and quote its output, or give no number and say which direction it moves. Never arithmetic in prose.
  • Never offer the real run as an opt-in after answering. "Here's a rough estimate — I can run the full analysis if you want" is the failure. The founder cannot tell that what they just read was not the analysis, so they will not ask for it.
  • Two ways to finish, and only two: run the full pipeline to completion, or run the quick-check path (Step 5-quick), which still runs a real producer. Both end with real artifacts on disk. Anything else is not a finished run.
  • If you are blocked, say BLOCKED and say why. A missing input, a failed hand-off, an unreadable document — name it and stop. Do not substitute your own reasoning for the pipeline and present the result as its output.

Artifact existence is the proof of execution: if no canonical artifact was written, the skill did not run, whatever the transcript says.

Step 2: Build Product Profile -> product_profile.json

Extract from the founder's materials or conversation: company name, product description, target customers, value propositions, differentiation claims, stage, sector, business model, and input_mode ("deck", "conversation", or "document").

For deck mode: Read ALL pages of the deck systematically — not just the competition slide. Problem, solution, traction, and team slides contain competitive claims and differentiation context that inform the analysis. If the deck has a competition slide, record it in product_profile.json under deck_competition_slide{axes: {x, y}, plotted: [{name, category}], claimed_position, source_slide} — capturing the axis pair, which companies the slide plots and how it categorizes them, where it places the startup, and which slide number it came from; this generalises the earlier deck_axes field and is what makes the later competition-slide cross-check and the report's basis-vs-deck delta note possible. If the deck has NO competition slide at all, write the documented absent form instead of leaving the field out or inventing an unschema'd note field: deck_competition_slide: {present: false, reason: "..."}, stating plainly why (e.g. "12-page deck; no slide named or shaped as competition"). This is what lets the CHECKLIST dispatch grade the competition-slide cross-check as a warn with a real reason, instead of having nothing at all to check against — never not_applicable, which would drop it out of the score denominator and hide the finding. Decks over ~10 pages: the Read tool requires an explicit page range for PDFs beyond that length (max 20 pages per call) — read in page-range chunks (e.g. pages: "1-10", then "11-20") rather than one call for the whole file.

Check the deck's vintage. If a footer date, copyright year, event slide, or embedded metadata shows the materials are noticeably older than today (a rule of thumb: more than ~12 months), flag this to the founder before proceeding — competitor pricing, funding, and positioning claims from a stale deck may already be outdated. Note the observed vintage in product_profile.json's source_materials (e.g. "pitch deck (PDF, copyright 2024)").

Write product_profile.json to $ANALYSIS_DIR. Consult references/artifact-schemas.md for the schema. Set INPUT_MODE to the chosen mode (deck, conversation, or document) — Step 6's checklist pipe passes it to checklist.py --input-mode so mode gating is applied correctly:

INPUT_MODE="deck"   # or "conversation" / "document"

If materials are sparse, use AskUserQuestion to gather missing fields. At minimum: product description, target customers, and what the founder believes differentiates them. All three are necessarily runtime-labelled — open-ended founder-specific answers, not a set of labels a fixed list could offer — so each question needs an affirmative option carrying any partial signal already derived, plus a free-text fallback (same shape as the founder-context basics above), not a literal bracket list.

Step 3: Identify Competitors -> landscape_draft.json

REQUIRED — read ${CLAUDE_PLUGIN_ROOT}/skills/competitive-positioning/references/competitive-analysis-methodology.md now.

Identify 5-7 competitors across categories: 2-3 direct, 1-2 adjacent, 1 do-nothing, 0-1 emerging. For each competitor, record: name, slug, category, description, key differentiators, and why included.

When one entry represents several companies as a cohort (e.g. "PCM/next-gen entrants (Rondo, Antora, Sunamp)"), record the member company names in an optional constituents: ["Rondo", "Antora", "Sunamp"] array on that entry. This turns the blind-recall duplicate check (Step 3.6) from a text heuristic into an exact lookup — without it, a recall candidate that IS one of the cohort's named members can misread as a genuine gap.

Select 2-3 candidate positioning axis pairs with rationale for each. Follow the axis selection principles from the methodology reference — axes must differentiate, matter to the buyer, and be measurable.

If the founder's deck mentions competitors you are excluding from the formal landscape (e.g., too small, different market segment, or redundant with an included competitor), note them with reasons in landscape_draft.json under a deck_competitors_excluded field. These will be referenced in the report to maintain deck alignment and prevent the NARR_03 checklist item from failing without explanation.

Write landscape_draft.json to $ANALYSIS_DIR.

Step 3.5: Adversarial Competitor Verification -> competitor_verification.json (Context A: COMPETITOR_VERIFICATION dispatch)

Before asking the founder to validate the set, independently challenge its precision — catch companies that landed in the draft on surface-level similarity ("both do scheduling") but don't actually compete. This runs as a fresh, independent Context A dispatch so the challenge is not self-review: the verification agent re-characterizes each competitor from its own WebSearch (deliberately NOT trusting the draft's description) and judges genuine overlap against the startup on a substitution test.

Dispatch the competitive-positioning sub-agent in Context A (COMPETITOR_VERIFICATION). Call the Task tool with subagent_type: "founder-skills:competitive-positioning".

Dispatch prompt template:

CONTEXT: COMPETITOR_VERIFICATION
OUTPUT_PATH: <HANDOFF_AGENT>/competitor_verification_output.json
RUN_ID: <RUN_ID>

You are the competitive-positioning agent dispatched in Context A (COMPETITOR_VERIFICATION).
Read landscape_draft.json at <ANALYSIS_DIR_AGENT>/landscape_draft.json and
product_profile.json at <ANALYSIS_DIR_AGENT>/product_profile.json.

Follow your agent body's COMPETITOR_VERIFICATION subtype procedure: characterize
the startup once; then for EACH competitor in landscape_draft.json, use WebSearch
to independently establish its real buyer, job-to-be-done, category, and
monetization — do NOT trust the draft's description field. Apply the substitution
test (would the same buyer put both in the same consideration set for the same
job?). Shared category words are NOT sufficient. Assign verdict
genuine/adjacent/not_a_competitor. Every non-genuine verdict MUST carry non-empty
reasoning and a populated independent_characterization (buyer + job_to_be_done).

Use your Write tool to write to OUTPUT_PATH the JSON matching verify_competitors.py:
{
  "startup_characterization": {"buyer": "...", "job_to_be_done": "...", "category": "...", "monetization": "...", "evidence_source": "founder_provided"},
  "verdicts": [ {"slug": "...", "verdict": "...", "independent_characterization": {...}, "overlap": {...}, "reasoning": "...", "confidence": "...", "recommended_action": "..."} ],
  "metadata": {"run_id": "<RUN_ID>"}
}
One verdict per competitor slug in landscape_draft.json; no extras.
Then return ONLY the receipt JSON in your final assistant message:
{"status": "complete", "output_path": "<echo of OUTPUT_PATH>"}
Do NOT write any file other than OUTPUT_PATH.

After the sub-agent returns: gate the hand-off per the Context A hand-off protocol (defined below — check_handoff.py, branch on exit codes). Do not pipe yet — Step 3.6 dispatches in parallel with this one, and both hand-offs go through a single verify_competitors.py call there, producing one artifact for Gate 1 to read. --landscape will point at landscape_draft.json (the set as drafted; enrichment has not run yet).

If the producer exits 1 on a show-your-work violation (a flag with no reasoning or no independent buyer/job), re-dispatch per the retry budget with one added line: "your flagged verdict for <slug> had no reasoning / no independent buyer+job — re-characterize it from your own research." Never hand-author a verdict.

Step 3.6: Blind Recall Check -> recall_gaps (Context A: COMPETITOR_RECALL dispatch)

Step 3.5 challenges the competitors that ARE on the list. This is its mirror: it asks who is missing. Both run against the same draft, so dispatch them in parallel — two Task calls in one message, exactly as Step 5 does for MOAT_SCORING + POSITIONING_SCORING. consumed after both return.

Why a separate dispatch rather than one more instruction to an existing one. Step 4's Phase B also looks for missing competitors, but it runs inside the dispatch that just spent its whole context enriching the draft, and it fires after Gate 1 — so it is anchored by construction and arrives after the founder has already validated the set. This dispatch is unanchored and lands before the decision.

The blind is enforced by what the agent is given, not by asking it not to look. Stage a redacted product summary and pass that path — never $ANALYSIS_DIR:

python3 - "$ANALYSIS_DIR/product_profile.json" "$HANDOFF_DIR/recall_input.json" <<'PY'
import json, sys
src, dst = sys.argv[1], sys.argv[2]
p = json.load(open(src, encoding="utf-8"))
# Drop every field that carries a competitor set or the founder's framing of one.
# deck_competition_slide.plotted[] is literally a competitor list with categories.
for k in ("deck_competition_slide", "deck_axes", "differentiation_claims", "competitors"):
    p.pop(k, None)
json.dump(p, open(dst, "w", encoding="utf-8"), indent=2)
PY

Redacting differentiation_claims is deliberate: "unlike the big platforms, we…" names a competitor class even when it names no company, and the point is an agent that reaches the market on its own.

Dispatch prompt template:

CONTEXT: COMPETITOR_RECALL
OUTPUT_PATH: <HANDOFF_AGENT>/competitor_recall_output.json
RUN_ID: <RUN_ID>

You are the competitive-positioning agent dispatched in Context A (COMPETITOR_RECALL).
Read ONLY <HANDOFF_AGENT>/recall_input.json. It is the only file you may read.

Follow your agent body's COMPETITOR_RECALL subtype procedure: establish the buyer
and job-to-be-done from that summary alone, then use WebSearch to find who that
buyer would realistically put in a consideration set for that job — direct
substitutes, adjacent tools, the incumbent, and the do-nothing/manual alternative.
Apply the substitution test.

Return 5-10 candidates, each with name, slug (kebab-case), category,
why_considered, and at least one source URL you actually retrieved. Return fewer
rather than padding — unsourced candidates are dropped downstream anyway.

Use your Write tool to write to OUTPUT_PATH:
{
  "candidates": [
    {"name": "...", "slug": "...", "category": "direct|adjacent|do_nothing|emerging",
     "why_considered": "why THIS buyer would weigh this for THIS job",
     "sources": ["https://..."]}
  ],
  "metadata": {"run_id": "<RUN_ID>"}
}
Then return ONLY the receipt JSON in your final assistant message:
{"status": "complete", "output_path": "<echo of OUTPUT_PATH>"}
Do NOT write any file other than OUTPUT_PATH.

After both sub-agents return: gate each hand-off per the Context A hand-off protocol, then pipe BOTH through the one producer — the diff belongs with the verdicts, in one artifact, so Gate 1 reads a single file:

cat "$HANDOFF_DIR/competitor_verification_output.json" | \
  python3 "$SCRIPTS/verify_competitors.py" --pretty --run-id "$RUN_ID" \
    --landscape "$ANALYSIS_DIR/landscape_draft.json" \
    --blind-set "$HANDOFF_DIR/competitor_recall_output.json" \
    -o "$ANALYSIS_DIR/competitor_verification.json"

If the recall dispatch fails or returns nothing usable, continue without it — pipe without --blind-set and proceed to Gate 1. A missing recall check degrades the analysis; it must never block a run that is otherwise complete.

Gate 1: Founder Validation of Competitor Set

MANDATORY STOP — TWO SEPARATE STEPS. DO NOT COMBINE THEM.

Step A: Output a chat message with the competitor list and candidate axes. Use a markdown table or formatted list. This is a normal assistant message — NOT an AskUserQuestion call.

Include the Step 3.5 challenges. Read competitor_verification.json. If summary.flagged > 0, add a "Companies I'd challenge" block under the list — one line per slug in summary.challenge_slugs, drawn from that verdict's reasoning: • <name> — I don't think this genuinely competes: <reasoning>. Keep it, drop it, or call it adjacent?. Read challenge_slugs; never re-derive it from flagged_slugs. flagged_slugs means only "not genuine", a different question — a draft-adjacent entry confirmed adjacent is endorsed, not challenged — and only the producer knows the exclusions. If nothing remains to challenge, the line depends on WHY; these are not interchangeable. flagged is 0: "All look like genuine competitors — none flagged." flagged > 0 but challenge_slugs empty: "<summary.flagged> came up for a second look during verification, but each held up under independent research — nothing to challenge." The "none flagged" line is false in the second case.

Include re-categorizations, in both directions. Read summary.category_disagreements. Each entry pairs a competitor's drafted category against what independent research found, tagged upgrade (research says it's a stronger, more genuine competitor than drafted) or downgrade (research says it's weaker or less relevant than drafted). If any exist, add a "Companies I'd re-categorise" block under the challenges — one line per entry: for an upgrade, • <name> — I drafted this as <drafted category>, but research says it's a more direct competitor than that.; for a downgrade, the mirror: • <name> — I drafted this as <drafted category>, but research says the overlap is weaker than that. An upgrade cuts against the startup — a competitor turning out stronger than drafted — so it must never be presented more quietly than a downgrade; give both the same visibility and phrasing weight.

Include the Step 3.6 recall gaps. Read recall_gaps from the same file. If unmatched is non-empty, add a "Companies you may be missing" block — one line per entry: • <name> — <why_considered> (<first source>), and when the entry carries possible_overlap_with, append (may already be covered by <that competitor's name>). Gate 1 is where the founder decides whether to add a candidate, and an undifferentiated list hides which entries likely duplicate competitors they already have. Never drop an annotated entry — it is a hint, not a verdict. Frame these as candidates found by an independent search that never saw your list, not as omissions the founder got wrong.

Two rules on this block. Never present draft_only as a challenge — the blind agent failing to surface a competitor is weak evidence of nothing, and Step 3.5's verdicts are the instrument for that question. And respect the cap: the set may hold at most 10 competitors (validate_landscape.py's MAX_COMPETITORS). Count the current draft; if adding every candidate would exceed 10, say so plainly in this block — I found <N> more, but the set is full at 10 — which matter most? — rather than offering additions that cannot be applied.

Step B: AFTER the chat message, call AskUserQuestion with a short question that names what's being confirmed so the founder isn't confirming blind. The question is plain text — still ONE SENTENCE, NO markdown/tables/bullets — but it MUST carry the key facts: the competitor count, any names you'd challenge, and any upgrades from the re-categorization check (downgrades stay in the Step-A message only — they don't change the risk picture the way an upgrade does).

Question (substitute <N>, the flagged names, and any upgraded names; drop each parenthetical that has nothing to report): Found <N> competitors (I'd challenge: <names>) (stronger than drafted: <upgraded names>) — does this set look right? Options: No changes — looks good as drafted / Missing competitors / Remove some / Change axes

The no-change option carries the reserved prefix No changes — , and exactly one option may. Whichever slot it lands in, that is the branch a founder picks to leave things as they are, and it must be identifiable without counting positions. Any option that adds, removes, or re-categorises a competitor, changes an axis, or changes the scoring basis is FORBIDDEN from using the prefix. The tail after the dash is yours — name the actual candidates, that is what makes these gates good. Measured across live runs, slot 1 was the accept branch on some runs and an adds-two-competitors branch on others while every option still opened "Looks good": position is not a safe handle and neither is a shared prefix that mutating options also carry. This one is safe because it is reserved.

CRITICAL: the question must be self-contained on the decision (count + flagged names + upgrades), as ONE plain-text sentence. The full table/rationale stays in the Step-A chat message — do NOT put a table or markdown in the question.

If founder requests changes, apply corrections and repeat Steps A+B.

Apply all corrections to landscape_draft.json before proceeding. This is also how an approved recall candidate enters the set — add it to landscape_draft.json as a draft entry (name, slug, category, description, key_differentiators, plus why_included citing the recall check), and Step 4 then enriches it like any other draft entry. Do not route it through Step 4's suggested_additions promotion path: that path operates on the Step 4 output's additions and does not exist yet at this point in the run. Never exceed MAX_COMPETITORS (10) — if the founder approves more than the remaining slots, ask which to keep rather than silently truncating.

A recall candidate the founder does NOT approve is not simply dropped. Write it into landscape_draft.json's top-level deferred_recall_candidates[] array — {name, slug, category, why_considered, sources}, copied from how the recall dispatch returned it — rather than discarding it. Step 4's additions gate below draws candidates from this array too, so a declined recall candidate stays reachable if the analysis later needs it, instead of becoming permanently unaddable the moment Step 4's own suggested_additions fill the remaining slots.

Context A hand-off protocol (file transport + gate)

Every Context A dispatch prompt carries an OUTPUT_PATH: line built from $HANDOFF_AGENT. The sub-agent WRITES its output JSON to that path with its Write tool and returns only a small receipt: {"status": "complete", "output_path": "<echo of OUTPUT_PATH>"}. The payload leaves the model exactly once (into the Write call) — never re-type sub-agent JSON into a heredoc.

$HANDOFF_AGENT and $HANDOFF_DIR name the SAME directory by two different paths — they are not interchangeable. $HANDOFF_DIR is the absolute VM path your shell uses (python3, check_handoff.py, producer pipes). $HANDOFF_AGENT is the relative path a sub-agent's file tools resolve against the outputs mount, and it is the ONLY one that goes in a dispatch prompt. Putting $HANDOFF_DIR in an OUTPUT_PATH line hands the sub-agent an absolute /sessions/... path the host-loop gate denies; putting $HANDOFF_AGENT in a shell command resolves it against the wrong cwd. Rule of thumb: agent namespace in prompts, shell namespace in bash.

The receipt is the ONE exemption from the never-re-type rule. "Never re-type" governs the payload — the extraction JSON, the coaching commentary, anything the founder's numbers pass through. The receipt is a two-field acknowledgement the sub-agent returns in its final message, and reading output_path out of it to pass to check_handoff.py --agent-path is expected, not a violation. If it were forbidden, the hand-off could not be gated at all.

Path idiom for dispatch prompts (host-loop path gate): OUTPUT_PATH and any under-outputs artifact READ path a sub-agent is given are relative to the sub-agent's file-tool cwd (the outputs mount) — built from the resolve_artifacts_root.py --agent namespace ($HANDOFF_AGENT / $ANALYSIS_DIR_AGENT). Never hand a sub-agent an absolute /sessions/... path for a file-tool Read/Write — the host-loop path gate denies it (steering shell work to the bash tool instead). Bundled references/*.md are the one exception: pass them as the literal ${CLAUDE_PLUGIN_ROOT}/skills/competitive-positioning/references/... token (it is pre-resolved to a host-readable path); do NOT substitute a find /sessions-discovered $REFS (a shell path a file tool can't read).

After EVERY Context A dispatch, gate before piping (<step> = the dispatch's file stem):

printf '%s' '<agent final message verbatim>' | \
  python3 "$SHARED_SCRIPTS/check_handoff.py" "$HANDOFF_DIR/<step>_output.json" \
    --agent-path "$HANDOFF_AGENT/<step>_output.json" --receipt-json -

Branch on the exit code (complete state machine — do not improvise):

  • Exit 0 → pipe the file through the producer: cat "$HANDOFF_DIR/<step>_output.json" | python3 "$SCRIPTS/<producer>.py" ...
  • Exit 3 (missing/empty file — receipt may be fabricated) → redo-dispatch: fresh Task, same prompt plus one line: "your receipt claimed a file at <path> but none exists; use Write to create exactly that path."
  • Exit 4 (file exists, invalid JSON) → repair-dispatch: fresh Task: "Read <OUTPUT_PATH>; it fails JSON parsing with <verbatim detail from the diagnostic>; fix and rewrite it; return the receipt."
  • Exit 5 (receipt echoes a different path) → repair-dispatch telling the agent the exact expected OUTPUT_PATH (it wrote somewhere else).
  • Exit 6 (receipt unparseable / no output_path key) → redo-dispatch with "return ONLY the receipt JSON — no fences, no prose."
  • Producer schema rejection (the pipe fails next) → repair-dispatch with the producer's stderr verbatim.
  • Any other exit (script crash etc.) → STOP with the stderr.
  • After ANY corrective dispatch, resume from check_handoff.py — never pipe to the producer unchecked.

Retry budget: max 2 corrective dispatches per step, of any kind, in any combination (max 3 total dispatches). After the second corrective dispatch fails any gate: STOP and report the exact diagnostic to the founder. The main thread MUST NOT author or patch analytical content itself — filling in the JSON is the fabrication failure mode this architecture exists to prevent. A status: "blocked" return is not a gate retry, but it is bounded: at most ONE input-fix re-dispatch per step; a second blocked return STOPs with both reasons quoted.

Mechanical fix vs. content authoring (the carve-out to the rule above): "must not author or patch analytical content" means the main thread never invents a finding, evidence string, score, or verdict the sub-agent didn't produce. It does NOT forbid mechanical operations that move or rename data the sub-agent already produced, unchanged in substance:

  • Renaming a near-miss field to its canonical name (e.g. auto-normalizing a schema-adjacent key) when the producer script does it deterministically — you are not authoring content, the script is normalizing a name.
  • Merging the sub-agent's own gated hand-off content into a canonical artifact per an explicit SKILL.md instruction (e.g. the Step 5 points merge below, or merging approved suggested_additions in Step 4) — you are relocating data the sub-agent wrote, not writing new data yourself.

Promoting an approved suggested_addition into competitors[] — enrichment is the promotion path. Moving the entry is a relocation (permitted above), but the target shape has a field the suggestion does not: key_differentiators. A suggestion that was never enriched has none, and writing one is content authoring — so promotion cannot happen by stub-filling the field. Re-dispatch LANDSCAPE_RESEARCH, scoped to the approved addition(s) by name, and let the sub-agent enrich them the same way it enriched the original draft: real key_differentiators, research_depth, sourced_fields_count, and evidence_source per field, each sourced. Then promote the enriched result — copy name / slug / category / partial_profile across along with the newly-researched fields. (The producer accepts key_differentiators: [] only when research_depth is "partial"; it rejects an empty list when research_depth is "full", so a thin re-dispatch still needs at least a partial research pass before it can be promoted.)

If you find yourself typing a NEW evidence string, score, or verdict that didn't come from a gated sub-agent artifact, that is content authoring and is forbidden — repair-dispatch instead.

Graceful degrade (fleet heterogeneity): if the FIRST corrective dispatch also exits 3 while the agent's receipt claims complete with the correctly echoed path, treat the host's filesystem topology as hand-off-incompatible: fall back to message-channel transport for the REST of this run (sub-agent returns full JSON in its final message; stage to $STAGING_DIR/<step>_input.json; same producer pipe), and note the fallback in your final summary.

Retries overwrite the same OUTPUT_PATH (the mount is write-allowed / delete-denied — never rm under $ANALYSIS_DIR). Hand-off files are not canonical artifacts: producers ignore them except via the explicit pipe, and compose_report.py never reads handoff/.

Ad-hoc scratch (NOT sub-agent hand-off) still goes to $STAGING_DIR in /tmp — see the reference (founder-skills/references/skill-execution-model.md, "STAGING_DIR pattern for ad-hoc/scratch files"). Hard rule: never stage scratch anywhere under the outputs mount (which includes $ANALYSIS_DIR), and never delete anything under it — see the append-only rule in Step 0.

Step 4: Research & Enrich Competitors -> landscape.json (Context A: LANDSCAPE_RESEARCH dispatch)

Dispatch the competitive-positioning sub-agent in Context A (LANDSCAPE_RESEARCH). The sub-agent declares WebSearch in its tool allowlist and performs the research itself. Call the Task tool with subagent_type: "founder-skills:competitive-positioning" so the research runs in an isolated context.

Dispatch prompt template:

CONTEXT: LANDSCAPE_RESEARCH
OUTPUT_PATH: <HANDOFF_AGENT>/landscape_research_output.json
RUN_ID: <RUN_ID>

You are the competitive-positioning agent dispatched in Context A (LANDSCAPE_RESEARCH).
Read landscape_draft.json at <ANALYSIS_DIR_AGENT>/landscape_draft.json and
product_profile.json at <ANALYSIS_DIR_AGENT>/product_profile.json.

You do NOT need to carry landscape_draft.json's deferred_recall_candidates array
through — the producer reads it directly. Ignore that field.

Phase A — Enrich existing competitors: For each competitor in landscape_draft.json,
use WebSearch to find pricing model, funding history, team size, target customers,
strengths, weaknesses. Issue separate searches per competitor as needed. Record
evidence_source per field: "researched" only when the value came from a WebSearch
result; "agent_estimate" when you fell back to training-cutoff knowledge.
Set research_depth per competitor — MUST be one of: full, partial, or
founder_provided. Set sourced_fields_count per competitor = the number of that
competitor's fields you stamped evidence_source:"researched" (an integer);
validate_landscape.py requires this field. Separately, compose_report.py's
SHALLOW_COMPETITOR_PROFILE warning fires for a "partial" competitor with fewer
than 3 sourced fields. For every field stamped
evidence_source:"researched", also add a matching entry in a "sources" object
(same field-name keys) citing the URL or the exact search query that produced it
— the main thread never sees your WebSearch results, only this artifact, so an
unsourced "researched" claim can't be spot-checked later. validate_landscape.py
warns (does not fail) on a "researched" field with no matching "sources" entry.

Also capture recent_developments[] per competitor where you find them: discrete DATED
moves (funding, pricing_change, product_launch, market_move, acquisition, leadership,
layoff) each with date (YYYY-MM or YYYY-MM-DD), summary, a source URL, and optional
relevance. A URL is required — a search query is not a valid source for a dated claim
about a named company — and evidence_source "agent_estimate" is rejected for this field.
An EMPTY ARRAY IS CORRECT for a competitor that has not visibly moved; do not stretch to
fill it. A present-tense fact is enrichment, not a development — only a dated change.
**recent_developments[] has an 18-month recency window** (validate_landscape.py rejects
anything dated more than 18 months before the as-of date, though it now retains a
rejected entry separately rather than failing the run). If a re

*Truncated - read the full file at https://github.com/yaniv-golan/founderskills-test/blob/493cdfa3110fcd7f0b52d42529551bf92d13fd03/founder-skills/skills/competitive-positioning/SKILL.md.*

Use it

Copy one of these into your project. Installing also returns the manifest and these snippets.

yaml
targets:
  - https://api.opensmartroute.ai/api/v1/registry/yaniv-golan-founderskills-test-competitive-positioning/manifest   # or paste the manifest below

Manifest

An Open Capability Manifest: the router reads it to know what this does, what it costs and when to pick it.

yaniv-golan-founderskills-test-competitive-positioning.ocm.jsonjson
{
  "ocm": "1",
  "id": "yaniv-golan-founderskills-test-competitive-positioning",
  "kind": "skill",
  "name": "competitive-positioning",
  "description": "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.",
  "publisher": "yaniv-golan",
  "version": "1.0.0",
  "capabilities": {
    "domains": [
      "math"
    ],
    "tags": [
      "skill-md",
      "github"
    ],
    "languages": [
      "en"
    ]
  },
  "quality_prior": 0.6,
  "examples": [
    "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."
  ],
  "primary": false,
  "metadata": {
    "source": {
      "provider": "github",
      "repository": "https://github.com/yaniv-golan/founderskills-test",
      "path": "founder-skills/skills/competitive-positioning/SKILL.md",
      "ref": "493cdfa3110fcd7f0b52d42529551bf92d13fd03",
      "url": "https://github.com/yaniv-golan/founderskills-test/blob/493cdfa3110fcd7f0b52d42529551bf92d13fd03/founder-skills/skills/competitive-positioning/SKILL.md",
      "key": "yaniv-golan/founderskills-test/founder-skills/skills/competitive-positioning/SKILL.md"
    }
  },
  "instructions": "# Competitive Positioning Skill\n\nHelp 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.\n\n## Skill Metadata\n\n- **Author:** lool-ventures\n- **Version:** managed in `founder-skills/.claude-plugin/plugin.json`\n- **Compatibility:** Python 3.10+ and `uv` for script execution.\n- **",
  "cost": {
    "context_tokens": 29576
  }
}

Fetch it by URL: GET /api/v1/registry/yaniv-golan-founderskills-test-competitive-positioning/manifest?version=1.0.0

Reviews

Star ratings from people who tried it. One review per account; edit yours any time.

No reviews yet. Install it, try it, and be the first to rate it.