diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b965a1..a4fde47 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,8 @@ jobs: - name: ShellCheck agent scripts run: | shopt -s globstar - shellcheck --severity=warning skills/*/scripts/*.sh tests/run-scenarios.sh tests/**/*.sh install.sh + shellcheck --severity=warning skills/*/scripts/*.sh lib/*.sh install.sh \ + $(find tests -name '*.sh' -not -path '*/issues/*') # ── cargo-agent scenarios ─────────────────────────────────────────── cargo-agent: diff --git a/.gitignore b/.gitignore index 482e34b..f1e13b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store *.swp *.swo +.ralph/ diff --git a/README.md b/README.md index cb3fdf7..5ec6b07 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,17 @@ Standard build tools produce walls of text. Agents waste context window parsing | Agent | Toolchain | Steps | |-------|-----------|-------| +| `ansible-agent` | Ansible | lint (ansible-lint), syntax (ansible-playbook --syntax-check) | +| `bash-agent` | Bash/Shell | syntax (bash -n), lint (shellcheck) | | `cargo-agent` | Rust | fmt, check, clippy, test (nextest) | +| `docker-agent` | Docker | lint (hadolint), build-check (BuildKit) | +| `gha-agent` | GitHub Actions | lint (actionlint) | +| `go-agent` | Go | fmt (gofmt), vet, staticcheck, test | +| `helm-agent` | Helm | lint, template | +| `kube-agent` | Kubernetes | validate (kubeconform/kubeval) | | `npm-agent` | Node.js | format, lint, typecheck, test, build | | `py-agent` | Python | format (ruff/black), lint (ruff/flake8), typecheck (mypy/pyright), test (pytest) | +| `sql-agent` | SQL | lint (sqlfluff), fix (sqlfluff fix) | | `terra-agent` | Terraform | fmt (check/fix), safe init, plan-safe, validate, lint (tflint) | ## Quick Start @@ -43,6 +51,9 @@ It also prints a short AGENTS.md/CLAUDE.md policy snippet you can copy/paste. ### Use directly (no install) ```sh +# Ansible project +path/to/x-agent/skills/ansible-agent/scripts/ansible-agent.sh + # Rust project path/to/x-agent/skills/cargo-agent/scripts/cargo-agent.sh @@ -52,12 +63,50 @@ path/to/x-agent/skills/npm-agent/scripts/npm-agent.sh # Python project path/to/x-agent/skills/py-agent/scripts/py-agent.sh +# Docker project +path/to/x-agent/skills/docker-agent/scripts/docker-agent.sh + +# GitHub Actions project +path/to/x-agent/skills/gha-agent/scripts/gha-agent.sh + +# Go project +path/to/x-agent/skills/go-agent/scripts/go-agent.sh + +# Helm project +path/to/x-agent/skills/helm-agent/scripts/helm-agent.sh + +# Kubernetes project +path/to/x-agent/skills/kube-agent/scripts/kube-agent.sh + +# SQL project +path/to/x-agent/skills/sql-agent/scripts/sql-agent.sh + # Terraform project path/to/x-agent/skills/terra-agent/scripts/terra-agent.sh ``` ## Usage +### ansible-agent + +```sh +ansible-agent.sh # full suite: lint + syntax +ansible-agent.sh lint # ansible-lint check only +ansible-agent.sh syntax # ansible-playbook --syntax-check only +FMT_MODE=fix ansible-agent.sh lint # auto-fix lint issues +``` + +`ansible-agent` runs `ansible-lint` for linting (auto-fix locally, check-only in CI) and `ansible-playbook --syntax-check` on discovered playbooks. Reports SKIP when no YAML files are found. + +### bash-agent + +```sh +bash-agent.sh # full suite: syntax + lint +bash-agent.sh syntax # bash -n syntax check only +bash-agent.sh lint # shellcheck lint only +SHELLCHECK_SEVERITY=error bash-agent.sh lint # only errors, ignore warnings +``` + ### cargo-agent ```sh @@ -68,6 +117,58 @@ cargo-agent.sh test # tests only cargo-agent.sh test -p api # tests in a specific crate ``` +### docker-agent + +```sh +docker-agent.sh # full suite: lint only (build-check off by default) +docker-agent.sh lint # hadolint check only +RUN_BUILD_CHECK=1 docker-agent.sh all # lint + BuildKit check +``` + +`docker-agent` discovers `Dockerfile`, `Dockerfile.*`, and `*.dockerfile` files recursively. `build-check` uses `docker build --check` (BuildKit lint mode) and defaults to OFF. Reports SKIP when no Dockerfiles are found. + +### gha-agent + +```sh +gha-agent.sh # lint all workflow files +gha-agent.sh lint # actionlint check only +``` + +`gha-agent` runs `actionlint` on `.github/workflows/*.yml` and `*.yaml` files. Reports SKIP when no workflows directory exists. + +### go-agent + +```sh +go-agent.sh # full suite: fmt + vet + staticcheck + test +go-agent.sh fmt # gofmt check/fix +go-agent.sh vet # go vet analysis +go-agent.sh test # tests only +FMT_MODE=fix go-agent.sh fmt # auto-fix formatting +``` + +`go-agent` uses `gofmt` for formatting (auto-fix locally, check-only in CI), `go vet` for analysis, optional `staticcheck` for linting, and `go test` for tests. + +### helm-agent + +```sh +helm-agent.sh # full suite: lint + template +helm-agent.sh lint # helm lint only +helm-agent.sh template # helm template only +CHART_DIR=charts/myapp helm-agent.sh all # explicit chart directory +``` + +`helm-agent` auto-detects chart directories by searching for `Chart.yaml`. Use `CHART_DIR` to override. Reports SKIP when no charts are found. + +### kube-agent + +```sh +kube-agent.sh # full suite: validate +kube-agent.sh validate # validate manifests only +KUBE_SCHEMAS_DIR=path kube-agent.sh all # custom schema location +``` + +`kube-agent` auto-detects kubeconform or kubeval and validates all `.yaml`/`.yml` files containing Kubernetes resource definitions (`apiVersion:` + `kind:`). Use `KUBE_SCHEMAS_DIR` for custom schemas. Reports SKIP when no manifests are found. + ### npm-agent ```sh @@ -91,6 +192,18 @@ py-agent.sh test -k login # tests matching "login" py-agent auto-detects your runner (uv, poetry, or plain python) and finds tools (ruff, black, flake8, mypy, pyright, pytest). +### sql-agent + +```sh +sql-agent.sh # full suite: lint only (fix off by default) +sql-agent.sh lint # sqlfluff lint only +sql-agent.sh fix # sqlfluff fix (auto-fix) +RUN_FIX=1 sql-agent.sh all # lint + fix (fix runs first) +SQLFLUFF_DIALECT=postgres sql-agent.sh lint # specify dialect +``` + +`sql-agent` discovers `.sql` files recursively and lints them with `sqlfluff`. Fix defaults to OFF — enable with `RUN_FIX=1` or `FMT_MODE=fix`. In CI, fix is forced to check-only mode. + ### terra-agent ```sh @@ -152,9 +265,17 @@ On **PASS**, temp logs are cleaned up automatically. On **FAIL** (or `KEEP_DIR=1 The `skills/` directory contains Claude Code skill definitions. After installing, agents like Claude Code can invoke these as skills: +- `/ansible-agent` — run Ansible playbook checks +- `/bash-agent` — run shell script checks - `/cargo-agent` — run Rust checks +- `/docker-agent` — run Dockerfile linting +- `/gha-agent` — run GitHub Actions workflow linting +- `/go-agent` — run Go checks +- `/helm-agent` — run Helm chart checks +- `/kube-agent` — run Kubernetes manifest validation - `/npm-agent` — run Node.js checks - `/py-agent` — run Python checks +- `/sql-agent` — run SQL linting/fixing - `/terra-agent` — run Terraform checks/fixes ## License diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..6b8ae85 --- /dev/null +++ b/TODO.md @@ -0,0 +1,18 @@ +# x-agent TODO + +Build and ship new agents one at a time, with one commit per item. +See `docs/agents/definition-of-done.md` for completion criteria. + +## Backlog (Priority Order) + +- [x] `terra-agent` core: Terraform checks (`fmt-check`, `fmt-fix`, `validate`, optional `tflint`) +- [x] `terra-agent` follow-up: add safe init step (`terraform init -backend=false -input=false`) +- [x] `py-agent`: Python checks (`format`, `lint`, `typecheck`, `test`) +- [ ] `bash-agent`: Bash/shell script checks (`bash -n` syntax validation, `shellcheck` linting) +- [ ] `go-agent`: Go checks (`fmt`, `vet`, optional `staticcheck`, `test`) +- [ ] `gha-agent`: GitHub Actions workflow linting (`actionlint`) +- [ ] `helm-agent`: Helm checks (`lint`, `template` render validation) +- [ ] `kube-agent`: Kubernetes manifest validation (`kubeconform`/`kubeval`) +- [ ] `docker-agent`: Dockerfile linting and optional image build check +- [ ] `ansible-agent`: Ansible lint and syntax validation +- [ ] `sql-agent`: SQL format/lint checks (`sqlfluff`) diff --git a/install.sh b/install.sh index b0fa175..86912c2 100755 --- a/install.sh +++ b/install.sh @@ -24,7 +24,7 @@ SOURCE_DIR="" SOURCE_MODE="remote" # Available skills to install (each has its own scripts/ subdirectory) -SKILLS="cargo-agent npm-agent py-agent terra-agent" +SKILLS="ansible-agent bash-agent cargo-agent docker-agent gha-agent go-agent helm-agent kube-agent npm-agent py-agent sql-agent terra-agent" SELECTED_SKILLS="" info() { @@ -192,6 +192,15 @@ resolve_source_dir() { SOURCE_DIR="${TMP_DIR}" SOURCE_MODE="remote" + # Fetch shared library + mkdir -p "${SOURCE_DIR}/lib" + src_url="${RAW_BASE}/lib/x-agent-common.sh" + dest="${SOURCE_DIR}/lib/x-agent-common.sh" + info "Fetching lib/x-agent-common.sh..." + if ! fetch_to_file "$src_url" "$dest"; then + die "Unable to download ${src_url}" + fi + # Fetch each selected skill's SKILL.md and scripts for skill in $SELECTED_SKILLS; do mkdir -p "${SOURCE_DIR}/skills/${skill}/scripts" @@ -236,6 +245,26 @@ install_skill_to_root() { fi } +# Install the shared library to a skills root so agent scripts can source it. +# For local installs, symlink the lib/ directory. +# For remote installs, copy the fetched lib/ directory. +install_lib_to_root() { + root="$1" + target="${root}/lib" + + mkdir -p "$root" + rm -rf "$target" + + if [ "$SOURCE_MODE" = "local" ]; then + ln -s "${SOURCE_DIR}/lib" "$target" + info "Symlinked lib to ${target} -> ${SOURCE_DIR}/lib" + else + mkdir -p "$target" + cp -R "${SOURCE_DIR}/lib/." "$target/" + info "Installed lib to ${target}" + fi +} + # Rewrite SKILL.md paths to point at the actual installed script location. patch_skill_paths() { root="$1" @@ -260,6 +289,84 @@ check_optional_deps() { info "Checking optional dependencies..." all_ok=1 + if skill_selected "ansible-agent"; then + if command -v ansible-lint >/dev/null 2>&1; then + info " Found: ansible-lint" + else + warn " Missing: ansible-lint (needed by ansible-agent)" + all_ok=0 + fi + + if command -v ansible-playbook >/dev/null 2>&1; then + info " Found: ansible-playbook" + else + warn " Missing: ansible-playbook (needed by ansible-agent)" + all_ok=0 + fi + fi + + if skill_selected "bash-agent"; then + if command -v shellcheck >/dev/null 2>&1; then + info " Found: shellcheck" + else + warn " Missing: shellcheck (needed by bash-agent)" + all_ok=0 + fi + fi + + if skill_selected "docker-agent"; then + if command -v hadolint >/dev/null 2>&1; then + info " Found: hadolint" + else + warn " Missing: hadolint (needed by docker-agent)" + all_ok=0 + fi + fi + + if skill_selected "gha-agent"; then + if command -v actionlint >/dev/null 2>&1; then + info " Found: actionlint" + else + warn " Missing: actionlint (needed by gha-agent)" + all_ok=0 + fi + fi + + if skill_selected "helm-agent"; then + if command -v helm >/dev/null 2>&1; then + info " Found: helm" + else + warn " Missing: helm (needed by helm-agent)" + all_ok=0 + fi + fi + + if skill_selected "kube-agent"; then + if command -v kubeconform >/dev/null 2>&1; then + info " Found: kubeconform" + elif command -v kubeval >/dev/null 2>&1; then + info " Found: kubeval" + else + warn " Missing: kubeconform or kubeval (needed by kube-agent)" + all_ok=0 + fi + fi + + if skill_selected "go-agent"; then + if command -v go >/dev/null 2>&1; then + info " Found: go" + else + warn " Missing: go (needed by go-agent)" + all_ok=0 + fi + + if command -v staticcheck >/dev/null 2>&1; then + info " Found: staticcheck" + else + warn " Missing: staticcheck (optional for go-agent staticcheck step)" + fi + fi + if skill_selected "cargo-agent"; then if command -v jq >/dev/null 2>&1; then info " Found: jq" @@ -301,6 +408,15 @@ check_optional_deps() { done fi + if skill_selected "sql-agent"; then + if command -v sqlfluff >/dev/null 2>&1; then + info " Found: sqlfluff" + else + warn " Missing: sqlfluff (needed by sql-agent)" + all_ok=0 + fi + fi + if skill_selected "terra-agent"; then if command -v terraform >/dev/null 2>&1; then info " Found: terraform" @@ -334,6 +450,27 @@ print_agents_md_snippet() { for skill in $SELECTED_SKILLS; do case "$skill" in + ansible-agent) + echo "- Ansible: use \`/ansible-agent\` (lint/syntax)." + ;; + bash-agent) + echo "- Bash/Shell: use \`/bash-agent\` (syntax/lint)." + ;; + docker-agent) + echo "- Docker: use \`/docker-agent\` (lint Dockerfiles)." + ;; + gha-agent) + echo "- GitHub Actions: use \`/gha-agent\` (lint)." + ;; + helm-agent) + echo "- Helm: use \`/helm-agent\` (lint/template)." + ;; + kube-agent) + echo "- Kubernetes: use \`/kube-agent\` (validate manifests)." + ;; + go-agent) + echo "- Go: use \`/go-agent\` (fmt/vet/staticcheck/test)." + ;; cargo-agent) echo "- Rust: use \`/cargo-agent\` (fmt/check/clippy/test)." ;; @@ -343,6 +480,9 @@ print_agents_md_snippet() { py-agent) echo "- Python: use \`/py-agent\` (format/lint/typecheck/test)." ;; + sql-agent) + echo "- SQL: use \`/sql-agent\` (lint/fix)." + ;; terra-agent) echo "- Terraform: use \`/terra-agent\` (fmt-check/fmt-fix/init/plan-safe/validate/lint)." ;; @@ -408,6 +548,7 @@ fi if [ "$INSTALL_CLAUDE" -eq 1 ]; then if prompt_yes_no "Install skills to ${CLAUDE_SKILLS_DIR}?" yes; then + install_lib_to_root "$CLAUDE_SKILLS_DIR" for skill in $SELECTED_SKILLS; do install_skill_to_root "$CLAUDE_SKILLS_DIR" "$skill" patch_skill_paths "$CLAUDE_SKILLS_DIR" "$skill" @@ -419,6 +560,7 @@ fi if [ "$INSTALL_CODEX" -eq 1 ]; then if prompt_yes_no "Install skills to ${CODEX_SKILLS_DIR}?" yes; then + install_lib_to_root "$CODEX_SKILLS_DIR" for skill in $SELECTED_SKILLS; do install_skill_to_root "$CODEX_SKILLS_DIR" "$skill" patch_skill_paths "$CODEX_SKILLS_DIR" "$skill" diff --git a/lib/x-agent-common.sh b/lib/x-agent-common.sh new file mode 100644 index 0000000..5d587e6 --- /dev/null +++ b/lib/x-agent-common.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +# x-agent-common.sh — shared boilerplate for x-agent workflow runners. +# Source this file from agent scripts; it produces no output or side effects. +# Bash 3.2 compatible. + +# --------------------------------------------------------------------------- +# Environment defaults (agent can override after sourcing) +# --------------------------------------------------------------------------- + +KEEP_DIR="${KEEP_DIR:-0}" +FAIL_FAST="${FAIL_FAST:-0}" +CHANGED_FILES="${CHANGED_FILES:-}" +TMPDIR_ROOT="${TMPDIR_ROOT:-/tmp}" + +# CI-aware MAX_LINES: unlimited in CI, concise locally. +if [[ "${CI:-}" == "true" || "${CI:-}" == "1" ]]; then + MAX_LINES="${MAX_LINES:-999999}" +else + MAX_LINES="${MAX_LINES:-40}" +fi + +# --------------------------------------------------------------------------- +# Output helpers +# --------------------------------------------------------------------------- + +hr() { echo "------------------------------------------------------------"; } + +STEP_START_SECONDS=0 + +step() { + local name="$1" + STEP_START_SECONDS=$SECONDS + hr + echo "Step: $name" +} + +fmt_elapsed() { + local elapsed=$(( SECONDS - STEP_START_SECONDS )) + echo "Time: ${elapsed}s" +} + +# --------------------------------------------------------------------------- +# Flow control +# --------------------------------------------------------------------------- + +# Returns 0 (continue) unless fail-fast is on and a step already failed. +# Caller must declare `overall_ok` before use. +should_continue() { [[ "$FAIL_FAST" != "1" || "$overall_ok" == "1" ]]; } + +# --------------------------------------------------------------------------- +# Dependency checking +# --------------------------------------------------------------------------- + +need() { + command -v "$1" >/dev/null 2>&1 || { echo "Missing required tool: $1" >&2; exit 2; } +} + +# --------------------------------------------------------------------------- +# Setup helpers (called explicitly by agents, not auto-executed) +# --------------------------------------------------------------------------- + +# Create a temp output directory and install a cleanup trap. +# Usage: setup_outdir +# Sets: OUTDIR +setup_outdir() { + local agent_name="$1" + OUTDIR="$(mktemp -d "${TMPDIR_ROOT%/}/${agent_name}.XXXXXX")" + + # Define the cleanup function inside setup_outdir so it captures OUTDIR. + _xagent_cleanup() { + local code="$?" + if [[ "${KEEP_DIR:-0}" == "1" || "$code" != "0" ]]; then + echo "Logs kept in: $OUTDIR" + else + rm -rf "$OUTDIR" + fi + exit "$code" + } + + trap _xagent_cleanup EXIT +} + +# Acquire a workflow lock to prevent concurrent agent runs. +# Usage: setup_lock +setup_lock() { + local agent_name="$1" + local lockfile="${TMPDIR_ROOT%/}/${agent_name}.lock" + + # Open fd 9 for locking. + exec 9>"$lockfile" + + if command -v flock >/dev/null 2>&1; then + if ! flock -n 9; then + echo "${agent_name}: waiting for another run to finish..." + flock 9 + fi + elif command -v perl >/dev/null 2>&1; then + # macOS: flock not available, use perl as a portable fallback. + perl -e ' + use Fcntl ":flock"; + open(my $fh, ">&=", 9) or die "fdopen: $!"; + if (!flock($fh, LOCK_EX | LOCK_NB)) { + print STDERR "'"${agent_name}"': waiting for another run to finish...\n"; + flock($fh, LOCK_EX) or die "flock: $!"; + } + ' + else + echo "Warning: neither flock nor perl available; skipping workflow lock" >&2 + fi +} + +# --------------------------------------------------------------------------- +# Result formatting +# --------------------------------------------------------------------------- + +# Print a step result with optional fix hint. +# Usage: print_result [fix_hint] +# ok: "1" for pass, "0" for fail +# log_path: path to the full log file +# fix_hint: text shown only on failure (optional but expected on FAIL) +print_result() { + local ok="$1" + local log_path="$2" + local fix_hint="${3:-}" + + echo + if [[ "$ok" == "1" ]]; then + echo "Result: PASS" + else + echo "Result: FAIL" + if [[ -n "$fix_hint" ]]; then + echo "Fix: $fix_hint" + fi + fi + echo "Full log: $log_path" + fmt_elapsed +} + +# Print the final overall summary. +# Usage: print_overall +# overall_ok: "1" for pass, "0" for fail +# The caller should exit with the appropriate code after calling this. +print_overall() { + local overall_ok="$1" + hr + if [[ "$overall_ok" == "1" ]]; then + echo "Overall: PASS" + else + echo "Overall: FAIL" + fi + echo "Logs: $OUTDIR" +} diff --git a/skills/ansible-agent/SKILL.md b/skills/ansible-agent/SKILL.md new file mode 100644 index 0000000..714ff27 --- /dev/null +++ b/skills/ansible-agent/SKILL.md @@ -0,0 +1,76 @@ +--- +name: ansible-agent +description: | + Run ansible-agent.sh — a lean Ansible playbook linter and syntax checker that produces agent-friendly output. + Use when: running Ansible checks, linting playbooks with ansible-lint, syntax-checking playbooks, + or when the user asks to validate ansible, run ansible lint, or check ansible syntax. + Triggers on: ansible agent, ansible lint, ansible checks, validate ansible, ansible syntax check. +context: fork +allowed-tools: + - Bash(scripts/ansible-agent.sh*) + - Bash(RUN_*=* scripts/ansible-agent.sh*) + - Bash(FMT_MODE=* scripts/ansible-agent.sh*) + - Bash(MAX_LINES=* scripts/ansible-agent.sh*) + - Bash(KEEP_DIR=* scripts/ansible-agent.sh*) + - Bash(FAIL_FAST=* scripts/ansible-agent.sh*) + - Bash(CHANGED_FILES=* scripts/ansible-agent.sh*) + - Bash(TMPDIR_ROOT=* scripts/ansible-agent.sh*) +--- + +# Ansible Agent + +Run the `ansible-agent.sh` script for lean, structured Ansible playbook linting and syntax checking output designed for coding agents. + +## Script Location + +``` +scripts/ansible-agent.sh +``` + +## Usage + +### Run Full Suite (lint + syntax) +```bash +scripts/ansible-agent.sh +``` + +### Run Individual Steps +```bash +scripts/ansible-agent.sh lint # ansible-lint check only +scripts/ansible-agent.sh syntax # ansible-playbook --syntax-check only +scripts/ansible-agent.sh all # full suite (default) +``` + +### Auto-Fix Lint Issues +```bash +FMT_MODE=fix scripts/ansible-agent.sh lint +``` + +## Environment Knobs + +| Variable | Default | Description | +|----------|---------|-------------| +| `RUN_LINT` | `1` | Set to `0` to skip lint step | +| `RUN_SYNTAX` | `1` | Set to `0` to skip syntax step | +| `FMT_MODE` | `auto` | `auto` = fix locally / check in CI; `check` = always check; `fix` = always fix | +| `FAIL_FAST` | `0` | Set to `1` to stop after first failure (or use `--fail-fast`) | +| `CHANGED_FILES` | _(empty)_ | Space-separated changed file paths; scopes checks to YAML files only | +| `MAX_LINES` | `40` | Max output lines printed per step (unlimited in CI) | +| `KEEP_DIR` | `0` | Set to `1` to keep temp log dir on success | + +## Output Format + +- Each step prints a header (`Step: lint`, `Step: syntax`) +- Results are `PASS`, `FAIL`, or `SKIP` +- On failure, output is truncated to `MAX_LINES` +- Full logs are saved to a temp directory (path printed in output) +- Overall result is printed at the end: `Overall: PASS` or `Overall: FAIL` + +## Important Notes + +- Discovers `.yml` and `.yaml` files recursively +- `ansible-lint` runs project-wide (not scoped to individual files) +- `syntax` step only checks files containing a `hosts:` key +- `CHANGED_FILES` controls skip decisions: if no YAML files are changed, steps are skipped +- In CI (`CI=true`), `FMT_MODE` is forced to `check` regardless of setting +- Reports SKIP when no YAML files or playbooks are found diff --git a/skills/ansible-agent/scripts/ansible-agent.sh b/skills/ansible-agent/scripts/ansible-agent.sh new file mode 100755 index 0000000..07c8910 --- /dev/null +++ b/skills/ansible-agent/scripts/ansible-agent.sh @@ -0,0 +1,288 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ansible-agent: lean Ansible playbook linter and syntax checker for coding agents +# deps: ansible-lint (required), ansible-playbook (required) + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +LIB_DIR="${SCRIPT_DIR}/../../../lib" +# shellcheck source=../../../lib/x-agent-common.sh +source "${LIB_DIR}/x-agent-common.sh" + +# ---- Agent-specific knobs ------------------------------------------------ + +RUN_LINT="${RUN_LINT:-1}" +RUN_SYNTAX="${RUN_SYNTAX:-1}" +FMT_MODE="${FMT_MODE:-auto}" # auto = fix locally, check in CI + +# ---- Usage ---------------------------------------------------------------- + +usage() { + cat <<'EOF' +ansible-agent — lean Ansible playbook linter and syntax checker for coding agents. + +Usage: ansible-agent.sh [options] [command] + +Commands: + lint Run ansible-lint (check or fix mode) + syntax Run ansible-playbook --syntax-check on discovered playbooks + all Run enabled steps (default) + help Show this help + +Options: + --fail-fast Stop after first failing step + +Environment: + RUN_LINT=0|1 Toggle lint step (default: 1) + RUN_SYNTAX=0|1 Toggle syntax step (default: 1) + FMT_MODE=auto|check|fix Lint mode (default: auto — fix locally, check in CI) + CHANGED_FILES="a b" Scope to specific files + MAX_LINES=N Max diagnostic lines per step (default: 40) + KEEP_DIR=0|1 Keep temp log dir on success (default: 0) + FAIL_FAST=0|1 Stop after first failure (default: 0) +EOF +} + +# ---- FMT_MODE resolution -------------------------------------------------- + +resolve_fmt_mode() { + case "$FMT_MODE" in + auto) + if [[ "${CI:-}" == "true" || "${CI:-}" == "1" ]]; then + FMT_MODE="check" + else + FMT_MODE="fix" + fi + ;; + check|fix) + # CI forces check mode regardless + if [[ "${CI:-}" == "true" || "${CI:-}" == "1" ]]; then + FMT_MODE="check" + fi + ;; + *) + echo "Invalid FMT_MODE: ${FMT_MODE} (expected: auto, check, fix)" >&2 + exit 2 + ;; + esac +} + +# ---- Playbook discovery ---------------------------------------------------- + +# Populates ANSIBLE_FILES (newline-separated list of .yml/.yaml paths relevant to ansible) +# and PLAYBOOKS_FOR_SYNTAX (subset with hosts: key for syntax checking). +discover_playbooks() { + ANSIBLE_FILES="" + PLAYBOOKS_FOR_SYNTAX="" + + if [[ -n "${CHANGED_FILES:-}" ]]; then + # Filter CHANGED_FILES to .yml/.yaml files that exist + local f + for f in $CHANGED_FILES; do + case "$f" in + *.yml|*.yaml) + if [[ -f "$f" ]]; then + if [[ -z "$ANSIBLE_FILES" ]]; then + ANSIBLE_FILES="$f" + else + ANSIBLE_FILES="${ANSIBLE_FILES} +$f" + fi + fi + ;; + esac + done + else + # Recursive find, excluding common non-project directories + ANSIBLE_FILES="$(find . \ + -name '.git' -prune -o \ + -name 'node_modules' -prune -o \ + -name 'vendor' -prune -o \ + -name '.venv' -prune -o \ + -name '.cache' -prune -o \ + -name '__pycache__' -prune -o \ + -type f \( -name '*.yml' -o -name '*.yaml' \) \ + -print | sort)" || true + fi + + # Build syntax target list: only files containing hosts: key + if [[ -n "$ANSIBLE_FILES" ]]; then + local f + while IFS= read -r f; do + if [[ -f "$f" ]] && grep -q '^[[:space:]]*-\{0,1\}[[:space:]]*hosts:' "$f" 2>/dev/null; then + if [[ -z "$PLAYBOOKS_FOR_SYNTAX" ]]; then + PLAYBOOKS_FOR_SYNTAX="$f" + else + PLAYBOOKS_FOR_SYNTAX="${PLAYBOOKS_FOR_SYNTAX} +$f" + fi + fi + done <<< "$ANSIBLE_FILES" + fi + + local yaml_count=0 syntax_count=0 + if [[ -n "$ANSIBLE_FILES" ]]; then + yaml_count="$(printf '%s\n' "$ANSIBLE_FILES" | wc -l | tr -d ' ')" + fi + if [[ -n "$PLAYBOOKS_FOR_SYNTAX" ]]; then + syntax_count="$(printf '%s\n' "$PLAYBOOKS_FOR_SYNTAX" | wc -l | tr -d ' ')" + fi + echo "Discovered ${yaml_count} YAML file(s), ${syntax_count} playbook(s) for syntax check" +} + +# ---- Steps ---------------------------------------------------------------- + +run_lint() { + step "lint" + + # CHANGED_FILES set but no YAML files matched + if [[ -n "${CHANGED_FILES:-}" ]] && [[ -z "$ANSIBLE_FILES" ]]; then + echo + echo "Result: SKIP (no YAML files in CHANGED_FILES)" + fmt_elapsed + return 0 + fi + + # No YAML files found at all + if [[ -z "$ANSIBLE_FILES" ]]; then + echo + echo "Result: SKIP (no YAML files found)" + fmt_elapsed + return 0 + fi + + local log="${OUTDIR}/lint.log" + local ok=1 + + if [[ "$FMT_MODE" == "fix" ]]; then + echo "Mode: fix (ansible-lint --fix)" + if ! ansible-lint --fix >"$log" 2>&1; then + ok=0 + fi + else + echo "Mode: check" + if ! ansible-lint >"$log" 2>&1; then + ok=0 + fi + fi + + if [[ "$ok" == "0" ]]; then + echo + echo "Output (first ${MAX_LINES} lines):" + head -n "$MAX_LINES" "$log" + fi + + local fix_hint + if [[ "$FMT_MODE" == "fix" ]]; then + fix_hint="resolve remaining lint issues above, then re-run: /ansible-agent lint" + else + fix_hint="run /ansible-agent lint with FMT_MODE=fix to auto-fix, then re-run: /ansible-agent lint" + fi + + print_result "$ok" "$log" "$fix_hint" + + return $(( 1 - ok )) +} + +run_syntax() { + step "syntax" + + # CHANGED_FILES set but no playbooks matched + if [[ -n "${CHANGED_FILES:-}" ]] && [[ -z "$PLAYBOOKS_FOR_SYNTAX" ]]; then + echo + echo "Result: SKIP (no playbooks in CHANGED_FILES)" + fmt_elapsed + return 0 + fi + + # No playbooks found for syntax checking + if [[ -z "$PLAYBOOKS_FOR_SYNTAX" ]]; then + echo + echo "Result: SKIP (no playbooks found for syntax check)" + fmt_elapsed + return 0 + fi + + local log="${OUTDIR}/syntax.log" + local ok=1 + + while IFS= read -r playbook; do + echo "--- $playbook ---" >> "$log" + if ! ansible-playbook --syntax-check "$playbook" >> "$log" 2>&1; then + ok=0 + fi + done <<< "$PLAYBOOKS_FOR_SYNTAX" + + if [[ "$ok" == "0" ]]; then + echo + echo "Output (first ${MAX_LINES} lines):" + head -n "$MAX_LINES" "$log" + fi + + print_result "$ok" "$log" \ + "resolve syntax errors above, then re-run: /ansible-agent syntax" + + return $(( 1 - ok )) +} + +# ---- Main ----------------------------------------------------------------- + +main() { + # Parse help before need() checks so --help works without tools installed + case "${1:-}" in + -h|--help|help) + usage + exit 0 + ;; + esac + + need ansible-lint + need ansible-playbook + + resolve_fmt_mode + + setup_outdir "ansible-agent" + + # Parse flags + while [[ "${1:-}" == --* ]]; do + case "$1" in + --fail-fast) + # shellcheck disable=SC2034 + FAIL_FAST=1 + shift + ;; + *) + break + ;; + esac + done + + local cmd="${1:-all}" + shift 2>/dev/null || true + local overall_ok=1 + + discover_playbooks + + case "$cmd" in + lint) + run_lint || overall_ok=0 + ;; + syntax) + run_syntax || overall_ok=0 + ;; + all) + if [[ "$RUN_LINT" == "1" ]] && should_continue; then run_lint || overall_ok=0; fi + if [[ "$RUN_SYNTAX" == "1" ]] && should_continue; then run_syntax || overall_ok=0; fi + ;; + *) + echo "Unknown command: $cmd" >&2 + usage + exit 2 + ;; + esac + + print_overall "$overall_ok" + [[ "$overall_ok" == "1" ]] +} + +main "$@" diff --git a/skills/bash-agent/SKILL.md b/skills/bash-agent/SKILL.md new file mode 100644 index 0000000..8c2ccea --- /dev/null +++ b/skills/bash-agent/SKILL.md @@ -0,0 +1,69 @@ +--- +name: bash-agent +description: | + Run bash-agent.sh — a lean shell script validation runner that produces agent-friendly output. + Use when: running shell/bash script checks (syntax, lint), verifying shell scripts before committing, + or when the user asks to run shellcheck, bash checks, or validate shell scripts. + Triggers on: bash agent, shell agent, shellcheck, bash lint, shell checks, verify shell scripts. +context: fork +allowed-tools: + - Bash(scripts/bash-agent.sh*) + - Bash(RUN_*=* scripts/bash-agent.sh*) + - Bash(SHELLCHECK_SEVERITY=* scripts/bash-agent.sh*) + - Bash(MAX_LINES=* scripts/bash-agent.sh*) + - Bash(KEEP_DIR=* scripts/bash-agent.sh*) + - Bash(FAIL_FAST=* scripts/bash-agent.sh*) + - Bash(CHANGED_FILES=* scripts/bash-agent.sh*) +--- + +# Bash Agent + +Run the `bash-agent.sh` script for lean, structured shell script validation output designed for coding agents. + +## Script Location + +``` +scripts/bash-agent.sh +``` + +## Usage + +### Run Full Suite (syntax + lint) +```bash +scripts/bash-agent.sh +``` + +### Run Individual Steps +```bash +scripts/bash-agent.sh syntax # bash -n syntax check only +scripts/bash-agent.sh lint # shellcheck lint only +scripts/bash-agent.sh all # full suite (default) +``` + +## Environment Knobs + +| Variable | Default | Description | +|----------|---------|-------------| +| `RUN_SYNTAX` | `1` | Set to `0` to skip syntax check | +| `RUN_LINT` | `1` | Set to `0` to skip shellcheck lint | +| `SHELLCHECK_SEVERITY` | `warning` | shellcheck `--severity` level (`error`, `warning`, `info`, `style`) | +| `FAIL_FAST` | `0` | Set to `1` to stop after first failure (or use `--fail-fast`) | +| `CHANGED_FILES` | _(empty)_ | Space-separated changed file paths; scopes checks to those `.sh` files | +| `MAX_LINES` | `40` | Max output lines printed per step (unlimited in CI) | +| `KEEP_DIR` | `0` | Set to `1` to keep temp log dir on success | + +## Output Format + +- Each step prints a header (`Step: syntax`, `Step: lint`) +- Results are `PASS`, `FAIL`, or `SKIP` +- On failure, output is truncated to `MAX_LINES` +- Failed shellcheck results include wiki links for each error code +- Full logs are saved to a temp directory (path printed in output) +- Overall result is printed at the end: `Overall: PASS` or `Overall: FAIL` + +## Important Notes + +- The script discovers all `.sh` files recursively, excluding `.git`, `node_modules`, and `vendor` +- `CHANGED_FILES` scopes checks to only the listed `.sh` files that exist +- `SHELLCHECK_SEVERITY` controls the minimum severity level for shellcheck warnings +- In CI (`CI=true`), `MAX_LINES` defaults to unlimited; locally it defaults to 40 diff --git a/skills/bash-agent/scripts/bash-agent.sh b/skills/bash-agent/scripts/bash-agent.sh new file mode 100755 index 0000000..167add6 --- /dev/null +++ b/skills/bash-agent/scripts/bash-agent.sh @@ -0,0 +1,220 @@ +#!/usr/bin/env bash +set -euo pipefail + +# bash-agent: lean shell script validation for coding agents +# deps: bash, shellcheck + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +LIB_DIR="${SCRIPT_DIR}/../../../lib" +# shellcheck source=../../../lib/x-agent-common.sh +source "${LIB_DIR}/x-agent-common.sh" + +# ---- Agent-specific knobs ------------------------------------------------ + +RUN_SYNTAX="${RUN_SYNTAX:-1}" +RUN_LINT="${RUN_LINT:-1}" +SHELLCHECK_SEVERITY="${SHELLCHECK_SEVERITY:-warning}" + +# ---- Usage ---------------------------------------------------------------- + +usage() { + cat <<'EOF' +bash-agent — lean shell script validation for coding agents. + +Usage: bash-agent.sh [options] [command] + +Commands: + syntax Run bash -n syntax check on all .sh files + lint Run shellcheck on all .sh files + all Run syntax + lint (default) + help Show this help + +Options: + --fail-fast Stop after first failing step + +Environment: + RUN_SYNTAX=0|1 Toggle syntax step (default: 1) + RUN_LINT=0|1 Toggle lint step (default: 1) + SHELLCHECK_SEVERITY=... shellcheck --severity level (default: warning) + CHANGED_FILES="a.sh b.sh" Scope to specific files + MAX_LINES=N Max diagnostic lines per step (default: 40) + KEEP_DIR=0|1 Keep temp log dir on success (default: 0) + FAIL_FAST=0|1 Stop after first failure (default: 0) +EOF +} + +# ---- File discovery ------------------------------------------------------- + +# Populates the SH_FILES variable (newline-separated list of .sh file paths). +collect_sh_files() { + SH_FILES="" + + if [[ -n "${CHANGED_FILES:-}" ]]; then + # Filter CHANGED_FILES to existing .sh files + local f + for f in $CHANGED_FILES; do + if [[ "$f" == *.sh ]] && [[ -f "$f" ]]; then + if [[ -z "$SH_FILES" ]]; then + SH_FILES="$f" + else + SH_FILES="${SH_FILES} +$f" + fi + fi + done + else + # Recursive scan excluding common non-project dirs + SH_FILES="$(find . \ + -name .git -prune -o \ + -name node_modules -prune -o \ + -name vendor -prune -o \ + -name '*.sh' -type f -print | sort)" + fi + + local count=0 + if [[ -n "$SH_FILES" ]]; then + count="$(printf '%s\n' "$SH_FILES" | wc -l | tr -d ' ')" + fi + echo "Discovered ${count} .sh file(s)" +} + +# ---- Steps ---------------------------------------------------------------- + +run_syntax() { + step "syntax" + + if [[ -z "$SH_FILES" ]]; then + echo + echo "Result: SKIP (no matching .sh files)" + fmt_elapsed + return 0 + fi + + local log="${OUTDIR}/syntax.log" + local ok=1 + + while IFS= read -r f; do + if ! bash -n "$f" >>"$log" 2>&1; then + ok=0 + fi + done <<< "$SH_FILES" + + if [[ "$ok" == "0" ]]; then + echo + echo "Output (first ${MAX_LINES} lines):" + head -n "$MAX_LINES" "$log" + fi + + print_result "$ok" "$log" \ + "resolve syntax errors above, then re-run: /bash-agent syntax" + + return $(( 1 - ok )) +} + +run_lint() { + step "lint" + + if [[ -z "$SH_FILES" ]]; then + echo + echo "Result: SKIP (no matching .sh files)" + fmt_elapsed + return 0 + fi + + local log="${OUTDIR}/lint.log" + local ok=1 + + # Build file list array for shellcheck invocation + local files=() + while IFS= read -r f; do + files+=("$f") + done <<< "$SH_FILES" + + if ! shellcheck --severity="$SHELLCHECK_SEVERITY" "${files[@]}" >"$log" 2>&1; then + ok=0 + fi + + local fix_hint="" + if [[ "$ok" == "0" ]]; then + echo + echo "Output (first ${MAX_LINES} lines):" + head -n "$MAX_LINES" "$log" + + # Extract unique SC codes and build wiki links + local codes + codes="$(grep -oE 'SC[0-9]+' "$log" | sort -u | head -n 5)" + if [[ -n "$codes" ]]; then + local links="" + while IFS= read -r code; do + if [[ -n "$links" ]]; then + links="${links} , https://www.shellcheck.net/wiki/${code}" + else + links="https://www.shellcheck.net/wiki/${code}" + fi + done <<< "$codes" + fix_hint="see ${links} — resolve issues, then re-run: /bash-agent lint" + else + fix_hint="resolve shellcheck issues above, then re-run: /bash-agent lint" + fi + fi + + print_result "$ok" "$log" "$fix_hint" + + return $(( 1 - ok )) +} + +# ---- Main ----------------------------------------------------------------- + +main() { + # Parse help before need() checks so --help works without tools installed + case "${1:-}" in + -h|--help|help) + usage + exit 0 + ;; + esac + + need bash + need shellcheck + + setup_outdir "bash-agent" + + # Parse flags + while [[ "${1:-}" == --* ]]; do + case "$1" in + --fail-fast) + # shellcheck disable=SC2034 + FAIL_FAST=1 + shift + ;; + *) + break + ;; + esac + done + + local cmd="${1:-all}" + shift 2>/dev/null || true + local overall_ok=1 + + collect_sh_files + + case "$cmd" in + syntax) run_syntax || overall_ok=0 ;; + lint) run_lint || overall_ok=0 ;; + all) + if [[ "$RUN_SYNTAX" == "1" ]] && should_continue; then run_syntax || overall_ok=0; fi + if [[ "$RUN_LINT" == "1" ]] && should_continue; then run_lint || overall_ok=0; fi + ;; + *) + echo "Unknown command: $cmd" >&2 + usage + exit 2 + ;; + esac + + print_overall "$overall_ok" + [[ "$overall_ok" == "1" ]] +} + +main "$@" diff --git a/skills/cargo-agent/scripts/cargo-agent.sh b/skills/cargo-agent/scripts/cargo-agent.sh index d99f6d1..f271359 100755 --- a/skills/cargo-agent/scripts/cargo-agent.sh +++ b/skills/cargo-agent/scripts/cargo-agent.sh @@ -5,14 +5,12 @@ set -euo pipefail # deps: bash, mktemp, jq # optional: cargo-nextest (for tests) +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +LIB_DIR="${SCRIPT_DIR}/../../../lib" +# shellcheck source=../../../lib/x-agent-common.sh +source "${LIB_DIR}/x-agent-common.sh" + JQ_BIN="${JQ_BIN:-jq}" -KEEP_DIR="${KEEP_DIR:-0}" # set to 1 to keep temp dir even on success -# In CI, show full output; locally, limit to 40 lines to keep things tidy. -if [[ "${CI:-}" == "true" || "${CI:-}" == "1" ]]; then - MAX_LINES="${MAX_LINES:-999999}" -else - MAX_LINES="${MAX_LINES:-40}" -fi RUN_TESTS="${RUN_TESTS:-1}" # set to 0 to skip tests RUN_CLIPPY="${RUN_CLIPPY:-1}" # set to 0 to skip clippy RUN_FMT="${RUN_FMT:-1}" # set to 0 to skip fmt @@ -20,77 +18,16 @@ RUN_CHECK="${RUN_CHECK:-1}" # set to 0 to skip check RUN_SQLX="${RUN_SQLX:-1}" # set to 0 to skip sqlx cache verify USE_NEXTEST="${USE_NEXTEST:-auto}" # auto|1|0 RUN_INTEGRATION="${RUN_INTEGRATION:-0}" # set to 1 to run integration tests -FAIL_FAST="${FAIL_FAST:-0}" # set to 1 or use --fail-fast to stop after first failure -CHANGED_FILES="${CHANGED_FILES:-}" # space-separated list of changed files; scopes to affected packages # Default to SQLx offline mode, but allow explicit overrides (e.g. CI sets false). export SQLX_OFFLINE="${SQLX_OFFLINE:-true}" -TMPDIR_ROOT="${TMPDIR_ROOT:-/tmp}" -OUTDIR="$(mktemp -d "${TMPDIR_ROOT%/}/cargo-agent.XXXXXX")" - -cleanup() { - local code="$?" - if [[ "$KEEP_DIR" == "1" || "$code" != "0" ]]; then - echo "Logs kept in: $OUTDIR" - else - rm -rf "$OUTDIR" - fi - exit "$code" -} -trap cleanup EXIT - -# Workflow-level lock: only one cargo-agent instance runs at a time. -# Prevents overlapping builds when agents invoke the script concurrently. -LOCKFILE="${TMPDIR_ROOT%/}/cargo-agent.lock" -exec 9>"$LOCKFILE" -if command -v flock >/dev/null 2>&1; then - if ! flock -n 9; then - echo "cargo-agent: waiting for another run to finish..." - flock 9 - fi -else - # macOS: flock not available, use perl as a portable fallback. - if ! command -v perl >/dev/null 2>&1; then - echo "Warning: neither flock nor perl available; skipping workflow lock" >&2 - else - perl -e ' - use Fcntl ":flock"; - open(my $fh, ">&=", 9) or die "fdopen: $!"; - if (!flock($fh, LOCK_EX | LOCK_NB)) { - print STDERR "cargo-agent: waiting for another run to finish...\n"; - flock($fh, LOCK_EX) or die "flock: $!"; - } - ' - fi -fi - -need() { - command -v "$1" >/dev/null 2>&1 || { echo "Missing required tool: $1" >&2; exit 2; } -} +setup_outdir "cargo-agent" +setup_lock "cargo-agent" need "$JQ_BIN" need cargo -hr() { echo "------------------------------------------------------------"; } - -# Returns 0 (continue) unless fail-fast is on and a step already failed. -should_continue() { [[ "$FAIL_FAST" != "1" || "$overall_ok" == "1" ]]; } - -STEP_START_SECONDS=0 - -step() { - local name="$1" - STEP_START_SECONDS=$SECONDS - hr - echo "Step: $name" -} - -fmt_elapsed() { - local elapsed=$(( SECONDS - STEP_START_SECONDS )) - echo "Time: ${elapsed}s" -} - # Extract compiler diagnostics (errors/warnings) from cargo JSON stream. # Outputs: lines like "error: message" and optionally a location line. extract_compiler_diags() { @@ -649,9 +586,7 @@ main() { ;; esac - hr - echo "Overall: $([[ "$overall_ok" == "1" ]] && echo PASS || echo FAIL)" - echo "Logs: $OUTDIR" + print_overall "$overall_ok" [[ "$overall_ok" == "1" ]] } diff --git a/skills/docker-agent/SKILL.md b/skills/docker-agent/SKILL.md new file mode 100644 index 0000000..67050e8 --- /dev/null +++ b/skills/docker-agent/SKILL.md @@ -0,0 +1,74 @@ +--- +name: docker-agent +description: | + Run docker-agent.sh — a lean Dockerfile linter that produces agent-friendly output. + Use when: running Dockerfile checks, linting Dockerfiles with hadolint, verifying Dockerfiles before committing, + or when the user asks to run hadolint, dockerfile lint, or docker checks. + Triggers on: docker agent, dockerfile lint, hadolint, docker checks, validate dockerfile. +context: fork +allowed-tools: + - Bash(scripts/docker-agent.sh*) + - Bash(RUN_*=* scripts/docker-agent.sh*) + - Bash(MAX_LINES=* scripts/docker-agent.sh*) + - Bash(KEEP_DIR=* scripts/docker-agent.sh*) + - Bash(FAIL_FAST=* scripts/docker-agent.sh*) + - Bash(CHANGED_FILES=* scripts/docker-agent.sh*) + - Bash(TMPDIR_ROOT=* scripts/docker-agent.sh*) +--- + +# Docker Agent + +Run the `docker-agent.sh` script for lean, structured Dockerfile linting output designed for coding agents. + +## Script Location + +``` +scripts/docker-agent.sh +``` + +## Usage + +### Run Full Suite (lint) +```bash +scripts/docker-agent.sh +``` + +### Run Individual Steps +```bash +scripts/docker-agent.sh lint # hadolint check only +scripts/docker-agent.sh build-check # BuildKit lint mode (requires docker) +scripts/docker-agent.sh all # full suite (default: lint only) +``` + +### Enable Build Check +```bash +RUN_BUILD_CHECK=1 scripts/docker-agent.sh all +``` + +## Environment Knobs + +| Variable | Default | Description | +|----------|---------|-------------| +| `RUN_LINT` | `1` | Set to `0` to skip lint step | +| `RUN_BUILD_CHECK` | `0` | Set to `1` to enable build-check step (requires docker) | +| `FAIL_FAST` | `0` | Set to `1` to stop after first failure (or use `--fail-fast`) | +| `CHANGED_FILES` | _(empty)_ | Space-separated changed file paths; scopes checks to Dockerfiles only | +| `MAX_LINES` | `40` | Max output lines printed per step (unlimited in CI) | +| `KEEP_DIR` | `0` | Set to `1` to keep temp log dir on success | + +## Output Format + +- Each step prints a header (`Step: lint`) +- Results are `PASS`, `FAIL`, or `SKIP` +- On failure, output is truncated to `MAX_LINES` +- Full logs are saved to a temp directory (path printed in output) +- Overall result is printed at the end: `Overall: PASS` or `Overall: FAIL` + +## Important Notes + +- Discovers `Dockerfile`, `Dockerfile.*`, and `*.dockerfile` files recursively +- `CHANGED_FILES` scopes checks to only Dockerfile-like files +- Reports SKIP when no Dockerfiles are found +- `build-check` defaults to OFF — it requires a Docker daemon and is expensive +- `build-check` skips with notice if `docker` is not installed +- In CI (`CI=true`), `MAX_LINES` defaults to unlimited; locally it defaults to 40 diff --git a/skills/docker-agent/scripts/docker-agent.sh b/skills/docker-agent/scripts/docker-agent.sh new file mode 100755 index 0000000..2429a65 --- /dev/null +++ b/skills/docker-agent/scripts/docker-agent.sh @@ -0,0 +1,229 @@ +#!/usr/bin/env bash +set -euo pipefail + +# docker-agent: lean Dockerfile linter for coding agents +# deps: hadolint (required), docker (optional, for build-check) + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +LIB_DIR="${SCRIPT_DIR}/../../../lib" +# shellcheck source=../../../lib/x-agent-common.sh +source "${LIB_DIR}/x-agent-common.sh" + +# ---- Agent-specific knobs ------------------------------------------------ + +RUN_LINT="${RUN_LINT:-1}" +RUN_BUILD_CHECK="${RUN_BUILD_CHECK:-0}" # opt-in, expensive + +# ---- Usage ---------------------------------------------------------------- + +usage() { + cat <<'EOF' +docker-agent — lean Dockerfile linter for coding agents. + +Usage: docker-agent.sh [options] [command] + +Commands: + lint Run hadolint on discovered Dockerfiles + build-check Run docker build --check (BuildKit lint mode, opt-in) + all Run enabled steps (default: lint only) + help Show this help + +Options: + --fail-fast Stop after first failing step + +Environment: + RUN_LINT=0|1 Toggle lint step (default: 1) + RUN_BUILD_CHECK=0|1 Toggle build-check step (default: 0) + CHANGED_FILES="a b" Scope to specific files + MAX_LINES=N Max diagnostic lines per step (default: 40) + KEEP_DIR=0|1 Keep temp log dir on success (default: 0) + FAIL_FAST=0|1 Stop after first failure (default: 0) +EOF +} + +# ---- Dockerfile discovery ------------------------------------------------- + +# Populates DOCKERFILES (newline-separated list of Dockerfile paths). +discover_dockerfiles() { + DOCKERFILES="" + + if [[ -n "${CHANGED_FILES:-}" ]]; then + local f + for f in $CHANGED_FILES; do + local base + base="$(basename "$f")" + case "$base" in + Dockerfile|Dockerfile.*|*.dockerfile) + if [[ -f "$f" ]]; then + if [[ -z "$DOCKERFILES" ]]; then + DOCKERFILES="$f" + else + DOCKERFILES="${DOCKERFILES} +$f" + fi + fi + ;; + esac + done + else + # Recursive find, excluding common non-project directories + DOCKERFILES="$(find . \ + -name '.git' -prune -o \ + -name 'node_modules' -prune -o \ + -name 'vendor' -prune -o \ + -type f \( -name 'Dockerfile' -o -name 'Dockerfile.*' -o -name '*.dockerfile' \) \ + -print | sort)" + fi + + local count=0 + if [[ -n "$DOCKERFILES" ]]; then + count="$(printf '%s\n' "$DOCKERFILES" | wc -l | tr -d ' ')" + fi + echo "Discovered ${count} Dockerfile(s)" +} + +# ---- Steps ---------------------------------------------------------------- + +run_lint() { + step "lint" + + if [[ -n "${CHANGED_FILES:-}" ]] && [[ -z "$DOCKERFILES" ]]; then + echo + echo "Result: SKIP (no Dockerfiles in CHANGED_FILES)" + fmt_elapsed + return 0 + fi + + if [[ -z "$DOCKERFILES" ]]; then + echo + echo "Result: SKIP (no Dockerfiles found)" + fmt_elapsed + return 0 + fi + + local log="${OUTDIR}/lint.log" + local ok=1 + + while IFS= read -r dockerfile; do + echo "--- $dockerfile ---" >> "$log" + if ! hadolint "$dockerfile" >> "$log" 2>&1; then + ok=0 + fi + done <<< "$DOCKERFILES" + + if [[ "$ok" == "0" ]]; then + echo + echo "Output (first ${MAX_LINES} lines):" + head -n "$MAX_LINES" "$log" + fi + + print_result "$ok" "$log" \ + "resolve hadolint issues above, then re-run: /docker-agent lint" + + return $(( 1 - ok )) +} + +run_build_check() { + step "build-check" + + if ! command -v docker >/dev/null 2>&1; then + echo + echo "Result: SKIP (docker not found)" + fmt_elapsed + return 0 + fi + + if [[ -n "${CHANGED_FILES:-}" ]] && [[ -z "$DOCKERFILES" ]]; then + echo + echo "Result: SKIP (no Dockerfiles in CHANGED_FILES)" + fmt_elapsed + return 0 + fi + + if [[ -z "$DOCKERFILES" ]]; then + echo + echo "Result: SKIP (no Dockerfiles found)" + fmt_elapsed + return 0 + fi + + local log="${OUTDIR}/build-check.log" + local ok=1 + + while IFS= read -r dockerfile; do + echo "--- $dockerfile ---" >> "$log" + if ! docker build --check -f "$dockerfile" . >> "$log" 2>&1; then + ok=0 + fi + done <<< "$DOCKERFILES" + + if [[ "$ok" == "0" ]]; then + echo + echo "Output (first ${MAX_LINES} lines):" + head -n "$MAX_LINES" "$log" + fi + + print_result "$ok" "$log" \ + "resolve build check errors above, then re-run: /docker-agent build-check" + + return $(( 1 - ok )) +} + +# ---- Main ----------------------------------------------------------------- + +main() { + # Parse help before need() checks so --help works without tools installed + case "${1:-}" in + -h|--help|help) + usage + exit 0 + ;; + esac + + need hadolint + + setup_outdir "docker-agent" + + # Parse flags + while [[ "${1:-}" == --* ]]; do + case "$1" in + --fail-fast) + # shellcheck disable=SC2034 + FAIL_FAST=1 + shift + ;; + *) + break + ;; + esac + done + + local cmd="${1:-all}" + shift 2>/dev/null || true + local overall_ok=1 + + discover_dockerfiles + + case "$cmd" in + lint) + run_lint || overall_ok=0 + ;; + build-check) + run_build_check || overall_ok=0 + ;; + all) + if [[ "$RUN_LINT" == "1" ]] && should_continue; then run_lint || overall_ok=0; fi + if [[ "$RUN_BUILD_CHECK" == "1" ]] && should_continue; then run_build_check || overall_ok=0; fi + ;; + *) + echo "Unknown command: $cmd" >&2 + usage + exit 2 + ;; + esac + + print_overall "$overall_ok" + [[ "$overall_ok" == "1" ]] +} + +main "$@" diff --git a/skills/gha-agent/SKILL.md b/skills/gha-agent/SKILL.md new file mode 100644 index 0000000..2287c17 --- /dev/null +++ b/skills/gha-agent/SKILL.md @@ -0,0 +1,64 @@ +--- +name: gha-agent +description: | + Run gha-agent.sh — a lean GitHub Actions workflow linter that produces agent-friendly output. + Use when: running GitHub Actions workflow checks, linting workflow files, verifying workflows before committing, + or when the user asks to run actionlint, workflow lint, or GitHub Actions checks. + Triggers on: gha agent, github actions lint, actionlint, workflow lint, github actions checks. +context: fork +allowed-tools: + - Bash(scripts/gha-agent.sh*) + - Bash(RUN_*=* scripts/gha-agent.sh*) + - Bash(MAX_LINES=* scripts/gha-agent.sh*) + - Bash(KEEP_DIR=* scripts/gha-agent.sh*) + - Bash(FAIL_FAST=* scripts/gha-agent.sh*) + - Bash(CHANGED_FILES=* scripts/gha-agent.sh*) +--- + +# GHA Agent + +Run the `gha-agent.sh` script for lean, structured GitHub Actions workflow linting output designed for coding agents. + +## Script Location + +``` +scripts/gha-agent.sh +``` + +## Usage + +### Run Full Suite (lint) +```bash +scripts/gha-agent.sh +``` + +### Run Individual Steps +```bash +scripts/gha-agent.sh lint # actionlint check only +scripts/gha-agent.sh all # full suite (default) +``` + +## Environment Knobs + +| Variable | Default | Description | +|----------|---------|-------------| +| `RUN_LINT` | `1` | Set to `0` to skip lint step | +| `FAIL_FAST` | `0` | Set to `1` to stop after first failure (or use `--fail-fast`) | +| `CHANGED_FILES` | _(empty)_ | Space-separated changed file paths; scopes checks to workflow files only | +| `MAX_LINES` | `40` | Max output lines printed per step (unlimited in CI) | +| `KEEP_DIR` | `0` | Set to `1` to keep temp log dir on success | + +## Output Format + +- Each step prints a header (`Step: lint`) +- Results are `PASS`, `FAIL`, or `SKIP` +- On failure, output is truncated to `MAX_LINES` +- Full logs are saved to a temp directory (path printed in output) +- Overall result is printed at the end: `Overall: PASS` or `Overall: FAIL` + +## Important Notes + +- The script discovers `.yml` and `.yaml` files in `.github/workflows/` +- `CHANGED_FILES` scopes checks to only workflow files (`.yml`/`.yaml` under `.github/workflows/`) +- Reports SKIP when no `.github/workflows/` directory exists +- In CI (`CI=true`), `MAX_LINES` defaults to unlimited; locally it defaults to 40 diff --git a/skills/gha-agent/scripts/gha-agent.sh b/skills/gha-agent/scripts/gha-agent.sh new file mode 100755 index 0000000..e1df55a --- /dev/null +++ b/skills/gha-agent/scripts/gha-agent.sh @@ -0,0 +1,186 @@ +#!/usr/bin/env bash +set -euo pipefail + +# gha-agent: lean GitHub Actions workflow linter for coding agents +# deps: actionlint + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +LIB_DIR="${SCRIPT_DIR}/../../../lib" +# shellcheck source=../../../lib/x-agent-common.sh +source "${LIB_DIR}/x-agent-common.sh" + +# ---- Agent-specific knobs ------------------------------------------------ + +RUN_LINT="${RUN_LINT:-1}" + +# ---- Usage ---------------------------------------------------------------- + +usage() { + cat <<'EOF' +gha-agent — lean GitHub Actions workflow linter for coding agents. + +Usage: gha-agent.sh [options] [command] + +Commands: + lint Run actionlint on workflow files + all Run lint (default) + help Show this help + +Options: + --fail-fast Stop after first failing step + +Environment: + RUN_LINT=0|1 Toggle lint step (default: 1) + CHANGED_FILES="a.yml b.yml" Scope to specific files + MAX_LINES=N Max diagnostic lines per step (default: 40) + KEEP_DIR=0|1 Keep temp log dir on success (default: 0) + FAIL_FAST=0|1 Stop after first failure (default: 0) +EOF +} + +# ---- Workflow discovery --------------------------------------------------- + +# Populates WORKFLOW_FILES (newline-separated list of workflow file paths). +collect_workflow_files() { + WORKFLOW_FILES="" + + if [[ -n "${CHANGED_FILES:-}" ]]; then + # Filter CHANGED_FILES to existing .yml/.yaml under .github/workflows/ + local f + for f in $CHANGED_FILES; do + case "$f" in + .github/workflows/*.yml|.github/workflows/*.yaml) + if [[ -f "$f" ]]; then + if [[ -z "$WORKFLOW_FILES" ]]; then + WORKFLOW_FILES="$f" + else + WORKFLOW_FILES="${WORKFLOW_FILES} +$f" + fi + fi + ;; + esac + done + else + if [[ ! -d ".github/workflows" ]]; then + WORKFLOW_FILES="" + return 0 + fi + # Discover all .yml/.yaml files in .github/workflows/ + WORKFLOW_FILES="$(find .github/workflows \ + -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) -print | sort)" + fi + + local count=0 + if [[ -n "$WORKFLOW_FILES" ]]; then + count="$(printf '%s\n' "$WORKFLOW_FILES" | wc -l | tr -d ' ')" + fi + echo "Discovered ${count} workflow file(s)" +} + +# ---- Steps ---------------------------------------------------------------- + +run_lint() { + step "lint" + + # No .github/workflows directory at all + if [[ -z "${CHANGED_FILES:-}" ]] && [[ ! -d ".github/workflows" ]]; then + echo + echo "Result: SKIP (no .github/workflows/ directory found)" + fmt_elapsed + return 0 + fi + + # CHANGED_FILES set but no workflow files matched + if [[ -n "${CHANGED_FILES:-}" ]] && [[ -z "$WORKFLOW_FILES" ]]; then + echo + echo "Result: SKIP (no workflow files in CHANGED_FILES)" + fmt_elapsed + return 0 + fi + + # No workflow files found via discovery + if [[ -z "$WORKFLOW_FILES" ]]; then + echo + echo "Result: SKIP (no .yml/.yaml files in .github/workflows/)" + fmt_elapsed + return 0 + fi + + local log="${OUTDIR}/lint.log" + local ok=1 + + # Build file list array for actionlint invocation + local files=() + while IFS= read -r f; do + files+=("$f") + done <<< "$WORKFLOW_FILES" + + if ! actionlint "${files[@]}" >"$log" 2>&1; then + ok=0 + fi + + if [[ "$ok" == "0" ]]; then + echo + echo "Output (first ${MAX_LINES} lines):" + head -n "$MAX_LINES" "$log" + fi + + print_result "$ok" "$log" \ + "resolve the workflow errors above, then re-run: /gha-agent lint" + + return $(( 1 - ok )) +} + +# ---- Main ----------------------------------------------------------------- + +main() { + # Parse help before need() checks so --help works without tools installed + case "${1:-}" in + -h|--help|help) + usage + exit 0 + ;; + esac + + need actionlint + + setup_outdir "gha-agent" + + # Parse flags + while [[ "${1:-}" == --* ]]; do + case "$1" in + --fail-fast) + # shellcheck disable=SC2034 + FAIL_FAST=1 + shift + ;; + *) + break + ;; + esac + done + + local cmd="${1:-all}" + shift 2>/dev/null || true + local overall_ok=1 + + collect_workflow_files + + case "$cmd" in + lint) run_lint || overall_ok=0 ;; + all) + if [[ "$RUN_LINT" == "1" ]] && should_continue; then run_lint || overall_ok=0; fi + ;; + *) + echo "Unknown command: $cmd" >&2 + usage + exit 2 + ;; + esac + + print_overall "$overall_ok" + [[ "$overall_ok" == "1" ]] +} + +main "$@" diff --git a/skills/go-agent/SKILL.md b/skills/go-agent/SKILL.md new file mode 100644 index 0000000..8861def --- /dev/null +++ b/skills/go-agent/SKILL.md @@ -0,0 +1,74 @@ +--- +name: go-agent +description: | + Run go-agent.sh — a lean Go workflow runner that produces agent-friendly output. + Use when: running Go checks (fmt, vet, staticcheck, test), verifying Go code before committing, + or when the user asks to run go checks, lint, format, or test a Go project. + Triggers on: go agent, run go checks, golang checks, go fmt vet test, verify go code. +context: fork +allowed-tools: + - Bash(scripts/go-agent.sh*) + - Bash(RUN_*=* scripts/go-agent.sh*) + - Bash(FMT_MODE=* scripts/go-agent.sh*) + - Bash(MAX_LINES=* scripts/go-agent.sh*) + - Bash(KEEP_DIR=* scripts/go-agent.sh*) + - Bash(FAIL_FAST=* scripts/go-agent.sh*) + - Bash(CHANGED_FILES=* scripts/go-agent.sh*) +--- + +# Go Agent + +Run the `go-agent.sh` script for lean, structured Go validation output designed for coding agents. + +## Script Location + +``` +scripts/go-agent.sh +``` + +## Usage + +### Run Full Suite (fmt + vet + staticcheck + test) +```bash +scripts/go-agent.sh +``` + +### Run Individual Steps +```bash +scripts/go-agent.sh fmt # gofmt formatting check/fix +scripts/go-agent.sh vet # go vet analysis +scripts/go-agent.sh staticcheck # staticcheck linter +scripts/go-agent.sh test # go test +scripts/go-agent.sh all # full suite (default) +``` + +## Environment Knobs + +| Variable | Default | Description | +|----------|---------|-------------| +| `RUN_FMT` | `1` | Set to `0` to skip fmt step | +| `RUN_VET` | `1` | Set to `0` to skip vet step | +| `RUN_STATICCHECK` | `1` | Set to `0` to skip staticcheck step | +| `RUN_TESTS` | `1` | Set to `0` to skip test step | +| `FMT_MODE` | `auto` | `auto` = fix locally, check in CI; `check` = list only; `fix` = rewrite | +| `FAIL_FAST` | `0` | Set to `1` to stop after first failure (or use `--fail-fast`) | +| `CHANGED_FILES` | _(empty)_ | Space-separated changed file paths; scopes checks to those `.go` packages | +| `MAX_LINES` | `40` | Max output lines printed per step (unlimited in CI) | +| `KEEP_DIR` | `0` | Set to `1` to keep temp log dir on success | + +## Output Format + +- Each step prints a header (`Step: fmt`, `Step: vet`, etc.) +- Results are `PASS`, `FAIL`, or `SKIP` +- On failure, output is truncated to `MAX_LINES` +- Failed test results include extracted failing test names +- `staticcheck` is skipped with a notice if not installed +- Full logs are saved to a temp directory (path printed in output) +- Overall result is printed at the end: `Overall: PASS` or `Overall: FAIL` + +## Important Notes + +- `FMT_MODE=auto` fixes formatting locally but only checks in CI (`CI=true`) +- `staticcheck` is optional; if not installed, the step is skipped (not failed) +- `CHANGED_FILES` scopes checks to only the listed `.go` file packages +- In CI (`CI=true`), `MAX_LINES` defaults to unlimited; locally it defaults to 40 diff --git a/skills/go-agent/scripts/go-agent.sh b/skills/go-agent/scripts/go-agent.sh new file mode 100755 index 0000000..a58ab64 --- /dev/null +++ b/skills/go-agent/scripts/go-agent.sh @@ -0,0 +1,328 @@ +#!/usr/bin/env bash +set -euo pipefail + +# go-agent: lean Go workflow runner for coding agents +# deps: go (required), staticcheck (optional) + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +LIB_DIR="${SCRIPT_DIR}/../../../lib" +# shellcheck source=../../../lib/x-agent-common.sh +source "${LIB_DIR}/x-agent-common.sh" + +# ---- Agent-specific knobs ------------------------------------------------ + +RUN_FMT="${RUN_FMT:-1}" +RUN_VET="${RUN_VET:-1}" +RUN_STATICCHECK="${RUN_STATICCHECK:-1}" +RUN_TESTS="${RUN_TESTS:-1}" +FMT_MODE="${FMT_MODE:-auto}" + +# ---- Usage ---------------------------------------------------------------- + +usage() { + cat <<'EOF' +go-agent — lean Go workflow runner for coding agents. + +Usage: go-agent.sh [options] [command] + +Commands: + fmt Run gofmt formatting check/fix + vet Run go vet analysis + staticcheck Run staticcheck linter (skipped if not installed) + test Run go test + all Run fmt + vet + staticcheck + test (default) + help Show this help + +Options: + --fail-fast Stop after first failing step + +Environment: + RUN_FMT=0|1 Toggle fmt step (default: 1) + RUN_VET=0|1 Toggle vet step (default: 1) + RUN_STATICCHECK=0|1 Toggle staticcheck step (default: 1) + RUN_TESTS=0|1 Toggle test step (default: 1) + FMT_MODE=auto|check|fix Format mode (default: auto — fix locally, check in CI) + CHANGED_FILES="a.go b.go" Scope to specific files/packages + MAX_LINES=N Max diagnostic lines per step (default: 40) + KEEP_DIR=0|1 Keep temp log dir on success (default: 0) + FAIL_FAST=0|1 Stop after first failure (default: 0) +EOF +} + +# ---- Scope resolution ----------------------------------------------------- + +# Resolve CHANGED_FILES to unique Go package directories. +# Sets SCOPED_DIRS (space-separated) and SCOPED_PKGS (./dir/... format). +resolve_scope() { + SCOPED_DIRS="" + SCOPED_PKGS="" + + if [[ -z "${CHANGED_FILES:-}" ]]; then + return 0 + fi + + local dirs="" + local f d + for f in $CHANGED_FILES; do + if [[ "$f" == *.go ]] && [[ -f "$f" ]]; then + d="$(dirname "$f")" + # Deduplicate + case " $dirs " in + *" $d "*) ;; + *) dirs="${dirs:+$dirs }$d" ;; + esac + fi + done + + if [[ -z "$dirs" ]]; then + return 0 + fi + + SCOPED_DIRS="$dirs" + # Build ./dir format for go tool commands + local pkgs="" + for d in $dirs; do + local pkg + if [[ "$d" == "." ]]; then + pkg="./..." + else + pkg="./${d#./}" + fi + pkgs="${pkgs:+$pkgs }$pkg" + done + SCOPED_PKGS="$pkgs" + + echo "Scoped to changed packages: ${SCOPED_PKGS}" +} + +# Returns the fmt targets: scoped dirs or "." for full project. +fmt_targets() { + if [[ -n "$SCOPED_DIRS" ]]; then + echo "$SCOPED_DIRS" + else + echo "." + fi +} + +# Returns the package targets: scoped packages or "./..." for full project. +pkg_targets() { + if [[ -n "$SCOPED_PKGS" ]]; then + echo "$SCOPED_PKGS" + else + echo "./..." + fi +} + +# ---- FMT_MODE resolution -------------------------------------------------- + +resolve_fmt_mode() { + case "$FMT_MODE" in + auto) + if [[ "${CI:-}" == "true" || "${CI:-}" == "1" ]]; then + FMT_MODE="check" + else + FMT_MODE="fix" + fi + ;; + check|fix) + # CI forces check regardless of user setting + if [[ "${CI:-}" == "true" || "${CI:-}" == "1" ]]; then + FMT_MODE="check" + fi + ;; + *) + echo "Invalid FMT_MODE: ${FMT_MODE} (expected: auto, check, fix)" >&2 + exit 2 + ;; + esac +} + +# ---- Steps ---------------------------------------------------------------- + +run_fmt() { + step "fmt" + + local log="${OUTDIR}/fmt.log" + local ok=1 + local targets + # shellcheck disable=SC2046 + targets=$(fmt_targets) + + if [[ "$FMT_MODE" == "check" ]]; then + # List files needing formatting + local unformatted + # gofmt -l exits 0 even if files need formatting; check output + # shellcheck disable=SC2086 + unformatted="$(gofmt -l $targets 2>"$log")" || true + if [[ -n "$unformatted" ]]; then + ok=0 + echo "$unformatted" >> "$log" + echo + echo "Files needing formatting:" + echo "$unformatted" | head -n "$MAX_LINES" + fi + else + # Fix mode: rewrite files + # shellcheck disable=SC2086 + if ! gofmt -w $targets >"$log" 2>&1; then + ok=0 + fi + fi + + local fix_hint="" + if [[ "$ok" == "0" ]]; then + fix_hint="run /go-agent fmt with FMT_MODE=fix, then re-run: /go-agent fmt" + fi + + print_result "$ok" "$log" "$fix_hint" + return $(( 1 - ok )) +} + +run_vet() { + step "vet" + + local log="${OUTDIR}/vet.log" + local ok=1 + local targets + targets=$(pkg_targets) + + # go vet writes diagnostics to stderr + # shellcheck disable=SC2086 + if ! go vet $targets >"$log" 2>&1; then + ok=0 + fi + + if [[ "$ok" == "0" ]]; then + echo + echo "Output (first ${MAX_LINES} lines):" + head -n "$MAX_LINES" "$log" + fi + + print_result "$ok" "$log" \ + "resolve the vet issues above, then re-run: /go-agent vet" + return $(( 1 - ok )) +} + +run_staticcheck() { + step "staticcheck" + + if ! command -v staticcheck >/dev/null 2>&1; then + echo + echo "Result: SKIP (staticcheck not found — install via go install honnef.co/go/tools/cmd/staticcheck@latest)" + fmt_elapsed + return 0 + fi + + local log="${OUTDIR}/staticcheck.log" + local ok=1 + local targets + targets=$(pkg_targets) + + # shellcheck disable=SC2086 + if ! staticcheck $targets >"$log" 2>&1; then + ok=0 + fi + + if [[ "$ok" == "0" ]]; then + echo + echo "Output (first ${MAX_LINES} lines):" + head -n "$MAX_LINES" "$log" + fi + + print_result "$ok" "$log" \ + "resolve the staticcheck issues above, then re-run: /go-agent staticcheck" + return $(( 1 - ok )) +} + +run_test() { + step "test" + + local log="${OUTDIR}/test.log" + local ok=1 + local targets + targets=$(pkg_targets) + + # shellcheck disable=SC2086 + if ! go test $targets >"$log" 2>&1; then + ok=0 + fi + + local fix_hint="" + if [[ "$ok" == "0" ]]; then + echo + echo "Output (first ${MAX_LINES} lines):" + head -n "$MAX_LINES" "$log" + + # Extract failing test names from --- FAIL: lines + local failing_tests + failing_tests="$(grep '^--- FAIL:' "$log" | sed 's/^--- FAIL: \([^ ]*\).*/\1/' | sort -u | paste -sd ', ' -)" || true + if [[ -n "$failing_tests" ]]; then + fix_hint="failing tests: ${failing_tests} — resolve and re-run: /go-agent test" + else + fix_hint="resolve the test failures above, then re-run: /go-agent test" + fi + fi + + print_result "$ok" "$log" "$fix_hint" + return $(( 1 - ok )) +} + +# ---- Main ----------------------------------------------------------------- + +main() { + # Parse help before need() checks so --help works without tools installed + case "${1:-}" in + -h|--help|help) + usage + exit 0 + ;; + esac + + need go + + setup_outdir "go-agent" + + # Parse flags + while [[ "${1:-}" == --* ]]; do + case "$1" in + --fail-fast) + # shellcheck disable=SC2034 + FAIL_FAST=1 + shift + ;; + *) + break + ;; + esac + done + + local cmd="${1:-all}" + shift 2>/dev/null || true + local overall_ok=1 + + resolve_fmt_mode + resolve_scope + + case "$cmd" in + fmt) run_fmt || overall_ok=0 ;; + vet) run_vet || overall_ok=0 ;; + staticcheck) run_staticcheck || overall_ok=0 ;; + test) run_test || overall_ok=0 ;; + all) + if [[ "$RUN_FMT" == "1" ]] && should_continue; then run_fmt || overall_ok=0; fi + if [[ "$RUN_VET" == "1" ]] && should_continue; then run_vet || overall_ok=0; fi + if [[ "$RUN_STATICCHECK" == "1" ]] && should_continue; then run_staticcheck || overall_ok=0; fi + if [[ "$RUN_TESTS" == "1" ]] && should_continue; then run_test || overall_ok=0; fi + ;; + *) + echo "Unknown command: $cmd" >&2 + usage + exit 2 + ;; + esac + + print_overall "$overall_ok" + [[ "$overall_ok" == "1" ]] +} + +main "$@" diff --git a/skills/helm-agent/SKILL.md b/skills/helm-agent/SKILL.md new file mode 100644 index 0000000..f67767f --- /dev/null +++ b/skills/helm-agent/SKILL.md @@ -0,0 +1,70 @@ +--- +name: helm-agent +description: | + Run helm-agent.sh — a lean Helm chart linter and template validator that produces agent-friendly output. + Use when: running Helm chart checks, linting charts, validating templates, verifying Helm charts before committing, + or when the user asks to run helm lint, helm template, or Helm chart checks. + Triggers on: helm agent, helm lint, helm checks, helm template, validate helm chart. +context: fork +allowed-tools: + - Bash(scripts/helm-agent.sh*) + - Bash(RUN_*=* scripts/helm-agent.sh*) + - Bash(CHART_DIR=* scripts/helm-agent.sh*) + - Bash(MAX_LINES=* scripts/helm-agent.sh*) + - Bash(KEEP_DIR=* scripts/helm-agent.sh*) + - Bash(FAIL_FAST=* scripts/helm-agent.sh*) + - Bash(CHANGED_FILES=* scripts/helm-agent.sh*) + - Bash(TMPDIR_ROOT=* scripts/helm-agent.sh*) +--- + +# Helm Agent + +Run the `helm-agent.sh` script for lean, structured Helm chart linting and template validation output designed for coding agents. + +## Script Location + +``` +scripts/helm-agent.sh +``` + +## Usage + +### Run Full Suite (lint + template) +```bash +scripts/helm-agent.sh +``` + +### Run Individual Steps +```bash +scripts/helm-agent.sh lint # helm lint only +scripts/helm-agent.sh template # helm template only +scripts/helm-agent.sh all # full suite (default) +``` + +## Environment Knobs + +| Variable | Default | Description | +|----------|---------|-------------| +| `RUN_LINT` | `1` | Set to `0` to skip lint step | +| `RUN_TEMPLATE` | `1` | Set to `0` to skip template step | +| `CHART_DIR` | _(empty)_ | Explicit chart directory (skips auto-detection) | +| `FAIL_FAST` | `0` | Set to `1` to stop after first failure (or use `--fail-fast`) | +| `CHANGED_FILES` | _(empty)_ | Space-separated changed file paths; scopes to affected charts | +| `MAX_LINES` | `40` | Max output lines printed per step (unlimited in CI) | +| `KEEP_DIR` | `0` | Set to `1` to keep temp log dir on success | + +## Output Format + +- Each step prints a header (`Step: lint`, `Step: template`) +- Results are `PASS`, `FAIL`, or `SKIP` +- On failure, output is truncated to `MAX_LINES` +- Full logs are saved to a temp directory (path printed in output) +- Overall result is printed at the end: `Overall: PASS` or `Overall: FAIL` + +## Important Notes + +- Auto-detects chart directories by searching for `Chart.yaml` +- `CHART_DIR` overrides auto-detection with a specific chart path +- `CHANGED_FILES` scopes checks to charts containing changed `.yaml`/`.yml`/`.tpl` files +- Reports SKIP when no `Chart.yaml` is found anywhere +- In CI (`CI=true`), `MAX_LINES` defaults to unlimited; locally it defaults to 40 diff --git a/skills/helm-agent/scripts/helm-agent.sh b/skills/helm-agent/scripts/helm-agent.sh new file mode 100755 index 0000000..4c53577 --- /dev/null +++ b/skills/helm-agent/scripts/helm-agent.sh @@ -0,0 +1,259 @@ +#!/usr/bin/env bash +set -euo pipefail + +# helm-agent: lean Helm chart linter and template validator for coding agents +# deps: helm + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +LIB_DIR="${SCRIPT_DIR}/../../../lib" +# shellcheck source=../../../lib/x-agent-common.sh +source "${LIB_DIR}/x-agent-common.sh" + +# ---- Agent-specific knobs ------------------------------------------------ + +RUN_LINT="${RUN_LINT:-1}" +RUN_TEMPLATE="${RUN_TEMPLATE:-1}" +CHART_DIR="${CHART_DIR:-}" + +# ---- Usage ---------------------------------------------------------------- + +usage() { + cat <<'EOF' +helm-agent — lean Helm chart linter and template validator for coding agents. + +Usage: helm-agent.sh [options] [command] + +Commands: + lint Run helm lint on chart directories + template Run helm template to validate rendering + all Run lint + template (default) + help Show this help + +Options: + --fail-fast Stop after first failing step + +Environment: + RUN_LINT=0|1 Toggle lint step (default: 1) + RUN_TEMPLATE=0|1 Toggle template step (default: 1) + CHART_DIR=path Explicit chart directory (skip auto-detection) + CHANGED_FILES="a.yaml b.tpl" Scope to charts containing these files + MAX_LINES=N Max diagnostic lines per step (default: 40) + KEEP_DIR=0|1 Keep temp log dir on success (default: 0) + FAIL_FAST=0|1 Stop after first failure (default: 0) +EOF +} + +# ---- Chart discovery ------------------------------------------------------ + +# Finds the nearest parent directory containing Chart.yaml for a given file. +# Prints the chart root or nothing if not found. +find_chart_root() { + local filepath="$1" + local dir + dir="$(dirname "$filepath")" + + while true; do + if [[ -f "${dir}/Chart.yaml" ]]; then + echo "$dir" + return 0 + fi + # Stop at current working directory or filesystem root + if [[ "$dir" == "." || "$dir" == "/" ]]; then + return 1 + fi + dir="$(dirname "$dir")" + done +} + +# Populates CHART_TARGETS (newline-separated list of chart root directories). +collect_chart_targets() { + CHART_TARGETS="" + + # Priority 1: explicit CHART_DIR + if [[ -n "$CHART_DIR" ]]; then + if [[ -d "$CHART_DIR" ]] && [[ -f "${CHART_DIR}/Chart.yaml" ]]; then + CHART_TARGETS="$CHART_DIR" + echo "Using explicit CHART_DIR: ${CHART_DIR}" + return 0 + fi + echo "CHART_DIR set but no Chart.yaml found at: ${CHART_DIR}" + return 0 + fi + + # Priority 2: derive chart roots from CHANGED_FILES + if [[ -n "${CHANGED_FILES:-}" ]]; then + local seen="" + local f root + for f in $CHANGED_FILES; do + case "$f" in + *.yaml|*.yml|*.tpl) + if [[ -f "$f" ]]; then + root="$(find_chart_root "$f")" || continue + # Dedupe + case " ${seen} " in + *" ${root} "*) ;; + *) + seen="${seen} ${root}" + if [[ -z "$CHART_TARGETS" ]]; then + CHART_TARGETS="$root" + else + CHART_TARGETS="${CHART_TARGETS} +${root}" + fi + ;; + esac + fi + ;; + esac + done + + if [[ -n "$CHART_TARGETS" ]]; then + local count + count="$(printf '%s\n' "$CHART_TARGETS" | wc -l | tr -d ' ')" + echo "Discovered ${count} chart(s) from CHANGED_FILES" + else + echo "CHANGED_FILES set but no chart-related files found" + fi + return 0 + fi + + # Priority 3: recursive Chart.yaml search + local found + found="$(find . -name Chart.yaml -not -path '*/charts/*' -print 2>/dev/null | sort)" || true + if [[ -z "$found" ]]; then + echo "No Chart.yaml found in directory tree" + return 0 + fi + + local chart_file chart_root + while IFS= read -r chart_file; do + chart_root="$(dirname "$chart_file")" + if [[ -z "$CHART_TARGETS" ]]; then + CHART_TARGETS="$chart_root" + else + CHART_TARGETS="${CHART_TARGETS} +${chart_root}" + fi + done <<< "$found" + + local count + count="$(printf '%s\n' "$CHART_TARGETS" | wc -l | tr -d ' ')" + echo "Discovered ${count} chart(s) via recursive search" +} + +# ---- Steps ---------------------------------------------------------------- + +run_lint() { + step "lint" + + if [[ -z "$CHART_TARGETS" ]]; then + echo + echo "Result: SKIP (no charts discovered)" + fmt_elapsed + return 0 + fi + + local ok=1 idx=0 chart_dir log + while IFS= read -r chart_dir; do + idx=$((idx + 1)) + log="${OUTDIR}/lint.${idx}.log" + echo "Linting: ${chart_dir}" + + if ! helm lint "$chart_dir" >"$log" 2>&1; then + ok=0 + echo + echo "Output (first ${MAX_LINES} lines):" + head -n "$MAX_LINES" "$log" + fi + done <<< "$CHART_TARGETS" + + print_result "$ok" "${OUTDIR}/lint.*.log" \ + "resolve the chart errors above, then re-run: /helm-agent lint" + + return $(( 1 - ok )) +} + +run_template() { + step "template" + + if [[ -z "$CHART_TARGETS" ]]; then + echo + echo "Result: SKIP (no charts discovered)" + fmt_elapsed + return 0 + fi + + local ok=1 idx=0 chart_dir log + while IFS= read -r chart_dir; do + idx=$((idx + 1)) + log="${OUTDIR}/template.${idx}.log" + echo "Rendering: ${chart_dir}" + + if ! helm template "$chart_dir" >"$log" 2>&1; then + ok=0 + echo + echo "Output (first ${MAX_LINES} lines):" + head -n "$MAX_LINES" "$log" + fi + done <<< "$CHART_TARGETS" + + print_result "$ok" "${OUTDIR}/template.*.log" \ + "resolve the template errors above, then re-run: /helm-agent template" + + return $(( 1 - ok )) +} + +# ---- Main ----------------------------------------------------------------- + +main() { + # Parse help before need() checks so --help works without helm installed + case "${1:-}" in + -h|--help|help) + usage + exit 0 + ;; + esac + + need helm + + setup_outdir "helm-agent" + + # Parse flags + while [[ "${1:-}" == --* ]]; do + case "$1" in + --fail-fast) + # shellcheck disable=SC2034 + FAIL_FAST=1 + shift + ;; + *) + break + ;; + esac + done + + local cmd="${1:-all}" + shift 2>/dev/null || true + local overall_ok=1 + + collect_chart_targets + + case "$cmd" in + lint) run_lint || overall_ok=0 ;; + template) run_template || overall_ok=0 ;; + all) + if [[ "$RUN_LINT" == "1" ]] && should_continue; then run_lint || overall_ok=0; fi + if [[ "$RUN_TEMPLATE" == "1" ]] && should_continue; then run_template || overall_ok=0; fi + ;; + *) + echo "Unknown command: $cmd" >&2 + usage + exit 2 + ;; + esac + + print_overall "$overall_ok" + [[ "$overall_ok" == "1" ]] +} + +main "$@" diff --git a/skills/kube-agent/SKILL.md b/skills/kube-agent/SKILL.md new file mode 100644 index 0000000..8d90abb --- /dev/null +++ b/skills/kube-agent/SKILL.md @@ -0,0 +1,71 @@ +--- +name: kube-agent +description: | + Run kube-agent.sh — a lean Kubernetes manifest validator that produces agent-friendly output. + Use when: running Kubernetes manifest validation, kubeconform checks, kubeval checks, verifying K8s manifests before applying, + or when the user asks to validate Kubernetes YAML, run k8s checks, or check manifests. + Triggers on: kube agent, kubernetes validate, kubeconform, kubeval, k8s checks, validate manifests. +context: fork +allowed-tools: + - Bash(scripts/kube-agent.sh*) + - Bash(RUN_*=* scripts/kube-agent.sh*) + - Bash(KUBE_SCHEMAS_DIR=* scripts/kube-agent.sh*) + - Bash(KUBE_IGNORE_MISSING_SCHEMAS=* scripts/kube-agent.sh*) + - Bash(MAX_LINES=* scripts/kube-agent.sh*) + - Bash(KEEP_DIR=* scripts/kube-agent.sh*) + - Bash(FAIL_FAST=* scripts/kube-agent.sh*) + - Bash(CHANGED_FILES=* scripts/kube-agent.sh*) + - Bash(TMPDIR_ROOT=* scripts/kube-agent.sh*) +--- + +# Kube Agent + +Run the `kube-agent.sh` script for lean, structured Kubernetes manifest validation output designed for coding agents. + +## Script Location + +``` +scripts/kube-agent.sh +``` + +## Usage + +### Run Full Suite (validate) +```bash +scripts/kube-agent.sh +``` + +### Run Individual Steps +```bash +scripts/kube-agent.sh validate # validate manifests only +scripts/kube-agent.sh all # full suite (default) +``` + +## Environment Knobs + +| Variable | Default | Description | +|----------|---------|-------------| +| `RUN_VALIDATE` | `1` | Set to `0` to skip validate step | +| `KUBE_SCHEMAS_DIR` | _(empty)_ | Custom schema location for validator | +| `KUBE_IGNORE_MISSING_SCHEMAS` | `0` | Set to `1` to skip resources with missing schemas (useful for CRDs, offline) | +| `FAIL_FAST` | `0` | Set to `1` to stop after first failure (or use `--fail-fast`) | +| `CHANGED_FILES` | _(empty)_ | Space-separated changed file paths; scopes to matching manifests | +| `MAX_LINES` | `40` | Max output lines printed per step (unlimited in CI) | +| `KEEP_DIR` | `0` | Set to `1` to keep temp log dir on success | + +## Output Format + +- Each step prints a header (`Step: validate`) +- Results are `PASS`, `FAIL`, or `SKIP` +- On failure, output is truncated to `MAX_LINES` +- Full logs are saved to a temp directory (path printed in output) +- Overall result is printed at the end: `Overall: PASS` or `Overall: FAIL` + +## Important Notes + +- Prefers `kubeconform` over `kubeval`; exits with code 2 if neither is installed +- Discovers `.yml`/`.yaml` files containing both `apiVersion:` and `kind:` +- Excludes `.git/`, `.github/`, `node_modules/`, and `charts/` directories +- `CHANGED_FILES` scopes validation to only the specified files +- Reports SKIP when no Kubernetes manifests are found +- In CI (`CI=true`), `MAX_LINES` defaults to unlimited; locally it defaults to 40 diff --git a/skills/kube-agent/scripts/kube-agent.sh b/skills/kube-agent/scripts/kube-agent.sh new file mode 100755 index 0000000..c357f63 --- /dev/null +++ b/skills/kube-agent/scripts/kube-agent.sh @@ -0,0 +1,279 @@ +#!/usr/bin/env bash +set -euo pipefail + +# kube-agent: lean Kubernetes manifest validator for coding agents +# deps: kubeconform OR kubeval + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +LIB_DIR="${SCRIPT_DIR}/../../../lib" +# shellcheck source=../../../lib/x-agent-common.sh +source "${LIB_DIR}/x-agent-common.sh" + +# ---- Agent-specific knobs ------------------------------------------------ + +RUN_VALIDATE="${RUN_VALIDATE:-1}" +KUBE_SCHEMAS_DIR="${KUBE_SCHEMAS_DIR:-}" +KUBE_IGNORE_MISSING_SCHEMAS="${KUBE_IGNORE_MISSING_SCHEMAS:-0}" + +# ---- Usage ---------------------------------------------------------------- + +usage() { + cat <<'EOF' +kube-agent — lean Kubernetes manifest validator for coding agents. + +Usage: kube-agent.sh [options] [command] + +Commands: + validate Validate Kubernetes manifests against schemas + all Run validate (default) + help Show this help + +Options: + --fail-fast Stop after first failing step + +Environment: + RUN_VALIDATE=0|1 Toggle validate step (default: 1) + KUBE_SCHEMAS_DIR=path Custom schema location for validator + KUBE_IGNORE_MISSING_SCHEMAS=0|1 Skip resources with missing schemas (default: 0) + CHANGED_FILES="a.yaml b.yml" Scope to only these files + MAX_LINES=N Max diagnostic lines per step (default: 40) + KEEP_DIR=0|1 Keep temp log dir on success (default: 0) + FAIL_FAST=0|1 Stop after first failure (default: 0) +EOF +} + +# ---- Tool detection ------------------------------------------------------- + +KUBE_VALIDATOR="" + +resolve_validator() { + if command -v kubeconform >/dev/null 2>&1; then + KUBE_VALIDATOR="kubeconform" + elif command -v kubeval >/dev/null 2>&1; then + KUBE_VALIDATOR="kubeval" + else + echo "Missing required tool: kubeconform or kubeval" >&2 + echo "Install kubeconform: go install github.com/yannh/kubeconform/cmd/kubeconform@latest" >&2 + echo "Or kubeval: https://www.kubeval.com/installation/" >&2 + exit 2 + fi + echo "Validator: $KUBE_VALIDATOR" +} + +# ---- Manifest discovery --------------------------------------------------- + +# Populates MANIFEST_FILES (newline-separated list of K8s manifest paths). +MANIFEST_FILES="" + +discover_manifests() { + # Find all .yml/.yaml files, pruning excluded directories + local candidates + candidates="$(find . \ + -name .git -prune -o \ + -name .github -prune -o \ + -name node_modules -prune -o \ + -name charts -prune -o \ + \( -name '*.yml' -o -name '*.yaml' \) -print 2>/dev/null | sort)" || true + + if [[ -z "$candidates" ]]; then + return 0 + fi + + # Apply CHANGED_FILES scoping if set + if [[ -n "${CHANGED_FILES:-}" ]]; then + local scoped="" f candidate + for candidate in $candidates; do + # Normalize ./path to match CHANGED_FILES entries + local norm="${candidate#./}" + for f in $CHANGED_FILES; do + f="${f#./}" + if [[ "$norm" == "$f" ]]; then + if [[ -z "$scoped" ]]; then + scoped="$candidate" + else + scoped="${scoped} +${candidate}" + fi + break + fi + done + done + candidates="$scoped" + if [[ -z "$candidates" ]]; then + echo "CHANGED_FILES set but no matching YAML files found" + return 0 + fi + fi + + # Filter to Kubernetes manifests: files containing both apiVersion: and kind: + local file + while IFS= read -r file; do + [[ -z "$file" ]] && continue + [[ -f "$file" ]] || continue + if grep -q 'apiVersion:' "$file" 2>/dev/null && grep -q 'kind:' "$file" 2>/dev/null; then + if [[ -z "$MANIFEST_FILES" ]]; then + MANIFEST_FILES="$file" + else + MANIFEST_FILES="${MANIFEST_FILES} +${file}" + fi + fi + done <<< "$candidates" +} + +# ---- Steps ---------------------------------------------------------------- + +run_validate() { + step "validate" + + if [[ -z "$MANIFEST_FILES" ]]; then + echo + echo "Result: SKIP (no Kubernetes manifests found)" + fmt_elapsed + return 0 + fi + + local count + count="$(printf '%s\n' "$MANIFEST_FILES" | wc -l | tr -d ' ')" + echo "Found ${count} Kubernetes manifest(s)" + + local log="${OUTDIR}/validate.log" + local ok=1 + + # Build file list as array + local files=() + while IFS= read -r f; do + [[ -n "$f" ]] && files+=("$f") + done <<< "$MANIFEST_FILES" + + if [[ "$KUBE_VALIDATOR" == "kubeconform" ]]; then + local args=(-summary -output json) + if [[ -n "$KUBE_SCHEMAS_DIR" ]]; then + args+=(-schema-location "$KUBE_SCHEMAS_DIR") + fi + if [[ "$KUBE_IGNORE_MISSING_SCHEMAS" == "1" ]]; then + args+=(-ignore-missing-schemas) + fi + if ! kubeconform "${args[@]}" "${files[@]}" >"$log" 2>&1; then + ok=0 + fi + # Parse JSON summary for resource counts + parse_kubeconform_summary "$log" + else + local args=(--strict) + if [[ -n "$KUBE_SCHEMAS_DIR" ]]; then + args+=(--schema-location "$KUBE_SCHEMAS_DIR") + fi + if [[ "$KUBE_IGNORE_MISSING_SCHEMAS" == "1" ]]; then + args+=(--ignore-missing-schemas) + fi + if ! kubeval "${args[@]}" "${files[@]}" >"$log" 2>&1; then + ok=0 + fi + # Parse kubeval output for resource counts + parse_kubeval_output "$log" + fi + + if [[ "$ok" == "0" ]]; then + echo + echo "Output (first ${MAX_LINES} lines):" + head -n "$MAX_LINES" "$log" + fi + + print_result "$ok" "${OUTDIR}/validate.log" \ + "resolve schema validation errors above, then re-run: /kube-agent validate" + + return $(( 1 - ok )) +} + +parse_kubeconform_summary() { + local log="$1" + # kubeconform -output json -summary produces pretty-printed JSON with + # a "summary" object containing "valid", "invalid", "errors", "skipped". + # Extract counts with grep+sed — no jq dependency needed. + local valid=0 invalid=0 errors=0 + if [[ -f "$log" ]]; then + local val + val="$(grep '"valid"' "$log" | tail -1 | sed 's/[^0-9]//g')" || true + [[ -n "$val" ]] && valid="$val" + val="$(grep '"invalid"' "$log" | tail -1 | sed 's/[^0-9]//g')" || true + [[ -n "$val" ]] && invalid="$val" + val="$(grep '"errors"' "$log" | tail -1 | sed 's/[^0-9]//g')" || true + [[ -n "$val" ]] && errors="$val" + echo "Resources: ${valid} valid, ${invalid} invalid, ${errors} errors" + fi +} + +parse_kubeval_output() { + local log="$1" + if [[ ! -f "$log" ]]; then + return 0 + fi + # kubeval outputs lines like: + # PASS - file.yaml contains a valid Deployment (apps/v1) + # ERR - file.yaml contains an invalid Deployment (apps/v1) - ... + # WARN - ... + local valid=0 invalid=0 errors=0 + local line + while IFS= read -r line; do + case "$line" in + PASS\ -*) valid=$((valid + 1)) ;; + ERR\ -*) invalid=$((invalid + 1)) ;; + WARN\ -*) errors=$((errors + 1)) ;; + esac + done < "$log" + echo "Resources: ${valid} valid, ${invalid} invalid, ${errors} warnings" +} + +# ---- Main ----------------------------------------------------------------- + +main() { + # Parse help before dependency checks so --help works without validators + case "${1:-}" in + -h|--help|help) + usage + exit 0 + ;; + esac + + resolve_validator + + setup_outdir "kube-agent" + + # Parse flags + while [[ "${1:-}" == --* ]]; do + case "$1" in + --fail-fast) + # shellcheck disable=SC2034 + FAIL_FAST=1 + shift + ;; + *) + break + ;; + esac + done + + local cmd="${1:-all}" + shift 2>/dev/null || true + local overall_ok=1 + + discover_manifests + + case "$cmd" in + validate) run_validate || overall_ok=0 ;; + all) + if [[ "$RUN_VALIDATE" == "1" ]] && should_continue; then run_validate || overall_ok=0; fi + ;; + *) + echo "Unknown command: $cmd" >&2 + usage + exit 2 + ;; + esac + + print_overall "$overall_ok" + [[ "$overall_ok" == "1" ]] +} + +main "$@" diff --git a/skills/npm-agent/scripts/npm-agent.sh b/skills/npm-agent/scripts/npm-agent.sh index 9842b40..661e4d6 100755 --- a/skills/npm-agent/scripts/npm-agent.sh +++ b/skills/npm-agent/scripts/npm-agent.sh @@ -5,53 +5,18 @@ set -euo pipefail # deps: bash, mktemp # optional: biome, eslint, prettier -KEEP_DIR="${KEEP_DIR:-0}" # set to 1 to keep temp dir even on success -# In CI, show full output; locally, limit to 40 lines to keep things tidy. -if [[ "${CI:-}" == "true" || "${CI:-}" == "1" ]]; then - MAX_LINES="${MAX_LINES:-999999}" -else - MAX_LINES="${MAX_LINES:-40}" -fi +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +LIB_DIR="${SCRIPT_DIR}/../../../lib" +# shellcheck source=../../../lib/x-agent-common.sh +source "${LIB_DIR}/x-agent-common.sh" + RUN_LINT="${RUN_LINT:-1}" # set to 0 to skip lint RUN_TYPECHECK="${RUN_TYPECHECK:-1}" # set to 0 to skip typecheck RUN_FORMAT="${RUN_FORMAT:-1}" # set to 0 to skip format RUN_TESTS="${RUN_TESTS:-1}" # set to 0 to skip tests RUN_BUILD="${RUN_BUILD:-1}" # set to 0 to skip build -FAIL_FAST="${FAIL_FAST:-0}" # set to 1 or use --fail-fast to stop after first failure -CHANGED_FILES="${CHANGED_FILES:-}" # space-separated list of changed files; scopes lint/format - -TMPDIR_ROOT="${TMPDIR_ROOT:-/tmp}" -OUTDIR="$(mktemp -d "${TMPDIR_ROOT%/}/npm-agent.XXXXXX")" - -cleanup() { - local code="$?" - if [[ "$KEEP_DIR" == "1" || "$code" != "0" ]]; then - echo "Logs kept in: $OUTDIR" - else - rm -rf "$OUTDIR" - fi - exit "$code" -} -trap cleanup EXIT -hr() { echo "------------------------------------------------------------"; } - -# Returns 0 (continue) unless fail-fast is on and a step already failed. -should_continue() { [[ "$FAIL_FAST" != "1" || "$overall_ok" == "1" ]]; } - -STEP_START_SECONDS=0 - -step() { - local name="$1" - STEP_START_SECONDS=$SECONDS - hr - echo "Step: $name" -} - -fmt_elapsed() { - local elapsed=$(( SECONDS - STEP_START_SECONDS )) - echo "Time: ${elapsed}s" -} +setup_outdir "npm-agent" # Detect which package manager is in use. detect_pm() { @@ -344,6 +309,7 @@ EOF main() { while [[ "${1:-}" == --* ]]; do + # shellcheck disable=SC2034 # FAIL_FAST used by should_continue() in x-agent-common.sh case "$1" in --fail-fast) FAIL_FAST=1; shift ;; *) break ;; @@ -381,9 +347,7 @@ main() { ;; esac - hr - echo "Overall: $([[ "$overall_ok" == "1" ]] && echo PASS || echo FAIL)" - echo "Logs: $OUTDIR" + print_overall "$overall_ok" [[ "$overall_ok" == "1" ]] } diff --git a/skills/sql-agent/SKILL.md b/skills/sql-agent/SKILL.md new file mode 100644 index 0000000..7a3ea29 --- /dev/null +++ b/skills/sql-agent/SKILL.md @@ -0,0 +1,86 @@ +--- +name: sql-agent +description: | + Run sql-agent.sh — a lean SQL linter that produces agent-friendly output with sqlfluff. + Use when: running SQL checks, linting SQL files with sqlfluff, validating SQL style, + or when the user asks to run sqlfluff, sql lint, sql format, or sql checks. + Triggers on: sql agent, sqlfluff, sql lint, sql format, sql checks, validate sql. +context: fork +allowed-tools: + - Bash(scripts/sql-agent.sh*) + - Bash(RUN_*=* scripts/sql-agent.sh*) + - Bash(FMT_MODE=* scripts/sql-agent.sh*) + - Bash(SQLFLUFF_DIALECT=* scripts/sql-agent.sh*) + - Bash(MAX_LINES=* scripts/sql-agent.sh*) + - Bash(KEEP_DIR=* scripts/sql-agent.sh*) + - Bash(FAIL_FAST=* scripts/sql-agent.sh*) + - Bash(CHANGED_FILES=* scripts/sql-agent.sh*) + - Bash(TMPDIR_ROOT=* scripts/sql-agent.sh*) + - Bash(CI=* scripts/sql-agent.sh*) +--- + +# SQL Agent + +Run the `sql-agent.sh` script for lean, structured SQL linting output designed for coding agents. + +## Script Location + +``` +scripts/sql-agent.sh +``` + +## Usage + +### Run Full Suite (lint) +```bash +scripts/sql-agent.sh +``` + +### Run Individual Steps +```bash +scripts/sql-agent.sh lint # sqlfluff lint only +scripts/sql-agent.sh fix # sqlfluff fix (auto-fix) +scripts/sql-agent.sh all # full suite (default: lint only) +``` + +### Enable Auto-Fix +```bash +RUN_FIX=1 scripts/sql-agent.sh all +FMT_MODE=fix scripts/sql-agent.sh all +``` + +### Specify Dialect +```bash +SQLFLUFF_DIALECT=postgres scripts/sql-agent.sh lint +SQLFLUFF_DIALECT=mysql scripts/sql-agent.sh lint +``` + +## Environment Knobs + +| Variable | Default | Description | +|----------|---------|-------------| +| `RUN_LINT` | `1` | Set to `0` to skip lint step | +| `RUN_FIX` | `0` | Set to `1` to enable fix step | +| `FMT_MODE` | `auto` | `auto` = check in CI, respects RUN_FIX locally; `check` or `fix` | +| `SQLFLUFF_DIALECT` | `ansi` | SQL dialect (ansi, postgres, mysql, bigquery, etc.) | +| `FAIL_FAST` | `0` | Set to `1` to stop after first failure (or use `--fail-fast`) | +| `CHANGED_FILES` | _(empty)_ | Space-separated changed file paths; scopes checks to .sql files only | +| `MAX_LINES` | `40` | Max output lines printed per step (unlimited in CI) | +| `KEEP_DIR` | `0` | Set to `1` to keep temp log dir on success | + +## Output Format + +- Each step prints a header (`Step: lint`) +- Results are `PASS`, `FAIL`, or `SKIP` +- On failure, output is truncated to `MAX_LINES` +- Full logs are saved to a temp directory (path printed in output) +- Overall result is printed at the end: `Overall: PASS` or `Overall: FAIL` + +## Important Notes + +- Discovers `.sql` files recursively (excludes `.git/`, `node_modules/`, `vendor/`) +- `CHANGED_FILES` scopes checks to only `.sql` files +- Reports SKIP when no `.sql` files are found +- Fix defaults to OFF — enable with `RUN_FIX=1` or `FMT_MODE=fix` +- In CI (`CI=true`), fix is forced to check-only mode +- `--force` flag is used with `sqlfluff fix` to avoid interactive prompts diff --git a/skills/sql-agent/scripts/sql-agent.sh b/skills/sql-agent/scripts/sql-agent.sh new file mode 100755 index 0000000..19641b2 --- /dev/null +++ b/skills/sql-agent/scripts/sql-agent.sh @@ -0,0 +1,235 @@ +#!/usr/bin/env bash +set -euo pipefail + +# sql-agent: lean SQL linter for coding agents +# deps: sqlfluff (required) + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +LIB_DIR="${SCRIPT_DIR}/../../../lib" +# shellcheck source=../../../lib/x-agent-common.sh +source "${LIB_DIR}/x-agent-common.sh" + +# ---- Agent-specific knobs ------------------------------------------------ + +RUN_LINT="${RUN_LINT:-1}" +RUN_FIX="${RUN_FIX:-0}" # opt-in +FMT_MODE="${FMT_MODE:-auto}" # auto = check in CI, respects RUN_FIX locally +SQLFLUFF_DIALECT="${SQLFLUFF_DIALECT:-ansi}" # postgres, mysql, bigquery, etc. + +# ---- Usage ---------------------------------------------------------------- + +usage() { + cat <<'EOF' +sql-agent — lean SQL linter for coding agents. + +Usage: sql-agent.sh [options] [command] + +Commands: + lint Run sqlfluff lint on discovered SQL files + fix Run sqlfluff fix (auto-fix lint issues) + all Run enabled steps (default: lint only; fix before lint when enabled) + help Show this help + +Options: + --fail-fast Stop after first failing step + +Environment: + RUN_LINT=0|1 Toggle lint step (default: 1) + RUN_FIX=0|1 Toggle fix step (default: 0) + FMT_MODE=auto|check|fix Format mode (default: auto — check in CI, respects RUN_FIX locally) + SQLFLUFF_DIALECT=DIALECT SQL dialect for sqlfluff (default: ansi) + CHANGED_FILES="a.sql b.sql" Scope to specific files + MAX_LINES=N Max diagnostic lines per step (default: 40) + KEEP_DIR=0|1 Keep temp log dir on success (default: 0) + FAIL_FAST=0|1 Stop after first failure (default: 0) +EOF +} + +# ---- FMT_MODE resolution -------------------------------------------------- + +resolve_fmt_mode() { + case "$FMT_MODE" in + auto) + if [[ "${CI:-}" == "true" || "${CI:-}" == "1" ]]; then + FMT_MODE="check" + else + # In auto mode locally, respect RUN_FIX + if [[ "$RUN_FIX" == "1" ]]; then + FMT_MODE="fix" + else + FMT_MODE="check" + fi + fi + ;; + check|fix) + # CI forces check regardless of user setting + if [[ "${CI:-}" == "true" || "${CI:-}" == "1" ]]; then + FMT_MODE="check" + fi + ;; + *) + echo "Invalid FMT_MODE: ${FMT_MODE} (expected: auto, check, fix)" >&2 + exit 2 + ;; + esac +} + +# ---- SQL file discovery --------------------------------------------------- + +# Populates SQL_FILES (newline-separated list of .sql file paths). +discover_sql_files() { + SQL_FILES="" + + if [[ -n "${CHANGED_FILES:-}" ]]; then + local f + for f in $CHANGED_FILES; do + case "$f" in + *.sql) + if [[ -f "$f" ]]; then + if [[ -z "$SQL_FILES" ]]; then + SQL_FILES="$f" + else + SQL_FILES="${SQL_FILES} +$f" + fi + fi + ;; + esac + done + else + # Recursive find, excluding common non-project directories + SQL_FILES="$(find . \ + -name '.git' -prune -o \ + -name 'node_modules' -prune -o \ + -name 'vendor' -prune -o \ + -name '.venv' -prune -o \ + -name '__pycache__' -prune -o \ + -type f -name '*.sql' \ + -print | sort)" + fi + + local count=0 + if [[ -n "$SQL_FILES" ]]; then + count="$(printf '%s\n' "$SQL_FILES" | wc -l | tr -d ' ')" + fi + echo "Discovered ${count} SQL file(s)" +} + +# ---- Steps ---------------------------------------------------------------- + +# Returns 0 (skip) if no SQL files; 1 (continue) otherwise. +check_sql_files() { + if [[ -n "${CHANGED_FILES:-}" ]] && [[ -z "$SQL_FILES" ]]; then + echo; echo "Result: SKIP (no .sql files in CHANGED_FILES)"; fmt_elapsed; return 0 + fi + if [[ -z "$SQL_FILES" ]]; then + echo; echo "Result: SKIP (no .sql files found)"; fmt_elapsed; return 0 + fi + return 1 +} + +run_lint() { + step "lint" + check_sql_files && return 0 + + local log="${OUTDIR}/lint.log" ok=1 + # shellcheck disable=SC2086 + sqlfluff lint --dialect "$SQLFLUFF_DIALECT" $SQL_FILES >"$log" 2>&1 || ok=0 + + if [[ "$ok" == "0" ]]; then + echo; echo "Output (first ${MAX_LINES} lines):"; head -n "$MAX_LINES" "$log" + fi + print_result "$ok" "$log" \ + "run /sql-agent fix or FMT_MODE=fix /sql-agent to auto-fix, then re-run: /sql-agent lint" + return $(( 1 - ok )) +} + +run_fix() { + step "fix" + check_sql_files && return 0 + + local log="${OUTDIR}/fix.log" ok=1 + # shellcheck disable=SC2086 + sqlfluff fix --dialect "$SQLFLUFF_DIALECT" --force $SQL_FILES >"$log" 2>&1 || ok=0 + + if [[ "$ok" == "0" ]]; then + echo; echo "Output (first ${MAX_LINES} lines):"; head -n "$MAX_LINES" "$log" + fi + print_result "$ok" "$log" \ + "some issues cannot be auto-fixed — resolve manually, then re-run: /sql-agent lint" + return $(( 1 - ok )) +} + +# ---- Main ----------------------------------------------------------------- + +main() { + # Parse help before need() checks so --help works without tools installed + case "${1:-}" in + -h|--help|help) + usage + exit 0 + ;; + esac + + need sqlfluff + + setup_outdir "sql-agent" + + # Parse flags + while [[ "${1:-}" == --* ]]; do + case "$1" in + --fail-fast) + # shellcheck disable=SC2034 + FAIL_FAST=1 + shift + ;; + *) + break + ;; + esac + done + + local cmd="${1:-all}" + shift 2>/dev/null || true + local overall_ok=1 + + resolve_fmt_mode + discover_sql_files + + # Determine if fix should run in 'all' mode. + # Use FMT_MODE (post-resolution) as single source of truth — it already + # incorporates RUN_FIX for local auto mode and forces check in CI. + local fix_enabled=0 + if [[ "$FMT_MODE" == "fix" ]]; then + fix_enabled=1 + fi + + case "$cmd" in + lint) + run_lint || overall_ok=0 + ;; + fix) + if [[ "${CI:-}" == "true" || "${CI:-}" == "1" ]]; then + echo "CI detected — fix is disabled; running lint instead" + run_lint || overall_ok=0 + else + run_fix || overall_ok=0 + fi + ;; + all) + # When fix is enabled, run fix BEFORE lint so lint reports post-fix state + if [[ "$fix_enabled" == "1" ]] && should_continue; then run_fix || overall_ok=0; fi + if [[ "$RUN_LINT" == "1" ]] && should_continue; then run_lint || overall_ok=0; fi + ;; + *) + echo "Unknown command: $cmd" >&2 + usage + exit 2 + ;; + esac + + print_overall "$overall_ok" + [[ "$overall_ok" == "1" ]] +} + +main "$@" diff --git a/skills/terra-agent/scripts/terra-agent.sh b/skills/terra-agent/scripts/terra-agent.sh index f24eeb8..8497138 100755 --- a/skills/terra-agent/scripts/terra-agent.sh +++ b/skills/terra-agent/scripts/terra-agent.sh @@ -5,13 +5,11 @@ set -euo pipefail # deps: bash, mktemp, terraform # optional: tflint -KEEP_DIR="${KEEP_DIR:-0}" # set to 1 to keep temp dir even on success -# In CI, show full output; locally, limit to 40 lines to keep things tidy. -if [[ "${CI:-}" == "true" || "${CI:-}" == "1" ]]; then - MAX_LINES="${MAX_LINES:-999999}" -else - MAX_LINES="${MAX_LINES:-40}" -fi +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +LIB_DIR="${SCRIPT_DIR}/../../../lib" +# shellcheck source=../../../lib/x-agent-common.sh +source "${LIB_DIR}/x-agent-common.sh" + RUN_FMT="${RUN_FMT:-1}" # set to 0 to skip fmt RUN_INIT="${RUN_INIT:-1}" # set to 0 to skip init RUN_VALIDATE="${RUN_VALIDATE:-1}" # set to 0 to skip validate @@ -21,47 +19,10 @@ FMT_MODE="${FMT_MODE:-check}" # check|fix FMT_RECURSIVE="${FMT_RECURSIVE:-1}" # set to 0 to disable recursive fmt TFLINT_RECURSIVE="${TFLINT_RECURSIVE:-1}" # set to 0 to disable recursive tflint TERRAFORM_CHDIR="${TERRAFORM_CHDIR:-${TF_CHDIR:-.}}" -FAIL_FAST="${FAIL_FAST:-0}" # set to 1 or use --fail-fast to stop after first failure -CHANGED_FILES="${CHANGED_FILES:-}" # space-separated list; auto-sets TERRAFORM_CHDIR from .tf files -TMPDIR_ROOT="${TMPDIR_ROOT:-/tmp}" -OUTDIR="$(mktemp -d "${TMPDIR_ROOT%/}/terra-agent.XXXXXX")" +setup_outdir "terra-agent" TF_DATA_DIR_PATH="${TF_DATA_DIR:-$OUTDIR/tf-data}" -cleanup() { - local code="$?" - if [[ "$KEEP_DIR" == "1" || "$code" != "0" ]]; then - echo "Logs kept in: $OUTDIR" - else - rm -rf "$OUTDIR" - fi - exit "$code" -} -trap cleanup EXIT - -need() { - command -v "$1" >/dev/null 2>&1 || { echo "Missing required tool: $1" >&2; exit 2; } -} - -hr() { echo "------------------------------------------------------------"; } - -# Returns 0 (continue) unless fail-fast is on and a step already failed. -should_continue() { [[ "$FAIL_FAST" != "1" || "$overall_ok" == "1" ]]; } - -STEP_START_SECONDS=0 - -step() { - local name="$1" - STEP_START_SECONDS=$SECONDS - hr - echo "Step: $name" -} - -fmt_elapsed() { - local elapsed=$(( SECONDS - STEP_START_SECONDS )) - echo "Time: ${elapsed}s" -} - normalize_dir() { if [[ -z "$TERRAFORM_CHDIR" ]]; then TERRAFORM_CHDIR="." @@ -416,6 +377,7 @@ EOF main() { while [[ "${1:-}" == --* ]]; do + # shellcheck disable=SC2034 # FAIL_FAST used by should_continue() in x-agent-common.sh case "$1" in --fail-fast) FAIL_FAST=1; shift ;; *) break ;; @@ -460,9 +422,7 @@ main() { ;; esac - hr - echo "Overall: $([[ "$overall_ok" == "1" ]] && echo PASS || echo FAIL)" - echo "Logs: $OUTDIR" + print_overall "$overall_ok" [[ "$overall_ok" == "1" ]] } diff --git a/tasks/prd-x-agent-backlog.md b/tasks/prd-x-agent-backlog.md new file mode 100644 index 0000000..716dba7 --- /dev/null +++ b/tasks/prd-x-agent-backlog.md @@ -0,0 +1,282 @@ +# PRD: x-agent Backlog — Shared Library + 8 New Agents + +## Introduction + +The x-agent project provides lean workflow runner scripts for coding agents. Three agents exist (cargo-agent, npm-agent, terra-agent) but contain significant duplicated boilerplate. This PRD covers extracting shared helpers into a common library, refactoring existing agents, and shipping 8 new agents. Each agent is a separate deliverable with its own PR targeting main. + +## Goals + +- Extract ~100-150 lines of duplicated boilerplate into `lib/x-agent-common.sh` +- Refactor existing agents (cargo, npm, terra) to source the shared library +- Ship 8 new agents, each with full scenario tests, SKILL.md, and install.sh integration +- Every agent supports fix/auto-format where the underlying tool provides it +- Maintain Bash 3.2 compatibility and structured output contract across all agents + +## User Stories + +**Definition of Done (applies to all stories):** +- All acceptance criteria met +- `shellcheck --severity=warning` passes on all new/modified scripts +- `tests/run-scenarios.sh` passes for all affected agents +- Bash 3.2 compatible (no associative arrays, no `readarray`, no `|&`) +- Structured output contract followed (Step/Result/Fix/Overall) +- Every `Result: FAIL` includes a `Fix:` hint + +--- + +### US-001: Extract shared library and refactor existing agents +**Description:** As a contributor, I want common boilerplate extracted into a shared library so that new agents are smaller and consistent. + +**Acceptance Criteria:** +- [ ] `lib/x-agent-common.sh` created with: `hr()`, `step()`, `fmt_elapsed()`, `should_continue()`, `need()`, cleanup trap setup, workflow lock setup, MAX_LINES/CI detection, standard variable defaults +- [ ] cargo-agent, npm-agent, terra-agent refactored to `source` the shared lib +- [ ] All three existing agents produce identical output before/after refactor +- [ ] `tests/run-scenarios.sh` passes for all existing agents after refactor +- [ ] `shellcheck --severity=warning` passes on `lib/x-agent-common.sh` and all refactored scripts +- [ ] Shared lib is Bash 3.2 compatible + +--- + +### US-002: bash-agent +**Description:** As a developer, I want a bash-agent that validates shell scripts for syntax errors and lint issues so I can catch problems before CI. + +**Steps:** +- `syntax` — `bash -n` on each `.sh` file (required) +- `lint` — `shellcheck --severity=warning` (required) + +**Required tools:** bash, shellcheck +**Fix mode:** None (neither tool supports auto-fix). `Fix:` hints reference shellcheck wiki URLs. + +**Knobs:** `RUN_SYNTAX=1`, `RUN_LINT=1`, `SHELLCHECK_SEVERITY=warning` (configurable), `CHANGED_FILES` scoping + +**Acceptance Criteria:** +- [ ] `skills/bash-agent/scripts/bash-agent.sh` sources `lib/x-agent-common.sh` +- [ ] `syntax` step runs `bash -n` on all `.sh` files (or scoped via `CHANGED_FILES`) +- [ ] `lint` step runs `shellcheck` on all `.sh` files (or scoped) +- [ ] Failed shellcheck results include wiki link in `Fix:` hint (e.g. `See https://www.shellcheck.net/wiki/SC2086`) +- [ ] Commands: `syntax`, `lint`, `all` (default runs both) +- [ ] `SKILL.md` with trigger language and allowed-tools patterns +- [ ] `install.sh` updated with `bash-agent` in SKILLS list and dep checks +- [ ] `tests/bash-agent/clean/` and `tests/bash-agent/issues/` scenarios pass +- [ ] Script passes `shellcheck --severity=warning` + +--- + +### US-003: go-agent +**Description:** As a developer, I want a go-agent that checks formatting, runs vet/lint, and executes tests so I can validate Go code before CI. + +**Steps:** +- `fmt` — `gofmt -l` to check, `gofmt -w` to fix (required) +- `vet` — `go vet ./...` (required) +- `staticcheck` — `staticcheck ./...` (optional, skip with notice if not installed) +- `test` — `go test ./...` (required) + +**Required tools:** go +**Optional tools:** staticcheck +**Fix mode:** `gofmt -w` when `FMT_MODE=fix` + +**Knobs:** `RUN_FMT=1`, `RUN_VET=1`, `RUN_STATICCHECK=1`, `RUN_TESTS=1`, `FMT_MODE=check|fix` (check in CI, fix locally) + +**Acceptance Criteria:** +- [ ] Sources `lib/x-agent-common.sh` +- [ ] `fmt` step uses `gofmt -l` in check mode, `gofmt -w` in fix mode; CI forces check mode +- [ ] `vet` step runs `go vet ./...`, reports diagnostics on failure +- [ ] `staticcheck` step skips with notice if not installed, runs if available +- [ ] `test` step runs `go test ./...`, extracts failing test names in `Fix:` hint +- [ ] Commands: `fmt`, `vet`, `staticcheck`, `test`, `all` (default) +- [ ] `CHANGED_FILES` scoping: maps changed `.go` files to packages +- [ ] `SKILL.md`, `install.sh` updated, clean/issues scenarios pass +- [ ] Script passes `shellcheck --severity=warning` + +--- + +### US-004: gha-agent +**Description:** As a developer, I want a gha-agent that lints GitHub Actions workflow files so I can catch workflow errors before pushing. + +**Steps:** +- `lint` — `actionlint` on `.github/workflows/*.yml` (required) + +**Required tools:** actionlint +**Fix mode:** None (actionlint is check-only) + +**Knobs:** `RUN_LINT=1`, `CHANGED_FILES` scoping (filters to `.github/workflows/*.yml`) + +**Acceptance Criteria:** +- [ ] Sources `lib/x-agent-common.sh` +- [ ] `lint` step runs `actionlint` on workflow files +- [ ] If no `.github/workflows/` directory exists, reports SKIP with reason +- [ ] `CHANGED_FILES` scoping filters to only `.yml`/`.yaml` files under `.github/workflows/` +- [ ] Commands: `lint`, `all` (default) +- [ ] `SKILL.md`, `install.sh` updated, clean/issues scenarios pass +- [ ] Script passes `shellcheck --severity=warning` + +--- + +### US-005: helm-agent +**Description:** As a developer, I want a helm-agent that lints Helm charts and validates template rendering so I can catch chart errors before CI. + +**Steps:** +- `lint` — `helm lint ` (required) +- `template` — `helm template ` to verify templates render without error (required) + +**Required tools:** helm +**Fix mode:** None (both commands are validation-only) + +**Knobs:** `RUN_LINT=1`, `RUN_TEMPLATE=1`, `CHART_DIR=.` (auto-detect from `Chart.yaml` or `CHANGED_FILES`) + +**Acceptance Criteria:** +- [ ] Sources `lib/x-agent-common.sh` +- [ ] `lint` step runs `helm lint` on the chart directory +- [ ] `template` step runs `helm template` and checks for render errors (output discarded on success, shown on failure) +- [ ] Auto-detects chart directory from `Chart.yaml` if `CHART_DIR` not set +- [ ] If no `Chart.yaml` found, reports SKIP with reason +- [ ] `CHANGED_FILES` scoping: detects chart dir from changed files under a chart path +- [ ] Commands: `lint`, `template`, `all` (default) +- [ ] `SKILL.md`, `install.sh` updated, clean/issues scenarios pass +- [ ] Script passes `shellcheck --severity=warning` + +--- + +### US-006: kube-agent +**Description:** As a developer, I want a kube-agent that validates Kubernetes manifests so I can catch schema errors before applying to a cluster. + +**Steps:** +- `validate` — `kubeconform` (preferred) or `kubeval` on YAML manifests (required — at least one must be installed) + +**Required tools:** kubeconform OR kubeval (prefers kubeconform if both installed) +**Fix mode:** None (validation-only) + +**Knobs:** `RUN_VALIDATE=1`, `KUBE_SCHEMAS_DIR` (optional, for offline/custom schemas), `CHANGED_FILES` scoping (filters to `.yml`/`.yaml`) + +**Acceptance Criteria:** +- [ ] Sources `lib/x-agent-common.sh` +- [ ] Detects which validator is installed: prefers `kubeconform`, falls back to `kubeval` +- [ ] If neither installed, exits with code 2 and message naming both options +- [ ] `validate` step runs the detected validator on all YAML files (or scoped via `CHANGED_FILES`) +- [ ] Skips known non-Kubernetes YAML (e.g. files without `apiVersion`/`kind` fields) to avoid false positives +- [ ] `KUBE_SCHEMAS_DIR` passes through to the validator's schema location flag +- [ ] Commands: `validate`, `all` (default) +- [ ] `SKILL.md`, `install.sh` updated, clean/issues scenarios pass +- [ ] Script passes `shellcheck --severity=warning` + +--- + +### US-007: docker-agent +**Description:** As a developer, I want a docker-agent that lints Dockerfiles and optionally runs BuildKit checks so I can catch issues before CI builds. + +**Steps:** +- `lint` — `hadolint` on Dockerfiles (required) +- `build-check` — `docker build --check` BuildKit lint mode (optional, opt-in) + +**Required tools:** hadolint +**Optional tools:** docker (for build-check step) +**Fix mode:** None (both tools are check-only) + +**Knobs:** `RUN_LINT=1`, `RUN_BUILD_CHECK=0` (opt-in), `DOCKERFILE=Dockerfile` (path, auto-detects from `CHANGED_FILES`) + +**Acceptance Criteria:** +- [ ] Sources `lib/x-agent-common.sh` +- [ ] `lint` step runs `hadolint` on the target Dockerfile(s) +- [ ] Discovers Dockerfiles automatically (`Dockerfile`, `Dockerfile.*`, `*.dockerfile`) or scopes via `CHANGED_FILES` +- [ ] `build-check` step defaults to off (`RUN_BUILD_CHECK=0`), runs `docker build --check` when enabled +- [ ] `build-check` skips with notice if docker is not installed +- [ ] Commands: `lint`, `build-check`, `all` (default) +- [ ] `SKILL.md`, `install.sh` updated, clean/issues scenarios pass +- [ ] Script passes `shellcheck --severity=warning` + +--- + +### US-008: ansible-agent +**Description:** As a developer, I want an ansible-agent that lints and syntax-checks Ansible playbooks/roles so I can catch issues before running them. + +**Steps:** +- `lint` — `ansible-lint` with optional `--fix` mode (required) +- `syntax` — `ansible-playbook --syntax-check` on playbooks (required) + +**Required tools:** ansible-lint, ansible-playbook +**Fix mode:** `ansible-lint --fix` when `FMT_MODE=fix` + +**Knobs:** `RUN_LINT=1`, `RUN_SYNTAX=1`, `FMT_MODE=check|fix` (check in CI, fix locally) + +**Acceptance Criteria:** +- [ ] Sources `lib/x-agent-common.sh` +- [ ] `lint` step runs `ansible-lint` in check mode by default, `ansible-lint --fix` when `FMT_MODE=fix` +- [ ] CI forces check mode regardless of `FMT_MODE` +- [ ] `syntax` step runs `ansible-playbook --syntax-check` on discovered playbooks +- [ ] Auto-discovers playbooks (files matching common patterns: `playbook*.yml`, `site.yml`, or files containing `hosts:` key) +- [ ] `CHANGED_FILES` scoping filters to `.yml`/`.yaml` files +- [ ] Commands: `lint`, `syntax`, `all` (default) +- [ ] `SKILL.md`, `install.sh` updated, clean/issues scenarios pass +- [ ] Script passes `shellcheck --severity=warning` + +--- + +### US-009: sql-agent +**Description:** As a developer, I want an sql-agent that lints and optionally auto-fixes SQL files so I can maintain consistent SQL style. + +**Steps:** +- `lint` — `sqlfluff lint` (required) +- `fix` — `sqlfluff fix` (optional, opt-in) + +**Required tools:** sqlfluff +**Fix mode:** `sqlfluff fix` when `FMT_MODE=fix` or `RUN_FIX=1` + +**Knobs:** `RUN_LINT=1`, `RUN_FIX=0` (opt-in), `FMT_MODE=check|fix` (check in CI), `SQLFLUFF_DIALECT=ansi` (configurable), `CHANGED_FILES` scoping + +**Acceptance Criteria:** +- [ ] Sources `lib/x-agent-common.sh` +- [ ] `lint` step runs `sqlfluff lint` on all `.sql` files (or scoped via `CHANGED_FILES`) +- [ ] `fix` step defaults to off, runs `sqlfluff fix` when enabled +- [ ] CI forces check/lint-only regardless of `FMT_MODE` +- [ ] `SQLFLUFF_DIALECT` passed through to `--dialect` flag (defaults to `ansi`) +- [ ] Auto-discovers `.sql` files recursively from project root +- [ ] Commands: `lint`, `fix`, `all` (default runs lint only unless fix enabled) +- [ ] `SKILL.md`, `install.sh` updated, clean/issues scenarios pass +- [ ] Script passes `shellcheck --severity=warning` + +--- + +## Functional Requirements + +- FR-1: All agents source `lib/x-agent-common.sh` for shared boilerplate +- FR-2: All agents support universal knobs: `KEEP_DIR`, `MAX_LINES`, `FAIL_FAST`, `TMPDIR_ROOT`, `CHANGED_FILES` +- FR-3: All agents support per-step toggles via `RUN_=0|1` +- FR-4: All agents produce structured output: `Step:`, `Result: PASS|FAIL|SKIP`, `Fix:` (on fail), `Full log:`, `Time:`, `Overall: PASS|FAIL`, `Logs:` +- FR-5: All agents support `--fail-fast` CLI flag and `--help`/`help`/`-h` usage output +- FR-6: Required tools checked with `need()` (exit 2 if missing); optional tools skip with notice +- FR-7: Cleanup trap preserves logs on failure or `KEEP_DIR=1` +- FR-8: Workflow lock (flock with Perl fallback) prevents concurrent runs per agent +- FR-9: `MAX_LINES` defaults to 40 locally, 999999 when `CI=true|1` +- FR-10: Agents with fix mode use `FMT_MODE=check|fix`; CI forces check mode +- FR-11: Each agent includes `SKILL.md` with trigger language and `allowed-tools` patterns +- FR-12: `install.sh` updated per agent with SKILLS list entry and dependency checks +- FR-13: Each agent has `tests/-agent/clean/` and `tests/-agent/issues/` scenario fixtures + +## Non-Goals + +- No cross-agent orchestration (running multiple agents in sequence) +- No daemon/watch mode for any agent +- No remote/cloud tool integration (e.g., no Terraform Cloud, no GitHub API calls) +- No full Docker image builds (only BuildKit `--check` lint mode) +- No custom rule authoring for any linter — agents use tool defaults or simple knobs +- No package manager integration (agents don't install missing tools, just report them) + +## Technical Considerations + +- All scripts must be Bash 3.2 compatible (stock macOS) +- `lib/x-agent-common.sh` must be sourceable without side effects beyond function definitions and variable defaults +- Existing agents (cargo, npm, terra) must not change behavior after refactor — only internal structure +- Scenario test fixtures should use minimal synthetic projects (not real codebases) +- `install.sh` symlinks skills into `~/.claude/skills/` and `~/.codex/skills/`; the shared lib must be accessible from the installed location + +## Success Metrics + +- All 8 new agents pass their scenario tests (clean + issues fixtures) +- Existing agents pass their scenario tests after shared library refactor +- `shellcheck --severity=warning` passes on all scripts including `lib/x-agent-common.sh` +- Each new agent script is under ~200 lines of domain-specific code (shared boilerplate in lib) +- `tests/run-scenarios.sh` discovers and runs all scenarios successfully + +## Open Questions + +None — all resolved during clarifying questions. diff --git a/tests/ansible-agent/clean/playbook.yml b/tests/ansible-agent/clean/playbook.yml new file mode 100644 index 0000000..fe39494 --- /dev/null +++ b/tests/ansible-agent/clean/playbook.yml @@ -0,0 +1,8 @@ +--- +- name: Test playbook + hosts: localhost + gather_facts: false + tasks: + - name: Print message + ansible.builtin.debug: + msg: "Hello from ansible-agent test" diff --git a/tests/ansible-agent/clean/scenario.env b/tests/ansible-agent/clean/scenario.env new file mode 100644 index 0000000..fc9954d --- /dev/null +++ b/tests/ansible-agent/clean/scenario.env @@ -0,0 +1,5 @@ +SCENARIO_NAME="ansible-agent clean" +AGENT_SCRIPT="skills/ansible-agent/scripts/ansible-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=0 +REQUIRED_TOOLS="ansible-lint ansible-playbook" diff --git a/tests/ansible-agent/issues/playbook.yml b/tests/ansible-agent/issues/playbook.yml new file mode 100644 index 0000000..cb43f24 --- /dev/null +++ b/tests/ansible-agent/issues/playbook.yml @@ -0,0 +1,5 @@ +--- +- hosts: localhost + tasks: + - shell: echo hello + - command: ls -la diff --git a/tests/ansible-agent/issues/scenario.env b/tests/ansible-agent/issues/scenario.env new file mode 100644 index 0000000..b078e65 --- /dev/null +++ b/tests/ansible-agent/issues/scenario.env @@ -0,0 +1,5 @@ +SCENARIO_NAME="ansible-agent issues" +AGENT_SCRIPT="skills/ansible-agent/scripts/ansible-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=1 +REQUIRED_TOOLS="ansible-lint ansible-playbook" diff --git a/tests/ansible-agent/scoped-match/playbook.yml b/tests/ansible-agent/scoped-match/playbook.yml new file mode 100644 index 0000000..0f5cec1 --- /dev/null +++ b/tests/ansible-agent/scoped-match/playbook.yml @@ -0,0 +1,8 @@ +--- +- name: Test playbook + hosts: localhost + gather_facts: false + tasks: + - name: Print message + ansible.builtin.debug: + msg: "Hello from ansible-agent scoped test" diff --git a/tests/ansible-agent/scoped-match/scenario.env b/tests/ansible-agent/scoped-match/scenario.env new file mode 100644 index 0000000..1e94cde --- /dev/null +++ b/tests/ansible-agent/scoped-match/scenario.env @@ -0,0 +1,6 @@ +SCENARIO_NAME="ansible-agent scoped-match" +AGENT_SCRIPT="skills/ansible-agent/scripts/ansible-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=0 +REQUIRED_TOOLS="ansible-lint ansible-playbook" +export CHANGED_FILES="playbook.yml" diff --git a/tests/ansible-agent/scoped-no-match/scenario.env b/tests/ansible-agent/scoped-no-match/scenario.env new file mode 100644 index 0000000..c32ad56 --- /dev/null +++ b/tests/ansible-agent/scoped-no-match/scenario.env @@ -0,0 +1,6 @@ +SCENARIO_NAME="ansible-agent scoped-no-match" +AGENT_SCRIPT="skills/ansible-agent/scripts/ansible-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=0 +REQUIRED_TOOLS="ansible-lint ansible-playbook" +export CHANGED_FILES="README.md package.json" diff --git a/tests/bash-agent/clean/scenario.env b/tests/bash-agent/clean/scenario.env new file mode 100644 index 0000000..c119d15 --- /dev/null +++ b/tests/bash-agent/clean/scenario.env @@ -0,0 +1,5 @@ +SCENARIO_NAME="bash-agent clean" +AGENT_SCRIPT="skills/bash-agent/scripts/bash-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT="0" +REQUIRED_TOOLS="bash shellcheck" diff --git a/tests/bash-agent/clean/scripts/good.sh b/tests/bash-agent/clean/scripts/good.sh new file mode 100644 index 0000000..78e36a8 --- /dev/null +++ b/tests/bash-agent/clean/scripts/good.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +# A clean, valid shell script that passes both bash -n and shellcheck. + +greet() { + local name="$1" + echo "Hello, ${name}!" +} + +main() { + if [[ $# -lt 1 ]]; then + echo "Usage: good.sh " >&2 + return 1 + fi + greet "$1" +} + +main "$@" diff --git a/tests/bash-agent/issues/scenario.env b/tests/bash-agent/issues/scenario.env new file mode 100644 index 0000000..c3ed46a --- /dev/null +++ b/tests/bash-agent/issues/scenario.env @@ -0,0 +1,5 @@ +SCENARIO_NAME="bash-agent issues" +AGENT_SCRIPT="skills/bash-agent/scripts/bash-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT="1" +REQUIRED_TOOLS="bash shellcheck" diff --git a/tests/bash-agent/issues/scripts/bad.sh b/tests/bash-agent/issues/scripts/bad.sh new file mode 100644 index 0000000..63ae32b --- /dev/null +++ b/tests/bash-agent/issues/scripts/bad.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +# This script has shellcheck violations at warning severity. + +# SC2034 (warning): my_unused appears unused. Verify use (or export/eval). +my_unused="this is never used" + +# SC2154 (warning): unset_var is referenced but not assigned. +echo "$unset_var" diff --git a/tests/docker-agent/clean/Dockerfile b/tests/docker-agent/clean/Dockerfile new file mode 100644 index 0000000..2c0b06d --- /dev/null +++ b/tests/docker-agent/clean/Dockerfile @@ -0,0 +1,3 @@ +FROM alpine:3.19 +COPY . /app +CMD ["/app/start.sh"] diff --git a/tests/docker-agent/clean/scenario.env b/tests/docker-agent/clean/scenario.env new file mode 100644 index 0000000..1f4213a --- /dev/null +++ b/tests/docker-agent/clean/scenario.env @@ -0,0 +1,5 @@ +SCENARIO_NAME="docker-agent clean" +AGENT_SCRIPT="skills/docker-agent/scripts/docker-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=0 +REQUIRED_TOOLS="hadolint" diff --git a/tests/docker-agent/issues/Dockerfile b/tests/docker-agent/issues/Dockerfile new file mode 100644 index 0000000..64019eb --- /dev/null +++ b/tests/docker-agent/issues/Dockerfile @@ -0,0 +1,3 @@ +FROM ubuntu:latest +RUN apt-get update && apt-get install -y curl +ADD . /app diff --git a/tests/docker-agent/issues/scenario.env b/tests/docker-agent/issues/scenario.env new file mode 100644 index 0000000..ec2776e --- /dev/null +++ b/tests/docker-agent/issues/scenario.env @@ -0,0 +1,5 @@ +SCENARIO_NAME="docker-agent issues" +AGENT_SCRIPT="skills/docker-agent/scripts/docker-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=1 +REQUIRED_TOOLS="hadolint" diff --git a/tests/docker-agent/no-dockerfiles/not-a-dockerfile.txt b/tests/docker-agent/no-dockerfiles/not-a-dockerfile.txt new file mode 100644 index 0000000..f21afd5 --- /dev/null +++ b/tests/docker-agent/no-dockerfiles/not-a-dockerfile.txt @@ -0,0 +1 @@ +This is not a Dockerfile. diff --git a/tests/docker-agent/no-dockerfiles/scenario.env b/tests/docker-agent/no-dockerfiles/scenario.env new file mode 100644 index 0000000..1c41c8a --- /dev/null +++ b/tests/docker-agent/no-dockerfiles/scenario.env @@ -0,0 +1,5 @@ +SCENARIO_NAME="docker-agent no-dockerfiles skip" +AGENT_SCRIPT="skills/docker-agent/scripts/docker-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=0 +REQUIRED_TOOLS="hadolint" diff --git a/tests/docker-agent/scoped-match/Dockerfile b/tests/docker-agent/scoped-match/Dockerfile new file mode 100644 index 0000000..2c0b06d --- /dev/null +++ b/tests/docker-agent/scoped-match/Dockerfile @@ -0,0 +1,3 @@ +FROM alpine:3.19 +COPY . /app +CMD ["/app/start.sh"] diff --git a/tests/docker-agent/scoped-match/scenario.env b/tests/docker-agent/scoped-match/scenario.env new file mode 100644 index 0000000..98483cd --- /dev/null +++ b/tests/docker-agent/scoped-match/scenario.env @@ -0,0 +1,6 @@ +SCENARIO_NAME="docker-agent scoped-match" +AGENT_SCRIPT="skills/docker-agent/scripts/docker-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=0 +REQUIRED_TOOLS="hadolint" +export CHANGED_FILES="Dockerfile" diff --git a/tests/docker-agent/scoped-no-match/Dockerfile b/tests/docker-agent/scoped-no-match/Dockerfile new file mode 100644 index 0000000..fea6942 --- /dev/null +++ b/tests/docker-agent/scoped-no-match/Dockerfile @@ -0,0 +1,4 @@ +FROM alpine:3.19 +RUN apk add --no-cache curl +COPY . /app +CMD ["/app/start.sh"] diff --git a/tests/docker-agent/scoped-no-match/scenario.env b/tests/docker-agent/scoped-no-match/scenario.env new file mode 100644 index 0000000..2d75526 --- /dev/null +++ b/tests/docker-agent/scoped-no-match/scenario.env @@ -0,0 +1,6 @@ +SCENARIO_NAME="docker-agent scoped-no-match" +AGENT_SCRIPT="skills/docker-agent/scripts/docker-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=0 +REQUIRED_TOOLS="hadolint" +export CHANGED_FILES="README.md package.json" diff --git a/tests/gha-agent/clean/.github/workflows/ci.yml b/tests/gha-agent/clean/.github/workflows/ci.yml new file mode 100644 index 0000000..c6e15ca --- /dev/null +++ b/tests/gha-agent/clean/.github/workflows/ci.yml @@ -0,0 +1,8 @@ +name: CI +on: [push] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: echo "hello" diff --git a/tests/gha-agent/clean/scenario.env b/tests/gha-agent/clean/scenario.env new file mode 100644 index 0000000..be99ac2 --- /dev/null +++ b/tests/gha-agent/clean/scenario.env @@ -0,0 +1,5 @@ +SCENARIO_NAME="gha-agent clean" +AGENT_SCRIPT="skills/gha-agent/scripts/gha-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=0 +REQUIRED_TOOLS="actionlint" diff --git a/tests/gha-agent/issues/.github/workflows/bad.yml b/tests/gha-agent/issues/.github/workflows/bad.yml new file mode 100644 index 0000000..2773699 --- /dev/null +++ b/tests/gha-agent/issues/.github/workflows/bad.yml @@ -0,0 +1,8 @@ +name: Bad +on: [push] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: echo ${{ github.event.inputs.name }} diff --git a/tests/gha-agent/issues/scenario.env b/tests/gha-agent/issues/scenario.env new file mode 100644 index 0000000..0e2185d --- /dev/null +++ b/tests/gha-agent/issues/scenario.env @@ -0,0 +1,5 @@ +SCENARIO_NAME="gha-agent issues" +AGENT_SCRIPT="skills/gha-agent/scripts/gha-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=1 +REQUIRED_TOOLS="actionlint" diff --git a/tests/go-agent/clean/go.mod b/tests/go-agent/clean/go.mod new file mode 100644 index 0000000..0af8d78 --- /dev/null +++ b/tests/go-agent/clean/go.mod @@ -0,0 +1,3 @@ +module example.com/clean + +go 1.21 diff --git a/tests/go-agent/clean/main.go b/tests/go-agent/clean/main.go new file mode 100644 index 0000000..d8fa929 --- /dev/null +++ b/tests/go-agent/clean/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("hello") +} diff --git a/tests/go-agent/clean/scenario.env b/tests/go-agent/clean/scenario.env new file mode 100644 index 0000000..23d6340 --- /dev/null +++ b/tests/go-agent/clean/scenario.env @@ -0,0 +1,5 @@ +SCENARIO_NAME="go-agent clean" +AGENT_SCRIPT="skills/go-agent/scripts/go-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT="0" +REQUIRED_TOOLS="go" diff --git a/tests/go-agent/issues/go.mod b/tests/go-agent/issues/go.mod new file mode 100644 index 0000000..bcaba24 --- /dev/null +++ b/tests/go-agent/issues/go.mod @@ -0,0 +1,3 @@ +module example.com/issues + +go 1.21 diff --git a/tests/go-agent/issues/main.go b/tests/go-agent/issues/main.go new file mode 100644 index 0000000..7e44647 --- /dev/null +++ b/tests/go-agent/issues/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Printf("%d", "not-a-number") +} diff --git a/tests/go-agent/issues/scenario.env b/tests/go-agent/issues/scenario.env new file mode 100644 index 0000000..5107643 --- /dev/null +++ b/tests/go-agent/issues/scenario.env @@ -0,0 +1,5 @@ +SCENARIO_NAME="go-agent issues" +AGENT_SCRIPT="skills/go-agent/scripts/go-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT="1" +REQUIRED_TOOLS="go" diff --git a/tests/helm-agent/clean/Chart.yaml b/tests/helm-agent/clean/Chart.yaml new file mode 100644 index 0000000..6ec2875 --- /dev/null +++ b/tests/helm-agent/clean/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +name: test-chart +description: A minimal test chart +version: 0.1.0 diff --git a/tests/helm-agent/clean/scenario.env b/tests/helm-agent/clean/scenario.env new file mode 100644 index 0000000..380a2b4 --- /dev/null +++ b/tests/helm-agent/clean/scenario.env @@ -0,0 +1,5 @@ +SCENARIO_NAME="helm-agent clean" +AGENT_SCRIPT="skills/helm-agent/scripts/helm-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=0 +REQUIRED_TOOLS="helm" diff --git a/tests/helm-agent/clean/templates/configmap.yaml b/tests/helm-agent/clean/templates/configmap.yaml new file mode 100644 index 0000000..3441f1f --- /dev/null +++ b/tests/helm-agent/clean/templates/configmap.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.appName }}-config +data: + replicas: "{{ .Values.replicas }}" diff --git a/tests/helm-agent/clean/values.yaml b/tests/helm-agent/clean/values.yaml new file mode 100644 index 0000000..22a29d4 --- /dev/null +++ b/tests/helm-agent/clean/values.yaml @@ -0,0 +1,2 @@ +appName: my-app +replicas: 1 diff --git a/tests/helm-agent/issues/Chart.yaml b/tests/helm-agent/issues/Chart.yaml new file mode 100644 index 0000000..e1c646a --- /dev/null +++ b/tests/helm-agent/issues/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +name: bad-chart +description: A chart with template errors +version: 0.1.0 diff --git a/tests/helm-agent/issues/scenario.env b/tests/helm-agent/issues/scenario.env new file mode 100644 index 0000000..e5c6673 --- /dev/null +++ b/tests/helm-agent/issues/scenario.env @@ -0,0 +1,5 @@ +SCENARIO_NAME="helm-agent issues" +AGENT_SCRIPT="skills/helm-agent/scripts/helm-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=1 +REQUIRED_TOOLS="helm" diff --git a/tests/helm-agent/issues/templates/bad.yaml b/tests/helm-agent/issues/templates/bad.yaml new file mode 100644 index 0000000..1d40387 --- /dev/null +++ b/tests/helm-agent/issues/templates/bad.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.appName }}-config +data: + required_value: {{ .Values.requiredField | required "requiredField must be set" }} diff --git a/tests/helm-agent/issues/values.yaml b/tests/helm-agent/issues/values.yaml new file mode 100644 index 0000000..cf6f8fa --- /dev/null +++ b/tests/helm-agent/issues/values.yaml @@ -0,0 +1 @@ +appName: my-app diff --git a/tests/helm-agent/no-chart/scenario.env b/tests/helm-agent/no-chart/scenario.env new file mode 100644 index 0000000..ee73f48 --- /dev/null +++ b/tests/helm-agent/no-chart/scenario.env @@ -0,0 +1,5 @@ +SCENARIO_NAME="helm-agent no-chart skip" +AGENT_SCRIPT="skills/helm-agent/scripts/helm-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=0 +REQUIRED_TOOLS="helm" diff --git a/tests/kube-agent/clean/deployment.yaml b/tests/kube-agent/clean/deployment.yaml new file mode 100644 index 0000000..2913d05 --- /dev/null +++ b/tests/kube-agent/clean/deployment.yaml @@ -0,0 +1,17 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-app +spec: + replicas: 1 + selector: + matchLabels: + app: test + template: + metadata: + labels: + app: test + spec: + containers: + - name: app + image: nginx:latest diff --git a/tests/kube-agent/clean/scenario.env b/tests/kube-agent/clean/scenario.env new file mode 100644 index 0000000..e354555 --- /dev/null +++ b/tests/kube-agent/clean/scenario.env @@ -0,0 +1,6 @@ +SCENARIO_NAME="kube-agent clean" +AGENT_SCRIPT="skills/kube-agent/scripts/kube-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=0 +REQUIRED_TOOLS="kubeconform" +export KUBE_IGNORE_MISSING_SCHEMAS=1 diff --git a/tests/kube-agent/issues/bad-deployment.yaml b/tests/kube-agent/issues/bad-deployment.yaml new file mode 100644 index 0000000..1ea4455 --- /dev/null +++ b/tests/kube-agent/issues/bad-deployment.yaml @@ -0,0 +1,15 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test +spec: + replicas: "not-a-number" + selector: + matchLabels: + app: test + template: + spec: + containers: + - name: app + ports: + - containerPort: [invalid diff --git a/tests/kube-agent/issues/scenario.env b/tests/kube-agent/issues/scenario.env new file mode 100644 index 0000000..c4e13d2 --- /dev/null +++ b/tests/kube-agent/issues/scenario.env @@ -0,0 +1,6 @@ +SCENARIO_NAME="kube-agent issues" +AGENT_SCRIPT="skills/kube-agent/scripts/kube-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=1 +REQUIRED_TOOLS="kubeconform" +export KUBE_IGNORE_MISSING_SCHEMAS=1 diff --git a/tests/kube-agent/kubeval-clean/deployment.yaml b/tests/kube-agent/kubeval-clean/deployment.yaml new file mode 100644 index 0000000..2913d05 --- /dev/null +++ b/tests/kube-agent/kubeval-clean/deployment.yaml @@ -0,0 +1,17 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-app +spec: + replicas: 1 + selector: + matchLabels: + app: test + template: + metadata: + labels: + app: test + spec: + containers: + - name: app + image: nginx:latest diff --git a/tests/kube-agent/kubeval-clean/scenario.env b/tests/kube-agent/kubeval-clean/scenario.env new file mode 100644 index 0000000..b48b0cd --- /dev/null +++ b/tests/kube-agent/kubeval-clean/scenario.env @@ -0,0 +1,6 @@ +SCENARIO_NAME="kube-agent kubeval-clean" +AGENT_SCRIPT="tests/kube-agent/run-with-kubeval.sh" +RUN_ARGS="all" +EXPECT_EXIT=0 +REQUIRED_TOOLS="kubeval" +export KUBE_IGNORE_MISSING_SCHEMAS=1 diff --git a/tests/kube-agent/kubeval-issues/bad-deployment.yaml b/tests/kube-agent/kubeval-issues/bad-deployment.yaml new file mode 100644 index 0000000..1ea4455 --- /dev/null +++ b/tests/kube-agent/kubeval-issues/bad-deployment.yaml @@ -0,0 +1,15 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test +spec: + replicas: "not-a-number" + selector: + matchLabels: + app: test + template: + spec: + containers: + - name: app + ports: + - containerPort: [invalid diff --git a/tests/kube-agent/kubeval-issues/scenario.env b/tests/kube-agent/kubeval-issues/scenario.env new file mode 100644 index 0000000..bdf06ea --- /dev/null +++ b/tests/kube-agent/kubeval-issues/scenario.env @@ -0,0 +1,6 @@ +SCENARIO_NAME="kube-agent kubeval-issues" +AGENT_SCRIPT="tests/kube-agent/run-with-kubeval.sh" +RUN_ARGS="all" +EXPECT_EXIT=1 +REQUIRED_TOOLS="kubeval" +export KUBE_IGNORE_MISSING_SCHEMAS=1 diff --git a/tests/kube-agent/no-manifests/scenario.env b/tests/kube-agent/no-manifests/scenario.env new file mode 100644 index 0000000..ea6d1e8 --- /dev/null +++ b/tests/kube-agent/no-manifests/scenario.env @@ -0,0 +1,5 @@ +SCENARIO_NAME="kube-agent no-manifests skip" +AGENT_SCRIPT="skills/kube-agent/scripts/kube-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=0 +REQUIRED_TOOLS="kubeconform" diff --git a/tests/kube-agent/non-k8s-yaml/config.yaml b/tests/kube-agent/non-k8s-yaml/config.yaml new file mode 100644 index 0000000..a29ed6c --- /dev/null +++ b/tests/kube-agent/non-k8s-yaml/config.yaml @@ -0,0 +1,7 @@ +database: + host: localhost + port: 5432 + name: myapp +logging: + level: info + format: json diff --git a/tests/kube-agent/non-k8s-yaml/scenario.env b/tests/kube-agent/non-k8s-yaml/scenario.env new file mode 100644 index 0000000..429b2bf --- /dev/null +++ b/tests/kube-agent/non-k8s-yaml/scenario.env @@ -0,0 +1,5 @@ +SCENARIO_NAME="kube-agent non-k8s-yaml" +AGENT_SCRIPT="skills/kube-agent/scripts/kube-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=0 +REQUIRED_TOOLS="kubeconform" diff --git a/tests/kube-agent/non-k8s-yaml/settings.yml b/tests/kube-agent/non-k8s-yaml/settings.yml new file mode 100644 index 0000000..e2876db --- /dev/null +++ b/tests/kube-agent/non-k8s-yaml/settings.yml @@ -0,0 +1,6 @@ +app: + name: my-service + version: 1.0.0 +features: + enabled: true + debug: false diff --git a/tests/kube-agent/run-with-kubeval.sh b/tests/kube-agent/run-with-kubeval.sh new file mode 100755 index 0000000..65ec4ee --- /dev/null +++ b/tests/kube-agent/run-with-kubeval.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Wrapper that hides kubeconform from PATH so kube-agent falls back to kubeval. +# Used by kubeval-* test fixtures to exercise the fallback branch (FR-6). +# +# Instead of removing entire PATH directories (which drops co-located tools +# like kubeval), this creates shadow directories with symlinks to everything +# EXCEPT kubeconform. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +AGENT_SCRIPT="${SCRIPT_DIR}/../../skills/kube-agent/scripts/kube-agent.sh" + +SHADOW_ROOT="$(mktemp -d)" +cleanup() { rm -rf "$SHADOW_ROOT"; } +trap cleanup EXIT + +NEW_PATH="" +IFS=':' +for dir in $PATH; do + if [[ -x "${dir}/kubeconform" ]]; then + # Create a shadow directory with symlinks to all binaries except kubeconform + safe_name="$(echo "$dir" | tr '/' '_')" + shadow="${SHADOW_ROOT}/${safe_name}" + mkdir -p "$shadow" + for bin in "$dir"/*; do + [[ -e "$bin" ]] || continue + bn="$(basename "$bin")" + [[ "$bn" == "kubeconform" ]] && continue + ln -sf "$bin" "$shadow/$bn" 2>/dev/null || true + done + dir="$shadow" + fi + if [[ -z "$NEW_PATH" ]]; then + NEW_PATH="$dir" + else + NEW_PATH="${NEW_PATH}:${dir}" + fi +done +unset IFS + +export PATH="$NEW_PATH" + +exec "$AGENT_SCRIPT" "$@" diff --git a/tests/kube-agent/schema-dir/deployment.yaml b/tests/kube-agent/schema-dir/deployment.yaml new file mode 100644 index 0000000..2913d05 --- /dev/null +++ b/tests/kube-agent/schema-dir/deployment.yaml @@ -0,0 +1,17 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-app +spec: + replicas: 1 + selector: + matchLabels: + app: test + template: + metadata: + labels: + app: test + spec: + containers: + - name: app + image: nginx:latest diff --git a/tests/kube-agent/schema-dir/scenario.env b/tests/kube-agent/schema-dir/scenario.env new file mode 100644 index 0000000..793f5ab --- /dev/null +++ b/tests/kube-agent/schema-dir/scenario.env @@ -0,0 +1,7 @@ +SCENARIO_NAME="kube-agent schema-dir passthrough" +AGENT_SCRIPT="skills/kube-agent/scripts/kube-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=0 +REQUIRED_TOOLS="kubeconform" +export KUBE_IGNORE_MISSING_SCHEMAS=1 +export KUBE_SCHEMAS_DIR="default" diff --git a/tests/kube-agent/scoped-match/deployment.yaml b/tests/kube-agent/scoped-match/deployment.yaml new file mode 100644 index 0000000..1216ada --- /dev/null +++ b/tests/kube-agent/scoped-match/deployment.yaml @@ -0,0 +1,17 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: scoped-app +spec: + replicas: 1 + selector: + matchLabels: + app: scoped + template: + metadata: + labels: + app: scoped + spec: + containers: + - name: app + image: nginx:latest diff --git a/tests/kube-agent/scoped-match/scenario.env b/tests/kube-agent/scoped-match/scenario.env new file mode 100644 index 0000000..3f9169d --- /dev/null +++ b/tests/kube-agent/scoped-match/scenario.env @@ -0,0 +1,7 @@ +SCENARIO_NAME="kube-agent scoped-match" +AGENT_SCRIPT="skills/kube-agent/scripts/kube-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=0 +REQUIRED_TOOLS="kubeconform" +export KUBE_IGNORE_MISSING_SCHEMAS=1 +export CHANGED_FILES="deployment.yaml" diff --git a/tests/kube-agent/scoped-no-match/deployment.yaml b/tests/kube-agent/scoped-no-match/deployment.yaml new file mode 100644 index 0000000..658d75c --- /dev/null +++ b/tests/kube-agent/scoped-no-match/deployment.yaml @@ -0,0 +1,17 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ignored-app +spec: + replicas: 1 + selector: + matchLabels: + app: ignored + template: + metadata: + labels: + app: ignored + spec: + containers: + - name: app + image: nginx:latest diff --git a/tests/kube-agent/scoped-no-match/scenario.env b/tests/kube-agent/scoped-no-match/scenario.env new file mode 100644 index 0000000..8fa2a3c --- /dev/null +++ b/tests/kube-agent/scoped-no-match/scenario.env @@ -0,0 +1,6 @@ +SCENARIO_NAME="kube-agent scoped-no-match" +AGENT_SCRIPT="skills/kube-agent/scripts/kube-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=0 +REQUIRED_TOOLS="kubeconform" +export CHANGED_FILES="not-here.yaml other-file.txt" diff --git a/tests/run-scenarios.sh b/tests/run-scenarios.sh index e705f05..b9f4b95 100755 --- a/tests/run-scenarios.sh +++ b/tests/run-scenarios.sh @@ -126,7 +126,7 @@ run_shellcheck() { local scripts=() while IFS= read -r -d '' f; do scripts+=("$f") - done < <(find "$ROOT_DIR/skills" -name '*.sh' -print0; find "$TESTS_DIR" -name '*.sh' -print0; find "$ROOT_DIR" -maxdepth 1 -name '*.sh' -print0) + done < <(find "$ROOT_DIR/skills" -name '*.sh' -print0; find "$ROOT_DIR/lib" -name '*.sh' -print0 2>/dev/null; find "$TESTS_DIR" -name '*.sh' -not -path '*/issues/*' -print0; find "$ROOT_DIR" -maxdepth 1 -name '*.sh' -print0) if shellcheck --severity=warning "${scripts[@]}" >/dev/null 2>&1; then print_status "PASS" "shellcheck" diff --git a/tests/sql-agent/ci-check/queries/bad.sql b/tests/sql-agent/ci-check/queries/bad.sql new file mode 100644 index 0000000..d023741 --- /dev/null +++ b/tests/sql-agent/ci-check/queries/bad.sql @@ -0,0 +1 @@ +SELECT id,name,email FROM users WHERE active=1 ORDER BY name diff --git a/tests/sql-agent/ci-check/scenario.env b/tests/sql-agent/ci-check/scenario.env new file mode 100644 index 0000000..c5357a6 --- /dev/null +++ b/tests/sql-agent/ci-check/scenario.env @@ -0,0 +1,7 @@ +SCENARIO_NAME="sql-agent ci-check" +AGENT_SCRIPT="skills/sql-agent/scripts/sql-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=1 +REQUIRED_TOOLS="sqlfluff" +export CI="true" +export RUN_FIX="1" diff --git a/tests/sql-agent/clean-dialect/queries/select.sql b/tests/sql-agent/clean-dialect/queries/select.sql new file mode 100644 index 0000000..24120dd --- /dev/null +++ b/tests/sql-agent/clean-dialect/queries/select.sql @@ -0,0 +1,10 @@ +SELECT + id, + name, + email +FROM + users +WHERE + active = 1 +ORDER BY + name; diff --git a/tests/sql-agent/clean-dialect/scenario.env b/tests/sql-agent/clean-dialect/scenario.env new file mode 100644 index 0000000..3a0b63d --- /dev/null +++ b/tests/sql-agent/clean-dialect/scenario.env @@ -0,0 +1,6 @@ +SCENARIO_NAME="sql-agent clean-dialect" +AGENT_SCRIPT="skills/sql-agent/scripts/sql-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=0 +REQUIRED_TOOLS="sqlfluff" +export SQLFLUFF_DIALECT="mysql" diff --git a/tests/sql-agent/clean/queries/select.sql b/tests/sql-agent/clean/queries/select.sql new file mode 100644 index 0000000..24120dd --- /dev/null +++ b/tests/sql-agent/clean/queries/select.sql @@ -0,0 +1,10 @@ +SELECT + id, + name, + email +FROM + users +WHERE + active = 1 +ORDER BY + name; diff --git a/tests/sql-agent/clean/scenario.env b/tests/sql-agent/clean/scenario.env new file mode 100644 index 0000000..c33c897 --- /dev/null +++ b/tests/sql-agent/clean/scenario.env @@ -0,0 +1,5 @@ +SCENARIO_NAME="sql-agent clean" +AGENT_SCRIPT="skills/sql-agent/scripts/sql-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=0 +REQUIRED_TOOLS="sqlfluff" diff --git a/tests/sql-agent/fix-command/queries/bad.sql b/tests/sql-agent/fix-command/queries/bad.sql new file mode 100644 index 0000000..d023741 --- /dev/null +++ b/tests/sql-agent/fix-command/queries/bad.sql @@ -0,0 +1 @@ +SELECT id,name,email FROM users WHERE active=1 ORDER BY name diff --git a/tests/sql-agent/fix-command/scenario.env b/tests/sql-agent/fix-command/scenario.env new file mode 100644 index 0000000..4c645ec --- /dev/null +++ b/tests/sql-agent/fix-command/scenario.env @@ -0,0 +1,5 @@ +SCENARIO_NAME="sql-agent fix-command" +AGENT_SCRIPT="skills/sql-agent/scripts/sql-agent.sh" +RUN_ARGS="fix" +EXPECT_EXIT=0 +REQUIRED_TOOLS="sqlfluff" diff --git a/tests/sql-agent/issues/queries/bad.sql b/tests/sql-agent/issues/queries/bad.sql new file mode 100644 index 0000000..d023741 --- /dev/null +++ b/tests/sql-agent/issues/queries/bad.sql @@ -0,0 +1 @@ +SELECT id,name,email FROM users WHERE active=1 ORDER BY name diff --git a/tests/sql-agent/issues/scenario.env b/tests/sql-agent/issues/scenario.env new file mode 100644 index 0000000..2700f6a --- /dev/null +++ b/tests/sql-agent/issues/scenario.env @@ -0,0 +1,5 @@ +SCENARIO_NAME="sql-agent issues" +AGENT_SCRIPT="skills/sql-agent/scripts/sql-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=1 +REQUIRED_TOOLS="sqlfluff" diff --git a/tests/sql-agent/no-sql/scenario.env b/tests/sql-agent/no-sql/scenario.env new file mode 100644 index 0000000..105ceba --- /dev/null +++ b/tests/sql-agent/no-sql/scenario.env @@ -0,0 +1,5 @@ +SCENARIO_NAME="sql-agent no-sql" +AGENT_SCRIPT="skills/sql-agent/scripts/sql-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=0 +REQUIRED_TOOLS="sqlfluff" diff --git a/tests/sql-agent/scoped-match/queries/select.sql b/tests/sql-agent/scoped-match/queries/select.sql new file mode 100644 index 0000000..24120dd --- /dev/null +++ b/tests/sql-agent/scoped-match/queries/select.sql @@ -0,0 +1,10 @@ +SELECT + id, + name, + email +FROM + users +WHERE + active = 1 +ORDER BY + name; diff --git a/tests/sql-agent/scoped-match/scenario.env b/tests/sql-agent/scoped-match/scenario.env new file mode 100644 index 0000000..76e251f --- /dev/null +++ b/tests/sql-agent/scoped-match/scenario.env @@ -0,0 +1,6 @@ +SCENARIO_NAME="sql-agent scoped-match" +AGENT_SCRIPT="skills/sql-agent/scripts/sql-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=0 +REQUIRED_TOOLS="sqlfluff" +export CHANGED_FILES="queries/select.sql" diff --git a/tests/sql-agent/scoped-no-match/scenario.env b/tests/sql-agent/scoped-no-match/scenario.env new file mode 100644 index 0000000..7895361 --- /dev/null +++ b/tests/sql-agent/scoped-no-match/scenario.env @@ -0,0 +1,6 @@ +SCENARIO_NAME="sql-agent scoped-no-match" +AGENT_SCRIPT="skills/sql-agent/scripts/sql-agent.sh" +RUN_ARGS="all" +EXPECT_EXIT=0 +REQUIRED_TOOLS="sqlfluff" +export CHANGED_FILES="README.md package.json"