OR Key
drop another .md file to compare - side-by-side diff against skool-q-scanner

skool-q-scanner

Catches fresh questions in your community so you can reply fast.
personal 2 files 10 recent evals

What it does for you

Catches fresh questions in your community so you can reply fast.

What it produces

A recent result, so you can see the kind of work it returns.

loading…

How to get it

These run inside the Snappy workspace. Want this working in your business? I set skills like this up with you, in one focused week.

Work with me
For developers how this skill is built, graded, and how it runs

at a glance- the short version

eval modeauto

what's inside - the parts that make up a skill 3/4 present

A skill is just a few plain-text files. Only the main one is required. The rest are optional, added as the work needs them. This is what the skill is made of; how it runs is just below.

The skill
state/skills/skool-q-scanner/SKILL.md present
the skill itself, in plain text
The main file. It says what the skill is and lays out the steps in plain English.
Code
state/lib/skool-q-scanner.ts present
code the skill can run
Reusable code this skill can call when it needs to.
Scripts
state/bin/skool-q-scanner/ not present
helper scripts
Optional. Added when a skill has a few commands to run.
Loader
state/skills/skool-q-scanner/AGENTS.md present
what the AI loads on the fly
Loaded automatically the moment this skill is needed. Kept short on purpose.

how it's graded - what counts as a good run 5 criteria · 5 deterministic

Each row is one thing a good run has to get right. deterministic means a quick check decides, pass or fail. judge means the AI reads the result and rates it. Grading each piece on its own (instead of one overall score) shows exactly where a run fell short, so the fix is obvious.

name
kind
check
fetch_success
deterministic
Posts fetched without error from community.ts
recency_filter
deterministic
Only posts from last 30 minutes are included
question_classifier
deterministic
Question heuristic correctly identifies posts with ? or question-word starters
threshold_applied
deterministic
Only posts with >=2 comments OR question classification are included
output_shape
deterministic
Each output line is valid JSON with required fields (kind, post_id, title, actions)

how it runs - the shared frame every skill uses 3/5 present

Every skill runs the same way. One part does the work, a separate part checks it, and a short loader hands the AI exactly what it needs for the job. Anything this skill doesn't use shows a one-line note saying why, on purpose, not by accident.

makes the work The worker
not present

No work step here. This is probably a skill that reads or coordinates, not one that produces something.

checks the work The reviewer
not present

No separate check found. Without one, the part that makes the work could end up approving its own work, worth a closer look.

frame
learns Self-correction
present
fixes itself learns from gaps
When a run hits a gap, the skill gets edited on the spot [FIXED] or queued for a bigger rewrite [LOGGED], so it keeps getting better.
tidies up Background fixes
present
queued for rewrite runs in the background
Bigger fixes that can't be made on the spot get queued and rewritten in the background later.
remembers Run history
present
state/log/evals.ndjson auto runs
Every run is written down here, so the next time this skill is used it already knows how the last runs went.
Critical rules the things this skill must not get wrong
No must-not-break rules called out for this skill. Anything important lives in the writeup below.

what it has learned - fixes written back in over time sample

When a run hits something this skill didn't handle, the fix gets written back into the skill so it doesn't happen again. FIXED means it was corrected on the spot. LOGGED means it's queued for a bigger rewrite. Either way, the skill gets a little better and never makes the same mistake twice.

  1. Loading feedback rows…

SKILL.md- the skill, written out in plain English

Skool Q-Scanner v3

Scans the Snappy community feed on Skool for recent posts (last 30 minutes) and surfaces:

  • Posts with >=2 comments (active discussion)
  • Posts with clear question indicators (?-marks, "how do", "what is", "can I", etc.)

Outputs one kind=question_card per qualifying post with action buttons for skool/post and skool/dm.

Steps

  1. Fetch recent posts via npx tsx state/lib/community.ts posts.
  2. Filter by recency: keep only posts from the last 30 minutes (compare created_at to now).
  3. Classify as question if:
  • Contains ? in title or content, OR
  • Title/content starts with question words: "how", "what", "where", "when", "why", "can", "could", "should", "do", "does", "is", "are"
  1. Apply threshold: include if (classified as question) OR (comment_count >= 2).
  2. Format output: for each qualifying post, emit a question_card:
{
  "kind": "question_card",
  "post_id": "<skool post id>",
  "post_slug": "<skool post name>",
  "title": "<post title>",
  "author": "<author name>",
  "comment_count": <number>,
  "preview": "<first 120 chars of content>",
  "permalink": "https://www.skool.com/snappy/<slug>",
  "actions": [
    { "label": "View Post", "handler": "skool/post", "args": { "slug": "<slug>" } },
    { "label": "DM Author", "handler": "skool/dm", "args": { "user_id": "<author_id>" } }
  ]
}
  1. Write to output: each card as one JSON line to /tmp/skool-q-scanner-output.ndjson.
  2. Log count: print summary to stdout: Found N questions/active threads in the last 30 minutes.
  3. Score eval: deterministic rubric (see below).

Rubric

- name: fetch_success
  kind: deterministic
  check: Posts fetched without error from community.ts

- name: recency_filter
  kind: deterministic
  check: Only posts from last 30 minutes are included

- name: question_classifier
  kind: deterministic
  check: Question heuristic correctly identifies posts with ? or question-word starters

- name: threshold_applied
  kind: deterministic
  check: Only posts with >=2 comments OR question classification are included

- name: output_shape
  kind: deterministic
  check: Each output line is valid JSON with required fields (kind, post_id, title, actions)

Known gaps

  • Does not fetch post detail (comments, full text) - uses feed preview only.
  • Question classifier is keyword-based; may miss implicit questions ("need help with X").
  • DM handler (skool/dm) is a placeholder; actual DM send requires browser automation (not wired yet).

AGENTS.md- what the AI loads when this skill comes up

skool-q-scanner - per-turn loader

UI Resources

  • state/skills/skool-q-scanner/resources/ui.openui

What: Scans the Snappy community on Skool for recent posts (last 30 minutes) and surfaces questions or active threads (>=2 comments).

When to use: Scheduled agent wants to monitor community engagement and surface questions that need responses.

Output: Writes /tmp/skool-q-scanner-output.ndjson with one kind=question_card per qualifying post.

Implementation

The canonical Steps live in state/skills/skool-q-scanner/SKILL.md. Run:

npx tsx state/lib/skool-q-scanner.ts

The library:

  • Fetches posts via listSkoolPosts() from state/lib/community.ts
  • Filters by recency (last 30 min)
  • Classifies as question (keyword heuristic)
  • Applies threshold (question OR comment_count >= 2)
  • Writes question_card JSON per post
  • Logs eval row to state/log/evals.ndjson

Quick ref

# Run scanner
npx tsx state/lib/skool-q-scanner.ts

# View output
cat /tmp/skool-q-scanner-output.ndjson | jq .

# Check recent evals
tail -5 state/log/evals.ndjson | jq 'select(.skill=="skool-q-scanner")'

Action handlers

  • skool/post → opens post URL in browser (handler TBD)
  • skool/dm → sends DM to author (requires browser automation, placeholder for now)

Known gaps (2026-04-29)

  • Question classifier is keyword-based; misses implicit asks like "need help with X"
  • DM handler not wired yet (requires sendSkoolReply or new DM function)
  • Does not fetch full post content (uses feed preview only)

When you hit a gap: log [LOGGED] to state/log/loader-feedback.log, enqueue via state/regen/enqueue.sh, let the Stop hook drain it.


Self-correction footer: If this loader is incomplete or wrong, either (a) fix it inline + log [FIXED] below, or (b) log [LOGGED] + append the gap to state/log/loader-feedback.log. The Stop hook will regenerate.

<!-- No fixes yet -->


<!-- kernel-ok: PiD self-correcting loader footer -->

Self-correcting loader (PID feedback)

Before you finish, log the result:

echo "[$(date -u +%FT%TZ)] skool-q-scanner: <what was missing or fixed> [FIXED|LOGGED] action_kind=<kind>" >> state/log/loader-feedback.log

OpenUI Resource

  • Skill-owned OpenUI Lang resource: state/skills/skool-q-scanner/resources/ui.openui. Read it before rendering or editing this skill's generated component surface.
  • Treat this resource as a first-class artifact of the skill, not a generic chat response. Improve it when the skill's user-facing output needs to become richer.
  • System resources compose OpenUI primitives and inherit SnappyChat tokens. Use ui_contract: branded in SKILL.md only for deliberate platform or client visuals.

api.ts- the code it can call

#!/usr/bin/env node
/**
 * snappy-skool-q-scanner/api.ts
 * 
 * Scans Skool community posts from the last 30 minutes and surfaces questions or active threads.
 * Outputs question_card JSON for qualifying posts.
 */

import { listSkoolPosts } from './community.ts';
import { score } from './eval.ts';
import { writeFileSync } from 'fs';

interface SkoolPost {
  id: string;
  slug: string;
  title: string;
  content: string;
  author: {
    id: string;
    name: string;
  };
  comment_count: number;
  created_at: string;
}

interface QuestionCard {
  kind: 'question_card';
  post_id: string;
  post_slug: string;
  title: string;
  author: string;
  comment_count: number;
  preview: string;
  permalink: string;
  actions: Array<{
    label: string;
    handler: string;
    args: Record<string, string>;
  }>;
}

const QUESTION_WORDS = ['how', 'what', 'where', 'when', 'why', 'can', 'could', 'should', 'do', 'does', 'is', 'are'];

/**
 * Check if text contains question indicators
 */
function isQuestion(text: string): boolean {
  if (!text) return false;
  
  // Check for question mark
  if (text.includes('?')) return true;
  
  // Check for question word starters
  const lower = text.toLowerCase().trim();
  return QUESTION_WORDS.some(word => lower.startsWith(word + ' '));
}

/**
 * Check if post was created in the last N minutes
 */
function isRecent(createdAt: string, minutesAgo: number): boolean {
  const postTime = new Date(createdAt).getTime();
  const cutoff = Date.now() - (minutesAgo * 60 * 1000);
  return postTime >= cutoff;
}

/**
 * Format post as question_card
 */
function formatQuestionCard(post: SkoolPost): QuestionCard {
  const preview = post.content.slice(0, 120).trim();
  
  return {
    kind: 'question_card',
    post_id: post.id,
    post_slug: post.slug,
    title: post.title,
    author: post.author.name,
    comment_count: post.comment_count,
    preview,
    permalink: `https://www.skool.com/snappy/${post.slug}`,
    actions: [
      {
        label: 'View Post',
        handler: 'skool/post',
        args: { slug: post.slug }
      },
      {
        label: 'DM Author',
        handler: 'skool/dm',
        args: { user_id: post.author.id }
      }
    ]
  };
}

/**
 * Main scanner function
 */
export async function scanRecentQuestions(
  minutesAgo: number = 30,
  commentThreshold: number = 2
): Promise<QuestionCard[]> {
  const runId = `skool-q-scanner-${Date.now()}`;
  
  try {
    // Fetch posts
    const response = await listSkoolPosts(1, 50); // Fetch more to ensure we catch recent ones
    const allPosts = response.posts || [];
    
    // Filter by recency
    const recentPosts = allPosts.filter((p: SkoolPost) => isRecent(p.created_at, minutesAgo));
    
    // Filter by criteria: question OR active (>=2 comments)
    const qualifying = recentPosts.filter((p: SkoolPost) => {
      const hasQuestion = isQuestion(p.title) || isQuestion(p.content);
      const isActive = p.comment_count >= commentThreshold;
      return hasQuestion || isActive;
    });
    
    // Format as question cards
    const cards = qualifying.map(formatQuestionCard);
    
    // Write output
    const outputPath = '/tmp/skool-q-scanner-output.ndjson';
    const lines = cards.map(c => JSON.stringify(c)).join('\n');
    writeFileSync(outputPath, lines + '\n');
    
    // Log summary
    console.log(`Found ${cards.length} questions/active threads in the last ${minutesAgo} minutes.`);
    console.log(`Output: ${outputPath}`);
    
    // Score eval
    score('skool-q-scanner', runId, {
      
      
      score: 1,
      criteria: {
        fetch_success: { score: 1, rationale: `Fetched ${allPosts.length} posts successfully` },
        recency_filter: { score: 1, rationale: `Filtered to ${recentPosts.length} posts from last ${minutesAgo}min` },
        question_classifier: { score: 1, rationale: `Applied question heuristic (? or question words)` },
        threshold_applied: { score: 1, rationale: `Applied threshold: question OR comment_count >= ${commentThreshold}` },
        output_shape: { score: 1, rationale: `Wrote ${cards.length} valid question_card JSON lines` }
      },
      note: `Scanned ${recentPosts.length} recent posts, surfaced ${cards.length} qualifying items`
    });
    
    return cards;
    
  } catch (error: any) {
    // Log failure
    score('skool-q-scanner', runId, {
      
      
      score: 0,
      criteria: {
        fetch_success: { score: 0, rationale: `Failed: ${error.message}` },
        recency_filter: { score: 0, rationale: 'Not reached' },
        question_classifier: { score: 0, rationale: 'Not reached' },
        threshold_applied: { score: 0, rationale: 'Not reached' },
        output_shape: { score: 0, rationale: 'Not reached' }
      },
      note: `Error during scan: ${error.message}`
    });
    
    throw error;
  }
}

// CLI entrypoint
if (import.meta.url === `file://${process.argv[1]}`) {
  scanRecentQuestions()
    .then(() => process.exit(0))
    .catch((err) => {
      console.error('Scan failed:', err);
      process.exit(1);
    });
}

scripts- helper scripts it can run

prose-only skill - 2 inline code blocks live in SKILL.md above (no state/bin/ sidecar yet).

how we check it- the checks, plus the last 10 runs

rubric auto no rubric declared
recent mean 1.00 · 10 runs actor/auditor: unverifiable
deps none declared
timestamp verb score primary_issue artifact
2026-05-03 15:31Z - 1.00 - -
2026-05-03 14:31Z - 1.00 - -
2026-05-03 13:31Z - 1.00 - -
2026-05-03 12:31Z - 1.00 - -
2026-05-03 11:31Z - 1.00 - -
2026-05-03 10:31Z - 1.00 - -
2026-05-03 09:31Z - 1.00 - -
2026-05-03 08:31Z - 1.00 - -
2026-05-03 08:00Z - 1.00 - -
2026-05-03 07:31Z - 1.00 - -