diff --git a/.github/workflows/unzip-repository.yml b/.github/workflows/unzip-repository.yml index 9d22a42..3f331ab 100644 --- a/.github/workflows/unzip-repository.yml +++ b/.github/workflows/unzip-repository.yml @@ -1,4 +1,4 @@ -name: Unpack repository archive +name: Validate repository archive on: push: @@ -9,21 +9,28 @@ on: workflow_dispatch: permissions: - contents: write + contents: read jobs: - unpack: + archive-validation: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Unpack archive into repository root + - name: Validate archive without mutating the checkout shell: bash run: | set -euo pipefail ZIP='agents-profiles-main.zip' + if [ ! -f "$ZIP" ]; then + echo 'No repository archive is present; archive validation is not applicable.' + exit 0 + fi + + unzip -t "$ZIP" WORK="$(mktemp -d)" + trap 'rm -rf "$WORK"' EXIT unzip -q "$ZIP" -d "$WORK" shopt -s dotglob nullglob @@ -34,25 +41,4 @@ jobs: SOURCE="$WORK" fi - for item in "$SOURCE"/*; do - name="$(basename "$item")" - [ "$name" = '.git' ] && continue - [ "$name" = '.github' ] && continue - cp -a "$item" ./ - done - - rm -f "$ZIP" - - - name: Commit unpacked files - shell: bash - run: | - set -euo pipefail - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add -A - if git diff --cached --quiet; then - echo 'No changes to commit.' - exit 0 - fi - git commit -m 'Unpack repository source archive' - git push + echo "Archive validated for read-only inspection from: $SOURCE" diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..ed66028 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,52 @@ +name: Validate ForgeMesh + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + root-validation: + name: Root corpus validation + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Run portable root validator + run: bash validate.sh + + cli-validation: + name: Nested CLI validation + runs-on: ubuntu-latest + defaults: + run: + working-directory: agents-profiles-cli + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + cache-dependency-path: agents-profiles-cli/package-lock.json + + - name: Install nested CLI dependencies + run: npm ci + + - name: Typecheck nested CLI + run: npm run typecheck + + - name: Build nested CLI + run: npm run build diff --git a/README.md b/README.md index a48168e..da9e851 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,12 @@ ForgeMesh addresses a practical coordination problem: fixed AI-agent rosters do The implemented product surface is a workforce setup and compilation tool. It does not claim to be a live autonomous engineering runtime; the generated profiles and handoff rules are inputs for supported AI coding environments. + + +### Dependency boundary + +The root repository is a canonical profile corpus plus Bash/Python validation and generation scripts; it intentionally has no root npm dependency lockfile. The only npm dependency boundary is [agents-profiles-cli](agents-profiles-cli), whose package-lock.json is authoritative for the TypeScript CLI and is validated with npm ci, typecheck and build in CI. The root validator and CLI validation do not require an API key. + ## Get started ### Validate the canonical corpus diff --git a/validate.sh b/validate.sh index 004adbb..ad1f67c 100755 --- a/validate.sh +++ b/validate.sh @@ -35,16 +35,27 @@ echo "" echo "--- File Counts ---" # Count only agent files in category directories (not root docs) agent_files=$(find "${CATEGORY_DIRS[@]}" -type f -name '*.md' 2>/dev/null | wc -l) +readme_links=$(python3 - <<'PY' +import re +from pathlib import Path + +readme = Path("README.md").read_text(encoding="utf-8") +for link in sorted(set(re.findall(r"\(([^()]+\.md)\)", readme))): + print(link) +PY +) echo "Source agent files: $agent_files" -echo "README links: $(grep -oP '\([^()]+\.md\)' README.md | sed 's/[()]//g' | sort -u | wc -l)" +echo "README links: $(printf '%s\n' "$readme_links" | sed '/^$/d' | wc -l)" echo "" # 2. Broken link check echo "--- Broken Links ---" broken=0 -while read -r f; do - [ ! -f "$f" ] && echo " BROKEN: $f" && broken=$((broken + 1)) -done < <(grep -oP '\([^()]+\.md\)' README.md | sed 's/[()]//g' | sort -u) +if [ -n "$readme_links" ]; then + while read -r f; do + [ ! -f "$f" ] && echo " BROKEN: $f" && broken=$((broken + 1)) + done <<< "$readme_links" +fi [ "$broken" -eq 0 ] && echo " All README links resolve ✓" echo "" @@ -142,7 +153,17 @@ echo "" # 9. Verify README metadata matches actual count echo "--- README Metadata ---" -readme_count=$(grep -oP 'total_agents: \K\d+' README.md) +readme_count=$(python3 - <<'PY' +import re +from pathlib import Path + +readme = Path("README.md").read_text(encoding="utf-8") +match = re.search(r"total_agents:\s*(\d+)", readme) +if not match: + raise SystemExit("README metadata missing total_agents") +print(match.group(1)) +PY +) source_count=$(find "${CATEGORY_DIRS[@]}" -type f -name '*.md' 2>/dev/null | wc -l) echo " README claims: $readme_count agents" echo " Actual source: $source_count agents"