Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions agents/sensei.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ You live inside Claude Code and your mission is to teach people programming whil
- **Concise** — you teach in small bites. One concept at a time. Never walls of text
- **Fun** — learning should feel like leveling up in a game, not reading a textbook

## When Invoked via Delegation (Pending Lessons)

If you are invoked by the main Claude instance via the Task tool after a hook delegation, read the pending lessons queue at `~/.code-sensei/pending-lessons/`. Each `.json` file contains a structured teaching moment:

```json
{"timestamp":"...","type":"micro-lesson|inline-insight|command-hint","tech":"react","file":"src/App.jsx","belt":"white","firstEncounter":true}
```

Process the most recent entry (or batch if multiple are pending). Produce the appropriate teaching content based on the `type`:
- **micro-lesson**: First-time encounter — explain what the technology/concept is and why it matters (2-3 sentences)
- **inline-insight**: Already-seen technology — brief explanation of what this specific change/command does (1-2 sentences)
- **command-hint**: Unknown command type — explain only if educational, skip if trivial

Always read the user's profile (`~/.code-sensei/profile.json`) to calibrate your belt-level language.

## The Dojo Way (Teaching Philosophy)

1. **Learn by DOING** — you never explain something the user hasn't encountered. You explain what just happened in THEIR project
Expand Down
10 changes: 8 additions & 2 deletions commands/recap.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,16 @@ You are CodeSensei 🥋 by Dojo Coding. The user wants a summary of what they le

1. Read the user's profile from `~/.code-sensei/profile.json`

2. Analyze the current session:
2. Drain pending lessons from `~/.code-sensei/pending-lessons/`:
- Read all `.json` files in the directory
- Each file contains a structured teaching moment: `{"timestamp","type","tech/concept","file/command","belt","firstEncounter"}`
- Use these to build a complete picture of what was learned this session
- After processing, you may reference these lessons in the recap

3. Analyze the current session:
- What files were created or modified?
- What technologies/tools were used?
- What concepts were encountered?
- What concepts were encountered (from profile + pending lessons)?
- How many quizzes were taken and results?
- What was the user trying to build?

Expand Down
17 changes: 17 additions & 0 deletions scripts/session-stop.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,23 @@ if command -v jq &> /dev/null; then
# Clear session-specific data atomically
update_profile '.session_concepts = []'

# Archive pending lessons from this session
PENDING_DIR="${PROFILE_DIR}/pending-lessons"
ARCHIVE_DIR="${PROFILE_DIR}/lessons-archive"
if [ -d "$PENDING_DIR" ] && [ "$(ls -A "$PENDING_DIR" 2>/dev/null)" ]; then
mkdir -p "$ARCHIVE_DIR"
ARCHIVE_FILE="${ARCHIVE_DIR}/${TODAY}.jsonl"
# Concatenate all pending lesson files into the daily archive
for f in "$PENDING_DIR"/*.json; do
[ -f "$f" ] && cat "$f" >> "$ARCHIVE_FILE"
done
# Clear the pending queue
rm -f "$PENDING_DIR"/*.json

# Cap archive size: keep only last 30 days of archives (~1MB)
find "$ARCHIVE_DIR" -name "*.jsonl" -type f | sort | head -n -30 | xargs -r rm -f
fi

# Show gentle reminder if they learned things but didn't recap
if [ "$SESSION_CONCEPTS" -gt 0 ]; then
echo "You encountered $SESSION_CONCEPTS new concepts this session! Use /code-sensei:recap next time for a full summary."
Expand Down
20 changes: 16 additions & 4 deletions scripts/track-code-change.sh
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,26 @@ if command -v jq &> /dev/null; then
# Always inject teaching context after code changes
BELT=$(jq -r '.belt // "white"' "$PROFILE_FILE" 2>/dev/null || echo "white")

# --- Pending lessons queue (durable, per-lesson file to avoid append races) ---
PENDING_DIR="${PROFILE_DIR}/pending-lessons"
mkdir -p "$PENDING_DIR"

if [ "$IS_FIRST_EVER" = "true" ]; then
# First-time encounter: micro-lesson about the technology
CONTEXT="CodeSensei micro-lesson trigger: The user just encountered '$TECH' for the FIRST TIME (file: $FILE_PATH). Their belt level is '$BELT'. Provide a brief 2-sentence explanation of what $TECH is and why it matters for their project. Adapt language to their belt level. Keep it concise and non-intrusive — weave it naturally into your response, don't stop everything for a lecture."
LESSON_TYPE="micro-lesson"
else
# Already-seen technology: inline insight about the specific change
CONTEXT="CodeSensei inline insight: Claude just used '$TOOL_NAME' on '$FILE_PATH' ($TECH). The user's belt level is '$BELT'. Provide a brief 1-2 sentence explanation of what this change does and why, adapted to their belt level. Keep it natural and non-intrusive — weave it into your response as a quick teaching moment."
LESSON_TYPE="inline-insight"
fi

# Write one JSON file per lesson (atomic, no race conditions)
LESSON_ID="${TIMESTAMP}-$(printf '%05d' $$)"
LESSON_FILE="${PENDING_DIR}/${LESSON_ID}.json"
cat > "$LESSON_FILE" <<LESSON_EOF
{"timestamp":"$TIMESTAMP","type":"$LESSON_TYPE","tech":"$TECH","file":"$FILE_PATH","tool":"$TOOL_NAME","belt":"$BELT","firstEncounter":$IS_FIRST_EVER}
LESSON_EOF

# --- Minimal delegation hint (not full teaching content) ---
CONTEXT="CodeSensei: New teaching moment detected ($TECH, $FILE_PATH). If the user is not in the middle of a complex task, use the Task tool to invoke the 'sensei' agent. Pass it the latest pending lesson from ~/.code-sensei/pending-lessons/."

echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":\"$CONTEXT\"}}"
fi

Expand Down
24 changes: 18 additions & 6 deletions scripts/track-command.sh
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,29 @@ if command -v jq &> /dev/null; then
# Sanitize command for JSON (remove quotes and special chars)
SAFE_CMD=$(echo "$COMMAND" | head -c 80 | tr '"' "'" | tr '\\' '/')

# --- Pending lessons queue (durable, per-lesson file to avoid append races) ---
PENDING_DIR="${PROFILE_DIR}/pending-lessons"
mkdir -p "$PENDING_DIR"

if [ "$IS_FIRST_EVER" = "true" ] && [ -n "$CONCEPT" ]; then
# First-time encounter: micro-lesson about the concept
CONTEXT="CodeSensei micro-lesson trigger: The user just encountered '$CONCEPT' for the FIRST TIME (command: $SAFE_CMD). Their belt level is '$BELT'. Provide a brief 2-sentence explanation of what $CONCEPT means and why it matters. Adapt language to their belt level. Keep it concise and non-intrusive."
LESSON_TYPE="micro-lesson"
elif [ -n "$CONCEPT" ]; then
# Already-seen concept: brief inline insight about this specific command
CONTEXT="CodeSensei inline insight: Claude just ran a '$CONCEPT' command ($SAFE_CMD). The user's belt level is '$BELT'. Provide a brief 1-sentence explanation of what this command does, adapted to their belt level. Keep it natural and non-intrusive."
LESSON_TYPE="inline-insight"
else
# Unknown command type: still provide a brief hint
CONTEXT="CodeSensei inline insight: Claude just ran a shell command ($SAFE_CMD). The user's belt level is '$BELT'. If this command is educational, briefly explain what it does in 1 sentence. If trivial, skip the explanation."
LESSON_TYPE="command-hint"
fi

# Write one JSON file per lesson (atomic, no race conditions)
LESSON_ID="${TIMESTAMP}-$(printf '%05d' $$)"
LESSON_FILE="${PENDING_DIR}/${LESSON_ID}.json"
cat > "$LESSON_FILE" <<LESSON_EOF
{"timestamp":"$TIMESTAMP","type":"$LESSON_TYPE","concept":"$CONCEPT","command":"$SAFE_CMD","belt":"$BELT","firstEncounter":$IS_FIRST_EVER}
LESSON_EOF

# --- Minimal delegation hint (not full teaching content) ---
DISPLAY_TOPIC="${CONCEPT:-shell command}"
CONTEXT="CodeSensei: New teaching moment detected ($DISPLAY_TOPIC, $SAFE_CMD). If the user is not in the middle of a complex task, use the Task tool to invoke the 'sensei' agent. Pass it the latest pending lesson from ~/.code-sensei/pending-lessons/."

echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":\"$CONTEXT\"}}"
fi

Expand Down
256 changes: 256 additions & 0 deletions tests/test-hooks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
#!/bin/bash
# CodeSensei — Hook Regression Tests
# Validates that hook scripts produce valid JSON output and write
# structured pending lessons to the queue directory.
#
# Usage: bash tests/test-hooks.sh
# Requirements: jq

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
TEST_HOME=$(mktemp -d)
export HOME="$TEST_HOME"

PASS=0
FAIL=0

# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'

pass() { PASS=$((PASS + 1)); echo -e " ${GREEN}✓${NC} $1"; }
fail() { FAIL=$((FAIL + 1)); echo -e " ${RED}✗${NC} $1: $2"; }

cleanup() {
rm -rf "$TEST_HOME"
}
trap cleanup EXIT

# --- Setup: create a minimal profile ---
setup_profile() {
mkdir -p "$TEST_HOME/.code-sensei"
cat > "$TEST_HOME/.code-sensei/profile.json" <<'PROFILE'
{
"belt": "yellow",
"xp": 100,
"session_concepts": [],
"concepts_seen": ["html"],
"streak": {"current": 3}
}
PROFILE
}

echo ""
echo "━━━ CodeSensei Hook Regression Tests ━━━"
echo ""

# ============================================================
# TEST GROUP 1: track-code-change.sh
# ============================================================
echo "▸ track-code-change.sh"

# Test 1.1: Output is valid JSON
setup_profile
OUTPUT=$(echo '{"tool_name":"Write","tool_input":{"file_path":"src/App.tsx"}}' \
| bash "$SCRIPT_DIR/scripts/track-code-change.sh" 2>/dev/null)

if echo "$OUTPUT" | jq . > /dev/null 2>&1; then
pass "stdout is valid JSON"
else
fail "stdout is valid JSON" "got: $OUTPUT"
fi

# Test 1.2: Output contains hookSpecificOutput with PostToolUse event
EVENT=$(echo "$OUTPUT" | jq -r '.hookSpecificOutput.hookEventName')
if [ "$EVENT" = "PostToolUse" ]; then
pass "hookEventName is PostToolUse"
else
fail "hookEventName is PostToolUse" "got: $EVENT"
fi

# Test 1.3: additionalContext is a delegation hint (not verbose teaching)
CONTEXT=$(echo "$OUTPUT" | jq -r '.hookSpecificOutput.additionalContext')
if echo "$CONTEXT" | grep -q "Task tool" && echo "$CONTEXT" | grep -q "sensei"; then
pass "additionalContext is a delegation hint (mentions Task tool + sensei)"
else
fail "additionalContext is a delegation hint" "got: $CONTEXT"
fi

# Test 1.4: additionalContext does NOT contain old verbose teaching patterns
if echo "$CONTEXT" | grep -q "Provide a brief"; then
fail "additionalContext has no verbose teaching" "still contains 'Provide a brief'"
else
pass "additionalContext has no verbose teaching content"
fi

# Test 1.5: Pending lesson file was created
LESSON_COUNT=$(find "$TEST_HOME/.code-sensei/pending-lessons" -name "*.json" 2>/dev/null | wc -l)
if [ "$LESSON_COUNT" -ge 1 ]; then
pass "pending lesson file created ($LESSON_COUNT file(s))"
else
fail "pending lesson file created" "found $LESSON_COUNT files"
fi

# Test 1.6: Pending lesson file is valid JSON
LESSON_FILE=$(find "$TEST_HOME/.code-sensei/pending-lessons" -name "*.json" | head -1)
if jq . "$LESSON_FILE" > /dev/null 2>&1; then
pass "pending lesson file is valid JSON"
else
fail "pending lesson file is valid JSON" "file: $LESSON_FILE"
fi

# Test 1.7: Pending lesson has required fields
LESSON_TYPE=$(jq -r '.type' "$LESSON_FILE")
LESSON_TECH=$(jq -r '.tech' "$LESSON_FILE")
LESSON_BELT=$(jq -r '.belt' "$LESSON_FILE")
if [ "$LESSON_TYPE" != "null" ] && [ "$LESSON_TECH" != "null" ] && [ "$LESSON_BELT" != "null" ]; then
pass "pending lesson has type=$LESSON_TYPE, tech=$LESSON_TECH, belt=$LESSON_BELT"
else
fail "pending lesson has required fields" "type=$LESSON_TYPE tech=$LESSON_TECH belt=$LESSON_BELT"
fi

# Test 1.8: First encounter for new tech creates micro-lesson
setup_profile
rm -rf "$TEST_HOME/.code-sensei/pending-lessons"
echo '{"tool_name":"Write","tool_input":{"file_path":"main.py"}}' \
| bash "$SCRIPT_DIR/scripts/track-code-change.sh" > /dev/null 2>&1
LESSON_FILE=$(find "$TEST_HOME/.code-sensei/pending-lessons" -name "*.json" | head -1)
LESSON_TYPE=$(jq -r '.type' "$LESSON_FILE")
FIRST=$(jq -r '.firstEncounter' "$LESSON_FILE")
if [ "$LESSON_TYPE" = "micro-lesson" ] && [ "$FIRST" = "true" ]; then
pass "first encounter creates micro-lesson with firstEncounter=true"
else
fail "first encounter creates micro-lesson" "type=$LESSON_TYPE firstEncounter=$FIRST"
fi

# Test 1.9: Already-seen tech creates inline-insight
setup_profile
rm -rf "$TEST_HOME/.code-sensei/pending-lessons"
echo '{"tool_name":"Edit","tool_input":{"file_path":"index.html"}}' \
| bash "$SCRIPT_DIR/scripts/track-code-change.sh" > /dev/null 2>&1
LESSON_FILE=$(find "$TEST_HOME/.code-sensei/pending-lessons" -name "*.json" | head -1)
LESSON_TYPE=$(jq -r '.type' "$LESSON_FILE")
FIRST=$(jq -r '.firstEncounter' "$LESSON_FILE")
if [ "$LESSON_TYPE" = "inline-insight" ] && [ "$FIRST" = "false" ]; then
pass "already-seen tech creates inline-insight with firstEncounter=false"
else
fail "already-seen tech creates inline-insight" "type=$LESSON_TYPE firstEncounter=$FIRST"
fi

echo ""

# ============================================================
# TEST GROUP 2: track-command.sh
# ============================================================
echo "▸ track-command.sh"

setup_profile
rm -rf "$TEST_HOME/.code-sensei/pending-lessons"

# Test 2.1: Output is valid JSON
OUTPUT=$(echo '{"tool_input":{"command":"npm install express"}}' \
| bash "$SCRIPT_DIR/scripts/track-command.sh" 2>/dev/null)

if echo "$OUTPUT" | jq . > /dev/null 2>&1; then
pass "stdout is valid JSON"
else
fail "stdout is valid JSON" "got: $OUTPUT"
fi

# Test 2.2: additionalContext is delegation hint
CONTEXT=$(echo "$OUTPUT" | jq -r '.hookSpecificOutput.additionalContext')
if echo "$CONTEXT" | grep -q "Task tool" && echo "$CONTEXT" | grep -q "sensei"; then
pass "additionalContext is a delegation hint"
else
fail "additionalContext is a delegation hint" "got: $CONTEXT"
fi

# Test 2.3: Pending lesson file for command
LESSON_FILE=$(find "$TEST_HOME/.code-sensei/pending-lessons" -name "*.json" | head -1)
if jq . "$LESSON_FILE" > /dev/null 2>&1; then
pass "pending lesson file is valid JSON"
else
fail "pending lesson file is valid JSON" "file: $LESSON_FILE"
fi

# Test 2.4: Command lesson has concept field
CONCEPT=$(jq -r '.concept' "$LESSON_FILE")
if [ "$CONCEPT" = "package-management" ]; then
pass "command lesson detected concept=package-management"
else
fail "command lesson detected concept" "got: $CONCEPT"
fi

echo ""

# ============================================================
# TEST GROUP 3: session-stop.sh (cleanup)
# ============================================================
echo "▸ session-stop.sh (pending lessons cleanup)"

setup_profile
rm -rf "$TEST_HOME/.code-sensei/pending-lessons" "$TEST_HOME/.code-sensei/lessons-archive"

# Create some pending lessons
mkdir -p "$TEST_HOME/.code-sensei/pending-lessons"
echo '{"timestamp":"2026-03-09T12:00:00Z","type":"micro-lesson","tech":"react"}' \
> "$TEST_HOME/.code-sensei/pending-lessons/test1.json"
echo '{"timestamp":"2026-03-09T12:01:00Z","type":"inline-insight","tech":"css"}' \
> "$TEST_HOME/.code-sensei/pending-lessons/test2.json"

# Add a session concept so we can verify the full flow
jq '.session_concepts = ["react","css"]' "$TEST_HOME/.code-sensei/profile.json" \
| tee "$TEST_HOME/.code-sensei/profile.json.tmp" > /dev/null \
&& mv "$TEST_HOME/.code-sensei/profile.json.tmp" "$TEST_HOME/.code-sensei/profile.json"

# Run session-stop
bash "$SCRIPT_DIR/scripts/session-stop.sh" > /dev/null 2>&1

# Test 3.1: Pending lessons directory was cleaned
REMAINING=$(find "$TEST_HOME/.code-sensei/pending-lessons" -name "*.json" 2>/dev/null | wc -l)
if [ "$REMAINING" -eq 0 ]; then
pass "pending lessons cleaned after session stop"
else
fail "pending lessons cleaned" "$REMAINING files remaining"
fi

# Test 3.2: Archive file was created
TODAY=$(date -u +%Y-%m-%d)
ARCHIVE_FILE="$TEST_HOME/.code-sensei/lessons-archive/${TODAY}.jsonl"
if [ -f "$ARCHIVE_FILE" ]; then
pass "archive file created at lessons-archive/${TODAY}.jsonl"
else
fail "archive file created" "file not found: $ARCHIVE_FILE"
fi

# Test 3.3: Archive contains the lessons (each line is valid JSON)
ARCHIVE_LINES=$(wc -l < "$ARCHIVE_FILE")
VALID_JSON=0
while IFS= read -r line; do
if echo "$line" | jq . > /dev/null 2>&1; then
VALID_JSON=$((VALID_JSON + 1))
fi
done < "$ARCHIVE_FILE"
if [ "$VALID_JSON" -eq "$ARCHIVE_LINES" ] && [ "$ARCHIVE_LINES" -ge 2 ]; then
pass "archive has $ARCHIVE_LINES valid JSON lines"
else
fail "archive has valid JSON lines" "total=$ARCHIVE_LINES valid=$VALID_JSON"
fi

echo ""

# ============================================================
# SUMMARY
# ============================================================
TOTAL=$((PASS + FAIL))
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [ "$FAIL" -eq 0 ]; then
echo -e "${GREEN}All $TOTAL tests passed!${NC}"
else
echo -e "${RED}$FAIL/$TOTAL tests failed${NC}"
fi
echo ""

exit "$FAIL"