diff --git a/agents/sensei.md b/agents/sensei.md index 36bfbb8..8c80501 100644 --- a/agents/sensei.md +++ b/agents/sensei.md @@ -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 diff --git a/commands/recap.md b/commands/recap.md index c5b8b33..4ffa602 100644 --- a/commands/recap.md +++ b/commands/recap.md @@ -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? diff --git a/scripts/session-stop.sh b/scripts/session-stop.sh index 42fa573..e6c31a9 100755 --- a/scripts/session-stop.sh +++ b/scripts/session-stop.sh @@ -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." diff --git a/scripts/track-code-change.sh b/scripts/track-code-change.sh index 90605e5..1aeeae9 100755 --- a/scripts/track-code-change.sh +++ b/scripts/track-code-change.sh @@ -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" < /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" < "$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"