| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103 |
- #!/usr/bin/env bash
- #
- # The drift gate: regenerate the committed trees from content/agents/**, then fail if the
- # build changed anything. This is the structural guarantee the canonical refactor rests on —
- # generated trees stay COMMITTED, so a hand-edit to one must turn CI red rather than merely
- # being discouraged.
- #
- # ── Why a write build and not `oac build --check` ────────────────────────────────────────
- #
- # `check()` (BuildPipeline.ts) compares the PLANNED files against disk and reports orphans
- # from the manifest. It never compares `.oac/build-manifest.json` itself — the manifest is
- # only ever written by `write()`. Since the manifest is committed (it is the ledger orphan
- # pruning reasons from), a `--check` gate would let the manifest drift silently, and a fresh
- # clone with a stale ledger prunes the wrong set. So: real build, then ask git.
- #
- # ── Why before/after and not a bare `git status` ─────────────────────────────────────────
- #
- # In CI the checkout is clean, so "clean after the build" and "unchanged by the build" are
- # the same statement. Locally they are not: `.opencode/agent/eval-runner.md` is a real,
- # shipped, hand-authored agent with no canonical source, and any contributor may have
- # unrelated work in progress under these roots. Comparing the status BEFORE the build to the
- # status AFTER attributes drift to the build rather than to the worktree — pre-existing dirt
- # appears in both snapshots and cancels. The gate stays honest and stays runnable locally,
- # which is the only way contributors ever see it fail before CI does.
- #
- set -euo pipefail
- # `oac build` resolves content/ against the CWD — from anywhere else it exits 1 with ENOENT.
- ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
- cd "$ROOT"
- # The trees `oac build` emits IN PLACE, and therefore everything this gate watches.
- # plugins/claude-code/** is deliberately absent: it stages to .tmp/oac-build/ (gitignored)
- # and is compared only, never emitted. Permissions no longer block emitting it in place --
- # oac.overrides.claude-code settled those -- but prompt content does: the shipped files carry
- # dispatch <example> blocks and capability-adapted bodies that canonical has no field for.
- # See the docblock in packages/cli/src/commands/build.ts.
- GENERATED=(.opencode/agent registry.json .oac/build-manifest.json)
- if [ ! -f packages/cli/dist/index.js ]; then
- echo "error: packages/cli/dist/index.js is missing — build the CLI first:" >&2
- echo " pnpm --dir packages/compatibility-layer run build" >&2
- echo " pnpm --dir packages/cli run build" >&2
- exit 1
- fi
- snapshot() { git status --porcelain -- "${GENERATED[@]}"; }
- before="$(snapshot)"
- bun packages/cli/dist/index.js build
- after="$(snapshot)"
- if [ "$before" = "$after" ]; then
- echo ""
- echo "✅ No drift — the generated trees match content/agents/**."
- exit 0
- fi
- cat >&2 <<'EOF'
- ❌ The generated trees drift from the canonical source.
- Files under .opencode/agent/, registry.json and .oac/build-manifest.json are BUILD OUTPUT.
- They are committed, but they are not hand-edited: `oac build` regenerates them from
- content/agents/**, and this gate fails when the committed bytes disagree with what the
- build produces.
- To fix:
- 1. Make your change in content/agents/** (the canonical source), not in the output.
- 2. Run: make build-canonical
- 3. Commit the regenerated files along with your source change.
- Paths the build changed:
- EOF
- # Only the paths whose status the BUILD moved. Diffing the whole roots instead would dump
- # every unrelated dirty file in the worktree — including `.opencode/agent/eval-runner.md`,
- # which is hand-authored, has no canonical source, and is none of this gate's business.
- #
- # BOTH sides of the delta matter, and the `<` side is the one that is easy to get wrong.
- # When a contributor hand-edits a generated file, the build REWRITES it back to canonical, so
- # the path is dirty BEFORE and clean AFTER — it leaves the status as a `<` line, never a `>`.
- # Matching only `>` would make the single most likely drift print an empty list.
- # `sed 's/^[<>] ...//'` drops diff's marker, then git's two-char status field and its space.
- drifted=()
- while IFS= read -r line; do
- [ -n "$line" ] && drifted+=("$line")
- done < <(diff <(printf '%s\n' "$before") <(printf '%s\n' "$after") |
- sed -n 's/^[<>] ...//p' | sort -u)
- if [ ${#drifted[@]} -eq 0 ]; then
- # Unreachable: the snapshots differ, so some path moved. Fail loudly rather than pretend.
- echo " (could not attribute the drift to a path — snapshots differ but no path parsed)" >&2
- exit 1
- fi
- printf ' %s\n' "${drifted[@]}" >&2
- echo "" >&2
- echo "Diff:" >&2
- git --no-pager diff -- "${drifted[@]}" >&2
- exit 1
|