[OPIK-6998] [CI] feat: keep agent configs in sync across main and worktrees - #7396
[OPIK-6998] [CI] feat: keep agent configs in sync across main and worktrees#7396JetoPistola wants to merge 1 commit into
Conversation
⏱️ pre-commit per-hook timing
⏭️ 40 skipped (no matching files changed)
Warning Lint failed. Fix locally, then push again: pip install pre-commit # or: brew install pre-commit
make hooks # install the git hook (once per clone)
make precommit # run the same checks on your changed filesFormatters fix files in place — review the changes, |
5032cc3 to
753fb3f
Compare
753fb3f to
305d807
Compare
305d807 to
80da46a
Compare
2fe6cd3 to
af092df
Compare
af092df to
ea929db
Compare
| REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) | ||
| cd "$REPO_ROOT" || exit 0 | ||
|
|
||
| command -v make >/dev/null 2>&1 && make agent-configs || true |
There was a problem hiding this comment.
Duplicated hook bodies drift
.hooks/post-mergeduplicates the repo-root resolution andmake agent-configstail from.hooks/post-checkout, so changes to the sync command, error handling, or repository lookup have to be made twice — should we factor that shared tail into scripts/agent-configs-hook.shand leave the checkout-specific guard inpost-checkout`?
Want Baz to fix this for you? Activate Fixer
| # Manifest matches source. If a surface is opted in but its output is | ||
| # missing, this tree was pulled/created without regenerating. | ||
| if [[ -d ".claude" ]] && [[ ! -d ".claude/skills" ]]; then | ||
| echo "⚠️ .claude/ present but skills not generated in this tree." >&2 | ||
| echo " Run 'make agent-configs' to regenerate." >&2 | ||
| exit 1 | ||
| fi |
There was a problem hiding this comment.
Validator ignores Cursor/Codex outputs
make check-agent-configs only verifies .claude/skills after matching .agents/.sync-manifest, so breaking .cursor, .codex, or AGENTS.override.md still passes the check even though scripts/sync-agent-configs.sh regenerates those surfaces — should we validate them too?
Want Baz to fix this for you? Activate Fixer
Other fix methods
Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
scripts/agent-configs-manifest.sh around lines 52-58 inside the `check` case, the
opted-in output validation only checks `.claude`/`.claude/skills` and misses broken
`.cursor`, `.codex`, and `AGENTS.override.md` outputs. Refactor this section to add
additional assertions that mirror the targets/conditions in
scripts/sync-agent-configs.sh (i.e., if those surfaces are expected to exist in this
tree, verify the corresponding `.cursor` symlink/dir, `.codex` symlink/dir, and
`AGENTS.override.md` file are present; otherwise print the same “Run `make
agent-configs` to regenerate” error and exit 1). This ensures deleting/breaking those
outputs while leaving `.agents/.sync-manifest` unchanged will no longer incorrectly pass
`make check-agent-configs`.
ea929db to
0e64a14
Compare
| define require_no_hookspath | ||
| @hp=$$(git config --get core.hooksPath || true); \ | ||
| if [ -n "$$hp" ]; then \ | ||
| echo "Error: core.hooksPath is set to '$$hp'; hooks in .git/hooks would be ignored."; \ | ||
| echo " Clear it, then re-run 'make $(1)':"; \ | ||
| echo " git config --unset core.hooksPath # local (this repo)"; \ | ||
| echo " git config --global --unset core.hooksPath # if it was set globally"; \ | ||
| exit 1; \ | ||
| fi |
There was a problem hiding this comment.
Hook-path guard checks wrong repository
require_no_hookspath runs git config --get core.hooksPath in Make’s current directory while HOOKS_DEST is rooted at $(MAKEFILE_DIR), so make -f /path/to/repo/Makefile agent-hooks from another repo can pass the guard for the caller repo and still install into a target repo that ignores .git/hooks — should we run the config lookup from $(MAKEFILE_DIR) too?
Want Baz to fix this for you? Activate Fixer
Other fix methods
Prompt for AI Agents
Before applying, verify this suggestion against the current code. In Makefile around
lines 46-54, update the `require_no_hookspath` macro so its `git config --get
core.hooksPath` lookup runs in the target repository used to compute `HOOKS_DEST` (i.e.,
anchored to `$(MAKEFILE_DIR)`), not in Make’s current working directory. Refactor the
macro to execute the git command with `-C $(MAKEFILE_DIR)` (or by doing `cd
"$(MAKEFILE_DIR)"` before running `git config`). Then verify `hooks` and `agent-hooks`
still call the macro unchanged, but now the guard correctly fails when the target
repo’s `core.hooksPath` is set.
| REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) | ||
| cd "$REPO_ROOT" || exit 0 | ||
|
|
||
| command -v make >/dev/null 2>&1 && make agent-configs || true |
There was a problem hiding this comment.
Branch checkout enables code execution
The hook runs make agent-configs from the checked-out tree, which reaches ./scripts/sync-agent-configs.sh in Makefile:142-143, so a malicious commit can execute branch-controlled code on checkout before users review it; the same issue exists in .hooks/post-merge. Should we use a trusted pinned implementation outside the branch, or add an explicit trust/confirmation boundary before running it?
Want Baz to fix this for you? Activate Fixer
Other fix methods
Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
.hooks/post-checkout around lines 8-11 (the REPO_ROOT cd + `make agent-configs`
invocation), avoid executing branch-controlled scripts from the freshly checked-out
working tree. Refactor so the hook runs the sync logic from a trusted, immutable
location (e.g., the hooks’ installed directory or a vendored script path with a fixed
revision/checksum) rather than whatever `Makefile`/`./scripts/sync-agent-configs.sh` the
branch provides. Add the same protection to .hooks/post-merge and, if you can’t fully
pin, introduce an explicit trust/confirmation gate (e.g., require user confirmation or
refuse to run when the branch/commit isn’t trusted) before calling `make
agent-configs`.
…ktrees Regenerate opted-in agent-config surfaces (Claude/Cursor/Codex) from each tree's own .agents/, and detect drift so stale skills can't silently ship. .agents/ stays the single source of truth; vendored output is not committed. - make agent-configs: regenerate opted-in surfaces (detected by folder presence) for whatever tree it runs in — each worktree owns its own .claude/ from its own .agents/, so a skill-editing branch sees its own skills - committed .agents/.sync-manifest (source hash) + make check-agent-configs: fails loudly when .agents/ changed but wasn't re-synced, or opted-in output is missing; runs in pre-commit (on .agents/ changes) and CI - post-merge/post-checkout hooks auto-run the sync (best-effort convenience), installed via a separate make agent-hooks (never CI); shared core.hooksPath guard via one require_no_hookspath macro - convert-mcp.sh never overrides an existing .mcp.json (first-setup only); entity-aware reconciliation deferred to OPIK-7271 - .claude/rules left to the branch's own tracked files (rules -> Skills, OPIK-7272) - sandbox test suite (temp repos, never the real checkout) + CI workflow Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
0e64a14 to
f9131da
Compare
| # Install the local post-merge/post-checkout hooks. Kept separate from `make | ||
| # hooks` (the pre-commit framework) because agent-config sync must never run in CI. | ||
| agent-hooks: | ||
| @if [ -z "$(HOOKS_DEST)" ]; then \ | ||
| echo "Error: $(MAKEFILE_DIR) is not in a git repository."; \ | ||
| exit 1; \ | ||
| fi | ||
| $(call require_no_hookspath,agent-hooks) | ||
| @cd "$(MAKEFILE_DIR)" && \ | ||
| if [ ! -d "$(HOOKS_SRC)" ]; then echo "Error: $(MAKEFILE_DIR)/$(HOOKS_SRC)/ does not exist."; exit 1; fi && \ | ||
| if [ ! -d "$(HOOKS_DEST)" ]; then echo "Error: $(HOOKS_DEST)/ does not exist."; exit 1; fi && \ | ||
| for h in post-checkout post-merge; do \ |
There was a problem hiding this comment.
Hook synchronization can regress silently
The new agent-hooks install flow and .hooks/post-checkout/.hooks/post-merge runtime behavior don't have focused regression coverage here, so changes could silently break automatic synchronization. Should we add a shell/integration test that covers the correct worktree/common hooks directory, core.hooksPath refusal, $3=0 skips, and make agent-configs on branch checkout and merge?
Want Baz to fix this for you? Activate Fixer
Other fix methods
Prompt for AI Agents
Before applying, verify this suggestion against the current code. In Makefile around
lines 195-209, the new `agent-hooks` target (and its installed `.hooks/post-checkout` /
`.hooks/post-merge` behavior) is not covered by regression tests. Add a focused
shell/integration test that creates a temporary git repo/worktree, runs `make
agent-hooks`, and asserts the hook files are installed into the computed git common
hooks directory (`GIT_COMMON_DIR/hooks`) rather than the repo’s local `.git/hooks`;
also assert the command fails with a clear error when `core.hooksPath` is set. Extend
the same test to execute the installed hook scripts with scenarios that cover “branch
checkout and merge invoke `make agent-configs`” and the skip case where the hook
receives `$3=0`, using a spy/stub that records whether `agent-configs` (or
`scripts/sync-agent-configs.sh`) was invoked. Place the test in the project’s existing
test harness (e.g., a new script under `scripts/` or `tests/`) and ensure it runs in CI
so future changes can’t silently break syncing.
Details
Keeps Claude/Cursor/Codex agent configs fresh from
.agents/so skills don't drift stale after teammates merge updates, and makes git worktrees work (they previously loaded zero skills)..agents/stays the single source of truth — vendored output is not committed.make agent-configsregenerates only the surfaces a user opted into (detected by.claude//.cursor/.codexpresence) for whatever tree it runs in. Each worktree owns its own.claude/regenerated from its own.agents/, so a skill-editing branch sees that branch's skills — not main's. (A Claude Code session reads its own tree's.claude/; verified.).agents/.sync-manifest(deterministic source hash) travels with every branch/worktree.make check-agent-configsfails loudly when.agents/changed but wasn't re-synced, or when opted-in output is missing. Runs in pre-commit (on.agents/changes) and CI — stale skills can't silently ship.post-merge/post-checkouthooks auto-run the sync (best-effort; installed via a separatemake agent-hooks, never in CI). Sharedcore.hooksPathguard via onerequire_no_hookspathmacro.convert-mcp.shnever overrides an existing.mcp.json(generated only on first setup) so personal MCP tokens are never destroyed. Entity-aware reconciliation of new upstream servers → follow-up OPIK-7271..claude/rulesis left to the branch's own tracked files; rule content is migrating to Agent Skills → follow-up OPIK-7272.Design note: composed from established patterns — source-hash manifest, per-tree regeneration, and CI drift-check — since
.agents-vendoring has no single off-the-shelf recipe. Skills hot-reload in a running session (only.mcp.jsonneeds a restart), so once files are fresh on disk they propagate immediately.Change checklist
Issues
AI-WATERMARK
AI-WATERMARK: yes
Testing
Local (macOS) + CI:
scripts/test-agent-configs.sh(plain bash, throwaway temp repos, never the real checkout), wired into.github/workflows/agent_configs_tests.yml: covers first-setup vs never-override MCP, opt-in no-op detection, manifest drift-detection (edit.agents/→ check fails → re-write → passes), and per-worktree independence (a worktree resolves to its own tree, not main).make check-agent-configsverified: in-sync passes,.agents/edit without re-sync fails with the "run make agent-configs" nudge, manifest is deterministic across recomputes (CI-safe).🤖🔄 agent configs in sync) fires on.agents/changes; actionlint passes.core.hooksPathguard, real worktree regeneration.Not run: Java/FE/SDK suites — no files in those areas changed.
Documentation
N/A — developer-tooling change;
make help, in-file comments, and a shareable architecture diagram (diagrams/opik-6998-diagram.html) document the model. User-facing docs unaffected.