From f3a4b33d41ff00e12f4eda13143d0852494f5ad1 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Thu, 12 Feb 2026 14:11:33 -0800 Subject: [PATCH] fix: harden CI validators, shell scripts, and expand test suite - Add try-catch around readFileSync in validate-agents, validate-commands, validate-skills to handle TOCTOU races and file read errors - Add validate-hooks.js and all test suites to package.json test script (was only running 4/5 validators and 0/4 test files) - Fix shell variable injection in observe.sh: use os.environ instead of interpolating $timestamp/$OBSERVATIONS_FILE into Python string literals - Fix $? always being 0 in start-observer.sh: capture exit code before conditional since `if !` inverts the status - Add OLD_VERSION validation in release.sh and use pipe delimiter in sed to avoid issues with slash-containing values - Add jq dependency check in evaluate-session.sh before parsing config - Sync .cursor/ copies of all modified shell scripts --- .../agents/start-observer.sh | 8 +++++--- .../continuous-learning-v2/hooks/observe.sh | 17 ++++++++--------- .../continuous-learning/evaluate-session.sh | 8 ++++++-- package.json | 2 +- scripts/ci/validate-agents.js | 9 ++++++++- scripts/ci/validate-commands.js | 9 ++++++++- scripts/ci/validate-skills.js | 9 ++++++++- scripts/release.sh | 10 +++++++--- .../agents/start-observer.sh | 8 +++++--- skills/continuous-learning-v2/hooks/observe.sh | 17 ++++++++--------- skills/continuous-learning/evaluate-session.sh | 8 ++++++-- 11 files changed, 70 insertions(+), 35 deletions(-) diff --git a/.cursor/skills/continuous-learning-v2/agents/start-observer.sh b/.cursor/skills/continuous-learning-v2/agents/start-observer.sh index 42a5f1b3..6ba6f11f 100755 --- a/.cursor/skills/continuous-learning-v2/agents/start-observer.sh +++ b/.cursor/skills/continuous-learning-v2/agents/start-observer.sh @@ -88,10 +88,12 @@ case "${1:-start}" in # Use Claude Code with Haiku to analyze observations # This spawns a quick analysis session if command -v claude &> /dev/null; then - if ! claude --model haiku --max-turns 3 --print \ + exit_code=0 + claude --model haiku --max-turns 3 --print \ "Read $OBSERVATIONS_FILE and identify patterns. If you find 3+ occurrences of the same pattern, create an instinct file in $CONFIG_DIR/instincts/personal/ following the format in the observer agent spec. Be conservative - only create instincts for clear patterns." \ - >> "$LOG_FILE" 2>&1; then - echo "[$(date)] Claude analysis failed (exit $?)" >> "$LOG_FILE" + >> "$LOG_FILE" 2>&1 || exit_code=$? + if [ "$exit_code" -ne 0 ]; then + echo "[$(date)] Claude analysis failed (exit $exit_code)" >> "$LOG_FILE" fi else echo "[$(date)] claude CLI not found, skipping analysis" >> "$LOG_FILE" diff --git a/.cursor/skills/continuous-learning-v2/hooks/observe.sh b/.cursor/skills/continuous-learning-v2/hooks/observe.sh index 3db1a2cf..98b6b9d2 100755 --- a/.cursor/skills/continuous-learning-v2/hooks/observe.sh +++ b/.cursor/skills/continuous-learning-v2/hooks/observe.sh @@ -103,10 +103,10 @@ PARSED_OK=$(echo "$PARSED" | python3 -c "import json,sys; print(json.load(sys.st if [ "$PARSED_OK" != "True" ]; then # Fallback: log raw input for debugging timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - echo "$INPUT_JSON" | python3 -c " -import json, sys + TIMESTAMP="$timestamp" echo "$INPUT_JSON" | python3 -c " +import json, sys, os raw = sys.stdin.read()[:2000] -print(json.dumps({'timestamp': '$timestamp', 'event': 'parse_error', 'raw': raw})) +print(json.dumps({'timestamp': os.environ['TIMESTAMP'], 'event': 'parse_error', 'raw': raw})) " >> "$OBSERVATIONS_FILE" exit 0 fi @@ -124,12 +124,12 @@ fi # Build and write observation timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") -echo "$PARSED" | python3 -c " -import json, sys +TIMESTAMP="$timestamp" echo "$PARSED" | python3 -c " +import json, sys, os parsed = json.load(sys.stdin) observation = { - 'timestamp': '$timestamp', + 'timestamp': os.environ['TIMESTAMP'], 'event': parsed['event'], 'tool': parsed['tool'], 'session': parsed['session'] @@ -140,9 +140,8 @@ if parsed['input']: if parsed['output']: observation['output'] = parsed['output'] -with open('$OBSERVATIONS_FILE', 'a') as f: - f.write(json.dumps(observation) + '\n') -" +print(json.dumps(observation)) +" >> "$OBSERVATIONS_FILE" # Signal observer if running OBSERVER_PID_FILE="${CONFIG_DIR}/.observer.pid" diff --git a/.cursor/skills/continuous-learning/evaluate-session.sh b/.cursor/skills/continuous-learning/evaluate-session.sh index f13208a1..b6c008f3 100755 --- a/.cursor/skills/continuous-learning/evaluate-session.sh +++ b/.cursor/skills/continuous-learning/evaluate-session.sh @@ -32,8 +32,12 @@ MIN_SESSION_LENGTH=10 # Load config if exists if [ -f "$CONFIG_FILE" ]; then - MIN_SESSION_LENGTH=$(jq -r '.min_session_length // 10' "$CONFIG_FILE") - LEARNED_SKILLS_PATH=$(jq -r '.learned_skills_path // "~/.claude/skills/learned/"' "$CONFIG_FILE" | sed "s|~|$HOME|") + if ! command -v jq &>/dev/null; then + echo "[ContinuousLearning] jq is required to parse config.json but not installed, using defaults" >&2 + else + MIN_SESSION_LENGTH=$(jq -r '.min_session_length // 10' "$CONFIG_FILE") + LEARNED_SKILLS_PATH=$(jq -r '.learned_skills_path // "~/.claude/skills/learned/"' "$CONFIG_FILE" | sed "s|~|$HOME|") + fi fi # Ensure learned skills directory exists diff --git a/package.json b/package.json index 5d40ee53..fb38548b 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "scripts": { "postinstall": "echo '\\n ecc-universal installed!\\n Run: npx ecc-install typescript\\n Docs: https://github.com/affaan-m/everything-claude-code\\n'", "lint": "eslint . && markdownlint '**/*.md' --ignore node_modules", - "test": "node scripts/ci/validate-agents.js && node scripts/ci/validate-commands.js && node scripts/ci/validate-rules.js && node scripts/ci/validate-skills.js" + "test": "node scripts/ci/validate-agents.js && node scripts/ci/validate-commands.js && node scripts/ci/validate-rules.js && node scripts/ci/validate-skills.js && node scripts/ci/validate-hooks.js && node tests/lib/utils.test.js && node tests/lib/package-manager.test.js && node tests/hooks/hooks.test.js && node tests/integration/hooks.test.js" }, "devDependencies": { "@eslint/js": "^9.39.2", diff --git a/scripts/ci/validate-agents.js b/scripts/ci/validate-agents.js index 80d03dbc..12bf336c 100644 --- a/scripts/ci/validate-agents.js +++ b/scripts/ci/validate-agents.js @@ -40,7 +40,14 @@ function validateAgents() { for (const file of files) { const filePath = path.join(AGENTS_DIR, file); - const content = fs.readFileSync(filePath, 'utf-8'); + let content; + try { + content = fs.readFileSync(filePath, 'utf-8'); + } catch (err) { + console.error(`ERROR: ${file} - ${err.message}`); + hasErrors = true; + continue; + } const frontmatter = extractFrontmatter(content); if (!frontmatter) { diff --git a/scripts/ci/validate-commands.js b/scripts/ci/validate-commands.js index 640e4add..3e15a43d 100644 --- a/scripts/ci/validate-commands.js +++ b/scripts/ci/validate-commands.js @@ -19,7 +19,14 @@ function validateCommands() { for (const file of files) { const filePath = path.join(COMMANDS_DIR, file); - const content = fs.readFileSync(filePath, 'utf-8'); + let content; + try { + content = fs.readFileSync(filePath, 'utf-8'); + } catch (err) { + console.error(`ERROR: ${file} - ${err.message}`); + hasErrors = true; + continue; + } // Validate the file is non-empty readable markdown if (content.trim().length === 0) { diff --git a/scripts/ci/validate-skills.js b/scripts/ci/validate-skills.js index 632bd312..220a25f1 100644 --- a/scripts/ci/validate-skills.js +++ b/scripts/ci/validate-skills.js @@ -27,7 +27,14 @@ function validateSkills() { continue; } - const content = fs.readFileSync(skillMd, 'utf-8'); + let content; + try { + content = fs.readFileSync(skillMd, 'utf-8'); + } catch (err) { + console.error(`ERROR: ${dir}/SKILL.md - ${err.message}`); + hasErrors = true; + continue; + } if (content.trim().length === 0) { console.error(`ERROR: ${dir}/SKILL.md - Empty file`); hasErrors = true; diff --git a/scripts/release.sh b/scripts/release.sh index 9e6e349e..cd7c16fc 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -47,15 +47,19 @@ fi # Read current version OLD_VERSION=$(grep -oE '"version": *"[^"]*"' "$PLUGIN_JSON" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') +if [[ -z "$OLD_VERSION" ]]; then + echo "Error: Could not extract current version from $PLUGIN_JSON" + exit 1 +fi echo "Bumping version: $OLD_VERSION -> $VERSION" -# Update version in plugin.json (cross-platform sed) +# Update version in plugin.json (cross-platform sed, pipe-delimiter avoids issues with slashes) if [[ "$OSTYPE" == "darwin"* ]]; then # macOS - sed -i '' "s/\"version\": *\"[^\"]*\"/\"version\": \"$VERSION\"/" "$PLUGIN_JSON" + sed -i '' "s|\"version\": *\"[^\"]*\"|\"version\": \"$VERSION\"|" "$PLUGIN_JSON" else # Linux - sed -i "s/\"version\": *\"[^\"]*\"/\"version\": \"$VERSION\"/" "$PLUGIN_JSON" + sed -i "s|\"version\": *\"[^\"]*\"|\"version\": \"$VERSION\"|" "$PLUGIN_JSON" fi # Stage, commit, tag, and push diff --git a/skills/continuous-learning-v2/agents/start-observer.sh b/skills/continuous-learning-v2/agents/start-observer.sh index 42a5f1b3..6ba6f11f 100755 --- a/skills/continuous-learning-v2/agents/start-observer.sh +++ b/skills/continuous-learning-v2/agents/start-observer.sh @@ -88,10 +88,12 @@ case "${1:-start}" in # Use Claude Code with Haiku to analyze observations # This spawns a quick analysis session if command -v claude &> /dev/null; then - if ! claude --model haiku --max-turns 3 --print \ + exit_code=0 + claude --model haiku --max-turns 3 --print \ "Read $OBSERVATIONS_FILE and identify patterns. If you find 3+ occurrences of the same pattern, create an instinct file in $CONFIG_DIR/instincts/personal/ following the format in the observer agent spec. Be conservative - only create instincts for clear patterns." \ - >> "$LOG_FILE" 2>&1; then - echo "[$(date)] Claude analysis failed (exit $?)" >> "$LOG_FILE" + >> "$LOG_FILE" 2>&1 || exit_code=$? + if [ "$exit_code" -ne 0 ]; then + echo "[$(date)] Claude analysis failed (exit $exit_code)" >> "$LOG_FILE" fi else echo "[$(date)] claude CLI not found, skipping analysis" >> "$LOG_FILE" diff --git a/skills/continuous-learning-v2/hooks/observe.sh b/skills/continuous-learning-v2/hooks/observe.sh index 3db1a2cf..98b6b9d2 100755 --- a/skills/continuous-learning-v2/hooks/observe.sh +++ b/skills/continuous-learning-v2/hooks/observe.sh @@ -103,10 +103,10 @@ PARSED_OK=$(echo "$PARSED" | python3 -c "import json,sys; print(json.load(sys.st if [ "$PARSED_OK" != "True" ]; then # Fallback: log raw input for debugging timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - echo "$INPUT_JSON" | python3 -c " -import json, sys + TIMESTAMP="$timestamp" echo "$INPUT_JSON" | python3 -c " +import json, sys, os raw = sys.stdin.read()[:2000] -print(json.dumps({'timestamp': '$timestamp', 'event': 'parse_error', 'raw': raw})) +print(json.dumps({'timestamp': os.environ['TIMESTAMP'], 'event': 'parse_error', 'raw': raw})) " >> "$OBSERVATIONS_FILE" exit 0 fi @@ -124,12 +124,12 @@ fi # Build and write observation timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") -echo "$PARSED" | python3 -c " -import json, sys +TIMESTAMP="$timestamp" echo "$PARSED" | python3 -c " +import json, sys, os parsed = json.load(sys.stdin) observation = { - 'timestamp': '$timestamp', + 'timestamp': os.environ['TIMESTAMP'], 'event': parsed['event'], 'tool': parsed['tool'], 'session': parsed['session'] @@ -140,9 +140,8 @@ if parsed['input']: if parsed['output']: observation['output'] = parsed['output'] -with open('$OBSERVATIONS_FILE', 'a') as f: - f.write(json.dumps(observation) + '\n') -" +print(json.dumps(observation)) +" >> "$OBSERVATIONS_FILE" # Signal observer if running OBSERVER_PID_FILE="${CONFIG_DIR}/.observer.pid" diff --git a/skills/continuous-learning/evaluate-session.sh b/skills/continuous-learning/evaluate-session.sh index f13208a1..b6c008f3 100755 --- a/skills/continuous-learning/evaluate-session.sh +++ b/skills/continuous-learning/evaluate-session.sh @@ -32,8 +32,12 @@ MIN_SESSION_LENGTH=10 # Load config if exists if [ -f "$CONFIG_FILE" ]; then - MIN_SESSION_LENGTH=$(jq -r '.min_session_length // 10' "$CONFIG_FILE") - LEARNED_SKILLS_PATH=$(jq -r '.learned_skills_path // "~/.claude/skills/learned/"' "$CONFIG_FILE" | sed "s|~|$HOME|") + if ! command -v jq &>/dev/null; then + echo "[ContinuousLearning] jq is required to parse config.json but not installed, using defaults" >&2 + else + MIN_SESSION_LENGTH=$(jq -r '.min_session_length // 10' "$CONFIG_FILE") + LEARNED_SKILLS_PATH=$(jq -r '.learned_skills_path // "~/.claude/skills/learned/"' "$CONFIG_FILE" | sed "s|~|$HOME|") + fi fi # Ensure learned skills directory exists