Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@

https://agentskills.io/

Common skills share for various platforms. Symlink used to link skills for `.claude/skills`. The symlinks are source controlled by default.
Common skills and hooks shared across agent platforms. Symlinks are source controlled by default.

```shell
# symlink skills
ln -sfn ../.agents/skills .claude/skills

# hooks
# .claude/settings.json references .agents/hooks directly
# .codex/config.toml enables hooks and .codex/hooks.json references .agents/hooks
# Hooks resolve the project root from AGENTS_PROJECT_DIR, agent-specific env vars,
# hook JSON cwd fields, the hook script location, or git.

# symlink context
ln -s AGENTS.md CLAUDE.md
```
136 changes: 136 additions & 0 deletions .agents/hooks/lib/project-root.sh
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: /, "")
print
}
/^\*\*\* Move to: / {
sub(/^\*\*\* Move to: /, "")
print
}
' <<<"$command"
fi
}
Copy link
Copy Markdown
Collaborator Author

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

6 changes: 6 additions & 0 deletions .agents/hooks/notification.sh
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"'
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added extra guard for macos only notifications

fi
167 changes: 167 additions & 0 deletions .agents/hooks/post-tool-use-edit-write.sh
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n .agents/hooks/post-tool-use-edit-write.sh | head -150

Repository: 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 run_prettier, run_vale, and run_stylelint functions.

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
   fi

Also applies to: 103-106, 140-143

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.agents/hooks/post-tool-use-edit-write.sh around lines 25 - 28, The current
conditional in the post-tool-use hook only marks failure when both exit_code is
non-zero and output is non-empty, allowing tools to fail silently; update the
condition in the blocks used by run_prettier, run_vale, and run_stylelint so
that any non-zero ${exit_code:-0} triggers mark_failed (i.e., drop the -n
"$output" requirement), but still only echo "$output" when it is non-empty;
locate the failing blocks that reference exit_code and output and change the
logic to call mark_failed whenever exit_code != 0 and separately guard the echo
with a non-empty output test.

}

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the file
find . -name "post-tool-use-edit-write.sh" -type f

Repository: 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}")
EOF

Repository: 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.sh

Repository: 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 2>/dev/null || true, and line 64's jq expression fails when ruleId is null (confirmed: null cannot be matched, as it is not a string). The fallback || hard_errors="0" masks this failure, causing the hook to incorrectly return success. This is inconsistent with Prettier, Vale, and Stylelint patterns in the same file, which explicitly check exit codes.

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)] | length

Also applies to: Line 67 (same null-handling issue in total_errors jq expression).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.agents/hooks/post-tool-use-edit-write.sh around lines 61 - 65, The ESLint
invocation and jq parsing are suppressing real errors and failing on null
ruleId; stop swallowing failures from the pnpm exec eslint call (remove the
unconditional 2>/dev/null || true and capture its exit status), and make the jq
expressions null-safe by coalescing ruleId to an empty string before testing
(e.g. use (.ruleId // "") | test($soft) to avoid "null cannot be matched"); do
the same null-safe change for both hard_errors and total_errors calculations and
ensure you default the shell variables to "0" if jq fails so the hook accurately
reflects ESLint exit status.


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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 --format json (for programmatic error classification) and again in the default readable format (for display). With --cache, the second run is fast, but it still adds fork + analysis overhead per edited file. An alternative is to post-process the cached JSON output into readable text via jq or eslint-formatter-*, eliminating the second process.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.agents/hooks/post-tool-use-edit-write.sh around lines 61 - 82, Run ESLint
only once and derive the human-readable output from the JSON rather than
invoking eslint twice; replace the second invocation that sets readable with a
conversion of json_output (the variable set by the pnpm exec eslint --format
json call) into a readable format (e.g., using jq to pretty-print messages or an
eslint formatter library to transform the JSON into classic CLI output), then
reuse that readable value for both echoing and error handling (references:
json_output, readable, the pnpm exec eslint invocation).

}

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
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
set -euo pipefail

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
HOOK_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
source "$HOOK_DIR/lib/project-root.sh"
COMMAND=$(hook_command_from_input "$INPUT" || true)

# Exit early if not a git command
if [[ -z "$COMMAND" ]] || ! echo "$COMMAND" | grep -qE '^\s*git\s'; then
Expand Down
Loading