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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 39 additions & 5 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,43 @@ Every run ends with a per-item report line (`[created]`, `[updated]`,
`[skipped]`, or `[failed]`) plus aggregate counts. The script exits non-zero
if any item failed.

## `validate-ai-workspace.sh` (AAASM-3944, not yet built)
## `validate-ai-workspace.sh` (AAASM-3944)

Planned companion script that will check an installed workspace (and each
selected repo inside it) against `.claude/WORKSPACE.md`'s expected layout,
reporting missing files, broken symlinks, and visible repo-local overrides. It
will live alongside `bootstrap-ai-workspace.sh` in this directory once built.
Checks an installed workspace root (and, optionally, one or more repo
checkouts inside it) against `.claude/WORKSPACE.md`'s expected layout,
reporting missing files, broken symlinks, and visible local overrides.

### Usage

```bash
./scripts/validate-ai-workspace.sh ~/ai-agent-assembly
```

Add repo paths to also get a lightweight, informational check for each repo's
own `.claude/CLAUDE.md` and `AGENTS.md`:

```bash
./scripts/validate-ai-workspace.sh ~/ai-agent-assembly ~/ai-agent-assembly/agent-assembly ~/ai-agent-assembly/python-sdk
```

### What it checks

For each of `CLAUDE.md`, `AGENTS.md`, `.claude/rules/`, and `.claude/skills/`
at the workspace root, it reports one of:

- **Missing** — the item does not exist.
- **Broken symlink** — the item is a symlink whose target does not exist.
- **Local override** — the item exists but isn't a symlink back to a
recognized `.github` clone (a contributor's intentional local override, per
the override model in `.claude/WORKSPACE.md`). Reported for visibility, not
as an error.
- **OK** — correctly symlinked back to a `.github` clone.

It also sweeps the workspace's `.claude/` tree for any other broken symlinks
(e.g. under `.claude/commands/`).

### Exit codes

`0` if everything is present and either correctly symlinked or a recognized
local override (overrides do not fail validation). `1` if anything is missing
or a symlink is broken. Safe to wire into CI as a gate.
216 changes: 216 additions & 0 deletions scripts/validate-ai-workspace.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
#!/usr/bin/env bash
#
# validate-ai-workspace.sh — check an installed AI onboarding workspace root
# (and, optionally, one or more org repo checkouts) against the layout
# defined in .claude/WORKSPACE.md (AAASM-3939) and installed by
# bootstrap-ai-workspace.sh (AAASM-3943).
#
# Usage:
# ./scripts/validate-ai-workspace.sh <workspace-root-path> [repo-path...]
#
# Checks the workspace root for CLAUDE.md, AGENTS.md, .claude/rules/, and
# .claude/skills/, classifying each as: missing, a broken symlink, a local
# override (present but not a symlink back to a recognized .github clone),
# or correctly symlinked (OK). Local overrides are reported for visibility
# but do not fail validation — see the override model in .claude/WORKSPACE.md.
#
# Any extra repo-path arguments get a lightweight, informational check for
# their own `.claude/CLAUDE.md` and `AGENTS.md` (repo-level context gaps do
# not affect the exit code — this script validates the workspace scaffold,
# not repo-level completeness).
#
# Exit codes:
# 0 - every expected workspace item is present and either correctly
# symlinked or a recognized local override. Safe to use as a CI gate.
# 1 - at least one expected item is missing, or a symlink is broken.
#
# This is a plain local script: no network calls, only filesystem checks
# and local (non-network) `git remote -v` reads.

set -euo pipefail

usage() {
echo "Usage: $(basename "${BASH_SOURCE[0]}") <workspace-root-path> [repo-path...]" >&2
}

if [[ $# -lt 1 ]]; then
echo "Error: <workspace-root-path> is required." >&2
usage
exit 1
fi

case "$1" in
-h|--help)
usage
exit 0
;;
esac

WORKSPACE_ROOT="$1"
shift
declare -a REPO_PATHS=("$@")

# --- Report buckets ----------------------------------------------------------
# bash 3.2 (macOS default) throws "unbound variable" under `set -u` when
# expanding an array that has zero elements, so every expansion of these is
# guarded with an `${#arr[@]} -gt 0` length check before use.
declare -a MISSING=()
declare -a BROKEN=()
declare -a OVERRIDES=()
declare -a OK=()
declare -a REPO_NOTES=()

# --- Helpers -------------------------------------------------------------

# is_dot_github_clone <dir>
# True if <dir> is inside a git checkout whose configured remotes reference
# the ai-agent-assembly/.github repo. `git remote -v` only reads local repo
# config; it makes no network calls.
is_dot_github_clone() {
local dir="$1"
local git_root
git_root="$(cd "${dir}" 2>/dev/null && git rev-parse --show-toplevel 2>/dev/null)" || return 1
[[ -n "${git_root}" ]] || return 1
git -C "${git_root}" remote -v 2>/dev/null | grep -qiE 'ai-agent-assembly/\.github(\.git)?'
}

# check_item <target-path> <label>
# Classifies one expected workspace item into MISSING / BROKEN / OVERRIDES / OK.
check_item() {
local target="$1"
local label="$2"

if [[ -L "${target}" ]]; then
if [[ ! -e "${target}" ]]; then
BROKEN+=("${label}: ${target} -> $(readlink "${target}") (symlink target does not exist)")
return
fi

local resolved check_dir
resolved="$(realpath "${target}")"
if [[ -d "${resolved}" ]]; then
check_dir="${resolved}"
else
check_dir="$(dirname "${resolved}")"
fi

if is_dot_github_clone "${check_dir}"; then
OK+=("${label}: ${target} -> ${resolved}")
else
OVERRIDES+=("${label}: ${target} is a symlink to ${resolved}, which is not inside a recognized .github clone (local override — intentional?)")
fi
return
fi

if [[ -e "${target}" ]]; then
OVERRIDES+=("${label}: ${target} is a real file/dir, not a symlink back to a .github clone (local override — intentional?)")
return
fi

MISSING+=("${label}: ${target} does not exist")
}

echo "Validating AI onboarding workspace: ${WORKSPACE_ROOT}"
echo

check_item "${WORKSPACE_ROOT}/CLAUDE.md" "CLAUDE.md"
check_item "${WORKSPACE_ROOT}/AGENTS.md" "AGENTS.md"
check_item "${WORKSPACE_ROOT}/.claude/rules" ".claude/rules"
check_item "${WORKSPACE_ROOT}/.claude/skills" ".claude/skills"

# --- Generic broken-symlink sweep under the workspace's .claude/ tree -------
# Catches broken symlinks beyond the four expected items above (e.g. a
# contributor's own additions under .claude/commands/). `find` does not
# descend into symlinked directories by default, so this reports symlink
# dirents directly under .claude/ without re-traversing .claude/rules or
# .claude/skills (already checked above).
if [[ -d "${WORKSPACE_ROOT}/.claude" ]]; then
while IFS= read -r link; do
[[ -z "${link}" ]] && continue
if [[ "${link}" == "${WORKSPACE_ROOT}/.claude/rules" || "${link}" == "${WORKSPACE_ROOT}/.claude/skills" ]]; then
continue
fi
if [[ ! -e "${link}" ]]; then
BROKEN+=("extra symlink: ${link} -> $(readlink "${link}") (target does not exist)")
fi
done < <(find "${WORKSPACE_ROOT}/.claude" -type l 2>/dev/null)
fi

# --- Optional lightweight repo-level checks ---------------------------------
if [[ ${#REPO_PATHS[@]} -gt 0 ]]; then
for repo in "${REPO_PATHS[@]}"; do
if [[ ! -d "${repo}" ]]; then
REPO_NOTES+=("${repo}: not a directory, skipped")
continue
fi
if [[ -f "${repo}/.claude/CLAUDE.md" ]]; then
REPO_NOTES+=("${repo}: has .claude/CLAUDE.md")
else
REPO_NOTES+=("${repo}: missing .claude/CLAUDE.md (repo-level context gap)")
fi
if [[ -f "${repo}/AGENTS.md" ]]; then
REPO_NOTES+=("${repo}: has AGENTS.md")
else
REPO_NOTES+=("${repo}: missing AGENTS.md (repo-level context gap)")
fi
done
fi

# --- Report ------------------------------------------------------------------
echo "Missing (${#MISSING[@]}):"
if [[ ${#MISSING[@]} -gt 0 ]]; then
for item in "${MISSING[@]}"; do
echo " - ${item}"
done
else
echo " (none)"
fi
echo

echo "Broken symlink (${#BROKEN[@]}):"
if [[ ${#BROKEN[@]} -gt 0 ]]; then
for item in "${BROKEN[@]}"; do
echo " - ${item}"
done
else
echo " (none)"
fi
echo

echo "Local override (${#OVERRIDES[@]}):"
if [[ ${#OVERRIDES[@]} -gt 0 ]]; then
for item in "${OVERRIDES[@]}"; do
echo " - ${item}"
done
else
echo " (none)"
fi
echo

echo "OK (${#OK[@]}):"
if [[ ${#OK[@]} -gt 0 ]]; then
for item in "${OK[@]}"; do
echo " - ${item}"
done
else
echo " (none)"
fi
echo

if [[ ${#REPO_PATHS[@]} -gt 0 ]]; then
echo "Repo-level context (informational, does not affect exit code):"
for item in "${REPO_NOTES[@]}"; do
echo " - ${item}"
done
echo
fi

echo "Summary: missing=${#MISSING[@]} broken=${#BROKEN[@]} override=${#OVERRIDES[@]} ok=${#OK[@]}"

if [[ ${#MISSING[@]} -gt 0 || ${#BROKEN[@]} -gt 0 ]]; then
echo "Result: FAIL — workspace scaffold has missing or broken items."
exit 1
fi

echo "Result: PASS — workspace scaffold is present and correctly linked (local overrides, if any, are allowed)."
exit 0