| 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. |
| when_to_use | Use ONLY when the user has asked for competitive landscape mapping, moat analysis, or positioning evaluation AND has provided enough context (a deck, a list of competitors, or a clearly named startup). Do not auto-invoke on general questions about competition or strategy.
|
| user-invocable | true |
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.json → deck-review, fundraise-readiness
report.json → ic-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 coaching_payload inlined in the dispatch prompt (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:
- If the message is wrapped in a
```json ... ``` (or plain ``` ... ```) fence, strip the fence first.
- Try to parse the stripped text directly as JSON.
- 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 }).
- 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
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)
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 | competitor_verification.json | Context A dispatch: COMPETITOR_VERIFICATION → verify_competitors.py |
| 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. Never mention file names, scripts, or JSON. After each analytical step (4-6), share a one-sentence finding before moving on.
Workflow
Step 0: Path Setup
Every Bash tool call runs in a fresh shell — variables do not persist. Prefix every Bash call that uses these paths with the variable block below, or substitute absolute paths directly:
SCRIPTS="${CLAUDE_PLUGIN_ROOT}/skills/competitive-positioning/scripts"
if [ ! -d "$SCRIPTS" ]; then
SCRIPTS="$(find /sessions -type d -path '*/skills/competitive-positioning/scripts' 2>/dev/null | head -1)"
fi
if [ -z "$SCRIPTS" ] || [ ! -d "$SCRIPTS" ]; then
SCRIPTS="$(find / -type d -path '*/skills/competitive-positioning/scripts' 2>/dev/null | head -1)"
fi
PLUGIN_ROOT="${SCRIPTS%/skills/*}"
REFS="$PLUGIN_ROOT/skills/competitive-positioning/references"
SHARED_SCRIPTS="$PLUGIN_ROOT/scripts"
SHARED_REFS="$PLUGIN_ROOT/references"
python3 "$SHARED_SCRIPTS/resolve_artifacts_root.py"
The path setup handles both Claude Code (local filesystem) and Cowork (mounted sessions). The Step 0 block self-heals when ${CLAUDE_PLUGIN_ROOT} doesn't resolve (Cowork). If it still comes up empty, locate the anchor manually: find / -path '*/skills/competitive-positioning/scripts/validate_landscape.py' 2>/dev/null | head -5 and derive the variables from it.
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):
ANALYSIS_DIR="$ARTIFACTS_ROOT/competitive-positioning-${SLUG}"
mkdir -p "$ANALYSIS_DIR"
RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)"
HANDOFF_DIR="$ANALYSIS_DIR/handoff/$RUN_ID"
mkdir -p "$HANDOFF_DIR"
python3 "$SHARED_SCRIPTS/resolve_artifacts_root.py" --agent
HANDOFF_AGENT="<printed AGENT_ARTIFACTS_ROOT>/competitive-positioning-${SLUG}/handoff/$RUN_ID"
ANALYSIS_DIR_AGENT="<printed AGENT_ARTIFACTS_ROOT>/competitive-positioning-${SLUG}"
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): This is normal for a first run — do not treat it as an error. Use AskUserQuestion (NOT plain chat) to ask for company name, stage, sector, and geography. Provide at least 2 options. Note in the report metadata that no cross-skill validation was performed. Then create:
python3 "$SHARED_SCRIPTS/founder_context.py" init \
--company-name "Acme Corp" --stage seed --sector "B2B SaaS" \
--geography "US" --artifacts-root "$ARTIFACTS_ROOT"
Exit 2 (multiple): Present the list, ask which company, re-read with --slug.
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 with its own positioning axes, record them in product_profile.json under deck_axes for potential use as a secondary positioning view.
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"
If materials are sparse, use AskUserQuestion to gather missing fields. At minimum: product description, target customers, and what the founder believes differentiates them.
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.
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), then pipe through the producer. --landscape points at landscape_draft.json (the set as drafted; enrichment has not run yet):
cat "$HANDOFF_DIR/competitor_verification_output.json" | \
python3 "$SCRIPTS/verify_competitors.py" --pretty --run-id "$RUN_ID" \
--landscape "$ANALYSIS_DIR/landscape_draft.json" \
-o "$ANALYSIS_DIR/competitor_verification.json"
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.
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 — for each slug in summary.flagged_slugs, one line drawn from that verdict's reasoning: • <name> — I don't think this genuinely competes: <reasoning>. Keep it, drop it, or call it adjacent?. If summary.flagged == 0, add one line: "All look like genuine competitors — none flagged." These challenges are the substance behind the gate; present them plainly and let the founder decide (never auto-remove).
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 and any names you'd challenge.
Question (substitute <N> and the flagged names; drop the parenthetical if summary.flagged == 0): Found <N> competitors (I'd challenge: <names>) — does this set look right?
Options: Looks good / Missing competitors / Remove some / Change axes
CRITICAL: the question must be self-contained on the decision (count + flagged names), 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.
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.
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.
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 under $ANALYSIS_DIR, and never delete anything under it.
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.
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.
Phase B — Gap detection: After enriching, check for missing competitor categories.
Use WebSearch ("<product category> competitors", "<adjacent category> tools", etc.)
to discover competitors absent from the draft. Add them to suggested_additions[]
with merged: false. Do NOT add to competitors[] — only to suggested_additions[].
Use your Write tool to write to OUTPUT_PATH — exactly the shape expected by
validate_landscape.py:
{
"competitors": [...original competitors enriched, NOT new ones...],
"suggested_additions": [...newly discovered...],
"suggested_axes": [],
"assessment_mode": "sub-agent",
"research_depth": "full",
"input_mode": "<from product_profile>",
"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 — canonical artifacts are
producer-script-only; anything else you write bypasses schema validation and
run_id stamping.
After the sub-agent returns: gate the hand-off per the Context A hand-off protocol. Read suggested_additions from the gated file; if any exist, present them to the founder and ask which to include. If the founder approves additions, merge them into competitors[] by writing a merged copy to $STAGING_DIR/landscape_input.json (start from the hand-off file's contents — move approved entries from suggested_additions[] to competitors[], changing nothing else) and pipe that; with no approved additions, pipe the hand-off file directly:
cat "$HANDOFF_DIR/landscape_research_output.json" | \
python3 "$SCRIPTS/validate_landscape.py" --pretty --run-id "$RUN_ID" -o "$ANALYSIS_DIR/landscape.json"
Fix any errors (exit 1) and re-run. Warnings are acceptable — address medium-severity ones in the report.
Gate 2: Founder Validation of Axis Selection
MANDATORY STOP — TWO SEPARATE STEPS, same pattern as Gate 1.
At this point no competitor coordinates exist yet — those are produced in Step 5 (POSITIONING_SCORING) and written to positioning.json. Gate 2 validates which axis pair(s) to plot on and which competitors belong on the map, NOT coordinate positions.
Step A: Output a chat message with the chosen axis pair(s) (the candidate axes from Step 3, with their rationale) and the confirmed competitor set that will be positioned.
Step B: AFTER the chat message, call AskUserQuestion with a short question that names the axis pair so the founder isn't confirming blind. Plain text, one sentence, no markdown/tables.
Question (substitute the two chosen axis names): I'll plot competitors on <axis-X> × <axis-Y> — do these axes look right?
Options: Proceed to scoring / Change axes / Adjust competitor set / Other changes
If the founder changes an axis pair or the competitor set, apply the change before proceeding to Step 5. Founder adjustments to individual coordinates happen later — at the Step 5 founder-override flow, after coordinates have been assigned.
Step 5: Positioning & Moat Assessment -> positioning.json + Dispatch Moat/Positioning Scoring (Context A)
REQUIRED — read ${CLAUDE_PLUGIN_ROOT}/skills/competitive-positioning/references/moat-definitions.md now.
Write positioning.json to $ANALYSIS_DIR (consult references/artifact-schemas.md for the schema). Then dispatch the sub-agent twice in parallel (two Task calls in one message, both with subagent_type: "founder-skills:competitive-positioning") — once for MOAT_SCORING and once for POSITIONING_SCORING.
MOAT_SCORING dispatch prompt:
CONTEXT: MOAT_SCORING
OUTPUT_PATH: <HANDOFF_AGENT>/moat_scoring_output.json
RUN_ID: <RUN_ID>
You are the competitive-positioning agent dispatched in Context A (MOAT_SCORING).
Read positioning.json at <ANALYSIS_DIR_AGENT>/positioning.json and landscape.json at
<ANALYSIS_DIR_AGENT>/landscape.json.
Score every slug (including _startup) across the 6 canonical moat dimensions from
${CLAUDE_PLUGIN_ROOT}/skills/competitive-positioning/references/moat-definitions.md:
network_effects, data_advantages, switching_costs, regulatory_barriers,
cost_structure, brand_reputation.
Each moat: status (strong/moderate/weak/absent/not_applicable), evidence (required),
evidence_source (researched/agent_estimate/founder_override), trajectory
(building/stable/eroding).
For trajectory and any moat where landscape.json evidence is thin, use WebSearch
to find recent (last 12 months) signals — funding rounds, M&A, hiring, executive
changes, patent filings, product launches. Stamp evidence_source: "researched"
only when the signal came from a WebSearch result.
Use your Write tool to write to OUTPUT_PATH — exactly the shape expected by
score_moats.py:
{
"moat_assessments": {
"_startup": {"moats": [...]},
"<slug>": {"moats": [...]},
...
},
"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 — canonical artifacts are
producer-script-only; anything else you write bypasses schema validation and
run_id stamping.
POSITIONING_SCORING dispatch prompt:
CONTEXT: POSITIONING_SCORING
OUTPUT_PATH: <HANDOFF_AGENT>/positioning_scoring_output.json
RUN_ID: <RUN_ID>
You are the competitive-positioning agent dispatched in Context A (POSITIONING_SCORING).
Read positioning.json at <ANALYSIS_DIR_AGENT>/positioning.json.
For each view in positioning.json, assign coordinates (0-100) for every competitor
and _startup on both axes. Every point needs x_evidence, y_evidence, and provenance.
Assess differentiation claims: verifiable (boolean), evidence, challenge, verdict
(holds/partially_holds/does_not_hold).
The axes themselves drive the search queries — when an axis is "customer support
depth" or "pricing transparency," issue WebSearch queries targeting that specific
dimension per competitor. Stamp x_evidence_source / y_evidence_source as
"researched" only when the coordinate came from a WebSearch result. For each
differentiation_claim, use WebSearch to find supporting or contradicting evidence
before assigning a verdict.
Use your Write tool to write to OUTPUT_PATH — exactly the shape expected by
score_positioning.py:
{
"views": [
{
"id": "...", "x_axis": {"name": "..."}, "y_axis": {"name": "..."},
"x_axis_rationale": "...", "y_axis_rationale": "...",
"points": [
{"competitor": "...", "x": 0-100, "y": 0-100,
"x_evidence": "...", "y_evidence": "...",
"x_evidence_source": "researched|agent_estimate",
"y_evidence_source": "researched|agent_estimate"}
]
}
],
"differentiation_claims": [...],
"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 — canonical artifacts are
producer-script-only; anything else you write bypasses schema validation and
run_id stamping.
After both sub-agents return: gate EACH hand-off per the Context A hand-off protocol (run check_handoff.py per file, branch on exit codes). Then pipe each file through its producer:
cat "$HANDOFF_DIR/moat_scoring_output.json" | \
python3 "$SCRIPTS/score_moats.py" --pretty --run-id "$RUN_ID" -o "$ANALYSIS_DIR/moat_scores.json"
cat "$HANDOFF_DIR/positioning_scoring_output.json" | \
python3 "$SCRIPTS/score_positioning.py" --pretty --run-id "$RUN_ID" -o "$ANALYSIS_DIR/positioning_scores.json"
Founder coordinate-override flow (optional): Now that competitor coordinates exist, present the positioned map to the founder if they asked to adjust positions (or flagged "Adjust positions" at any earlier gate). If the founder corrects a specific coordinate, update the corresponding point in positioning.json and re-run score_positioning.py, stamping x_evidence_source / y_evidence_source: "founder_override" on the changed coordinate so compose_report.py records it via FOUNDER_OVERRIDE_COUNT. Re-pipe the updated positioning.json views through score_positioning.py to refresh positioning_scores.json.
Step 6: Score Checklist -> checklist.json (Context A: CHECKLIST dispatch)
REQUIRED — read ${CLAUDE_PLUGIN_ROOT}/skills/competitive-positioning/references/checklist-criteria.md now.
Dispatch the competitive-positioning sub-agent in Context A (CHECKLIST). Call the Task tool with subagent_type: "founder-skills:competitive-positioning".
Dispatch prompt template:
CONTEXT: CHECKLIST
OUTPUT_PATH: <HANDOFF_AGENT>/checklist_output.json
RUN_ID: <RUN_ID>
You are the competitive-positioning agent dispatched in Context A (CHECKLIST).
Read landscape.json, positioning.json, moat_scores.json, and positioning_scores.json
from <ANALYSIS_DIR_AGENT>. Also read
${CLAUDE_PLUGIN_ROOT}/skills/competitive-positioning/references/checklist-criteria.md.
Assess all 25 checklist items (COVER_01..05, POS_01..05, MOAT_01..04,
EVID_01..04, NARR_01..04, MISS_01..03). Mode-based gating applies: when
input_mode is conversation, research-dependent items auto-gate to not_applicable.
Evidence is MANDATORY for every item: every fail and warn MUST have a non-empty
evidence string citing specific findings. Every pass MUST have evidence noting
what was checked.
Use your Write tool to write to OUTPUT_PATH — the items array without a
summary (the producer script computes the summary):
{"items": [{"id": "COVER_01", "status": "pass", "evidence": "...", "notes": "..."}, ...all 25 items...]}
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 — canonical artifacts are
producer-script-only; anything else you write bypasses schema validation and
run_id stamping.
After the sub-agent returns: gate the hand-off per the Context A hand-off protocol, then pipe through the producer script. The sub-agent writes items only — pass the real input mode and run_id on the CLI so checklist.py gates the right items and stamps metadata.run_id:
cat "$HANDOFF_DIR/checklist_output.json" | python3 "$SCRIPTS/checklist.py" --pretty \
--input-mode "$INPUT_MODE" --run-id "$RUN_ID" -o "$ANALYSIS_DIR/checklist.json"
$INPUT_MODE is the mode established in Steps 1-2 (deck, conversation, or document). Without --input-mode, deck/document runs silently default to conversation and mis-gate NARR_03/EVID_04; without --run-id, checklist.json carries no run_id and the Step 7c verifier blocks.
Step 7: Compose, Validate, and Post-Compose Coaching
7a — Compose report JSON (two-pass pattern):
Pass 1 (discovery): Run compose WITHOUT --strict and WITHOUT accepted_warnings in positioning.json:
python3 "$SCRIPTS/compose_report.py" --dir "$ANALYSIS_DIR" --pretty \
-o "$ANALYSIS_DIR/report.json" \
--write-md "$ANALYSIS_DIR/report.md"
compose_report.py writes both report.json and report.md deterministically. Do NOT read report_markdown out of report.json and re-write it via heredoc.
Inspect the warnings in the output. Fix any high-severity warnings (missing artifacts, stale run_id, corrupt JSON, artifacts not written by their producer script) and re-run Pass 1.
Pass 2 (with acceptances): If any medium-severity warnings should be accepted, add accepted_warnings to positioning.json with the warning code, match pattern, and reason. Then re-run with --strict:
python3 "$SCRIPTS/compose_report.py" --dir "$ANALYSIS_DIR" --strict --pretty \
-o "$ANALYSIS_DIR/report.json" \
--write-md "$ANALYSIS_DIR/report.md"
Post-write verification: compose_report.py exits non-zero (code 2) if the declared output files don't exist or are empty after writing. If compose exits non-zero, stop and report the exact stderr — do not proceed.
7b — Cross-skill lookups: Use find_artifact.py to locate prior deck-review and market-sizing artifacts. If found, note findings for inclusion in coaching commentary.
7c — Post-Compose Coaching Commentary (Context B dispatch — POST_COMPOSE_COACHING):
Dispatch the competitive-positioning sub-agent in Context B. Call the Task tool with subagent_type: "founder-skills:competitive-positioning" after compose_report.py has successfully written both report.json and report.md.
Mitigation 2 protocol: the main thread reads the structured coaching_payload from report.json and inlines it into the dispatch prompt. The sub-agent does NOT Read full report.md — it consumes coaching_payload directly, composes the coaching commentary, and WRITES it to the OUTPUT_PATH hand-off file with its Write tool, returning only a small receipt (the same file transport as Context A — the commentary leaves the model exactly once, into the Write call; the main thread never re-types it). The main thread gates that file with check_handoff.py, then inserts it via the shared insert_coaching.py script (idempotency matrix, uuid-marker replacement, run_id-parity verification — all deterministic). See the competitive-positioning agent body's "Context B — Post-compose coaching dispatch (POST_COMPOSE_COACHING)" section for the full procedure.
python3 -c '
import json, sys
data = json.load(open(sys.argv[1]))
print(json.dumps(data["coaching_payload"], indent=2))
' "$ANALYSIS_DIR/report.json"
The payload prints to stdout — copy it from the tool result into the dispatch
prompt below. (Never capture it into a shell variable: each Bash call runs in a
fresh shell, so the variable would be unreadable and gone.)
Dispatch prompt template (substitute <HANDOFF_AGENT> with the Step-0 agent-namespace value — the same rule as every Context A dispatch; the sub-agent has no shell vars, so paste the printed value):
CONTEXT: POST_COMPOSE_COACHING
OUTPUT_PATH: <HANDOFF_AGENT>/coaching_commentary_output.json
You are dispatched to add coaching commentary to a competitive positioning review.
The compose_report.py script has finished. The structured `coaching_payload`
from report.json is:
<paste the coaching_payload JSON printed by the previous Bash command here verbatim>
Follow your agent body's Context B procedure (POST_COMPOSE_COACHING):
1. Compose commentary from the inlined coaching_payload (failed_items,
warned_items, summary, high_severity_warnings, company_name).
Do NOT Read the full report.md. Do NOT edit report.md or any canonical artifact.
No WebSearch in this context — commentary is payload-grounded only.
2. Use your Write tool to write to OUTPUT_PATH exactly:
{"commentary_markdown": "<your coaching commentary — markdown body text, WITHOUT
a '## Coaching Commentary' heading and WITHOUT the insertion_marker string>"}
Do NOT write any file other than OUTPUT_PATH — insertion into report.md is the
main thread's job, via the shared insert_coaching.py script.
3. Return:
{"status": "complete", "output_path": "<echo of OUTPUT_PATH>"}
OR, if the payload is unusable (write no file):
{"status": "blocked", "reason": "<specific gap>"}
Stop after returning the receipt JSON. Do not narrate.
After the sub-agent returns: if its final message is a {"status": "blocked", "reason": ...} object, stop and report the reason to the founder — do not run the gate. Otherwise gate the hand-off, then (on gate exit 0) insert deterministically. The commentary leaves the model exactly once (into the sub-agent's Write call) — NEVER re-type the sub-agent's JSON into a heredoc or a python -c argument.
SHARED_SCRIPTS="${CLAUDE_PLUGIN_ROOT}/scripts"
if [ ! -d "$SHARED_SCRIPTS" ]; then
CP_SCRIPTS="$(find /sessions -type d -path '*/skills/competitive-positioning/scripts' 2>/dev/null | head -1)"
[ -n "$CP_SCRIPTS" ] || CP_SCRIPTS="$(find / -type d -path '*/skills/competitive-positioning/scripts' 2>/dev/null | head -1)"
SHARED_SCRIPTS="${CP_SCRIPTS%/skills/*}/scripts"
fi
printf '%s' '<agent final message verbatim>' | \
python3 "$SHARED_SCRIPTS/check_handoff.py" "$HANDOFF_DIR/coaching_commentary_output.json" \
--agent-path "$HANDOFF_AGENT/coaching_commentary_output.json" --receipt-json -
On gate exit 0, insert the gated hand-off FILE (feed the file, never re-type the message):
SHARED_SCRIPTS="${CLAUDE_PLUGIN_ROOT}/scripts"
if [ ! -d "$SHARED_SCRIPTS" ]; then
CP_SCRIPTS="$(find /sessions -type d -path '*/skills/competitive-positioning/scripts' 2>/dev/null | head -1)"
[ -n "$CP_SCRIPTS" ] || CP_SCRIPTS="$(find / -type d -path '*/skills/competitive-positioning/scripts' 2>/dev/null | head -1)"
SHARED_SCRIPTS="${CP_SCRIPTS%/skills/*}/scripts"
fi
python3 "$SHARED_SCRIPTS/insert_coaching.py" \
--report "$ANALYSIS_DIR/report.md" \
--marker '<EXACT insertion_marker string from report.json coaching_payload>' \
--commentary-file "$HANDOFF_DIR/coaching_commentary_output.json" \
--verify-artifact "$ANALYSIS_DIR/landscape.json" \
--verify-artifact "$ANALYSIS_DIR/positioning.json" \
--verify-artifact "$ANALYSIS_DIR/moat_scores.json" \
--verify-artifact "$ANALYSIS_DIR/positioning_scores.json" \
--verify-artifact "$ANALYSIS_DIR/checklist.json"
The gate (check_handoff.py) verifies the sub-agent's hand-off file exists, parses, and matches the receipt's echoed path; on gate exit 0, insert_coaching.py performs the 6-state idempotency check, replaces the marker with ## Coaching Commentary + the commentary in a single in-place write, and verifies run_id parity across all 5 producer artifacts. Branch on the exit code (complete state machine — do not improvise):
- Exit 0 from the chain —
insert_coaching.py's receipt on stdout says inserted (or already_inserted on a resume). Present report_path to the founder and proceed.
check_handoff.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.
- Exit 6 (receipt unparseable / no
output_path key) → redo-dispatch with "return ONLY the receipt JSON — no fences, no prose." (A status: "blocked" final message is NOT exit 6 — it was handled before the gate.)
insert_coaching.py exit 1 (blocked; stdout carries {"status": "blocked", "reason": ...}) → stop and report the exact reason. Do NOT hand-edit report.md — if the reason mentions a truncated report or a missing marker, re-run compose_report.py --write-md and retry the chain. If the reason is commentary_markdown missing or empty, treat as a malformed hand-off: repair-dispatch quoting the reason.
- After ANY corrective dispatch, resume from the gate chain — never feed
insert_coaching.py an ungated file.
Retry budget: max 2 corrective dispatches (same rule as Context A). Graceful degrade: if the FIRST corrective dispatch also exits 3 while the receipt claims complete with the correctly echoed path, treat the host topology as hand-off-incompatible — fall back to message-channel transport: apply the tolerant JSON extraction protocol to the sub-agent's final message, stage its commentary_markdown payload to $ANALYSIS_DIR/coaching_commentary.json via a quoted <<'COACHING_EOF' heredoc (single-quoted → apostrophe-safe; NEVER python -c, NEVER the outputs/ root), and run the same insert_coaching.py --commentary-file "$ANALYSIS_DIR/coaching_commentary.json" against that staged file.
7d — Visualize (optional):
python3 "$SCRIPTS/visualize.py" --dir "$ANALYSIS_DIR" -o "$ANALYSIS_DIR/report.html"
Present the HTML file path to the user.
7e — Explorer (optional):
python3 "$SCRIPTS/explore.py" --dir "$ANALYSIS_DIR" -o "$ANALYSIS_DIR/explore.html"
Present the HTML file path to the user.
Step 8: Deliver Artifacts
Copy final deliverables to workspace root with clean names:
cp "$ANALYSIS_DIR/report.md" "./${COMPANY_NAME}_Competitive_Positioning.md"
cp "$ANALYSIS_DIR/report.html" "./${COMPANY_NAME}_Competitive_Positioning.html" 2>/dev/null
cp "$ANALYSIS_DIR/explore.html" "./${COMPANY_NAME}_Competitive_Explorer.html" 2>/dev/null
Scratch lives in $STAGING_DIR (/tmp, reclaimed by the sandbox) — no cleanup needed. Do not rm
anything under $ANALYSIS_DIR — it is the promoted outputs/ tree in Cowork, where deleting a
user-visible path is unsafe (and the parity gate flags it).
Where COMPANY_NAME is the company name with spaces replaced by underscores (e.g., "Acme Corp" -> "Acme_Corp"). Present the file paths to the user.
Presenting the report to the founder:
- Answer placement and moat questions from the points/evidence tables in report.md — never re-derive or restate coordinates from memory.
- If the founder disputes a coordinate (e.g., "we're faster than you placed us"), use the founder coordinate-override flow (Step 5): update the specific point in
positioning.json with x_evidence_source: "founder_override" and re-run score_positioning.py to refresh positioning_scores.json, then re-run compose_report.py. Do NOT re-explain a placement from chat context.
- For what-if competitive scenarios (e.g., "what if we added this moat?"), note the gap and invite the founder to re-run the full skill after updating the relevant data.
Scoring
Moat Scoring
- 6 canonical dimensions per company, each:
strong / moderate / weak / absent / not_applicable
- Moat count = dimensions rated
strong or moderate
- Overall defensibility:
high (2+ strong), moderate (1 strong or 2+ moderate), low (all else)
Positioning Scoring
- Distance-weighted differentiation: rank contributes 50% (where the startup ranks among competitors) + gap contributes 50% (how far ahead of the next-best competitor). This distinguishes "barely ahead" from "dramatically ahead" at the same rank.
- Vanity axis detection: >80% of competitors within 20% range on either axis
- Differentiation strength:
strong (top quartile both axes), moderate (top quartile one axis), weak (middle of pack), undifferentiated (bottom half both axes)
Checklist Scoring
- 25 items, each: pass / fail / warn / not_applicable
score_pct = (pass + 0.5 * warn) / (total - not_applicable) * 100
- Overall: "strong" (>=85%), "solid" (>=70%), "needs_work" (>=50%), "major_revision" (<50%)
Cross-Agent Integration
This skill imports artifacts from prior deck-review (competition slide claims) and market-sizing (market scope validation) analyses. Imported artifacts are recorded with dates so cross-skill findings can be cited with their provenance.
Main-Thread Return
This skill runs inline in the main thread (not as a sub-agent). The final outcome the main thread delivers to the founder is:
- The path to
$ANALYSIS_DIR/report.md — the primary deliverable.
- The headline outcome fields, sourced from the
coaching_payload you inlined in Step 7c (summary.overall_status, high_severity_warnings) and the producer artifacts (moat_scores.json for top moats), plus the insert_coaching.py receipt (status, report_path, run_id). The Context B sub-agent no longer echoes these — do not source them from its return.
- Optionally: the HTML report paths from Steps 7d and 7e.
Do NOT inline report_markdown in the assistant message. The founder reads the file via the path.
Feedback
If a run ends blocked or failed, after you report the reason to the founder, add one line:
If this looks wrong or didn't finish, you can flag it: /founder-skills:feedback.
On unsolicited praise or frustration, you may mention /founder-skills:feedback once — never routinely, never mid-workflow, never more than once per session.