Skip to content

Commit 64cb4fe

Browse files
authored
Merge pull request #12 from DojoCodingLabs/andres/doj-2436-restructure-hook-output
feat: restructure hook output for subagent pipeline (DOJ-2436)
2 parents 47295c1 + 3bd70aa commit 64cb4fe

File tree

6 files changed

+343
-10
lines changed

6 files changed

+343
-10
lines changed

agents/sensei.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,21 @@ You live inside Claude Code and your mission is to teach people programming whil
2222
- **Concise** — you teach in small bites. One concept at a time. Never walls of text
2323
- **Fun** — learning should feel like leveling up in a game, not reading a textbook
2424

25+
## When Invoked via Delegation (Pending Lessons)
26+
27+
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:
28+
29+
```json
30+
{"timestamp":"...","type":"micro-lesson|inline-insight|command-hint","tech":"react","file":"src/App.jsx","belt":"white","firstEncounter":true}
31+
```
32+
33+
Process the most recent entry (or batch if multiple are pending). Produce the appropriate teaching content based on the `type`:
34+
- **micro-lesson**: First-time encounter — explain what the technology/concept is and why it matters (2-3 sentences)
35+
- **inline-insight**: Already-seen technology — brief explanation of what this specific change/command does (1-2 sentences)
36+
- **command-hint**: Unknown command type — explain only if educational, skip if trivial
37+
38+
Always read the user's profile (`~/.code-sensei/profile.json`) to calibrate your belt-level language.
39+
2540
## The Dojo Way (Teaching Philosophy)
2641

2742
1. **Learn by DOING** — you never explain something the user hasn't encountered. You explain what just happened in THEIR project

commands/recap.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,16 @@ You are CodeSensei 🥋 by Dojo Coding. The user wants a summary of what they le
1010

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

13-
2. Analyze the current session:
13+
2. Drain pending lessons from `~/.code-sensei/pending-lessons/`:
14+
- Read all `.json` files in the directory
15+
- Each file contains a structured teaching moment: `{"timestamp","type","tech/concept","file/command","belt","firstEncounter"}`
16+
- Use these to build a complete picture of what was learned this session
17+
- After processing, you may reference these lessons in the recap
18+
19+
3. Analyze the current session:
1420
- What files were created or modified?
1521
- What technologies/tools were used?
16-
- What concepts were encountered?
22+
- What concepts were encountered (from profile + pending lessons)?
1723
- How many quizzes were taken and results?
1824
- What was the user trying to build?
1925

scripts/session-stop.sh

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,23 @@ if [ "$SESSION_CONCEPTS" -gt 0 ]; then
6363
echo "You encountered $SESSION_CONCEPTS new concepts this session! Use /code-sensei:recap next time for a full summary."
6464
fi
6565

66+
# Archive pending lessons from this session (DOJ-2436)
67+
PENDING_DIR="${PROFILE_DIR}/pending-lessons"
68+
ARCHIVE_DIR="${PROFILE_DIR}/lessons-archive"
69+
if [ -d "$PENDING_DIR" ] && [ "$(ls -A "$PENDING_DIR" 2>/dev/null)" ]; then
70+
mkdir -p "$ARCHIVE_DIR"
71+
ARCHIVE_FILE="${ARCHIVE_DIR}/${TODAY}.jsonl"
72+
# Concatenate all pending lesson files into the daily archive
73+
for f in "$PENDING_DIR"/*.json; do
74+
[ -f "$f" ] && cat "$f" >> "$ARCHIVE_FILE"
75+
done
76+
# Clear the pending queue
77+
rm -f "$PENDING_DIR"/*.json
78+
79+
# Cap archive size: keep only last 30 days of archives (~1MB)
80+
find "$ARCHIVE_DIR" -name "*.jsonl" -type f | sort | head -n -30 | xargs -r rm -f
81+
fi
82+
6683
rm -f "$SESSION_STATE"
6784
rm -f "$PROFILE_DIR/.jq-warned"
6885

scripts/track-code-change.sh

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,12 +167,30 @@ if [ $? -ne 0 ]; then
167167
BELT="white"
168168
fi
169169

170+
# --- Pending lessons queue (durable, per-lesson file to avoid append races) --- (DOJ-2436)
171+
PENDING_DIR="${PROFILE_DIR}/pending-lessons"
172+
mkdir -p "$PENDING_DIR"
173+
170174
if [ "$IS_FIRST_EVER" = "true" ]; then
171-
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."
175+
LESSON_TYPE="micro-lesson"
172176
else
173-
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."
177+
LESSON_TYPE="inline-insight"
174178
fi
175179

180+
# Write one JSON file per lesson (atomic, no race conditions)
181+
LESSON_ID="${TIMESTAMP}-$(printf '%05d' $$)"
182+
LESSON_FILE="${PENDING_DIR}/${LESSON_ID}.json"
183+
SAFE_FILE_PATH_LESSON=$(printf '%s' "$FILE_PATH" | sed 's/\\/\\\\/g; s/"/\\"/g')
184+
SAFE_TOOL_NAME_LESSON=$(printf '%s' "$TOOL_NAME" | sed 's/\\/\\\\/g; s/"/\\"/g')
185+
if ! printf '{"timestamp":"%s","type":"%s","tech":"%s","file":"%s","tool":"%s","belt":"%s","firstEncounter":%s}\n' \
186+
"$TIMESTAMP" "$LESSON_TYPE" "$TECH" "$SAFE_FILE_PATH_LESSON" "$SAFE_TOOL_NAME_LESSON" "$BELT" "$IS_FIRST_EVER" > "$LESSON_FILE"
187+
then
188+
log_error "$SCRIPT_NAME" "Failed to write pending lesson: $LESSON_FILE"
189+
fi
190+
191+
# --- Delegation hint: delegate teaching to sensei subagent --- (DOJ-2436)
192+
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/."
193+
176194
ESCAPED_CONTEXT=$(json_escape "$CONTEXT")
177195
printf '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":%s}}\n' "$ESCAPED_CONTEXT"
178196

scripts/track-command.sh

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -234,16 +234,37 @@ if [ $? -ne 0 ]; then
234234
BELT="white"
235235
fi
236236

237-
if [ "$ERROR_IS_FIRST_EVER" = "true" ] && [ -n "$ERROR_CONCEPT" ]; then
238-
CONTEXT="🥋 CodeSensei micro-lesson trigger: The user just encountered '$ERROR_CONCEPT' for the FIRST TIME while reading command output ($SAFE_CMD). Their belt level is '$BELT'. Provide a brief 2-sentence explanation of how to read this kind of error and why it matters. Adapt language to their belt level. Keep it supportive and practical."
237+
# --- Pending lessons queue (durable, per-lesson file to avoid append races) --- (DOJ-2436)
238+
PENDING_DIR="${PROFILE_DIR}/pending-lessons"
239+
mkdir -p "$PENDING_DIR"
240+
241+
if [ "$IS_FIRST_EVER" = "true" ] && [ -n "$CONCEPT" ]; then
242+
LESSON_TYPE="micro-lesson"
243+
elif [ "$ERROR_IS_FIRST_EVER" = "true" ] && [ -n "$ERROR_CONCEPT" ]; then
244+
LESSON_TYPE="micro-lesson"
245+
elif [ -n "$CONCEPT" ]; then
246+
LESSON_TYPE="inline-insight"
239247
elif [ -n "$ERROR_CONCEPT" ]; then
240-
CONTEXT="🥋 CodeSensei inline insight: An error appeared in the command output ($SAFE_CMD). The user's belt level is '$BELT'. This is a great moment to teach '$ERROR_CONCEPT' -- briefly explain how to read and interpret this type of error in 1-2 sentences, adapted to their belt level. Keep it supportive and practical."
241-
elif [ "$IS_FIRST_EVER" = "true" ]; then
242-
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."
248+
LESSON_TYPE="inline-insight"
243249
else
244-
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."
250+
LESSON_TYPE="command-hint"
245251
fi
246252

253+
# Write one JSON file per lesson (atomic, no race conditions)
254+
LESSON_ID="${TIMESTAMP}-$(printf '%05d' $$)"
255+
LESSON_FILE="${PENDING_DIR}/${LESSON_ID}.json"
256+
DISPLAY_TOPIC="${ERROR_CONCEPT:-${CONCEPT:-shell command}}"
257+
SAFE_DISPLAY_TOPIC=$(printf '%s' "$DISPLAY_TOPIC" | sed 's/\\/\\\\/g; s/"/\\"/g')
258+
if ! printf '{"timestamp":"%s","type":"%s","concept":"%s","command":"%s","belt":"%s","firstEncounter":%s}\n' \
259+
"$TIMESTAMP" "$LESSON_TYPE" "$SAFE_DISPLAY_TOPIC" "$SAFE_CMD" "$BELT" \
260+
"$([ "$IS_FIRST_EVER" = "true" ] || [ "$ERROR_IS_FIRST_EVER" = "true" ] && echo "true" || echo "false")" > "$LESSON_FILE"
261+
then
262+
log_error "$SCRIPT_NAME" "Failed to write pending lesson: $LESSON_FILE"
263+
fi
264+
265+
# --- Delegation hint: delegate teaching to sensei subagent --- (DOJ-2436)
266+
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/."
267+
247268
ESCAPED_CONTEXT=$(json_escape "$CONTEXT")
248269
printf '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":%s}}\n' "$ESCAPED_CONTEXT"
249270

tests/test-hooks.sh

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
#!/bin/bash
2+
# CodeSensei — Hook Regression Tests
3+
# Validates that hook scripts produce valid JSON output and write
4+
# structured pending lessons to the queue directory.
5+
#
6+
# Usage: bash tests/test-hooks.sh
7+
# Requirements: jq
8+
9+
set -euo pipefail
10+
11+
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
12+
TEST_HOME=$(mktemp -d)
13+
export HOME="$TEST_HOME"
14+
15+
PASS=0
16+
FAIL=0
17+
18+
# Colors
19+
GREEN='\033[0;32m'
20+
RED='\033[0;31m'
21+
NC='\033[0m'
22+
23+
pass() { PASS=$((PASS + 1)); echo -e " ${GREEN}${NC} $1"; }
24+
fail() { FAIL=$((FAIL + 1)); echo -e " ${RED}${NC} $1: $2"; }
25+
26+
cleanup() {
27+
rm -rf "$TEST_HOME"
28+
}
29+
trap cleanup EXIT
30+
31+
# --- Setup: create a minimal profile ---
32+
setup_profile() {
33+
mkdir -p "$TEST_HOME/.code-sensei"
34+
cat > "$TEST_HOME/.code-sensei/profile.json" <<'PROFILE'
35+
{
36+
"belt": "yellow",
37+
"xp": 100,
38+
"session_concepts": [],
39+
"concepts_seen": ["html"],
40+
"streak": {"current": 3}
41+
}
42+
PROFILE
43+
}
44+
45+
echo ""
46+
echo "━━━ CodeSensei Hook Regression Tests ━━━"
47+
echo ""
48+
49+
# ============================================================
50+
# TEST GROUP 1: track-code-change.sh
51+
# ============================================================
52+
echo "▸ track-code-change.sh"
53+
54+
# Test 1.1: Output is valid JSON
55+
setup_profile
56+
OUTPUT=$(echo '{"tool_name":"Write","tool_input":{"file_path":"src/App.tsx"}}' \
57+
| bash "$SCRIPT_DIR/scripts/track-code-change.sh" 2>/dev/null)
58+
59+
if echo "$OUTPUT" | jq . > /dev/null 2>&1; then
60+
pass "stdout is valid JSON"
61+
else
62+
fail "stdout is valid JSON" "got: $OUTPUT"
63+
fi
64+
65+
# Test 1.2: Output contains hookSpecificOutput with PostToolUse event
66+
EVENT=$(echo "$OUTPUT" | jq -r '.hookSpecificOutput.hookEventName')
67+
if [ "$EVENT" = "PostToolUse" ]; then
68+
pass "hookEventName is PostToolUse"
69+
else
70+
fail "hookEventName is PostToolUse" "got: $EVENT"
71+
fi
72+
73+
# Test 1.3: additionalContext is a delegation hint (not verbose teaching)
74+
CONTEXT=$(echo "$OUTPUT" | jq -r '.hookSpecificOutput.additionalContext')
75+
if echo "$CONTEXT" | grep -q "Task tool" && echo "$CONTEXT" | grep -q "sensei"; then
76+
pass "additionalContext is a delegation hint (mentions Task tool + sensei)"
77+
else
78+
fail "additionalContext is a delegation hint" "got: $CONTEXT"
79+
fi
80+
81+
# Test 1.4: additionalContext does NOT contain old verbose teaching patterns
82+
if echo "$CONTEXT" | grep -q "Provide a brief"; then
83+
fail "additionalContext has no verbose teaching" "still contains 'Provide a brief'"
84+
else
85+
pass "additionalContext has no verbose teaching content"
86+
fi
87+
88+
# Test 1.5: Pending lesson file was created
89+
LESSON_COUNT=$(find "$TEST_HOME/.code-sensei/pending-lessons" -name "*.json" 2>/dev/null | wc -l)
90+
if [ "$LESSON_COUNT" -ge 1 ]; then
91+
pass "pending lesson file created ($LESSON_COUNT file(s))"
92+
else
93+
fail "pending lesson file created" "found $LESSON_COUNT files"
94+
fi
95+
96+
# Test 1.6: Pending lesson file is valid JSON
97+
LESSON_FILE=$(find "$TEST_HOME/.code-sensei/pending-lessons" -name "*.json" | head -1)
98+
if jq . "$LESSON_FILE" > /dev/null 2>&1; then
99+
pass "pending lesson file is valid JSON"
100+
else
101+
fail "pending lesson file is valid JSON" "file: $LESSON_FILE"
102+
fi
103+
104+
# Test 1.7: Pending lesson has required fields
105+
LESSON_TYPE=$(jq -r '.type' "$LESSON_FILE")
106+
LESSON_TECH=$(jq -r '.tech' "$LESSON_FILE")
107+
LESSON_BELT=$(jq -r '.belt' "$LESSON_FILE")
108+
if [ "$LESSON_TYPE" != "null" ] && [ "$LESSON_TECH" != "null" ] && [ "$LESSON_BELT" != "null" ]; then
109+
pass "pending lesson has type=$LESSON_TYPE, tech=$LESSON_TECH, belt=$LESSON_BELT"
110+
else
111+
fail "pending lesson has required fields" "type=$LESSON_TYPE tech=$LESSON_TECH belt=$LESSON_BELT"
112+
fi
113+
114+
# Test 1.8: First encounter for new tech creates micro-lesson
115+
setup_profile
116+
rm -rf "$TEST_HOME/.code-sensei/pending-lessons"
117+
echo '{"tool_name":"Write","tool_input":{"file_path":"main.py"}}' \
118+
| bash "$SCRIPT_DIR/scripts/track-code-change.sh" > /dev/null 2>&1
119+
LESSON_FILE=$(find "$TEST_HOME/.code-sensei/pending-lessons" -name "*.json" | head -1)
120+
LESSON_TYPE=$(jq -r '.type' "$LESSON_FILE")
121+
FIRST=$(jq -r '.firstEncounter' "$LESSON_FILE")
122+
if [ "$LESSON_TYPE" = "micro-lesson" ] && [ "$FIRST" = "true" ]; then
123+
pass "first encounter creates micro-lesson with firstEncounter=true"
124+
else
125+
fail "first encounter creates micro-lesson" "type=$LESSON_TYPE firstEncounter=$FIRST"
126+
fi
127+
128+
# Test 1.9: Already-seen tech creates inline-insight
129+
setup_profile
130+
rm -rf "$TEST_HOME/.code-sensei/pending-lessons"
131+
echo '{"tool_name":"Edit","tool_input":{"file_path":"index.html"}}' \
132+
| bash "$SCRIPT_DIR/scripts/track-code-change.sh" > /dev/null 2>&1
133+
LESSON_FILE=$(find "$TEST_HOME/.code-sensei/pending-lessons" -name "*.json" | head -1)
134+
LESSON_TYPE=$(jq -r '.type' "$LESSON_FILE")
135+
FIRST=$(jq -r '.firstEncounter' "$LESSON_FILE")
136+
if [ "$LESSON_TYPE" = "inline-insight" ] && [ "$FIRST" = "false" ]; then
137+
pass "already-seen tech creates inline-insight with firstEncounter=false"
138+
else
139+
fail "already-seen tech creates inline-insight" "type=$LESSON_TYPE firstEncounter=$FIRST"
140+
fi
141+
142+
echo ""
143+
144+
# ============================================================
145+
# TEST GROUP 2: track-command.sh
146+
# ============================================================
147+
echo "▸ track-command.sh"
148+
149+
setup_profile
150+
rm -rf "$TEST_HOME/.code-sensei/pending-lessons"
151+
152+
# Test 2.1: Output is valid JSON
153+
OUTPUT=$(echo '{"tool_input":{"command":"npm install express"}}' \
154+
| bash "$SCRIPT_DIR/scripts/track-command.sh" 2>/dev/null)
155+
156+
if echo "$OUTPUT" | jq . > /dev/null 2>&1; then
157+
pass "stdout is valid JSON"
158+
else
159+
fail "stdout is valid JSON" "got: $OUTPUT"
160+
fi
161+
162+
# Test 2.2: additionalContext is delegation hint
163+
CONTEXT=$(echo "$OUTPUT" | jq -r '.hookSpecificOutput.additionalContext')
164+
if echo "$CONTEXT" | grep -q "Task tool" && echo "$CONTEXT" | grep -q "sensei"; then
165+
pass "additionalContext is a delegation hint"
166+
else
167+
fail "additionalContext is a delegation hint" "got: $CONTEXT"
168+
fi
169+
170+
# Test 2.3: Pending lesson file for command
171+
LESSON_FILE=$(find "$TEST_HOME/.code-sensei/pending-lessons" -name "*.json" | head -1)
172+
if jq . "$LESSON_FILE" > /dev/null 2>&1; then
173+
pass "pending lesson file is valid JSON"
174+
else
175+
fail "pending lesson file is valid JSON" "file: $LESSON_FILE"
176+
fi
177+
178+
# Test 2.4: Command lesson has concept field
179+
CONCEPT=$(jq -r '.concept' "$LESSON_FILE")
180+
if [ "$CONCEPT" = "package-management" ]; then
181+
pass "command lesson detected concept=package-management"
182+
else
183+
fail "command lesson detected concept" "got: $CONCEPT"
184+
fi
185+
186+
echo ""
187+
188+
# ============================================================
189+
# TEST GROUP 3: session-stop.sh (cleanup)
190+
# ============================================================
191+
echo "▸ session-stop.sh (pending lessons cleanup)"
192+
193+
setup_profile
194+
rm -rf "$TEST_HOME/.code-sensei/pending-lessons" "$TEST_HOME/.code-sensei/lessons-archive"
195+
196+
# Create some pending lessons
197+
mkdir -p "$TEST_HOME/.code-sensei/pending-lessons"
198+
echo '{"timestamp":"2026-03-09T12:00:00Z","type":"micro-lesson","tech":"react"}' \
199+
> "$TEST_HOME/.code-sensei/pending-lessons/test1.json"
200+
echo '{"timestamp":"2026-03-09T12:01:00Z","type":"inline-insight","tech":"css"}' \
201+
> "$TEST_HOME/.code-sensei/pending-lessons/test2.json"
202+
203+
# Add a session concept so we can verify the full flow
204+
jq '.session_concepts = ["react","css"]' "$TEST_HOME/.code-sensei/profile.json" \
205+
| tee "$TEST_HOME/.code-sensei/profile.json.tmp" > /dev/null \
206+
&& mv "$TEST_HOME/.code-sensei/profile.json.tmp" "$TEST_HOME/.code-sensei/profile.json"
207+
208+
# Run session-stop
209+
bash "$SCRIPT_DIR/scripts/session-stop.sh" > /dev/null 2>&1
210+
211+
# Test 3.1: Pending lessons directory was cleaned
212+
REMAINING=$(find "$TEST_HOME/.code-sensei/pending-lessons" -name "*.json" 2>/dev/null | wc -l)
213+
if [ "$REMAINING" -eq 0 ]; then
214+
pass "pending lessons cleaned after session stop"
215+
else
216+
fail "pending lessons cleaned" "$REMAINING files remaining"
217+
fi
218+
219+
# Test 3.2: Archive file was created
220+
TODAY=$(date -u +%Y-%m-%d)
221+
ARCHIVE_FILE="$TEST_HOME/.code-sensei/lessons-archive/${TODAY}.jsonl"
222+
if [ -f "$ARCHIVE_FILE" ]; then
223+
pass "archive file created at lessons-archive/${TODAY}.jsonl"
224+
else
225+
fail "archive file created" "file not found: $ARCHIVE_FILE"
226+
fi
227+
228+
# Test 3.3: Archive contains the lessons (each line is valid JSON)
229+
ARCHIVE_LINES=$(wc -l < "$ARCHIVE_FILE")
230+
VALID_JSON=0
231+
while IFS= read -r line; do
232+
if echo "$line" | jq . > /dev/null 2>&1; then
233+
VALID_JSON=$((VALID_JSON + 1))
234+
fi
235+
done < "$ARCHIVE_FILE"
236+
if [ "$VALID_JSON" -eq "$ARCHIVE_LINES" ] && [ "$ARCHIVE_LINES" -ge 2 ]; then
237+
pass "archive has $ARCHIVE_LINES valid JSON lines"
238+
else
239+
fail "archive has valid JSON lines" "total=$ARCHIVE_LINES valid=$VALID_JSON"
240+
fi
241+
242+
echo ""
243+
244+
# ============================================================
245+
# SUMMARY
246+
# ============================================================
247+
TOTAL=$((PASS + FAIL))
248+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
249+
if [ "$FAIL" -eq 0 ]; then
250+
echo -e "${GREEN}All $TOTAL tests passed!${NC}"
251+
else
252+
echo -e "${RED}$FAIL/$TOTAL tests failed${NC}"
253+
fi
254+
echo ""
255+
256+
exit "$FAIL"

0 commit comments

Comments
 (0)