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