Skill: hulkworks/mindgraph (version 1.0.0)

--- name: mindgraph description: Obsidian-style [[wikilink]] knowledge graph and learnable MindSkills for OpenClaw workspaces. Use for ALL of these: (1) Any workspace file read/write — always use [[wikilinks]] for people, projects, tools, concepts. (2) Running learned processes like knockout-test, seo-validator, competitor-analysis. (3) Querying knowledge — "what do I know about X", "show connections to Y". (4) Learning new repeatable processes — "learn a new mindskill called Z". (5) Memory maintenance — finding orphans, dead links, unconnected files. This skill is always active — treat [[wikilinks]] as standard practice in every markdown file you write. --- # MindGraph 🕸️🧠 A knowledge graph + learnable process engine for OpenClaw. Two core features: 1. **Knowledge Graph** — Obsidian-style `[[wikilinks]]` across all workspace files 2. **MindSkills** — Learned, repeatable processes that save graph-connected results ## Always-On Rules **Every time you write or edit a markdown file, use `[[wikilinks]]` for:** - People: `[[Alice]]`, `[[Bob]]` - Projects: `[[my-saas]]`, `[[landing-page]]` - Companies/tools: `[[Stripe]]`, `[[Vercel]]`, `[[GitHub]]` - Concepts/frameworks: `[[Knockout Test]]`, `[[B2B SaaS]]` - Other agents/models: `[[Claude Code]]`, `[[Sonnet]]` This is not optional. Links are how knowledge connects. No links = isolated notes = useless. **NEVER use `[[wikilinks]]` in messages to the user** (Telegram, Discord, etc.). Wikilinks are for workspace files only. In conversations, write names plain: "Alice", not "[[Alice]]". **After significant file changes, rebuild the index:** ```bash python3 skills/mindgraph/scripts/mindgraph.py index ``` ## Graph Commands ```bash # Build/rebuild index python3 skills/mindgraph/scripts/mindgraph.py index # Query a topic (backlinks + context + connections) python3 skills/mindgraph/scripts/mindgraph.py query "<name>" # Backlinks only (what references this?) python3 skills/mindgraph/scripts/mindgraph.py backlinks "<name>" # Forward links (what does this link to?) python3 skills/mindgraph/scripts/mindgraph.py links "<file>" # Bidirectional connections python3 skills/mindgraph/scripts/mindgraph.py connections "<name>" # ASCII tree visualization python3 skills/mindgraph/scripts/mindgraph.py tree "<name>" [depth] # Find orphans, dead links, unconnected files python3 skills/mindgraph/scripts/mindgraph.py orphans python3 skills/mindgraph/scripts/mindgraph.py deadlinks python3 skills/mindgraph/scripts/mindgraph.py lonely # Full statistics python3 skills/mindgraph/scripts/mindgraph.py stats ``` ## MindSkills — Learned Processes MindSkills are repeatable frameworks stored in `skills/mindgraph/mindskills/`. Each has a defined process and saves results as graph-connected markdown. ### Using a MindSkill ```bash # List all learned mindskills python3 skills/mindgraph/scripts/mindgraph.py skills # Show a mindskill's process python3 skills/mindgraph/scripts/mindgraph.py skill <name> # List results for a mindskill python3 skills/mindgraph/scripts/mindgraph.py results <name> ``` When a user asks to run a process (e.g., "run the knockout test on X"), follow this flow: 1. Read the mindskill's `PROCESS.md` for the process definition 2. Execute the process conversationally 3. Save the result to `skills/mindgraph/mindskills/<name>/results/<subject>.md` 4. Use `[[wikilinks]]` throughout the result file 5. Include YAML frontmatter with metadata 6. Rebuild the graph index Result file template: ```markdown --- mindskill: <skill-name> subject: <what was tested/analyzed> date: <YYYY-MM-DD> verdict: <outcome> aliases: [<aliases>] --- # [[<MindSkill Name>]]: [[<Subject>]] <Results following the process defined in PROCESS.md> ## Connections - Related: [[link1]], [[link2]] ``` ### Learning a New MindSkill When a user says "learn a mindskill called X" or describes a repeatable process: ```bash # Create a new mindskill python3 skills/mindgraph/scripts/mindgraph.py learn "<name>" ``` This creates the directory structure. Then write the PROCESS.md based on the user's description. A good PROCESS.md contains: - **Purpose**: What this process does and when to use it - **Trigger phrases**: What the user might say to invoke this - **Steps**: The actual process to follow (numbered) - **Output format**: What the result file should contain - **Verdict/scoring**: How to summarize the outcome (if applicable) ### Discovering MindSkills When a user's request matches a learned mindskill, proactively suggest it: - "Want me to run the [[Knockout Test]] on that?" - "I have an [[SEO Validator]] mindskill — should I audit that?" - "This looks like a [[Competitor Analysis]] — want the full framework?" ## Link Resolution Links match (case-insensitive) against: 1. File basenames: `[[MEMORY]]` → `MEMORY.md` 2. Project dirs: `[[my-saas]]` → `projects/my-saas/` 3. MindSkill results: `[[Pet Tracker KT]]` → knockout test result 4. YAML aliases: `aliases: [AV-Check]` → `[[AV-Check]]` resolves 5. Unresolved → concept node (still tracked for backlinks) ## File Locations - Graph index: `mindgraph.json` (workspace root) - MindSkills: `skills/mindgraph/mindskills/` - Script: `skills/mindgraph/scripts/mindgraph.py`

path: README.md
content: # mindgraph


path: mindskills/knockout-test/PROCESS.md
content: # [[Knockout Test]] 🥊

Idea validation framework. A structured sparring match against a business idea to decide: BUILD, PIVOT, or PARK.

## Trigger Phrases

- "Run a knockout test on X"
- "Knockout test this idea"
- "Should I build X?"
- "Validate this idea"
- "Is this worth building?"

## Process

### Round 1: Build It Up 💪
Be the optimist. Make the strongest case FOR the idea:
- **The pain**: What problem does this solve? How badly does it hurt?
- **The market**: How big? Who are the buyers? TAM/SAM?
- **The money**: Revenue model, pricing, willingness to pay
- **Why now**: What changed that makes this possible/needed today?
- **The hook**: One sentence that sells it

### Round 2: Tear It Down 👊
Be the pessimist. Attack every weakness:
- **Competition**: Who already does this? How established?
- **Challenges**: What's technically hard? What could break?
- **Why it fails**: The most likely death scenario
- **Barriers**: Trust, regulation, chicken-and-egg, data moats

### Round 3: Reality Check 🧐
Be the realist. Strip away the hype:
- **Feature vs Product**: Is this a feature of something bigger or a standalone business?
- **Who pays**: Actually pays, not "would be nice to have"
- **Distribution**: How do you get in front of buyers? SEO, ads, partnerships?
- **Differentiation**: What's the moat? Why can't someone copy this in a weekend?
- **Unit economics**: CAC vs LTV, margins, scale dynamics

### Round 4: Verdict 🏆
One of three outcomes:
- **✅ BUILD** — Clear market, real pain, viable economics, go ship it
- **🟡 PIVOT** — Core insight is good but execution angle needs to change
- **🅿️ PARK** — Not now. Document why and what would change your mind

## Output Format

Each result includes:
- Full analysis per round with `[[wikilinks]]` to competitors, markets, related projects
- Clear verdict with reasoning
- "What would change my mind" section for PIVOT/PARK verdicts
- Connected projects and ideas

## Scoring Signals

Strong BUILD indicators:
- Personal pain (founder uses it themselves)
- Existing market with overpriced incumbents
- Clear distribution channel
- Quick to MVP (<4 weeks)
- Measurable ROI for buyer

Strong PARK indicators:
- Well-funded competitors with product-market fit
- It's a feature, not a product
- "Would be nice" but nobody pays
- Requires massive scale to work
- Regulatory minefield


path: mindskills/knockout-test/results/example-pet-tracker.md
content: ---
mindskill: knockout-test
subject: Pet Tracker App
date: 2026-01-15
verdict: 🅿️ PARK
aliases: [Pet Tracker KT]
---
# [[Knockout Test]]: Pet Tracker App

## Round 1: Build It Up 💪
- **Pain**: Pet owners worry when pets roam. GPS collars exist but apps are clunky.
- **Market**: 67M US dog owners, $7B pet tech market
- **Money**: $9.99/mo subscription + hardware markup
- **Hook**: "Always know where your pet is"

## Round 2: Tear It Down 👊
- **Competition**: [[Whistle]], [[Fi]], [[Apple AirTag]] — massive players
- **Challenges**: Hardware is expensive to develop, cellular costs eat margins
- **Why it fails**: Apple AirTag at $29 killed the low-end. Fi raised $30M+.

## Round 3: Reality Check 🧐
- **Feature vs Product**: Apple/Google will absorb this into their ecosystems
- **Who pays**: Pet owners, but switching cost from existing tracker is low
- **Distribution**: Pet stores, vets — hard partnerships to crack
- **Moat**: None. Hardware + software combo requires serious capital.

## Round 4: Verdict 🏆
🅿️ **PARK** — Too capital-intensive, dominant players, no clear differentiation.

**What would change my mind**: A software-only angle (e.g., pet health tracking from existing wearable data) that avoids the hardware trap.

## Connections
- Competitors: [[Whistle]], [[Fi]], [[Apple AirTag]]
- Market: [[Pet Tech]], [[IoT]]
- Pattern: Hardware + subscription model


path: references/philosophy.md
content: # MindGraph — The Brain Analogy 🧠

MindGraph mirrors how a human brain works:

| Brain | MindGraph |
|-------|-----------|
| Neurons | Nodes (concepts, files, people, projects) |
| Synapses | `[[wikilinks]]` (connections between nodes) |
| Learned procedures (muscle memory) | MindSkills (Knockout Test, SEO Validator...) |
| Associative recall | Backlinks — "Drift" triggers → LeadPilot → Knockout Test → B2B SaaS |
| Weak/fading memories | Orphan nodes (no connections = useless) |
| Strong neural pathways | Most-referenced nodes (frequently linked = core concepts) |
| Learning | `mindgraph.py learn` — teach new repeatable processes |
| Recall | `mindgraph.py query` — pull everything connected to a topic |

**The more you use a connection, the stronger it gets.** Every time `[[LeadPilot]]` appears in a new file, that node gets richer, more connected, easier to find.

**Unlike a human brain:** These synapses don't decay. The agent wakes up fresh every session, but the graph remembers everything. Persistent memory through structure, not biology.

**MindSkills are muscle memory.** You don't re-learn how to ride a bike. Once the Knockout Test is learned, the process is encoded — just say "knockout test this" and it executes. Learn an SEO Validator, a Competitor Analysis, a Tech Stack Review — each one is a new capability permanently wired into the system.

The result: a modular AI brain that grows smarter with every interaction.


path: scripts/mindgraph.py
content: #!/usr/bin/env python3
"""
MindGraph — Obsidian-style [[wikilink]] knowledge graph for OpenClaw workspaces.
Indexes, queries, and traverses bidirectional links across all markdown files.
"""

import json
import os
import re
import sys
from collections import defaultdict
from pathlib import Path
from datetime import datetime, timezone

WORKSPACE = os.environ.get("MINDGRAPH_WORKSPACE", os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
GRAPH_FILE = os.path.join(WORKSPACE, "mindgraph.json")
WIKILINK_RE = re.compile(r'\[\[([^\]|]+?)(?:\|[^\]]+?)?\]\]')

def normalize(name):
    """Normalize a link target for matching."""
    return name.strip().lower().replace(" ", "-").replace("_", "-")

def scan_files():
    """Find all .md files in workspace."""
    files = []
    for root, dirs, fnames in os.walk(WORKSPACE):
        # Skip hidden dirs, node_modules, .git
        dirs[:] = [d for d in dirs if not d.startswith('.') and d != 'node_modules']
        for f in fnames:
            if f.endswith('.md'):
                files.append(os.path.join(root, f))
    return files

def extract_links(filepath):
    """Extract all [[wikilinks]] from a file."""
    try:
        with open(filepath, 'r', encoding='utf-8') as f:
            content = f.read()
    except (UnicodeDecodeError, FileNotFoundError):
        return [], "", []
    
    links = WIKILINK_RE.findall(content)
    
    # Extract aliases from frontmatter
    aliases = []
    if content.startswith('---'):
        end = content.find('---', 3)
        if end != -1:
            frontmatter = content[3:end]
            alias_match = re.search(r'aliases:\s*\[([^\]]*)\]', frontmatter)
            if alias_match:
                aliases = [a.strip().strip('"').strip("'") for a in alias_match.group(1).split(',') if a.strip()]
    
    return links, content, aliases

def extract_context(content, link_name, max_chars=150):
    """Extract surrounding context for a link mention."""
    pattern = re.compile(r'\[\[' + re.escape(link_name) + r'(?:\|[^\]]+?)?\]\]', re.IGNORECASE)
    contexts = []
    for match in pattern.finditer(content):
        start = max(0, match.start() - 60)
        end = min(len(content), match.end() + 60)
        ctx = content[start:end].replace('\n', ' ').strip()
        if start > 0:
            ctx = '...' + ctx
        if end < len(content):
            ctx = ctx + '...'
        contexts.append(ctx)
    return contexts

def node_id_from_path(filepath):
    """Create a node ID from a file path."""
    rel = os.path.relpath(filepath, WORKSPACE)
    name = rel.replace('/', '-').replace('\\', '-')
    if name.endswith('.md'):
        name = name[:-3]
    return normalize(name)

def build_index():
    """Build the full graph index."""
    files = scan_files()
    nodes = {}
    # Map from normalized name/alias to node_id
    name_to_node = {}
    
    # First pass: create nodes for all files
    for fp in files:
        node_id = node_id_from_path(fp)
        links, content, aliases = extract_links(fp)
        rel_path = os.path.relpath(fp, WORKSPACE)
        
        # Determine type
        if 'projects/' in rel_path:
            node_type = 'project'
        elif 'memory/' in rel_path:
            node_type = 'memory'
        elif rel_path in ('SOUL.md', 'IDENTITY.md', 'USER.md', 'AGENTS.md'):
            node_type = 'core'
        else:
            node_type = 'file'
        
        nodes[node_id] = {
            'type': node_type,
            'file': rel_path,
            'aliases': aliases,
            'outLinks': [normalize(l) for l in links],
            'inLinks': []  # populated in second pass
        }
        
        # Register name mappings
        name_to_node[node_id] = node_id
        basename = normalize(Path(fp).stem)
        name_to_node[basename] = node_id
        for alias in aliases:
            name_to_node[normalize(alias)] = node_id
    
    # Second pass: build inLinks (collect new concept nodes separately to avoid mutation during iteration)
    new_concepts = {}
    for node_id, node in list(nodes.items()):
        for target in node['outLinks']:
            target_node_id = name_to_node.get(target)
            if target_node_id and target_node_id in nodes:
                if node_id not in nodes[target_node_id]['inLinks']:
                    nodes[target_node_id]['inLinks'].append(node_id)
            else:
                if target in new_concepts:
                    if node_id not in new_concepts[target]['inLinks']:
                        new_concepts[target]['inLinks'].append(node_id)
                else:
                    new_concepts[target] = {
                        'type': 'concept',
                        'file': None,
                        'aliases': [],
                        'outLinks': [],
                        'inLinks': [node_id]
                    }
                    name_to_node[target] = target
    nodes.update(new_concepts)
    
    graph = {
        'nodes': nodes,
        'nameMap': name_to_node,
        'lastIndexed': datetime.now(timezone.utc).isoformat(),
        'fileCount': len(files),
        'nodeCount': len(nodes),
        'linkCount': sum(len(n['outLinks']) for n in nodes.values())
    }
    
    with open(GRAPH_FILE, 'w') as f:
        json.dump(graph, f, indent=2)
    
    file_nodes = sum(1 for n in nodes.values() if n['file'])
    concept_nodes = sum(1 for n in nodes.values() if not n['file'])
    print(f"✅ Indexed {len(files)} files → {file_nodes} file nodes + {concept_nodes} concept nodes")
    print(f"   {graph['linkCount']} links total")
    return graph

def load_graph():
    """Load the graph from disk."""
    if not os.path.exists(GRAPH_FILE):
        print("⚠️  No index found. Run: mindgraph.py index")
        sys.exit(1)
    with open(GRAPH_FILE) as f:
        return json.load(f)

def resolve_name(graph, name):
    """Resolve a name to a node_id."""
    n = normalize(name)
    name_map = graph.get('nameMap', {})
    if n in name_map:
        return name_map[n]
    # Fuzzy: check if any key contains the search term
    matches = [k for k in graph['nodes'] if n in k]
    if len(matches) == 1:
        return matches[0]
    if len(matches) > 1:
        print(f"Multiple matches for '{name}': {', '.join(matches)}")
        return matches[0]
    return n

def cmd_backlinks(graph, name):
    """Show what links TO this topic."""
    node_id = resolve_name(graph, name)
    node = graph['nodes'].get(node_id)
    
    if not node:
        print(f"❌ '{name}' not found in graph")
        return
    
    in_links = node.get('inLinks', [])
    if not in_links:
        print(f"No backlinks to '{name}'")
        return
    
    print(f"🔗 Backlinks to [[{name}]] ({len(in_links)}):\n")
    for src_id in sorted(in_links):
        src_node = graph['nodes'].get(src_id, {})
        src_file = src_node.get('file', '(concept)')
        print(f"  ← {src_file or src_id}")

def cmd_links(graph, name):
    """Show what this file/topic links TO."""
    node_id = resolve_name(graph, name)
    node = graph['nodes'].get(node_id)
    
    if not node:
        print(f"❌ '{name}' not found in graph")
        return
    
    out_links = node.get('outLinks', [])
    if not out_links:
        print(f"No outgoing links from '{name}'")
        return
    
    print(f"🔗 Links from [[{name}]] ({len(out_links)}):\n")
    for target_id in sorted(set(out_links)):
        target_node = graph['nodes'].get(target_id, {})
        target_file = target_node.get('file', '(concept)')
        print(f"  → {target_file or target_id}")

def cmd_query(graph, name):
    """Show all connections + context for a topic."""
    node_id = resolve_name(graph, name)
    node = graph['nodes'].get(node_id)
    
    if not node:
        print(f"❌ '{name}' not found in graph")
        return
    
    print(f"📊 [[{name}]]")
    print(f"   Type: {node.get('type', 'unknown')}")
    if node.get('file'):
        print(f"   File: {node['file']}")
    if node.get('aliases'):
        print(f"   Aliases: {', '.join(node['aliases'])}")
    
    in_links = node.get('inLinks', [])
    out_links = node.get('outLinks', [])
    
    if in_links:
        print(f"\n   ← Referenced by ({len(in_links)}):")
        for src_id in sorted(in_links):
            src_node = graph['nodes'].get(src_id, {})
            print(f"     {src_node.get('file') or src_id}")
            # Show context
            if src_node.get('file'):
                fp = os.path.join(WORKSPACE, src_node['file'])
                if os.path.exists(fp):
                    with open(fp, 'r', encoding='utf-8') as f:
                        content = f.read()
                    contexts = extract_context(content, name)
                    for ctx in contexts[:2]:
                        print(f"       \"{ctx}\"")
    
    if out_links:
        unique_out = sorted(set(out_links))
        print(f"\n   → Links to ({len(unique_out)}):")
        for target_id in unique_out:
            target_node = graph['nodes'].get(target_id, {})
            print(f"     {target_node.get('file') or target_id}")

def cmd_connections(graph, name):
    """Show bidirectional connections for a topic."""
    node_id = resolve_name(graph, name)
    node = graph['nodes'].get(node_id)
    
    if not node:
        print(f"❌ '{name}' not found in graph")
        return
    
    in_links = set(node.get('inLinks', []))
    out_links = set(node.get('outLinks', []))
    bidirectional = in_links & out_links
    only_in = in_links - out_links
    only_out = out_links - in_links
    
    print(f"🕸️  Connections for [[{name}]]:\n")
    
    if bidirectional:
        print(f"  ↔ Bidirectional ({len(bidirectional)}):")
        for nid in sorted(bidirectional):
            n = graph['nodes'].get(nid, {})
            print(f"    {n.get('file') or nid}")
    
    if only_in:
        print(f"  ← Inbound only ({len(only_in)}):")
        for nid in sorted(only_in):
            n = graph['nodes'].get(nid, {})
            print(f"    {n.get('file') or nid}")
    
    if only_out:
        print(f"  → Outbound only ({len(only_out)}):")
        for nid in sorted(only_out):
            n = graph['nodes'].get(nid, {})
            print(f"    {n.get('file') or nid}")

def cmd_orphans(graph):
    """Find concept nodes (linked but no file)."""
    orphans = [(nid, n) for nid, n in graph['nodes'].items() if not n.get('file')]
    if not orphans:
        print("✅ No orphan nodes — all links resolve to files")
        return
    
    print(f"👻 Orphan nodes ({len(orphans)}) — linked but no file:\n")
    for nid, node in sorted(orphans, key=lambda x: len(x[1].get('inLinks', [])), reverse=True):
        in_count = len(node.get('inLinks', []))
        print(f"  [[{nid}]] — referenced {in_count}x")

def cmd_deadlinks(graph):
    """Find links pointing to nothing (no file, no other references)."""
    dead = [(nid, n) for nid, n in graph['nodes'].items() 
            if not n.get('file') and len(n.get('inLinks', [])) == 1 and not n.get('outLinks')]
    if not dead:
        print("✅ No dead links")
        return
    
    print(f"💀 Dead links ({len(dead)}) — single reference, no file:\n")
    for nid, node in sorted(dead):
        src = node['inLinks'][0]
        src_node = graph['nodes'].get(src, {})
        print(f"  [[{nid}]] ← {src_node.get('file') or src}")

def cmd_lonely(graph):
    """Find files with no links in or out."""
    lonely = [(nid, n) for nid, n in graph['nodes'].items() 
              if n.get('file') and not n.get('inLinks') and not n.get('outLinks')]
    if not lonely:
        print("✅ All files are connected")
        return
    
    print(f"🏝️  Lonely files ({len(lonely)}) — no links in or out:\n")
    for nid, node in sorted(lonely):
        print(f"  {node['file']}")

def cmd_stats(graph):
    """Show graph statistics."""
    nodes = graph['nodes']
    file_nodes = sum(1 for n in nodes.values() if n.get('file'))
    concept_nodes = sum(1 for n in nodes.values() if not n.get('file'))
    total_links = sum(len(n.get('outLinks', [])) for n in nodes.values())
    
    by_type = defaultdict(int)
    for n in nodes.values():
        by_type[n.get('type', 'unknown')] += 1
    
    most_linked = sorted(nodes.items(), key=lambda x: len(x[1].get('inLinks', [])), reverse=True)[:10]
    most_linking = sorted(nodes.items(), key=lambda x: len(x[1].get('outLinks', [])), reverse=True)[:10]
    
    print(f"📊 MindGraph Stats")
    print(f"   Last indexed: {graph.get('lastIndexed', 'never')}")
    print(f"   Files scanned: {graph.get('fileCount', '?')}")
    print(f"   Total nodes: {len(nodes)} ({file_nodes} files + {concept_nodes} concepts)")
    print(f"   Total links: {total_links}")
    print()
    print(f"   By type:")
    for t, c in sorted(by_type.items()):
        print(f"     {t}: {c}")
    print()
    print(f"   🔥 Most referenced (top 10):")
    for nid, node in most_linked:
        in_count = len(node.get('inLinks', []))
        if in_count == 0:
            break
        label = node.get('file') or nid
        print(f"     [{in_count:3d}←] {label}")
    print()
    print(f"   🔗 Most outgoing links (top 10):")
    for nid, node in most_linking:
        out_count = len(node.get('outLinks', []))
        if out_count == 0:
            break
        label = node.get('file') or nid
        print(f"     [→{out_count:3d}] {label}")

def cmd_tree(graph, name, depth=2, _visited=None, _prefix="", _level=0):
    """Show ASCII tree of connections."""
    if _visited is None:
        _visited = set()
        print(f"🌳 Tree for [[{name}]] (depth {depth}):\n")
    
    node_id = resolve_name(graph, name)
    if node_id in _visited or _level > depth:
        return
    _visited.add(node_id)
    
    node = graph['nodes'].get(node_id)
    if not node:
        return
    
    label = node.get('file') or node_id
    type_icon = {'project': '📁', 'memory': '📝', 'core': '⭐', 'concept': '💡', 'file': '📄'}.get(node.get('type', ''), '📄')
    
    if _level == 0:
        print(f"  {type_icon} {label}")
    else:
        print(f"  {_prefix}{'└── ' if _level > 0 else ''}{type_icon} {label}")
    
    # Show connections
    all_connected = set(node.get('outLinks', [])) | set(node.get('inLinks', []))
    all_connected -= _visited
    
    for i, conn_id in enumerate(sorted(all_connected)):
        is_last = i == len(all_connected) - 1
        new_prefix = _prefix + ("    " if is_last or _level == 0 else "│   ")
        cmd_tree(graph, conn_id, depth, _visited, new_prefix, _level + 1)

MINDSKILLS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'mindskills')

def cmd_skills():
    """List all learned mindskills."""
    if not os.path.exists(MINDSKILLS_DIR):
        print("No mindskills learned yet. Use: mindgraph.py learn <name>")
        return
    
    skills = []
    for d in sorted(os.listdir(MINDSKILLS_DIR)):
        process_file = os.path.join(MINDSKILLS_DIR, d, 'PROCESS.md')
        if os.path.isdir(os.path.join(MINDSKILLS_DIR, d)) and os.path.exists(process_file):
            # Read first line after # heading for description
            with open(process_file, 'r') as f:
                lines = f.readlines()
            desc = ""
            for line in lines:
                line = line.strip()
                if line.startswith('#'):
                    continue
                if line:
                    desc = line[:80]
                    break
            
            # Count results
            results_dir = os.path.join(MINDSKILLS_DIR, d, 'results')
            result_count = 0
            if os.path.exists(results_dir):
                result_count = len([f for f in os.listdir(results_dir) if f.endswith('.md')])
            
            skills.append((d, desc, result_count))
    
    if not skills:
        print("No mindskills learned yet. Use: mindgraph.py learn <name>")
        return
    
    print(f"🧠 Learned MindSkills ({len(skills)}):\n")
    for name, desc, count in skills:
        print(f"  📋 {name} ({count} results)")
        if desc:
            print(f"     {desc}")

def cmd_skill(name):
    """Show a mindskill's process."""
    skill_dir = os.path.join(MINDSKILLS_DIR, name)
    process_file = os.path.join(skill_dir, 'PROCESS.md')
    
    if not os.path.exists(process_file):
        print(f"❌ MindSkill '{name}' not found")
        print(f"   Available: {', '.join(os.listdir(MINDSKILLS_DIR)) if os.path.exists(MINDSKILLS_DIR) else 'none'}")
        return
    
    with open(process_file, 'r') as f:
        print(f.read())

def cmd_results(name):
    """List results for a mindskill."""
    results_dir = os.path.join(MINDSKILLS_DIR, name, 'results')
    
    if not os.path.exists(results_dir):
        print(f"No results for '{name}' yet")
        return
    
    files = sorted([f for f in os.listdir(results_dir) if f.endswith('.md')])
    if not files:
        print(f"No results for '{name}' yet")
        return
    
    print(f"📊 Results for [[{name}]] ({len(files)}):\n")
    for fname in files:
        filepath = os.path.join(results_dir, fname)
        with open(filepath, 'r') as f:
            content = f.read()
        
        # Extract verdict from frontmatter
        verdict = ""
        subject = fname.replace('.md', '')
        if content.startswith('---'):
            end = content.find('---', 3)
            if end != -1:
                fm = content[3:end]
                for line in fm.split('\n'):
                    if line.startswith('verdict:'):
                        verdict = line.split(':', 1)[1].strip()
                    if line.startswith('subject:'):
                        subject = line.split(':', 1)[1].strip()
        
        verdict_str = f" → {verdict}" if verdict else ""
        print(f"  {subject}{verdict_str}")
        print(f"    {filepath}")

def cmd_learn(name):
    """Create a new mindskill directory structure."""
    skill_dir = os.path.join(MINDSKILLS_DIR, name)
    
    if os.path.exists(skill_dir):
        print(f"⚠️  MindSkill '{name}' already exists at {skill_dir}")
        return
    
    os.makedirs(os.path.join(skill_dir, 'results'), exist_ok=True)
    
    process_file = os.path.join(skill_dir, 'PROCESS.md')
    with open(process_file, 'w') as f:
        f.write(f"""# [[{name.replace('-', ' ').title()}]]

<!-- PURPOSE: What this process does and when to use it -->

## Trigger Phrases

- "Run a {name.replace('-', ' ')} on X"
- <!-- Add more trigger phrases -->

## Process

1. **Step 1** — <!-- Description -->
2. **Step 2** — <!-- Description -->
3. **Step 3** — <!-- Description -->
4. **Verdict** — <!-- How to summarize the outcome -->

## Output Format

Each result file should contain:
- Subject and date
- Findings per step
- Verdict/score
- `[[wikilinks]]` to related topics

## Scoring

<!-- Define how to score/rate results, if applicable -->
<!-- e.g., ✅ PASS / ⚠️ WARNING / ❌ FAIL -->
""")
    
    print(f"✅ MindSkill '{name}' created at {skill_dir}")
    print(f"   Edit: {process_file}")
    print(f"   Results will be saved to: {os.path.join(skill_dir, 'results/')}")

def main():
    if len(sys.argv) < 2:
        print("Usage: mindgraph.py <command> [args]")
        print()
        print("Graph Commands:")
        print("  index                  Build/rebuild the graph index")
        print("  backlinks <name>       What links TO this topic?")
        print("  links <name>           What does this topic link TO?")
        print("  query <name>           Full info + context for a topic")
        print("  connections <name>     Bidirectional connections")
        print("  orphans                Concept nodes with no file")
        print("  deadlinks              Single-reference dead links")
        print("  lonely                 Files with no connections")
        print("  stats                  Graph statistics")
        print("  tree <name> [depth]    ASCII tree visualization")
        print()
        print("MindSkill Commands:")
        print("  skills                 List all learned mindskills")
        print("  skill <name>           Show a mindskill's process")
        print("  results <name>         List results for a mindskill")
        print("  learn <name>           Create a new mindskill")
        sys.exit(1)
    
    cmd = sys.argv[1]
    
    if cmd == 'index':
        build_index()
    elif cmd == 'backlinks' and len(sys.argv) >= 3:
        cmd_backlinks(load_graph(), sys.argv[2])
    elif cmd == 'links' and len(sys.argv) >= 3:
        cmd_links(load_graph(), sys.argv[2])
    elif cmd == 'query' and len(sys.argv) >= 3:
        cmd_query(load_graph(), sys.argv[2])
    elif cmd == 'connections' and len(sys.argv) >= 3:
        cmd_connections(load_graph(), sys.argv[2])
    elif cmd == 'orphans':
        cmd_orphans(load_graph())
    elif cmd == 'deadlinks':
        cmd_deadlinks(load_graph())
    elif cmd == 'lonely':
        cmd_lonely(load_graph())
    elif cmd == 'stats':
        cmd_stats(load_graph())
    elif cmd == 'tree' and len(sys.argv) >= 3:
        depth = int(sys.argv[3]) if len(sys.argv) >= 4 else 2
        cmd_tree(load_graph(), sys.argv[2], depth)
    elif cmd == 'skills':
        cmd_skills()
    elif cmd == 'skill' and len(sys.argv) >= 3:
        cmd_skill(sys.argv[2])
    elif cmd == 'results' and len(sys.argv) >= 3:
        cmd_results(sys.argv[2])
    elif cmd == 'learn' and len(sys.argv) >= 3:
        cmd_learn(sys.argv[2])
    else:
        print(f"Unknown command or missing args: {cmd}")
        sys.exit(1)

if __name__ == '__main__':
    main()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (HIGH)