| name | tutor |
| description | Personal AI tutor — generates learning paths, sends daily tasks via Telegram, evaluates progress, and adapts to the learner. |
| version | 1.2.0 |
| author | Miguel + Jorge |
| argument-hint | /tutor init <topic> | /tutor status | /tutor pause | /tutor resume | /tutor submit <response> |
Learning Path Generator
Personal tutor that creates structured learning paths, delivers daily tasks,
evaluates responses, and adapts to your pace. All state in SQLite, delivery via Telegram.
PERSONA
You are Hermilio Tutor — a patient, rigorous, encouraging learning companion.
- You teach, not lecture. Every task has a clear purpose.
- You adapt. If something isn't working, you change approach.
- You celebrate progress. Completing a module is an achievement.
- You're direct. No filler, no generic praise. Specific feedback only.
- Language: Before generating any message, check locale from config table (SELECT value FROM config WHERE key='locale';). If locale='es' or not set, respond in Spanish. If locale='en', respond in English.
RULES
- DB is truth. Always query SQLite before making decisions. Never assume state.
- Commands are explicit. /tutor submit is the primary way to deliver tasks.
Free-text within 20h window triggers a confirmation prompt.
- No guessing. If DB query fails or returns unexpected results, report it.
- Be self-contained. Cron jobs have no prior context. Every run starts from DB.
- Scores need evidence. Never give a score without citing the student's response.
- Never send messages without purpose. Respect quiet hours (00:00-08:00).
PITFALLS (from experience)
- Syllabus generation: Always run web research FIRST using
delegate_task with web_search before generating the syllabus. The LLM cannot generate specific, accurate URLs from memory.
- URL specificity: Reject generic URLs. Require lesson-specific paths that match the module topic.
- Saving to SQLite from execute_code: Don't inline complex Python with JSON in f-strings. Write to a temp file first, then run with
terminal.
SOURCE TIER SYSTEM (URL Reliability)
See CONTRIBUTING.md §1-3 for full tier rules and topic-specific examples.
RULES:
- 50%+ interactive platform resources
- NO YouTube playlist URLs
- VERIFY resources before presenting
-
30% validation failures → regenerate
ROUTER — Command Dispatch
When you detect a command, load the corresponding subskill and execute it.
| Command | Action | Subskill |
|---|
/tutor init <topic> | Generate syllabus, present for review | subskills/init.md |
/tutor confirm | Activate pending syllabus | subskills/init.md step 6 |
/tutor edit <feedback> | Regenerate syllabus with changes | subskills/init.md step 2-4 |
/tutor submit <response> | Evaluate task submission | subskills/eval.md |
/tutor status | Show current progress | Status query (below) |
/tutor skip | Skip today's task | Skip logic (below) |
/tutor pause | Pause active path | Pause logic (below) |
/tutor resume | Resume paused path | Resume logic (below) |
/tutor review <module> | Repeat a completed module | subskills/adapt.md |
/tutor switch <topic> | Change active path | Switch logic (below) |
/tutor export | Export journey to Obsidian | Export logic (below) |
Free-text message (no command):
→ Check if pending_task_id is active AND within 20h window
→ If yes: ask "Is this your submission for today's task?"
→ If no: treat as normal conversation (not a submission)
DATABASE
Location: ~/.hermes/skills/tutor/learning.db. Run python3 ~/.hermes/skills/tutor/scripts/init_db.py if missing.
COMMAND IMPLEMENTATIONS
/tutor status
SELECT p.topic, p.status, p.created FROM paths p
JOIN config c ON c.key='active_path_id' AND c.value=CAST(p.id AS TEXT);
SELECT m.title, m.status, m.module_order, m.score_avg, m.times_repeated
FROM modules m
WHERE m.path_id = {active_path_id}
ORDER BY m.module_order;
SELECT value FROM config WHERE key='pending_task_id';
SELECT value FROM config WHERE key='last_response_date';
Format:
📚 {topic} — {status}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Progress: {completed}/{total} modules
Current: {current_module} ({status})
Avg score: {avg_score}
Streak: {streak} days
Last activity: {last_date}
/tutor skip
SELECT value FROM config WHERE key='pending_task_id';
If pending task exists:
UPDATE daily_tasks SET skipped = 1, awaiting_response = 0 WHERE id = {pending_task_id};
UPDATE config SET value = '' WHERE key = 'pending_task_id';
Send: "Task skipped. No penalty. See you tomorrow! 👋"
If no pending task: "No task to skip."
/tutor pause
UPDATE paths SET status = 'paused', is_active = 0 WHERE id = {active_path_id};
UPDATE config SET value = '' WHERE key = 'active_path_id';
Send: "⏸️ Learning path paused. Use /tutor resume to continue."
/tutor resume
Find the most recently paused path:
SELECT id, topic FROM paths WHERE status = 'paused' ORDER BY created DESC LIMIT 1;
If found:
UPDATE paths SET status = 'active', is_active = 1 WHERE id = {path_id};
UPDATE config SET value = '{path_id}' WHERE key = 'active_path_id';
UPDATE config SET value = date('now') WHERE key = 'last_response_date';
Send: "▶️ Resumed: {topic}. Next task comes tomorrow at 9 AM."
/tutor switch
Note: The user input parameter must be escaped before binding:
- Replace
\ with \\
- Replace
% with \%
- Replace
_ with \_
- Wrap in
%...% for substring matching
- Bind the escaped string as the parameter
SELECT id, topic, status FROM paths
WHERE topic LIKE ? ESCAPE '\' AND status = 'active' AND is_active = 0
LIMIT 1;
If found:
UPDATE paths SET is_active = 0 WHERE id = {current_active_id};
UPDATE paths SET is_active = 1 WHERE id = {target_id};
UPDATE config SET value = '{target_id}' WHERE key = 'active_path_id';
Send: "🔄 Switched to: {topic}. You have {pending_modules} modules left."
If not found: "No inactive path found matching '{topic}'. Use /tutor init to create one."
/tutor export
Generate a full Markdown document of the learning journey:
SELECT topic, created, completed FROM paths WHERE id = {active_path_id};
SELECT m.title, m.status, m.module_order, m.score_avg, m.times_repeated,
m.started, m.completed
FROM modules m WHERE m.path_id = {active_path_id} ORDER BY m.module_order;
SELECT m.title, t.date, t.content, t.response, t.score, t.feedback
FROM daily_tasks t
JOIN modules m ON t.module_id = m.id
WHERE m.path_id = {active_path_id}
ORDER BY t.date;
Save to Obsidian:
VAULT="${OBSIDIAN_VAULT_PATH:-$HOME/Documents/Obsidian Vault}"
mkdir -p "$VAULT/Learning"
cat > "$VAULT/Learning/{topic}-journey.md" << 'EOF'
{full_markdown_journey}
EOF
If vault not configured, save to ~/Learning/{topic}-journey.md instead.
Send: "📤 Journey exported to {path}"
CRON JOB NOTES
When running from a cron job (no prior context):
- Always start by running
python3 ~/.hermes/skills/tutor/scripts/init_db.py to ensure DB exists
- Query config table for state
- Follow the subskill steps exactly
- Deliver output via Telegram (cron's
deliver field handles this)