Exported functions in state/lib/mine.ts. .md file to compare - side-by-side diff against mine
mine
description: "Triggers on prompt mention of 'mine'."
What it does for you
Pulls reusable material out of your meetings and work.
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.
For developers how this skill is built, graded, and how it runs
at a glance- the short version
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.
state/skills/mine/SKILL.md
present
state/lib/mine.ts
present
state/bin/mine/
not present
state/skills/mine/AGENTS.md
present
how it's graded - what counts as a good run 3 criteria · 2 deterministic · 1 judge
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.
how it runs - the shared frame every skill uses 5/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.
state/log/pending-eval.ndjson - Mining drafts must CITE, never paraphrase — verbatim stitching from transcripts, plus checkTone + requireCitations gates before return (carryover rule from kernel mining pods)
- Extract Robert-signal: what Robert demos / brings up / repeats — not generic patterns. Do not surface decoy author-scoreboard counts.
- Mining "next moves" are RAW SIGNAL, not action plans — always filter through Robert's positioning before drafting client-facing output
- This is a Phase 0.5 port; the state/lib/mine.ts surface is mechanical. The kernel snappy-mine had richer mining-pod shape; cross-reference that .md if details are missing.
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.
- Loading feedback rows…
how the work flows- who makes it, who checks it
import from `state/lib/mine.ts` — `meetingsByParticipant`, `listAllTranscripts`, `listPendingTranscripts`, `getMineFiles`, `getManifest`, `getMinedSources`, `persistNuggets`, `computeMineMetric
SKILL.md- the skill, written out in plain English
mine
Content mining operations for all snappy-* skills.
Ported from kernel snappy-mine in Phase 0.5. See state/lib/mine.ts for the full API surface.
Steps
meetingsByParticipant()- seestate/lib/mine.tslistAllTranscripts()- seestate/lib/mine.tslistPendingTranscripts()- seestate/lib/mine.tsgetMineFiles()- seestate/lib/mine.tsgetManifest()- seestate/lib/mine.tsgetMinedSources()- seestate/lib/mine.tspersistNuggets()- seestate/lib/mine.tscomputeMineMetric()- seestate/lib/mine.ts
Eval
Actor: the exported functions in state/lib/mine.ts. Auditor: none wired yet - eval is manual (Robert review). File a state/log/pending-eval.ndjson row on each run.
Score convention:
| Outcome | Score |
|---|---|
| Pass on first try | 1.0 |
| Failed first, auto-fix applied, re-check passed | 0.5 |
| Still failing or unrecoverable | 0.0 |
Gotchas
via the Phase 0.5 driver. Only these rewrites were applied: already in state/lib/)
realpathSync(process.argv[1])CLI guard wrapped in try/catch
- See the kernel SKILL.md for the original long-form guidance if you need it
(read-only reference at the kernel path above).
Graduation
This skill is prose. Graduate by defining a deterministic auditor and flipping eval: auto.
Rubric
criteria:
- name: correct_function_call_trace
kind: deterministic
check: "The execution log verifies that the correct mining function from 'state/lib/mine.ts' was invoked based on the input provided."
- name: no_errors_in_log
kind: deterministic
check: "The 'state/log/pending-eval.ndjson' entry or skill execution logs contain no error messages related to the mining operation."
- name: output_matches_expected_shape
kind: judge
check: "The output structure produced by the mining function matches the expected data shape for the given operation."AGENTS.md- what the AI loads when this skill comes up
mine - loader
Per-turn rules for the mine skill. Full reference: state/skills/mine/SKILL.md. Do not skip these.
Critical Rules
- Mining drafts must CITE, never paraphrase - verbatim stitching from transcripts, plus
checkTone+requireCitationsgates before return (carryover rule from kernel mining pods) - Extract Robert-signal: what Robert demos / brings up / repeats - not generic patterns. Do not surface decoy author-scoreboard counts.
- Mining "next moves" are RAW SIGNAL, not action plans - always filter through Robert's positioning before drafting client-facing output
- This is a Phase 0.5 port; the
state/lib/mine.tssurface is mechanical. The kernel snappy-mine had richer mining-pod shape; cross-reference that .md if details are missing.
Commands
| ui dashboard | state/skills/mine/resources/ui.openui | |invoke: import from state/lib/mine.ts - meetingsByParticipant, listAllTranscripts, listPendingTranscripts, getMineFiles, getManifest, getMinedSources, persistNuggets, computeMineMetric |eval log: state/log/pending-eval.ndjson (manual review until shape gate added) |related: state/skills/content-mine/SKILL.md (the auto-eval mining verb with requireCitations() gate)
OpenUI Resource
- Skill-owned OpenUI Lang resource:
state/skills/mine/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: brandedin SKILL.md only for deliberate platform or client visuals.
Known Pitfalls
- Skill frontmatter says
eval: shapebut no auditor is wired - must logpending-eval.ndjson - The verb the agent usually wants is
content-mine(graduated, requireCitations gate). Reach formineonly when the lower-level transcript/manifest functions are needed. - Author-scoreboard numbers ("620 endpoints", "14 clients") cringe Robert - restructure as observations about the work
Self-Test
An agent reading this should correctly:
- [ ] Pick
content-mineoverminefor client-facing draft generation? - [ ] Cite verbatim instead of paraphrasing transcript content?
- [ ] Strip self-counts of output ("X endpoints built") before surfacing?
Self-report
If this loader fell short, append a line:
echo "[$(date -u +%FT%TZ)] mine: <what was missing>" >> state/log/loader-feedback.log
<!-- kernel-ok: historical attribution for the PID-loader pattern. -->
Self-correcting loader (PID feedback)
This is the rule that makes loaders learn. Ported from snappy-kernel, where it demonstrably worked: the snappy-dom-cartographer skill mapped Skool admin UI once, every later ship-agent inherited the full map for free.
Before you finish, do two things:
1. Fix gaps (P - proportional). MANDATORY. If this loader didn't cover your case - if you had to read another file, run an undocumented command, work around a wrong selector, or discover a quirk - you MUST attempt an Edit to this AGENTS.md before you log.
- Default action: edit this file. Surgical. Add the missing line, table
row, or code example where it belongs. No restructuring.
- Only
LOGGEDis allowed when: the fix needs >10 lines, spans multiple
files, or requires a structural rewrite. In that case the state/regen/drain.sh queue picks it up asynchronously.
- "I didn't have time" / "it's minor" / "the next agent will figure it out"
are NOT valid reasons. Minor gaps compound - that's the whole point of this loop.
- Concrete example of a valid P-fix: discover
agent-browser screenshot
takes path positionally not as --output <path>. One line in a table. Edit it in. Do not just log it.
- The goal: the next agent never has to leave the loader.
2. Log the result.
echo "[$(date -u +%FT%TZ)] mine: <what was missing or fixed> [FIXED|LOGGED]" >> state/log/loader-feedback.log
FIXED= you patched this loader inline (P-fix).LOGGED= too large for inline; the PostToolUse enqueue + Stop-hook drain
will rewrite the loader from scratch on next session-end.
Do not skip this. Every agent run must leave the system better than it found it. The loader is the setpoint; you are the sensor; the gap is the error signal; closing the gap is the correction.
api.ts- the code it can call
#!/usr/bin/env npx tsx
/**
* snappy-mine/api.ts -- Content mining operations for all snappy-* skills.
*
* Usage:
* npx tsx api.ts manifest # show manifest.json
* npx tsx api.ts sources # list all mined source files
* npx tsx api.ts files # list framework-mine-*.json files
* npx tsx api.ts persist deep-ray-sessions.json # persist nuggets to content engine DB
*
* Or import as module:
* import { getMineFiles, getManifest, persistNuggets } from "./mine.ts";
*/
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "fs";
import { join } from "path";
import { execSync } from "child_process";
import { env } from "./env.ts";
const MINED_DIR = join(process.env.HOME!, ".claude/corpus/mined");
const MANIFEST_PATH = join(MINED_DIR, "manifest.json");
const KRISP_DIR = join(process.env.HOME!, ".claude/corpus/krisp");
const KRISP_INDEX = join(KRISP_DIR, "index.json");
const SPEAKER_MAP_PATH = join(process.env.HOME!, ".claude/skills/snappy-mine/speaker-map.json");
// ---------------------------------------------------------------------------
// meetingsByParticipant
// ---------------------------------------------------------------------------
export interface MeetingMatch {
meeting_id: string;
date: string;
title: string;
participants: string[];
transcript_path: string;
}
interface SpeakerMapEntry {
full_name?: string;
aliases?: string[];
email?: string;
linkedin?: string | null;
xano_contact_id?: number | null;
}
function loadSpeakerMap(): Record<string, SpeakerMapEntry> {
if (!existsSync(SPEAKER_MAP_PATH)) return {};
try {
return JSON.parse(readFileSync(SPEAKER_MAP_PATH, "utf-8"));
} catch {
return {};
}
}
/**
* Parse frontmatter of a Krisp transcript + scrape speaker lines (`**Name | mm:ss**`).
* Returns the union of frontmatter attendees and in-transcript speakers.
* Reads only the first ~16KB of the file — enough for frontmatter + dozens of speaker lines.
*/
function readMeetingMeta(absPath: string): {
meeting_id: string;
date: string;
title: string;
participants: string[];
} | null {
let raw: string;
try {
const fd = readFileSync(absPath, "utf-8");
raw = fd.slice(0, 16_000);
} catch {
return null;
}
let meeting_id = "";
let date = "";
let title = "";
const fmAttendees: string[] = [];
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
if (fmMatch) {
const fm = fmMatch[1];
const idM = fm.match(/meeting_id:\s*(\S+)/);
if (idM) meeting_id = idM[1];
const dateM = fm.match(/date:\s*(\S+)/);
if (dateM) date = dateM[1];
const nameM = fm.match(/name:\s*"?([^"\n]+?)"?\s*$/m);
if (nameM) title = nameM[1].trim();
const attM = fm.match(/attendees:\s*\[([^\]]*)\]/);
if (attM && attM[1].trim()) {
for (const piece of attM[1].split(",")) {
const cleaned = piece.trim().replace(/^["']|["']$/g, "");
if (cleaned) fmAttendees.push(cleaned);
}
}
}
// Scrape speaker lines: **Name | 04:18**
const speakerSet = new Set<string>(fmAttendees);
const speakerRe = /\*\*([^*|]+?)\s*\|\s*\d{1,2}:\d{2}\*\*/g;
let m: RegExpExecArray | null;
while ((m = speakerRe.exec(raw)) !== null) {
const name = m[1].trim();
if (name && !/^speaker\s*\d+$/i.test(name)) speakerSet.add(name);
}
return {
meeting_id,
date,
title,
participants: Array.from(speakerSet),
};
}
/**
* Find Krisp meetings where a given participant appears.
* Matching precedence:
* 1. exact email (via speaker-map lookup)
* 2. exact name (case-insensitive)
* 3. fuzzy name (substring, case-insensitive)
*
* Returns MeetingMatch[] sorted by date desc. Capped at 50 results.
* Source of truth is the Krisp index.json — we only open transcript files
* whose filename already hints at a name match, then verify via scraped participants.
*
* NOTE: `recent_meetings` in snappy-knowledge.resolvePerson is currently []
* because this function did not exist. This is its backing store.
*/
export function meetingsByParticipant(
handle: string,
opts: { limit?: number } = {}
): MeetingMatch[] {
if (!existsSync(KRISP_INDEX)) return [];
const limit = opts.limit ?? 50;
const speakerMap = loadSpeakerMap();
// Resolve handle → set of candidate names to match against
const candidateNames = new Set<string>();
const isEmail = handle.includes("@");
if (isEmail) {
for (const [key, entry] of Object.entries(speakerMap)) {
if (entry?.email?.toLowerCase() === handle.toLowerCase()) {
candidateNames.add(key.toLowerCase());
if (entry.full_name) candidateNames.add(entry.full_name.toLowerCase());
for (const a of entry.aliases ?? []) candidateNames.add(a.toLowerCase());
}
}
if (candidateNames.size === 0) return []; // no email → can't match transcripts
} else {
candidateNames.add(handle.toLowerCase());
// Pull aliases from speaker map if this key exists
for (const [key, entry] of Object.entries(speakerMap)) {
const allNames = [key, entry?.full_name, ...(entry?.aliases ?? [])]
.filter(Boolean)
.map((s) => (s as string).toLowerCase());
if (allNames.includes(handle.toLowerCase())) {
for (const n of allNames) candidateNames.add(n);
}
}
}
const index: Record<string, string> = JSON.parse(readFileSync(KRISP_INDEX, "utf-8"));
const results: MeetingMatch[] = [];
// First pass: filename substring filter (cheap) against any candidate name token
const nameTokens = new Set<string>();
for (const n of candidateNames) {
for (const tok of n.split(/\s+/)) {
if (tok.length >= 3) nameTokens.add(tok);
}
}
for (const [meeting_id, relPath] of Object.entries(index)) {
const lowerPath = relPath.toLowerCase();
const filenameHit = Array.from(nameTokens).some((tok) => lowerPath.includes(tok));
// Always open the file if filename hits; otherwise skip (too expensive to scan all)
if (!filenameHit) continue;
const absPath = join(KRISP_DIR, relPath);
if (!existsSync(absPath)) continue;
const meta = readMeetingMeta(absPath);
if (!meta) continue;
// Verify: does any participant match a candidate name (exact or fuzzy)?
const participantsLower = meta.participants.map((p) => p.toLowerCase());
let matched = false;
for (const cand of candidateNames) {
if (participantsLower.includes(cand)) { matched = true; break; } // exact
}
if (!matched) {
for (const cand of candidateNames) {
if (participantsLower.some((p) => p.includes(cand) || cand.includes(p))) {
matched = true; break;
}
}
}
if (!matched) continue;
results.push({
meeting_id: meta.meeting_id || meeting_id,
date: meta.date,
title: meta.title || relPath,
participants: meta.participants,
transcript_path: absPath,
});
}
results.sort((a, b) => (b.date || "").localeCompare(a.date || ""));
return results.slice(0, limit);
}
/** Walk ~/.claude/corpus/krisp/YYYY/MM/*.md and return raw transcript paths. */
export function listAllTranscripts(): string[] {
const out: string[] = [];
if (!existsSync(KRISP_DIR)) return out;
for (const year of readdirSync(KRISP_DIR)) {
const yearDir = join(KRISP_DIR, year);
let stat;
try { stat = statSync(yearDir); } catch { continue; }
if (!stat.isDirectory()) continue;
for (const month of readdirSync(yearDir)) {
const monthDir = join(yearDir, month);
try { if (!statSync(monthDir).isDirectory()) continue; } catch { continue; }
for (const f of readdirSync(monthDir)) {
if (f.endsWith(".md") && !f.endsWith(".summary.md") && !f.endsWith(".nuggets.json")) {
out.push(join(monthDir, f));
}
}
}
}
return out;
}
/** Returns transcripts that are NOT yet referenced in mined/manifest.json. */
export function listPendingTranscripts(opts: { limit?: number } = {}): string[] {
const all = listAllTranscripts();
const manifest = getManifest();
const minedNames = new Set<string>();
for (const [key, entry] of Object.entries(manifest)) {
if (key === "_skipped") {
for (const s of (entry as any[]) || []) minedNames.add(typeof s === "string" ? s : s.file);
continue;
}
for (const s of (entry as any)?.source_files || []) minedNames.add(s);
}
const pending = all
.filter((p) => !minedNames.has(p.split("/").pop() || ""))
.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
return opts.limit ? pending.slice(0, opts.limit) : pending;
}
/** Lists framework-mine-*.json files in the mined directory. */
export function getMineFiles(): string[] {
if (!existsSync(MINED_DIR)) return [];
return readdirSync(MINED_DIR)
.filter((f) => f.startsWith("framework-mine-") && f.endsWith(".json"))
.sort();
}
/** Reads and returns the manifest.json contents. */
export function getManifest(): Record<string, unknown> {
if (!existsSync(MANIFEST_PATH)) return {};
return JSON.parse(readFileSync(MANIFEST_PATH, "utf-8"));
}
/** Extracts all mined source files from the manifest. */
export function getMinedSources(): string[] {
const manifest = getManifest();
const sources: string[] = [];
const sf = manifest.source_files;
if (Array.isArray(sf)) return sf;
if (sf && typeof sf === "object") {
for (const v of Object.values(sf as Record<string, unknown>)) {
if (Array.isArray(v)) sources.push(...v);
else if (typeof v === "string") sources.push(v);
}
}
return sources;
}
/** Persists nuggets from a JSON file to the content engine DB via persist-nuggets.ts. */
export function persistNuggets(jsonPath: string): string {
const absPath = jsonPath.startsWith("/") ? jsonPath : join(MINED_DIR, jsonPath);
if (!existsSync(absPath)) throw new Error(`File not found: ${absPath}`);
const script = join(process.env.HOME!, ".claude/skills/snappy-mine/persist-nuggets.ts");
return execSync(`npx tsx ${script} ${absPath}`, { encoding: "utf-8", timeout: 60_000 });
}
// --- Metrics (Step 7a) ---
const STAGED_ACTIONS_LOG = join(process.env.HOME!, ".claude/logs/staged-actions.ndjson");
type StagedRun = { ts: string; name: string; action: string };
function readStagedRunsMine(): StagedRun[] {
if (!existsSync(STAGED_ACTIONS_LOG)) return [];
const out: StagedRun[] = [];
for (const line of readFileSync(STAGED_ACTIONS_LOG, "utf-8").split("\n")) {
if (!line.trim()) continue;
try {
const j = JSON.parse(line);
if (typeof j?.name === "string" && typeof j?.ts === "string") {
out.push({ ts: j.ts, name: j.name, action: j.action || "" });
}
} catch { /* skip */ }
}
return out;
}
function withinLastDays(tsIso: string, days: number): boolean {
const t = new Date(tsIso).getTime();
if (isNaN(t)) return false;
return t >= Date.now() - days * 86400_000;
}
export function computeMineMetric(name: string): number | null {
const runs = readStagedRunsMine().filter((r) => withinLastDays(r.ts, 7));
switch (name) {
case "runs-per-week":
case "mine_runs_per_week":
return runs.filter((r) => r.name === "content-mine").length;
case "success-rate":
case "mine_success_rate": {
const mine = runs.filter((r) => r.name === "content-mine");
if (!mine.length) return null;
return mine.filter((r) => r.action !== "error").length / mine.length;
}
default:
return null;
}
}
// --- CLI ---
if ((() => { try { return import.meta.url === `file://${realpathSync(process.argv[1])}`; } catch { return false; } })()) {
(async () => {
const [, , cmd, ...args] = process.argv;
switch (cmd) {
case "metrics": {
const [name, ...rest] = args;
if (!name) {
console.error("Usage: api.ts metrics <name> [--json]");
console.error("Names: runs-per-week, success-rate");
process.exit(1);
}
const value = computeMineMetric(name);
if (rest.includes("--json")) console.log(JSON.stringify({ value }));
else console.log(value == null ? "null" : String(value));
break;
}
case "files": {
const files = getMineFiles();
console.log(`${files.length} framework-mine files:`);
for (const f of files) console.log(` ${f}`);
break;
}
case "manifest": {
const m = getManifest();
console.log(JSON.stringify(m, null, 2));
break;
}
case "sources": {
const sources = getMinedSources();
console.log(`${sources.length} mined sources:`);
for (const s of sources) console.log(` ${s}`);
break;
}
case "pending": {
const limit = args[0] ? parseInt(args[0], 10) : undefined;
const pending = listPendingTranscripts(limit ? { limit } : {});
if (args.includes("--json")) { console.log(JSON.stringify(pending, null, 2)); break; }
console.log(`${pending.length} unmined transcripts:`);
for (const p of pending) console.log(` ${p.replace(process.env.HOME!, "~")}`);
break;
}
case "meetings-by": {
const [handle, limitStr] = args;
if (!handle) { console.error("Usage: api.ts meetings-by <name|email> [limit]"); process.exit(1); }
const limit = limitStr ? parseInt(limitStr, 10) : 20;
const results = meetingsByParticipant(handle, { limit });
console.log(JSON.stringify(results, null, 2));
break;
}
case "persist": {
const [path] = args;
if (!path) { console.error("Usage: api.ts persist <json-file>"); process.exit(1); }
const output = persistNuggets(path);
console.log(output);
break;
}
default:
console.log("Usage: npx tsx api.ts [files|manifest|sources|persist] ...");
}
})();
}
scripts- helper scripts it can run
prose-only skill - 1 inline code block live in SKILL.md above (no state/bin/ sidecar yet).
how we check it- the checks, plus the last 10 runs
| timestamp | verb | score | primary_issue | artifact |
|---|---|---|---|---|
| 2026-04-25 04:11Z | - | 1.00 | - | - |
| 2026-04-21 15:58Z | - | 1.00 | - | - |
| 2026-04-21 15:56Z | - | 1.00 | - | - |
| 2026-04-21 03:53Z | - | 1.00 | - | - |
| 2026-04-25 04:11Z | - | 1.00 | - | - |
| 2026-04-21 15:58Z | - | 1.00 | - | - |
| 2026-04-21 15:56Z | - | 1.00 | - | - |
| 2026-04-21 03:53Z | - | 1.00 | - | - |
| 2026-04-25 04:11Z | - | 1.00 | - | - |
| 2026-04-21 15:58Z | - | 1.00 | - | - |