Files
boshu2__agentops/scripts/lint-codex-native.sh
T
Bo a5ecfa7818 fix(ci): suppress false-positive SC2088 in lint-codex-native.sh (ag-nrn7 #shellcheck-sc2088) (#623)
## What

`scripts/lint-codex-native.sh` line 145 (`Check 3`) runs `grep -n
'~/\.claude/' "$skill_file"` — the `~/.claude/` is a **literal grep
pattern** (the check detects non-portable `~/.claude/` strings inside
codex skill files), not a path meant to expand. shellcheck SC2088 flags
it as a non-expanding tilde, which is a false positive here. This was 1
of the 8 `pre-push-gate.sh` failures triaged in ag-nrn7.

## Fix

Add a scoped `# shellcheck disable=SC2088` with a rationale comment
directly above the line. 3 lines, single file.

## Test

- `shellcheck scripts/lint-codex-native.sh` → **exit 0, 0 findings**
(was: 1 SC2088 warning → gate FAIL).
- `bash -n` syntax OK; the grep pattern is unchanged so Check 3 behavior
is identical.
- `bats tests/scripts/pre-push-shellcheck-unconditional.bats` → green
(no regression).

Closes the **last actionable** item in ag-nrn7's full-gate triage. The
remaining ag-nrn7 failures are tolerate (mkdocs-venv; CI venv passes),
elsewhere (distribution-install-update canary → soc-3csze), or
local-state only — none blocks CI.

Closes-scenario: ag-nrn7#shellcheck-sc2088
Bounded-context: BC5-Runtime
Evidence: shellcheck scripts/lint-codex-native.sh
2026-05-30 03:39:43 -04:00

219 lines
7.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# lint-codex-native.sh — Lint skills-codex/ for Codex-native compliance
#
# Checks:
# 1. No slash-command invocations (must use $ prefix)
# 2. No Claude Code primitives in main execution flow (before ## References)
# 3. No ~/.claude/ paths (must use ~/.codex/)
# 4. No "Claude Code" runtime references (use "Codex" or runtime-neutral)
# 5. Required: Portability Appendix if Claude primitives exist in main flow
#
# Usage:
# scripts/lint-codex-native.sh [--strict] [--skill <name>]
#
# Exit codes:
# 0 — all checks pass
# 1 — violations found
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
SKILLS_DIR="$REPO_ROOT/skills-codex"
STRICT=false
FILTER_SKILL=""
ERRORS=0
WARNINGS=0
while [[ $# -gt 0 ]]; do
case "$1" in
--strict) STRICT=true; shift ;;
--skill) FILTER_SKILL="$2"; shift 2 ;;
*) echo "Unknown flag: $1"; exit 1 ;;
esac
done
# Colors
RED='\033[0;31m'
YELLOW='\033[0;33m'
GREEN='\033[0;32m'
NC='\033[0m'
error() {
echo -e "${RED} FAIL${NC}: $1"
ERRORS=$((ERRORS + 1))
}
warn() {
echo -e "${YELLOW} WARN${NC}: $1"
WARNINGS=$((WARNINGS + 1))
}
pass() {
if $STRICT; then
echo -e "${GREEN} PASS${NC}: $1"
fi
}
# Known skill names for slash-command detection (pipe-separated)
SKILL_NAMES="research|plan|pre-mortem|implement|crank|swarm|council|vibe|post-mortem|retro|evolve|release|status|goals|ratchet|rpi|brainstorm|bug-hunt|doc|forge|inject|knowledge|learn|extract|flywheel|handoff|recover|trace|provenance|beads|quickstart|readme|security|complexity|codex-team|pr-research|pr-plan|pr-implement|pr-validate|pr-prep|pr-retro|oss-docs|openai-docs|heal-skill|converter|update|product|reverse-engineer-rpi|security-suite|standards|shared|using-agentops"
# Claude-only primitives (should not appear in main execution flow)
CLAUDE_PRIMITIVES="TeamCreate|SendMessage|EnterPlanMode|ExitPlanMode|EnterWorktree"
# Find the line number where a section starts (0 if not found)
find_section_line() {
local file="$1"
local pattern="$2"
local line
line=$(grep -n "$pattern" "$file" | head -1 | cut -d: -f1)
echo "${line:-0}"
}
# Count matches in a string (handling empty input)
count_lines() {
local input="$1"
if [[ -z "$input" ]]; then
echo 0
else
echo "$input" | wc -l | tr -d ' '
fi
}
# Check a single skill
check_skill() {
local skill_name="$1"
local skill_file="$SKILLS_DIR/$skill_name/SKILL.md"
if [[ ! -f "$skill_file" ]]; then
warn "$skill_name: SKILL.md not found"
return
fi
local refs_line
refs_line=$(find_section_line "$skill_file" '^## Reference')
local port_line
port_line=$(find_section_line "$skill_file" '^## Portability')
local total_lines
total_lines=$(wc -l < "$skill_file" | tr -d ' ')
# Determine the "main flow" boundary (before References or Portability)
local main_end=$total_lines
if [[ $refs_line -gt 0 ]]; then
main_end=$refs_line
fi
if [[ $port_line -gt 0 && $port_line -lt $main_end ]]; then
main_end=$port_line
fi
# --- Check 1: Slash-command invocations ---
# Use perl lookbehind for accurate detection (avoids false positives from file paths)
# Real slash-commands: ` /research`, `"/council`, backtick-/skill
# False positives: `.agents/council/`, `skills/research/`, `merge/release`
local slash_hits
slash_hits=$(perl -ne "print \"$.: \$_\" if m{(?<![A-Za-z0-9_/.=\\\$-])/(${SKILL_NAMES})(?![A-Za-z0-9-])}" "$skill_file" 2>/dev/null || true)
if [[ -n "$slash_hits" ]]; then
local count
count=$(count_lines "$slash_hits")
error "$skill_name: $count slash-command invocation(s) — must use \$ prefix"
if $STRICT; then
echo "$slash_hits" | head -5 | sed 's/^/ /'
fi
else
pass "$skill_name: no slash-command invocations"
fi
# --- Check 2: Claude primitives in main execution flow ---
if [[ $main_end -gt 1 ]]; then
local prim_hits
prim_hits=$(head -n "$main_end" "$skill_file" | grep -En "(${CLAUDE_PRIMITIVES})" 2>/dev/null || true)
if [[ -n "$prim_hits" ]]; then
local count
count=$(count_lines "$prim_hits")
error "$skill_name: $count Claude primitive(s) in main execution flow (before line $main_end)"
if $STRICT; then
echo "$prim_hits" | head -5 | sed 's/^/ /'
fi
else
pass "$skill_name: no Claude primitives in main flow"
fi
fi
# --- Check 3: ~/.claude/ paths ---
local path_hits
# The tilde here is a literal grep PATTERN (matching the string "~/.claude/"
# in skill files), not a path meant to expand — SC2088 is a false positive.
# shellcheck disable=SC2088
path_hits=$(grep -n '~/\.claude/' "$skill_file" | grep -v 'Portability\|non-Codex\|appendix' || true)
if [[ -n "$path_hits" ]]; then
local count
count=$(count_lines "$path_hits")
if $STRICT; then
error "$skill_name: $count ~/.claude/ path reference(s) — use ~/.codex/"
else
warn "$skill_name: $count ~/.claude/ path reference(s)"
fi
else
pass "$skill_name: no ~/.claude/ paths"
fi
# --- Check 4: "Claude Code" runtime reference ---
local runtime_hits
runtime_hits=$(grep -in 'Claude Code' "$skill_file" | grep -vi 'Portability\|non-Codex\|appendix\|backend-claude\|claude-code-latest' || true)
if [[ -n "$runtime_hits" ]]; then
local count
count=$(count_lines "$runtime_hits")
warn "$skill_name: $count 'Claude Code' runtime reference(s)"
else
pass "$skill_name: no 'Claude Code' runtime references"
fi
# --- Check 5: Claude primitives anywhere + no Portability Appendix ---
local total_prims
total_prims=$(grep -cE "(${CLAUDE_PRIMITIVES})" "$skill_file" 2>/dev/null) || total_prims=0
if [[ "$total_prims" -gt 0 && "$port_line" -eq 0 ]]; then
if [[ "$refs_line" -gt 0 ]]; then
local main_prims
main_prims=$(head -n "$refs_line" "$skill_file" | grep -cE "(${CLAUDE_PRIMITIVES})" 2>/dev/null) || main_prims=0
if [[ "$main_prims" -gt 0 ]]; then
warn "$skill_name: $total_prims Claude primitive(s) total ($main_prims in main flow) — needs Portability Appendix"
fi
else
warn "$skill_name: $total_prims Claude primitive(s) but no Portability Appendix or References section"
fi
fi
}
echo "Codex-Native Skill Lint"
echo "======================="
echo "Directory: $SKILLS_DIR"
echo ""
if [[ -n "$FILTER_SKILL" ]]; then
echo "Checking: $FILTER_SKILL"
echo ""
check_skill "$FILTER_SKILL"
else
for skill_dir in "$SKILLS_DIR"/*/; do
skill_name=$(basename "$skill_dir")
check_skill "$skill_name"
done
fi
echo ""
echo "======================="
echo "Errors: $ERRORS | Warnings: $WARNINGS"
if [[ $ERRORS -gt 0 ]]; then
echo -e "${RED}FAIL${NC}: $ERRORS error(s) found"
exit 1
elif [[ $WARNINGS -gt 0 ]]; then
echo -e "${YELLOW}WARN${NC}: $WARNINGS warning(s) (pass with warnings)"
exit 0
else
echo -e "${GREEN}PASS${NC}: all checks clean"
exit 0
fi