-
Notifications
You must be signed in to change notification settings - Fork 1
chore(ci): refactor agents for codex compatibility #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| #!/usr/bin/env bash | ||
|
|
||
| normalize_dir() { | ||
| local dir="$1" | ||
|
|
||
| if [[ -z "$dir" || ! -d "$dir" ]]; then | ||
| return 1 | ||
| fi | ||
|
|
||
| (cd "$dir" 2>/dev/null && pwd -P) | ||
| } | ||
|
|
||
| looks_like_project_root() { | ||
| local dir="$1" | ||
|
|
||
| [[ -d "$dir/.git" || -f "$dir/.git" || -f "$dir/AGENTS.md" || -f "$dir/pnpm-workspace.yaml" || -d "$dir/.agents" ]] | ||
| } | ||
|
|
||
| candidate_from_json() { | ||
| local input="$1" | ||
|
|
||
| if [[ -z "$input" ]] || ! command -v jq >/dev/null 2>&1; then | ||
| return 1 | ||
| fi | ||
|
|
||
| jq -r '.cwd // .workspace_root // .workspaceRoot // .project_dir // .projectDir // empty' <<<"$input" 2>/dev/null | ||
| } | ||
|
|
||
| resolve_project_root() { | ||
| local input="${1:-}" | ||
| local hooks_dir="${2:-}" | ||
| local candidate normalized json_candidate | ||
|
|
||
| json_candidate=$(candidate_from_json "$input" || true) | ||
|
|
||
| for candidate in \ | ||
| "${AGENTS_PROJECT_DIR:-}" \ | ||
| "${CLAUDE_PROJECT_DIR:-}" \ | ||
| "${CODEX_PROJECT_DIR:-}" \ | ||
| "${CODEX_WORKSPACE_ROOT:-}" \ | ||
| "${WORKSPACE_ROOT:-}" \ | ||
| "${PROJECT_DIR:-}" \ | ||
| "$json_candidate" \ | ||
| "${PWD:-}"; do | ||
| normalized=$(normalize_dir "$candidate") || continue | ||
| if looks_like_project_root "$normalized"; then | ||
| printf '%s\n' "$normalized" | ||
| return 0 | ||
| fi | ||
| done | ||
|
|
||
| if [[ -n "$hooks_dir" ]]; then | ||
| normalized=$(normalize_dir "$hooks_dir/../..") || true | ||
| if [[ -n "${normalized:-}" ]] && looks_like_project_root "$normalized"; then | ||
| printf '%s\n' "$normalized" | ||
| return 0 | ||
| fi | ||
| fi | ||
|
|
||
| candidate=$(git rev-parse --show-toplevel 2>/dev/null || true) | ||
| normalized=$(normalize_dir "$candidate") || true | ||
| if [[ -n "${normalized:-}" ]]; then | ||
| printf '%s\n' "$normalized" | ||
| return 0 | ||
| fi | ||
|
|
||
| return 1 | ||
| } | ||
|
|
||
| resolve_hook_path() { | ||
| local project_root="$1" | ||
| local path="$2" | ||
|
|
||
| if [[ -z "$path" ]]; then | ||
| return 1 | ||
| fi | ||
|
|
||
| case "$path" in | ||
| /*) printf '%s\n' "$path" ;; | ||
| *) printf '%s/%s\n' "$project_root" "$path" ;; | ||
| esac | ||
| } | ||
|
|
||
| hook_relative_path() { | ||
| local project_root="$1" | ||
| local path="$2" | ||
|
|
||
| case "$path" in | ||
| "$project_root"/*) printf '%s\n' "${path#"$project_root"/}" ;; | ||
| *) printf '%s\n' "$path" ;; | ||
| esac | ||
| } | ||
|
|
||
| hook_command_from_input() { | ||
| local input="$1" | ||
|
|
||
| if [[ -z "$input" ]] || ! command -v jq >/dev/null 2>&1; then | ||
| return 1 | ||
| fi | ||
|
|
||
| jq -r '.tool_input.command // .command // empty' <<<"$input" 2>/dev/null | ||
| } | ||
|
|
||
| hook_file_paths_from_input() { | ||
| local input="$1" | ||
| local command | ||
|
|
||
| if [[ -n "$input" ]] && command -v jq >/dev/null 2>&1; then | ||
| jq -r ' | ||
| [ | ||
| .tool_input.file_path?, | ||
| .tool_input.filePath?, | ||
| .tool_input.path?, | ||
| .file_path?, | ||
| .filePath?, | ||
| .path? | ||
| ] | ||
| | .[] | ||
| | select(type == "string" and length > 0) | ||
| ' <<<"$input" 2>/dev/null || true | ||
| fi | ||
|
|
||
| command=$(hook_command_from_input "$input" || true) | ||
| if [[ -n "$command" ]]; then | ||
| awk ' | ||
| /^\*\*\* (Add|Update|Delete) File: / { | ||
| sub(/^\*\*\* (Add|Update|Delete) File: /, "") | ||
| } | ||
| /^\*\*\* Move to: / { | ||
| sub(/^\*\*\* Move to: /, "") | ||
| } | ||
| ' <<<"$command" | ||
| fi | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| if [[ "$(uname -s)" == "Darwin" ]] && command -v osascript >/dev/null 2>&1; then | ||
| osascript -e 'display notification "Agent needs your attention" with title "Agent"' | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added extra guard for macos only notifications |
||
| fi | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| INPUT=$(cat) | ||
| HOOK_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) | ||
| source "$HOOK_DIR/lib/project-root.sh" | ||
| PROJECT_ROOT=$(resolve_project_root "$INPUT" "$HOOK_DIR") || exit 0 | ||
| FILE_PATHS=$(hook_file_paths_from_input "$INPUT") | ||
|
|
||
| if [[ -z "$FILE_PATHS" ]]; then | ||
| exit 0 | ||
| fi | ||
|
|
||
| FAILED=0 | ||
|
|
||
| mark_failed() { | ||
| FAILED=1 | ||
| } | ||
|
|
||
| run_prettier() { | ||
| local output exit_code | ||
|
|
||
| output=$(cd "$PROJECT_ROOT" && pnpm exec prettier --write --ignore-unknown --no-error-on-unmatched-pattern "$FILE_PATH" 2>&1) || exit_code=$? | ||
|
|
||
| if [[ ${exit_code:-0} -ne 0 && -n "$output" ]]; then | ||
| echo "$output" >&2 | ||
| mark_failed | ||
| fi | ||
|
Comment on lines
+25
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n .agents/hooks/post-tool-use-edit-write.sh | head -150Repository: NVIDIA/elements Length of output: 4862 Tool failures should fail even when output is empty. Lines 25, 103, and 140 require both a non-zero exit code AND non-empty output to mark failure. A non-zero exit with empty output currently passes. This affects Suggested fix- if [[ ${exit_code:-0} -ne 0 && -n "$output" ]]; then
- echo "$output" >&2
+ if [[ ${exit_code:-0} -ne 0 ]]; then
+ [[ -n "$output" ]] && echo "$output" >&2
mark_failed
fiAlso applies to: 103-106, 140-143 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| run_eslint() { | ||
| case "$FILE_PATH" in | ||
| *.ts|*.js|*.css) ;; | ||
| *) return 0 ;; | ||
| esac | ||
|
|
||
| case "$FILE_PATH" in | ||
| */dist/*|*/node_modules/*|*/__screenshots__/*|*/generated/*) return 0 ;; | ||
| esac | ||
|
|
||
| local dir project_dir rel_path json_output hard_errors total_errors readable | ||
|
|
||
| dir=$(dirname "$FILE_PATH") | ||
| project_dir="" | ||
| while [[ "$dir" != "/" && "$dir" != "." ]]; do | ||
| if [[ -f "$dir/eslint.config.js" ]]; then | ||
| project_dir="$dir" | ||
| break | ||
| fi | ||
| dir=$(dirname "$dir") | ||
| done | ||
|
|
||
| if [[ -z "$project_dir" ]]; then | ||
| return 0 | ||
| fi | ||
|
|
||
| rel_path=$(hook_relative_path "$project_dir" "$FILE_PATH") | ||
|
|
||
| local soft_rules="no-unused-vars|@typescript-eslint/no-unused-vars" | ||
|
|
||
| json_output=$(cd "$project_dir" && pnpm exec eslint -c ./eslint.config.js --no-warn-ignored --cache --cache-location .eslintcache/ --format json "$rel_path" 2>/dev/null) || true | ||
|
|
||
| hard_errors=$(echo "$json_output" | jq -r --arg soft "$soft_rules" ' | ||
| [.[].messages[] | select(.severity == 2) | select(.ruleId | test($soft) | not)] | length | ||
| ') 2>/dev/null || hard_errors="0" | ||
|
Comment on lines
+61
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, locate and examine the file
find . -name "post-tool-use-edit-write.sh" -type fRepository: NVIDIA/elements Length of output: 103 🏁 Script executed: # Examine the relevant section of the script (lines 50-80)
cat -n .agents/hooks/post-tool-use-edit-write.sh | sed -n '50,80p'Repository: NVIDIA/elements Length of output: 1330 🏁 Script executed: # Check if ruleId can be null in ESLint JSON output and test the jq expression
# First, let's see what an ESLint JSON output looks like and test jq behavior
python3 << 'EOF'
import json
import subprocess
# Create a sample ESLint JSON output with a null ruleId (which can happen for parsing errors)
sample_output = [
{
"filePath": "test.js",
"messages": [
{
"ruleId": None, # null ruleId from parsing error
"severity": 2,
"message": "Parsing error"
},
{
"ruleId": "no-unused-vars",
"severity": 2,
"message": "Variable unused"
}
]
}
]
# Test the original jq expression with null ruleId
json_str = json.dumps(sample_output)
print("Sample ESLint output with null ruleId:")
print(json_str)
print("\n" + "="*60 + "\n")
# Test original expression: [.[].messages[] | select(.severity == 2) | select(.ruleId | test($soft) | not)] | length
test_expr = r'[.[].messages[] | select(.severity == 2) | select(.ruleId | test($soft) | not)] | length'
try:
result = subprocess.run(['jq', '-r', '--arg', 'soft', 'some_pattern', test_expr],
input=json_str, text=True, capture_output=True)
print(f"Original expression result:\nStdout: {result.stdout}\nStderr: {result.stderr}\nReturn code: {result.returncode}")
except Exception as e:
print(f"Error running jq: {e}")
print("\n" + "="*60 + "\n")
# Test fixed expression: [.[].messages[] | select(.severity == 2) | select(((.ruleId // "") | test($soft)) | not)] | length
fixed_expr = r'[.[].messages[] | select(.severity == 2) | select(((.ruleId // "") | test($soft)) | not)] | length'
try:
result = subprocess.run(['jq', '-r', '--arg', 'soft', 'some_pattern', fixed_expr],
input=json_str, text=True, capture_output=True)
print(f"Fixed expression result:\nStdout: {result.stdout}\nStderr: {result.stderr}\nReturn code: {result.returncode}")
except Exception as e:
print(f"Error running jq: {e}")
EOFRepository: NVIDIA/elements Length of output: 600 🏁 Script executed: # Check the full context around lines 61-73 to understand the flow
cat -n .agents/hooks/post-tool-use-edit-write.sh | sed -n '55,75p'Repository: NVIDIA/elements Length of output: 1090 🏁 Script executed: # Look for similar patterns with other formatters/linters to see consistency
grep -n "2>/dev/null\||| true" .agents/hooks/post-tool-use-edit-write.shRepository: NVIDIA/elements Length of output: 488 🏁 Script executed: # Check the error handling pattern for other tools (Prettier, Vale, Stylelint)
cat -n .agents/hooks/post-tool-use-edit-write.sh | sed -n '20,30p'
cat -n .agents/hooks/post-tool-use-edit-write.sh | sed -n '100,110p'
cat -n .agents/hooks/post-tool-use-edit-write.sh | sed -n '135,145p'Repository: NVIDIA/elements Length of output: 1136 Fix jq null-safety and error suppression in ESLint linting path. Line 61 suppresses all errors with Suggested fix- json_output=$(cd "$project_dir" && pnpm exec eslint -c ./eslint.config.js --no-warn-ignored --cache --cache-location .eslintcache/ --format json "$rel_path" 2>/dev/null) || true
+ local eslint_exit=0
+ json_output=$(cd "$project_dir" && pnpm exec eslint -c ./eslint.config.js --no-warn-ignored --cache --cache-location .eslintcache/ --format json "$rel_path" 2>&1) || eslint_exit=$?
+ if [[ $eslint_exit -eq 2 || -z "$json_output" ]]; then
+ echo "$json_output" >&2
+ mark_failed
+ return 0
+ fi
- [.[].messages[] | select(.severity == 2) | select(.ruleId | test($soft) | not)] | length
+ [.[].messages[] | select(.severity == 2) | select(((.ruleId // "") | test($soft)) | not)] | lengthAlso applies to: Line 67 (same null-handling issue in total_errors jq expression). 🤖 Prompt for AI Agents |
||
|
|
||
| total_errors=$(echo "$json_output" | jq -r ' | ||
| [.[].messages[] | select(.severity == 2)] | length | ||
| ') 2>/dev/null || total_errors="0" | ||
|
|
||
| if [[ "$total_errors" == "0" ]]; then | ||
| return 0 | ||
| fi | ||
|
|
||
| readable=$(cd "$project_dir" && pnpm exec eslint -c ./eslint.config.js --no-warn-ignored --color --cache --cache-location .eslintcache/ "$rel_path" 2>&1) || true | ||
|
|
||
| if [[ "$hard_errors" != "0" ]]; then | ||
| echo "$readable" >&2 | ||
| mark_failed | ||
| else | ||
| echo "$readable" >&2 | ||
| fi | ||
|
Comment on lines
+61
to
+82
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win ESLint is invoked twice per file with errors — consider capturing the readable output in a single run. ESLint runs first in 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| run_vale() { | ||
| case "$FILE_PATH" in | ||
| *.md|*.ts) ;; | ||
| *) return 0 ;; | ||
| esac | ||
|
|
||
| case "$FILE_PATH" in | ||
| *.test.*|*/starters/*|*/404/*|*/vendor/*|*/changelog/*|*/icons/*|*/generated/*|*/dist/*|*/LICENSE*|*/CHANGELOG*|*/NOTICE*) return 0 ;; | ||
| esac | ||
|
|
||
| case "$FILE_PATH" in | ||
| */.claude/plans/*|*/.claude/projects/*) return 0 ;; | ||
| esac | ||
|
|
||
| local output exit_code | ||
|
|
||
| output=$(cd "$PROJECT_ROOT" && config/vale/bin/vale --config .vale.ini "$FILE_PATH" 2>&1) || exit_code=$? | ||
|
|
||
| if [[ ${exit_code:-0} -ne 0 && -n "$output" ]]; then | ||
| echo "$output" >&2 | ||
| mark_failed | ||
| fi | ||
| } | ||
|
|
||
| run_stylelint() { | ||
| case "$FILE_PATH" in | ||
| *.css) ;; | ||
| *) return 0 ;; | ||
| esac | ||
|
|
||
| case "$FILE_PATH" in | ||
| */dist/*|*/node_modules/*|*/vendor/*) return 0 ;; | ||
| esac | ||
|
|
||
| local repo_root dir project_dir rel_path output exit_code | ||
|
|
||
| repo_root="$PROJECT_ROOT" | ||
| dir=$(dirname "$FILE_PATH") | ||
| project_dir="" | ||
| while [[ "$dir" != "/" && "$dir" != "." ]]; do | ||
| if [[ -f "$dir/package.json" ]] && jq -e '.wireit["lint:style"]' "$dir/package.json" >/dev/null 2>&1; then | ||
| project_dir="$dir" | ||
| break | ||
| fi | ||
| dir=$(dirname "$dir") | ||
| done | ||
|
|
||
| if [[ -z "$project_dir" ]]; then | ||
| return 0 | ||
| fi | ||
|
|
||
| rel_path=$(hook_relative_path "$project_dir" "$FILE_PATH") | ||
|
|
||
| output=$(cd "$project_dir" && pnpm exec stylelint --config="$repo_root/stylelint.config.mjs" --color "$rel_path" 2>&1) || exit_code=$? | ||
|
|
||
| if [[ ${exit_code:-0} -ne 0 && -n "$output" ]]; then | ||
| echo "$output" >&2 | ||
| mark_failed | ||
| fi | ||
| } | ||
|
|
||
| while IFS= read -r FILE_PATH; do | ||
| if [[ -z "$FILE_PATH" ]]; then | ||
| continue | ||
| fi | ||
|
|
||
| FILE_PATH=$(resolve_hook_path "$PROJECT_ROOT" "$FILE_PATH") | ||
|
|
||
| if [[ ! -e "$FILE_PATH" || -d "$FILE_PATH" ]]; then | ||
| continue | ||
| fi | ||
|
|
||
| run_prettier | ||
| run_eslint | ||
| run_vale | ||
| run_stylelint | ||
| done <<<"$FILE_PATHS" | ||
|
|
||
| if [[ "$FAILED" -ne 0 ]]; then | ||
| exit 2 | ||
| fi | ||
|
|
||
| exit 0 | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This combined the four separate lint/format scripts into a single script/hook. Reason this doesnt just call the ci/lint scripts is it determines which files were edited relative to the package.json and only runs against that subset. |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Utility to normalize how codex/claude determine the current working directory