diff --git a/scripts/SYNCS.md b/scripts/SYNCS.md index 8f0438f..dcab35d 100644 --- a/scripts/SYNCS.md +++ b/scripts/SYNCS.md @@ -26,7 +26,7 @@ flowchart LR CRATES["crates.io
(agentnative crate)"] end - SPEC -->|sync-spec.sh| CLI + SPEC -->|"sync-spec.sh (gh api, latest v* or --ref)"| CLI SPEC -->|sync-prose-tooling.sh| CLI SITE_IN -->|sync-skill-fixture.sh| CLI MAIN -->|sync-dev-after-release.sh
main → dev| CLI @@ -46,22 +46,22 @@ flowchart LR ## Upstream — data flowing INTO this repo -| Source | Mechanism | What's synced | Trigger / cadence | Drift check | -| ------------------------------------------------------------ | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `brettdavies/agentnative` (spec) @ latest `v*` tag | `scripts/sync-spec.sh` (manual; remote-first, falls back to `$SPEC_ROOT`) | `principles/p*-*.md` + top-level `VERSION` + `CHANGELOG.md` → `src/principles/spec/` | Rerun after every new `agentnative-spec` `v*` tag. The intended trigger is a `repository_dispatch` from the spec's publish workflow; until that exists, manual. | `build.rs` is *intentionally loud* — fails on missing `VERSION`, missing `principles/` dir, parse errors, duplicate IDs, or missing fields. `cargo test` (`integration::*` + `dangling_cover_ids`) catches `covers()` IDs that drift from the vendored registry. | -| `brettdavies/agentnative` (spec) @ `main` HEAD | `scripts/sync-prose-tooling.sh` (manual; `--check` mode for drift) | `BRAND.md` + `styles/brand/` (rule pack) + `styles/config/vocabularies/brand/` (vocab) + `scripts/test-prose-check.mjs` + `scripts/generate-pack-readme.mjs`. Per-consumer config (`.vale.ini`, `styles/config/vocabularies/cli/`) authored locally; not vendored. `scripts/prose-check.sh` is consumer-owned (un-vendored 2026-05-13); see the CONSUMER-OWNED header inside the script. | Rerun after any spec `main` push touching any path in the manifest. Faster cadence than spec tags by design — this is shared tooling, not contract; tag-pinning is for the principle contract via `sync-spec.sh`. Idempotent at a fixed spec SHA. | `--check` mode compares each vendored file byte-for-byte against upstream `main` HEAD. `scripts/prose-check.sh` is consumer-owned and not part of the manifest; universal pipeline changes need coordinated PRs across spec + site + cli + skill until the spec-side sidecar-config migration lands. | -| `brettdavies/agentnative-site` `src/data/skill.json` @ `dev` | `scripts/sync-skill-fixture.sh` (manual; `--check` in CI) | Skill bundle manifest (install map / hosts) → `src/skill_install/skill.json` | Rerun whenever the site changes `src/data/skill.json`. Pre-release checklist in `RELEASES.md` step 7 captures this for every release. | `.github/workflows/skill-fixture-drift.yml` runs `sync-skill-fixture.sh --check` on every PR + push to main/dev. Companion cargo test `host_map_matches_site_skill_json` catches drift between the Rust-codegen map and this fixture. | -| this repo's own `main` branch (release artifacts) | `scripts/sync-dev-after-release.sh vX.Y.Z` (manual; idempotent) | `Cargo.toml` `[package].version` (surgical, single-line awk) + regenerated `Cargo.lock` (`cargo build --release`) + `CHANGELOG.md` (verbatim from `origin/main`) → `dev` | Run AFTER (1) `release/v*` → `main` PR merges, (2) `git tag vX.Y.Z` pushed, (3) `finalize-release.yml` flips the GitHub Release to `published`. | n/a — single signed commit, surgical edits, idempotent re-run is a no-op. Pre-flight checks: working tree clean, tag exists locally, tag is reachable from `origin/main`. | +| Source | Mechanism | What's synced | Trigger / cadence | Drift check | +| -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `brettdavies/agentnative` (spec); default latest `v*` tag, or any branch/tag/SHA via `--ref` | `scripts/sync-spec.sh` (manual; `gh api`-based, falls back to `$SPEC_ROOT` when offline; `--ref ` or `SPEC_REF` env var vendors any explicit ref for cross-repo coordination of in-flight spec work) | `principles/p*-*.md` + top-level `VERSION` + `CHANGELOG.md` → `src/principles/spec/` | Rerun after every new `agentnative-spec` `v*` tag. The intended trigger is a `repository_dispatch` from the spec's publish workflow; until that exists, manual. Cross-repo coordination: rerun with `--ref dev` (or a SHA) to consume in-flight spec work before a release cuts; the script prints the resolved short SHA for the consumer PR body. | `build.rs` is *intentionally loud* — fails on missing `VERSION`, missing `principles/` dir, parse errors, duplicate IDs, or missing fields. `cargo test` (`integration::*` + `dangling_cover_ids`) catches `covers()` IDs that drift from the vendored registry. | +| `brettdavies/agentnative` (spec) @ `main` HEAD | `scripts/sync-prose-tooling.sh` (manual; `--check` mode for drift) | `BRAND.md` + `styles/brand/` (rule pack) + `styles/config/vocabularies/brand/` (vocab) + `scripts/test-prose-check.mjs` + `scripts/generate-pack-readme.mjs`. Per-consumer config (`.vale.ini`, `styles/config/vocabularies/cli/`) authored locally; not vendored. `scripts/prose-check.sh` is consumer-owned (un-vendored 2026-05-13); see the CONSUMER-OWNED header inside the script. | Rerun after any spec `main` push touching any path in the manifest. Faster cadence than spec tags by design — this is shared tooling, not contract; tag-pinning is for the principle contract via `sync-spec.sh`. Idempotent at a fixed spec SHA. | `--check` mode compares each vendored file byte-for-byte against upstream `main` HEAD. `scripts/prose-check.sh` is consumer-owned and not part of the manifest; universal pipeline changes need coordinated PRs across spec + site + cli + skill until the spec-side sidecar-config migration lands. | +| `brettdavies/agentnative-site` `src/data/skill.json` @ `dev` | `scripts/sync-skill-fixture.sh` (manual; `--check` in CI) | Skill bundle manifest (install map / hosts) → `src/skill_install/skill.json` | Rerun whenever the site changes `src/data/skill.json`. Pre-release checklist in `RELEASES.md` step 7 captures this for every release. | `.github/workflows/skill-fixture-drift.yml` runs `sync-skill-fixture.sh --check` on every PR + push to main/dev. Companion cargo test `host_map_matches_site_skill_json` catches drift between the Rust-codegen map and this fixture. | +| this repo's own `main` branch (release artifacts) | `scripts/sync-dev-after-release.sh vX.Y.Z` (manual; idempotent) | `Cargo.toml` `[package].version` (surgical, single-line awk) + regenerated `Cargo.lock` (`cargo build --release`) + `CHANGELOG.md` (verbatim from `origin/main`) → `dev` | Run AFTER (1) `release/v*` → `main` PR merges, (2) `git tag vX.Y.Z` pushed, (3) `finalize-release.yml` flips the GitHub Release to `published`. | n/a — single signed commit, surgical edits, idempotent re-run is a no-op. Pre-flight checks: working tree clean, tag exists locally, tag is reachable from `origin/main`. | ## Downstream — data flowing OUT of this repo -| Consumer | Mechanism | What's synced | Trigger / cadence | Drift check | -| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `brettdavies/agentnative-site` (`/coverage` page) | site's `scripts/sync-coverage-matrix.sh` `cp`s from `$ANC_ROOT/coverage/matrix.json` (default `$HOME/dev/agentnative-cli`) → `src/data/coverage-matrix.json` | `coverage/matrix.json` (`schema_version: "1.0"`), generated here by `anc emit coverage-matrix`, committed as a tracked artifact (not gitignored) | Run on the site after this repo bumps the matrix (new check, registry change, or `Check::covers()` change). | This repo's CI (via `cargo test`) runs `test_generate_coverage_matrix_drift_check_passes_on_committed_artifacts`, which invokes `anc emit coverage-matrix --check` and exits non-zero when `docs/coverage-matrix.md` or `coverage/matrix.json` disagree with the registry. Site has no automated drift check — the cli-side gate is authoritative. | -| `brettdavies/agentnative-site` (per-tool scorecards) | site's `scripts/regen-scorecards.sh` runs `anc audit --command [--audit-profile ] --output json` against each registry entry; writes `scorecards/-v.json` in the site repo | Per-tool scorecard JSONs (`schema_version: "0.5"`); the `anc` binary embeds `spec_version` at compile time (sourced from the vendored `src/principles/spec/VERSION`) | Run on the site after `anc` is upgraded on the box (`brew upgrade brettdavies/tap/agentnative`); also run on registry changes. Script enforces `MIN_ANC_VERSION` (currently `0.1.3`) unless `--allow-dev-build` is passed. | Site validates schema 0.5 invariants at build time (`bun test` + `bun run build`). Filename owns the canonical version anchor — the actually-installed `anc --version` determines the output filename, so a filename can never lie about which release was scored. | -| `brettdavies/homebrew-tap` (formula bump) | `.github/workflows/release.yml` → reusable `brettdavies/.github/.github/workflows/rust-release.yml@main` → `homebrew` job fires `repository_dispatch` (`event_type=update-formula`, payload: formula=`agentnative`, version=`X.Y.Z`, repo) | Triggers homebrew-tap to bump the `agentnative` formula and build bottles | On every `git tag v*.*.*` push to this repo. Authenticated via `CI_RELEASE_TOKEN` (fine-grained PAT with Contents R+W). | n/a at this boundary. Bottle-build success is observable via the homebrew-tap workflow run; bottle-upload back to this repo's Release assets is what triggers the inverse `finalize-release` dispatch (next row). | -| `brettdavies/agentnative-cli` (this repo's own `finalize-release.yml`) | Inverse `repository_dispatch` from homebrew-tap's publish workflow — `event_type=finalize-release` | Bottle SHAs uploaded to this Release's assets; `make_latest` flips from `false` → `true` on the GitHub Release | Fired by homebrew-tap after bottles upload. Idempotent — re-dispatch is safe. | n/a — the flip is observable on the Release page. | -| `crates.io` (`agentnative` crate) | Same `release.yml` → `publish-crate` job, `cargo publish` via OIDC Trusted Publishing (no static token after first publish) | The compiled crate at the tag's version | On every `git tag v*.*.*` push. First publish requires `CARGO_REGISTRY_TOKEN` one-time; subsequent publishes are token-less. | `check-version` job gates the pipeline: tag must match `Cargo.toml` `[package].version` exactly, else release aborts before any publish. | +| Consumer | Mechanism | What's synced | Trigger / cadence | Drift check | +| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `brettdavies/agentnative-site` (`/coverage` page) | site's `scripts/sync-coverage-matrix.sh` `cp`s from `$ANC_ROOT/coverage/matrix.json` (default `$HOME/dev/agentnative-cli`) → `src/data/coverage-matrix.json` | `coverage/matrix.json` (`schema_version: "1.0"`), generated here by `anc emit coverage-matrix`, committed as a tracked artifact (not gitignored) | Run on the site after this repo bumps the matrix (new check, registry change, or `Check::covers()` change). | This repo's CI (via `cargo test`) runs `test_generate_coverage_matrix_drift_check_passes_on_committed_artifacts`, which invokes `anc emit coverage-matrix --check` and exits non-zero when `docs/coverage-matrix.md` or `coverage/matrix.json` disagree with the registry. Site has no automated drift check — the cli-side gate is authoritative. | +| `brettdavies/agentnative-site` (per-tool scorecards) | site's `scripts/regen-scorecards.sh` runs `anc audit --command [--audit-profile ] --output json` against each registry entry; writes `scorecards/-v.json` in the site repo | Per-tool scorecard JSONs (`schema_version: "0.5"`); the `anc` binary embeds `spec_version` at compile time (sourced from the vendored `src/principles/spec/VERSION`) | Run on the site after `anc` is upgraded on the box (`brew upgrade brettdavies/tap/agentnative`); also run on registry changes. Script enforces `MIN_ANC_VERSION` (currently `0.1.3`) unless `--allow-dev-build` is passed. | Site validates schema 0.5 invariants at build time (`bun test` + `bun run build`). Filename owns the canonical version anchor — the actually-installed `anc --version` determines the output filename, so a filename can never lie about which release was scored. | +| `brettdavies/homebrew-tap` (formula bump) | `.github/workflows/release.yml` → reusable `brettdavies/.github/.github/workflows/rust-release.yml@main` → `homebrew` job fires `repository_dispatch` (`event_type=update-formula`, payload: formula=`agentnative`, version=`X.Y.Z`, repo) | Triggers homebrew-tap to bump the `agentnative` formula and build bottles | On every `git tag v*.*.*` push to this repo. Authenticated via `CI_RELEASE_TOKEN` (fine-grained PAT with Contents R+W). | n/a at this boundary. Bottle-build success is observable via the homebrew-tap workflow run; bottle-upload back to this repo's Release assets is what triggers the inverse `finalize-release` dispatch (next row). | +| `brettdavies/agentnative-cli` (this repo's own `finalize-release.yml`) | Inverse `repository_dispatch` from homebrew-tap's publish workflow — `event_type=finalize-release` | Bottle SHAs uploaded to this Release's assets; `make_latest` flips from `false` → `true` on the GitHub Release | Fired by homebrew-tap after bottles upload. Idempotent — re-dispatch is safe. | n/a — the flip is observable on the Release page. | +| `crates.io` (`agentnative` crate) | Same `release.yml` → `publish-crate` job, `cargo publish` via OIDC Trusted Publishing (no static token after first publish) | The compiled crate at the tag's version | On every `git tag v*.*.*` push. First publish requires `CARGO_REGISTRY_TOKEN` one-time; subsequent publishes are token-less. | `check-version` job gates the pipeline: tag must match `Cargo.toml` `[package].version` exactly, else release aborts before any publish. | ## Release / sync orchestration @@ -136,7 +136,9 @@ sequenceDiagram ## Reference -- [`scripts/sync-spec.sh`](sync-spec.sh) — header comment has detailed usage, env vars, and resync cadence. +- [`scripts/sync-spec.sh`](sync-spec.sh) — header comment has detailed usage, the `--ref ` / `SPEC_REF` + cross-repo coordination surface, `SPEC_REMOTE_URL` / `SPEC_ROOT` env vars, the `gh api` transport, and the + local-checkout fallback for offline runs. - [`scripts/sync-prose-tooling.sh`](sync-prose-tooling.sh) — header comment covers `--check` drift mode and the manifest. `scripts/prose-check.sh` is consumer-owned (un-vendored 2026-05-13); its CONSUMER-OWNED header explains why and what coordination universal pipeline changes now require. diff --git a/scripts/sync-spec.sh b/scripts/sync-spec.sh index 7109c0b..d1887a5 100755 --- a/scripts/sync-spec.sh +++ b/scripts/sync-spec.sh @@ -1,118 +1,241 @@ #!/usr/bin/env bash # Vendor agentnative-spec into src/principles/spec/. # -# Resolves the latest v* tag of agentnative-spec, preferring the remote -# repository, and falls back to a local checkout if the remote is -# unreachable. Extracts files via `git show :` so neither -# checkout's working tree is perturbed. The vendored tree is the -# build-time input for build.rs, which generates the REQUIREMENTS slice. +# Default behavior: resolves the latest v* tag of agentnative-spec via the +# GitHub API and pulls VERSION, CHANGELOG.md, and principles/p*-*.md at +# that tag. The vendored tree is the build-time input for build.rs, which +# generates the REQUIREMENTS slice consumed by `anc audit`. +# +# Override behavior (--ref / SPEC_REF): vendors an explicit branch HEAD, +# tag, or commit SHA instead of the latest v* tag. Use for cross-repo +# coordination of in-flight spec work that hasn't released yet (e.g., a +# CLI/site change that depends on a spec PR landed on `dev` but not yet +# tagged). The resolved short SHA is always printed alongside the ref so +# the user knows exactly what landed; record that SHA in any consumer PR +# body so the vendoring is traceable post-merge. +# +# Transport: `gh api` against the GitHub REST contents endpoint. Pulls +# files individually (no clone, no tarball) so branches, tags, and SHAs +# take the same code path — `?ref=` accepts all three. Requires `gh` +# authenticated against github.com. When the API path fails (network +# down, gh unauthenticated, repo unreachable), the script falls back to +# a local checkout for offline development. # # Usage: -# scripts/sync-spec.sh +# scripts/sync-spec.sh # latest v* tag (default) +# scripts/sync-spec.sh --ref dev # HEAD of dev branch +# scripts/sync-spec.sh --ref v0.4.0 # explicit tag +# scripts/sync-spec.sh --ref b4f4d02 # specific commit SHA +# SPEC_REF=dev scripts/sync-spec.sh # env-var form of --ref # SPEC_ROOT=/path/to/agentnative-spec scripts/sync-spec.sh # SPEC_REMOTE_URL=git@github.com:brettdavies/agentnative.git scripts/sync-spec.sh # +# Flags: +# --ref Branch name, tag, or commit SHA to vendor. Wins over +# SPEC_REF env var. When unset, the script resolves the +# latest v* tag. +# # Env vars: -# SPEC_REMOTE_URL Remote URL to query first. +# SPEC_REF Same as --ref but via env. CLI flag wins on conflict. +# SPEC_REMOTE_URL Remote URL identifying the repo. The script parses +# `/` out of it for the `gh api` calls and +# out of it for the local-fallback's remote-name lookup. # Default: https://github.com/brettdavies/agentnative.git -# SPEC_ROOT Local checkout to fall back to when the remote is +# SPEC_ROOT Local checkout to fall back to when the API is # unreachable. Default: $HOME/dev/agentnative-spec # -# Resync cadence: rerun after every new agentnative-spec tag. The remote -# query picks up new tags automatically; a local fallback only sees what -# the local checkout already has fetched. +# Resync cadence: rerun after every new agentnative-spec tag. The default +# API query picks up new tags automatically. Spec's +# `repository_dispatch:spec-release` event already fires to this repo on +# tag publish — a consumer-side handler that auto-PRs the resync is +# tracked as follow-up work. # -# Stale orphan files in src/principles/spec/principles/ (e.g., from a -# spec rename) are accepted; `git status` surfaces them at commit time. +# Stale orphan files in src/principles/spec/principles/ (e.g., from a spec +# rename) are accepted; `git status` surfaces them at commit time. set -euo pipefail SPEC_REMOTE_URL="${SPEC_REMOTE_URL:-https://github.com/brettdavies/agentnative.git}" SPEC_ROOT="${SPEC_ROOT:-$HOME/dev/agentnative-spec}" +SPEC_REF="${SPEC_REF:-}" + +# --- Argument parsing --------------------------------------------------- +# CLI --ref wins over SPEC_REF env. Other flags reserved for future use. +while [[ $# -gt 0 ]]; do + case "$1" in + --ref) + if [[ $# -lt 2 || -z "$2" ]]; then + echo "error: --ref requires a value (branch, tag, or SHA)" >&2 + exit 2 + fi + SPEC_REF="$2" + shift 2 + ;; + --ref=*) + SPEC_REF="${1#--ref=}" + if [[ -z "$SPEC_REF" ]]; then + echo "error: --ref= requires a value (branch, tag, or SHA)" >&2 + exit 2 + fi + shift + ;; + -h|--help) + sed -n '2,55p' "$0" | sed 's/^# \{0,1\}//' + exit 0 + ;; + *) + echo "error: unknown argument: $1" >&2 + echo " run \`$0 --help\` for usage" >&2 + exit 2 + ;; + esac +done REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" DEST_DIR="$REPO_ROOT/src/principles/spec" DEST_PRINCIPLES="$DEST_DIR/principles" -# Cleanup hook for the temp clone (set only after mktemp succeeds). -tmp_root="" -cleanup() { - if [[ -n "$tmp_root" && -d "$tmp_root" ]]; then - rm -rf "$tmp_root" +# Parse `/` out of SPEC_REMOTE_URL for `gh api` calls. +# Handles both URL shapes: +# https://github.com//.git +# git@github.com:/.git +spec_repo="${SPEC_REMOTE_URL%.git}" +spec_repo="${spec_repo#*github.com[/:]}" +spec_repo="${spec_repo%/}" +if [[ -z "$spec_repo" || "$spec_repo" == "$SPEC_REMOTE_URL" || "$spec_repo" != */* ]]; then + echo "error: could not parse owner/repo from SPEC_REMOTE_URL: $SPEC_REMOTE_URL" >&2 + exit 1 +fi + +# === Resolution ========================================================= +# spec_ref: the ref the user requested (or "" if auto-resolving latest tag) +# resolved_ref: what we actually vendor (e.g., "v0.4.0" or a SHA) +# resolved_sha: 7-char SHA for display (always set after resolution) +# source_label: human-readable origin string for the "vendoring" line +spec_ref="" +resolved_ref="" +resolved_sha="" +source_label="" + +# Try the API path first. Captures both branches: explicit user ref OR +# auto-resolve latest v* tag. +api_ok=false + +if [[ -n "$SPEC_REF" ]]; then + # User-specified ref. gh api accepts branches, tags, and SHAs at the + # same endpoint via `?ref=`. + if full_sha="$(gh api "repos/$spec_repo/commits/$SPEC_REF" --jq '.sha' 2>/dev/null)"; then + resolved_ref="$SPEC_REF" + resolved_sha="${full_sha:0:7}" + source_label="github.com:$spec_repo via gh api" + api_ok=true fi -} -trap cleanup EXIT - -# === Remote-first resolution =========================================== -spec_source="" -spec_tag="" - -echo "querying $SPEC_REMOTE_URL for latest v* tag..." -remote_tag="$(git ls-remote --tags --sort='-version:refname' \ - "$SPEC_REMOTE_URL" 'refs/tags/v*' 2>/dev/null \ - | awk '{print $2}' \ - | sed 's|refs/tags/||' \ - | grep -v '\^{}$' \ - | head -n 1 || true)" - -if [[ -n "$remote_tag" ]]; then - tmp_root="$(mktemp -d -t agentnative-spec-XXXXXX)" - if git clone --depth 1 --branch "$remote_tag" --quiet \ - "$SPEC_REMOTE_URL" "$tmp_root" 2>/dev/null; then - spec_source="$tmp_root" - spec_tag="$remote_tag" - resolved_sha="$(git -C "$spec_source" rev-parse --short=7 "$spec_tag^{commit}")" - echo "vendoring $spec_tag ($resolved_sha) from remote $SPEC_REMOTE_URL" +else + # Default: latest v* tag. Query all tags, filter to semver-shape v*, + # sort -V (version sort, descending), take the first. + latest_tag="$(gh api "repos/$spec_repo/tags?per_page=100" --jq '.[].name' 2>/dev/null \ + | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+' \ + | sort -V -r \ + | head -n 1 || true)" + if [[ -n "$latest_tag" ]]; then + if full_sha="$(gh api "repos/$spec_repo/commits/$latest_tag" --jq '.sha' 2>/dev/null)"; then + resolved_ref="$latest_tag" + resolved_sha="${full_sha:0:7}" + source_label="github.com:$spec_repo via gh api" + api_ok=true + fi fi fi -# === Local fallback ==================================================== -if [[ -z "$spec_source" ]]; then +# === Local fallback ===================================================== +# When the API path fails (offline, gh not authenticated, repo not +# reachable), use a local checkout instead. The fallback uses `git` +# against SPEC_ROOT — `gh` is not involved in this path so it works +# without network or auth. +if ! $api_ok; then if [[ ! -d "$SPEC_ROOT/.git" ]]; then - echo "error: remote unreachable and SPEC_ROOT is not a git repository: $SPEC_ROOT" >&2 + echo "error: API path failed and SPEC_ROOT is not a git repository: $SPEC_ROOT" >&2 echo " remote: $SPEC_REMOTE_URL" >&2 - echo " set SPEC_ROOT to your agentnative-spec checkout, or check network access." >&2 + if [[ -n "$SPEC_REF" ]]; then + echo " requested ref: $SPEC_REF" >&2 + fi + echo " check \`gh auth status\`, network access, or point SPEC_ROOT at a local checkout." >&2 exit 1 fi - echo "warning: remote query failed; falling back to local $SPEC_ROOT" >&2 + echo "warning: gh api unreachable; falling back to local $SPEC_ROOT" >&2 - spec_source="$SPEC_ROOT" - spec_tag="$(git -C "$spec_source" tag --list 'v*' --sort='-version:refname' | head -n 1)" - if [[ -z "$spec_tag" ]]; then - echo "error: no v* tags found in $SPEC_ROOT" >&2 - echo " try \`git -C $SPEC_ROOT fetch --tags\` to pick up upstream tags" >&2 - exit 1 + if [[ -n "$SPEC_REF" ]]; then + # User-provided ref must be resolvable in the local checkout. + if ! git -C "$SPEC_ROOT" rev-parse --verify --quiet "$SPEC_REF^{commit}" >/dev/null; then + echo "error: ref \`$SPEC_REF\` not found in $SPEC_ROOT" >&2 + echo " try \`git -C $SPEC_ROOT fetch --all --tags\` to pick up upstream refs," >&2 + echo " or pass a SHA the local checkout already contains." >&2 + exit 1 + fi + resolved_ref="$SPEC_REF" + else + # Latest v* tag in the local checkout. + resolved_ref="$(git -C "$SPEC_ROOT" tag --list 'v*' --sort='-version:refname' | head -n 1)" + if [[ -z "$resolved_ref" ]]; then + echo "error: no v* tags found in $SPEC_ROOT" >&2 + echo " try \`git -C $SPEC_ROOT fetch --tags\` to pick up upstream tags" >&2 + exit 1 + fi fi - resolved_sha="$(git -C "$spec_source" rev-parse --short=7 "$spec_tag^{commit}")" - echo "vendoring $spec_tag ($resolved_sha) from local $spec_source" + resolved_sha="$(git -C "$SPEC_ROOT" rev-parse --short=7 "$resolved_ref^{commit}")" + source_label="local $SPEC_ROOT" fi -# === Verify + extract (works identically for remote and local sources) = -if ! git -C "$spec_source" cat-file -e "$spec_tag:principles" 2>/dev/null; then - echo "error: $spec_tag has no principles/ directory in $spec_source" >&2 - exit 1 -fi +echo "vendoring $resolved_ref ($resolved_sha) from $source_label" + +# === Extract ============================================================= +# Fetcher: API path uses `gh api` with raw accept header; local path uses +# `git show`. Both write a single file to $2 from path $1. +fetch_file() { + local path="$1" + local dest="$2" + if $api_ok; then + gh api -H "Accept: application/vnd.github.raw" \ + "repos/$spec_repo/contents/$path?ref=$resolved_ref" >"$dest" + else + git -C "$SPEC_ROOT" show "$resolved_ref:$path" >"$dest" + fi +} + +# Lister: returns names (not paths) of files in a directory at the ref. +# API path uses the contents endpoint; local path uses ls-tree. +list_dir() { + local dir="$1" + if $api_ok; then + gh api "repos/$spec_repo/contents/$dir?ref=$resolved_ref" --jq '.[].name' + else + git -C "$SPEC_ROOT" ls-tree --name-only "$resolved_ref" "$dir/" \ + | sed "s|^$dir/||" + fi +} mkdir -p "$DEST_PRINCIPLES" # VERSION and CHANGELOG.md are top-level in the spec repo. -git -C "$spec_source" show "$spec_tag:VERSION" >"$DEST_DIR/VERSION" -git -C "$spec_source" show "$spec_tag:CHANGELOG.md" >"$DEST_DIR/CHANGELOG.md" +fetch_file "VERSION" "$DEST_DIR/VERSION" +fetch_file "CHANGELOG.md" "$DEST_DIR/CHANGELOG.md" -# Enumerate principle files at the tag and extract each one. +# Enumerate principle files at the ref and extract each one. Filter to +# p*-*.md so principles/AGENTS.md (spec-side design context, not consumed +# by the site) is skipped. copied=0 -while IFS= read -r path; do - case "$path" in - principles/p*-*.md) - dest_name="${path#principles/}" - git -C "$spec_source" show "$spec_tag:$path" >"$DEST_PRINCIPLES/$dest_name" +while IFS= read -r name; do + [[ -z "$name" ]] && continue + case "$name" in + p[0-9]-*.md|p[0-9][0-9]-*.md) + fetch_file "principles/$name" "$DEST_PRINCIPLES/$name" copied=$((copied + 1)) ;; esac -done < <(git -C "$spec_source" ls-tree --name-only "$spec_tag" principles/) +done < <(list_dir "principles") if [[ "$copied" -eq 0 ]]; then - echo "error: no principles/p*-*.md files found at $spec_tag" >&2 + echo "error: no principles/p*-*.md files found at ref \`$resolved_ref\`" >&2 exit 1 fi