Skip to content
Open
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
117 changes: 38 additions & 79 deletions agents/ai.just
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ set working-directory := '..'
# `just ci` → `just ai::apm-sync`).
nix_shell := if env('IN_NIX_SHELL', '') != '' { '' } else { 'nix develop --accept-flake-config -c' }

# Pinned to juspay's fork while the hook-idempotency fix (microsoft/apm#708)
# is unreleased upstream. Revert to `microsoft/apm` once the fix lands.
apm_cmd := nix_shell + ' uvx --from git+https://github.com/microsoft/apm apm'
# Pinned to juspay's fork on the feat/install-compile-root-flag branch
# while microsoft/apm#888 (apm install --root) is unreleased upstream.
# Revert to plain `microsoft/apm` once the flag lands there.
apm_cmd := nix_shell + ' uvx --from git+https://github.com/juspay/apm@feat/install-compile-root-flag apm'

# Install native APM integrations for Claude, Codex, and OpenCode.
#
Expand Down Expand Up @@ -40,89 +41,47 @@ apm-audit:

# Verify APM-managed outputs match sources + security audit
#
# Must NOT mutate the live .claude/ tree — concurrent Claude Code
# sessions in the same worktree would see stop hooks / rules / skills
# disappear mid-CI and crash. See #468. Instead we stage a fresh tree
# in a scratch dir and diff. The `apm` recipe above is destructive and
# stays a dev-only action. This workaround can be removed if upstream
# adds content verification to `apm audit --ci`: microsoft/apm#684.
# `apm audit --ci` (via apm-audit above) hash-verifies every
# APM-managed file -- closes microsoft/apm#684/#887. That covers
# .claude/, .codex/, .agents/, .opencode/, and opencode.json content
# drift without needing a scratch install + recursive diff.
#
# AGENTS.md is a compile output, NOT a lockfile-tracked deployed file,
# so we still verify it by staging install + compile into a scratch
# deploy root via `apm install --root` + `apm compile --root`
# (microsoft/apm#888) and diffing the AGENTS.md outputs. Sources
# (apm.yml, .apm/, the project tree) stay in $PWD throughout -- the
# `--root` flags handle write redirection internally.
apm-sync: apm-audit
#!/usr/bin/env bash
set -euo pipefail
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
# Distributed `apm compile` placement depends on the real repo layout, so
# mirror the worktree into scratch while excluding runtime/generated trees.
# `apm install` rewrites apm.lock.yaml's `generated_at` on every run, which
# is fine here because the scratch copy is disposable.
rsync -a \
--exclude='.git/' \
--exclude='node_modules/' \
--exclude='.direnv/' \
--exclude='.logs/' \
--exclude='.claude/' \
--exclude='.codex/' \
--exclude='.agents/' \
--exclude='.opencode/' \
--exclude='CLAUDE.md' \
"$PWD/" "$scratch/"
# Mirror the runtime folders that drive target detection for install.
# Compile is explicit so we only emit `AGENTS.md` for Codex/OpenCode.
mkdir -p "$scratch/.claude" "$scratch/.codex" "$scratch/.opencode" "$scratch/.agents"
# Keep user-owned runtime inputs in the scratch tree too. APM compile
# placement is file-layout sensitive, so omitting checked-in files that
# are present during local `just ai::apm` can produce a false sync diff.
[[ ! -e .claude/launch.json ]] || cp .claude/launch.json "$scratch/.claude/launch.json"
[[ ! -e .codex/config.toml ]] || cp .codex/config.toml "$scratch/.codex/config.toml"
(cd "$scratch" && {{ apm_cmd }} install >/dev/null)
(cd "$scratch" && {{ apm_cmd }} compile --target codex,opencode >/dev/null)
# Exclude files that apm doesn't manage: launch.json is user-owned,
# and `.claude/*` entries in .gitignore are runtime state written by
# Claude Code itself (worktree bookkeeping, local settings, session
# lockfiles). Keep this list in sync with .gitignore — if a new
# runtime file appears in .claude/, add it to .gitignore and here.
if ! diff -r \
-x launch.json \
-x worktrees \
-x settings.local.json \
-x scheduled_tasks.lock \
.claude "$scratch/.claude"; then
echo "ERROR: .claude/ out of sync with sources — run: just ai::apm" >&2
exit 1
fi
# `.codex/config.toml` is currently a checked-in manual fallback for
# project-local Codex MCP until microsoft/apm#803 lands; current APM
# does not emit it in the scratch tree yet.
if ! diff -r \
-x config.toml \
.codex "$scratch/.codex"; then
echo "ERROR: .codex/ out of sync with sources — run: just ai::apm" >&2
exit 1
fi
if ! diff -r .agents "$scratch/.agents"; then
echo "ERROR: .agents/ out of sync with sources — run: just ai::apm" >&2
exit 1
fi
if ! diff -r \
-x .gitignore \
-x node_modules \
-x package-lock.json \
-x package.json \
.opencode "$scratch/.opencode"; then
echo "ERROR: .opencode/ out of sync with sources — run: just ai::apm" >&2
exit 1
fi
if [[ -e opencode.json || -e "$scratch/opencode.json" ]]; then
if [[ ! -e opencode.json || ! -e "$scratch/opencode.json" ]] || \
! diff <(jq -S . opencode.json) <(jq -S . "$scratch/opencode.json"); then
echo "ERROR: opencode.json out of sync with sources — run: just ai::apm" >&2
# Seed the lockfile so install resolves to the SAME commits as the
# live tree. apm install rewrites apm.lock.yaml during the run, so
# this must be a copy rather than a symlink.
[[ ! -e apm.lock.yaml ]] || cp apm.lock.yaml "$scratch/apm.lock.yaml"
# apm install --root: writes apm_modules/, apm.lock.yaml, .claude/,
# .codex/, .agents/, .opencode/ under $scratch while reading apm.yml
# + .apm/ + local-path packages from $PWD.
{{ apm_cmd }} install --root "$scratch" >/dev/null
# apm compile --root: writes AGENTS.md / CLAUDE.md outputs under
# $scratch while reading apm.yml + .apm/ + the project tree (for
# placement scoring) from $PWD. No rsync, no cd, no symlinks.
{{ apm_cmd }} compile --root "$scratch" --target codex,opencode >/dev/null
# Diff every AGENTS.md under the scratch tree against its counterpart
# in the live tree. Distributed compile produces files at multiple
# depths (AGENTS.md, packages/AGENTS.md, etc.), so a single
# root-level diff is not enough. apm audit --ci above already
# covers content-integrity for everything `apm install` writes.
while IFS= read -r f; do
rel="${f#$scratch/}"
if ! diff "$rel" "$f" >/dev/null 2>&1; then
echo "ERROR: $rel out of sync with sources -- run: just ai::apm" >&2
diff "$rel" "$f" || true
exit 1
fi
fi
if ! diff AGENTS.md "$scratch/AGENTS.md"; then
echo "ERROR: AGENTS.md out of sync with sources — run: just ai::apm" >&2
exit 1
fi
done < <(find "$scratch" -name AGENTS.md -type f)

# Install APM config and launch coding agent (AI_AGENT overrides default)
[default]
Expand Down