Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
552 lines (507 loc) · 28 KB
/
Copy pathMakefile
File metadata and controls
552 lines (507 loc) · 28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
# Install AI-Rig plugins for Claude Code, Codex, and/or OpenCode from the GitHub remote.
# Codex sync also mirrors this checkout's model defaults, workflow approval policy, and personal policy into $CODEX_HOME.
# OpenCode sync registers the npm package from its repository subdirectory, installs its role agents,
# and writes the global delivery-principles instructions block through `setup`.
# Remote installs use pushed state — commit and push before running.
#
# Run from the project root:
# make sync-all # Claude + Codex + OpenCode scopes, plus standing Claude consent from an operator terminal or BRIDGE_OPERATOR_TOKEN (default target)
# make sync-all APPROVE_CLAUDE_CONSENT=0 # leave standing consent unchanged
# make sync-claude # Claude scope only
# make sync-codex # Codex scope only
# make sync-opencode # OpenCode scope only
# make sync-opencode OPENCODE_ADOPT_CLAUDE_INSTRUCTIONS=1 # also copy ~/.claude/CLAUDE.md into the global block
# make preflight-sync # check RTK availability without changing installations
# make sync-codex APPROVE_CLAUDE_CONSENT=1 # explicitly record standing Claude transfer consent
# make clear-all # teardown both scopes
# make clear-claude # teardown Claude scope only
# make clear-codex # teardown Codex scope only
# make rig-check # opencode-rig typecheck, format, tests, version, and package checks
#
# Requires GNU make (verified against GNU Make 3.81, the version Apple ships as /usr/bin/make;
# not BSD make). Every multi-statement recipe below is written the traditional portable way —
# explicit trailing `\` plus explicit `;` after every statement — rather than relying on the
# `.ONESHELL:` directive, which GNU Make 3.81 does not implement (confirmed empirically: state
# set on one un-continued recipe line does not survive to the next).
# Sync also requires RTK on PATH; install it before syncing: https://github.com/rtk-ai/rtk
#
# EXTERNAL_PLUGIN_TIMEOUT_SECONDS (env var, default 120) bounds every external
# marketplace/plugin command. There is no CLI-flag equivalent — set it in the environment:
# EXTERNAL_PLUGIN_TIMEOUT_SECONDS=300 make sync-claude
SHELL := /bin/bash
.SHELLFLAGS := -ec
.NOTPARALLEL:
.DEFAULT_GOAL := sync-all
PLUGINS := foundry oss develop research codemap-py bridge
EXTERNAL_PLUGINS := caveman@caveman
# OpenCode installs one package straight from the repository subdirectory. The package is
# not on npm yet, so the Git specifier is the supported route; replace it with
# `opencode-rig@<version>` in opencode.jsonc once the first release is published.
OPENCODE_RIG_SPEC := github:Borda/AI-Rig\#main::path:plugins/opencode-rig
OPENCODE_RIG_MANIFEST := https://raw.githubusercontent.com/Borda/AI-Rig/main/plugins/opencode-rig/package.json
# `init` never overwrites an edited role agent; set this to 1 to replace stamped files.
OPENCODE_RIG_UPGRADE_AGENTS ?= 0
# `setup` refuses to shadow an existing ~/.claude/CLAUDE.md; set this to 1 to copy it into the
# global OpenCode instructions block instead.
OPENCODE_ADOPT_CLAUDE_INSTRUCTIONS ?= 0
MARKETPLACE := $(shell jq -r '.name' .claude-plugin/marketplace.json)
SETTINGS := $(HOME)/.claude/settings.json
KNOWN_MARKETPLACES := $(HOME)/.claude/plugins/known_marketplaces.json
INSTALLED_PLUGINS := $(HOME)/.claude/plugins/installed_plugins.json
CACHE_DIR := $(HOME)/.claude/plugins/cache
# Orphaned plugin versions (Claude Code writes .orphaned_at, epoch ms, when an install replaces them) are removed
# by sync-claude once the marker is at least this old; younger ones may still back a running session.
ORPHAN_MIN_AGE_HOURS ?= 24
PROJECT_DIR := $(shell pwd)
MARKETPLACE_REMOTE := $(shell git -C $(PROJECT_DIR) remote get-url origin 2>/dev/null | sed 's/\.git$$//')
CODEX_SYNC_SCRIPT := $(PROJECT_DIR)/plugins/codex-rig/scripts/sync_codex.py
CODEX_HOME_SYNC_SCRIPT := $(PROJECT_DIR)/scripts/sync_codex_session_policy.py
BRIDGE_CONSENT_SCRIPT := $(PROJECT_DIR)/plugins/bridge_cc-codex/bin/bridge_consent.py
BRIDGE_CONSENT_LAUNCHER := $(PROJECT_DIR)/plugins/bridge_cc-codex/bin/python
# Full sync approves standing consent by default; respect command-line and environment overrides.
# CONSENT_EXPLICIT records whether the operator set the value (command line or environment): an explicit opt-in
# without operator proof fails, the implicit full-sync default skips. sync-all forwards it to the sub-make, where its
# own re-passed APPROVE_CLAUDE_CONSENT would otherwise always look explicit.
ifeq ($(origin APPROVE_CLAUDE_CONSENT),undefined)
sync-all: APPROVE_CLAUDE_CONSENT = 1
CONSENT_EXPLICIT := 0
else
CONSENT_EXPLICIT := 1
endif
APPROVE_CLAUDE_CONSENT ?= 0
ifneq ($(APPROVE_CLAUDE_CONSENT),0)
ifneq ($(APPROVE_CLAUDE_CONSENT),1)
$(error APPROVE_CLAUDE_CONSENT must be 0 or 1)
endif
endif
TIMEOUT_RUNNER := $(PROJECT_DIR)/scripts/run_with_timeout.py
VERIFY_PLUGINS_SCRIPT := $(PROJECT_DIR)/scripts/verify_claude_plugins.py
EXTERNAL_PLUGIN_TIMEOUT_SECONDS ?= 120
# Header hierarchy: double-line workflow, heavy section, thin stage.
# Sections indent two spaces; stages indent four. Usage: `@$(call stage,Text); \`.
# Rule of thumb: any section whose output runs past ~5 lines gets a banner; a
# short section (2-3 lines) keeps a plain `echo` so the banners stay meaningful.
# The body must stay on ONE line: GNU Make 3.81 keeps every newline inside a
# `define`, and a multi-line value would break the recipe's `\` continuation.
# Keep the text pure ASCII and free of commas and single quotes — macOS bash 3.2
# pads `%-58.58s` by bytes, so a multi-byte char shifts the right border.
WORKFLOW_RULE := ════════════════════════════════════════════════════════════
SECTION_RULE := ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
STAGE_RULE := ────────────────────────────────────────────────────────────
define workflow
printf '\n╔%s╗\n║ %-58.58s ║\n╚%s╝\n' '$(WORKFLOW_RULE)' '$(1)' '$(WORKFLOW_RULE)'
endef
define section
printf '\n ┏%s┓\n ┃ %-58.58s ┃\n ┗%s┛\n' '$(SECTION_RULE)' '$(1)' '$(SECTION_RULE)'
endef
define stage
printf '\n ┌%s┐\n │ %-58.58s │\n └%s┘\n' '$(STAGE_RULE)' '$(1)' '$(STAGE_RULE)'
endef
.PHONY: sync-all sync-claude sync-codex sync-opencode clear-all clear-claude clear-codex \
migrate-marketplace uninstall-claude-plugins refresh-ext-marketplace \
update-ext-plugins register-marketplace install-claude-plugins prune-claude-cache \
verify-claude-plugins warn-stale-claude-sessions \
install-codex-plugins sync-codex-home-policy sync-codex-bridge-consent \
register-opencode-rig install-opencode-agents setup-opencode \
banner-claude banner-codex banner-opencode preflight-sync \
rig-install rig-typecheck rig-test rig-format rig-pack rig-check \
prune-benchmarks prune-benchmarks-apply
## Meta targets ---------------------------------------------------------------
# Codex sync uses an explicit success chain, including under make -k; Claude stages
# run only after preflight succeeds, with their existing order retained. sync-all deliberately runs both
# phases regardless of either's outcome and aggregates the failure at the end,
# instead of a codex-phase failure silently discarding an earlier claude-phase
# failure count (which is what sync.sh's bare `set -e` does today).
#
# One banner per section only. `banner-claude`/`banner-codex` are ordinary
# prerequisites, not recipe lines, because a target's own recipe runs *after* its
# prerequisites — the banner has to precede the stages it heads. `.NOTPARALLEL:`
# above keeps prerequisite order left-to-right. sync-all prints its own banner and
# then inherits the section banners from the sub-makes it invokes.
banner-claude:
@$(call section,Claude sync - marketplace + plugins)
banner-codex:
@$(call section,Codex sync - plugins + session policy)
banner-opencode:
@$(call section,OpenCode sync - package from GitHub + role agents)
preflight-sync:
@command -v rtk >/dev/null 2>&1 || { \
echo "RTK is required for sync. Install RTK and put rtk on PATH: https://github.com/rtk-ai/rtk" >&2; \
exit 1; \
}
sync-all: preflight-sync
@$(call workflow,Full sync - Claude + Codex + OpenCode scopes); \
status=0; \
$(MAKE) sync-claude || status=1; \
$(MAKE) sync-codex APPROVE_CLAUDE_CONSENT=$(APPROVE_CLAUDE_CONSENT) CONSENT_EXPLICIT=$(CONSENT_EXPLICIT) || status=1; \
$(MAKE) sync-opencode || status=1; \
if [ $$status -ne 0 ]; then \
echo "⚠ sync-all finished with failures — see output above"; \
else \
echo "✓ sync-all complete"; \
fi; \
exit $$status
sync-claude: preflight-sync
@$(MAKE) banner-claude migrate-marketplace uninstall-claude-plugins refresh-ext-marketplace update-ext-plugins register-marketplace install-claude-plugins verify-claude-plugins prune-claude-cache warn-stale-claude-sessions && \
echo "✓ Claude sync complete"
sync-codex: preflight-sync banner-codex
@$(MAKE) install-codex-plugins && \
$(MAKE) sync-codex-home-policy && \
$(MAKE) sync-codex-bridge-consent && \
echo "✓ Codex sync complete"
# The OpenCode registration belongs to opencode.json(c), not to this Makefile: `plugin add`
# is idempotent and writes the Git specifier into the global config, so other checkouts load
# it too. The checked-in project entry is what makes this repository use it without that.
sync-opencode: preflight-sync banner-opencode
@$(MAKE) register-opencode-rig && \
$(MAKE) install-opencode-agents && \
$(MAKE) setup-opencode && \
echo "✓ OpenCode sync complete"
register-opencode-rig:
@command -v opencode >/dev/null 2>&1 || { \
echo "OpenCode CLI is required for sync. Install it and put opencode on PATH: https://opencode.ai" >&2; \
exit 1; \
}; \
command -v curl >/dev/null 2>&1 || { \
echo "curl is required to verify the pushed package before registering it." >&2; \
exit 1; \
}; \
curl -fsS --max-time 20 "$(OPENCODE_RIG_MANIFEST)" >/dev/null || { \
echo "plugins/opencode-rig is not on main yet — commit and push it before syncing." >&2; \
exit 1; \
}; \
$(call stage,Register OpenCode Rig from GitHub); \
opencode plugin add "$(OPENCODE_RIG_SPEC)"
# `plugin add` caches the package for loading but does not link its bin on PATH, so the
# CLI runs from this checkout. Role cards are packaged assets: sync from pushed state
# only (commit and push first), exactly like every other install target here.
install-opencode-agents:
@command -v node >/dev/null 2>&1 || { \
echo "Node.js 20 or newer is required to install the role agents: https://nodejs.org" >&2; \
exit 1; \
}; \
args="--agents-only"; \
if [ "$(OPENCODE_RIG_UPGRADE_AGENTS)" = "1" ]; then args="--agents-only --upgrade"; fi; \
node "$(OPENCODE_RIG_DIR)/bin/opencode-rig.mjs" init $$args
# `setup` is the full installation: role agents, the permission-rule merge, and the global
# delivery-principles instructions block. It is idempotent, so sync runs it after the agents.
setup-opencode:
@command -v node >/dev/null 2>&1 || { \
echo "Node.js 20 or newer is required to set up opencode-rig: https://nodejs.org" >&2; \
exit 1; \
}; \
args=""; \
if [ "$(OPENCODE_ADOPT_CLAUDE_INSTRUCTIONS)" = "1" ]; then args="--adopt-claude-instructions"; fi; \
$(call stage,Write global OpenCode instructions); \
node "$(OPENCODE_RIG_DIR)/bin/opencode-rig.mjs" setup $$args
clear-all:
@$(call workflow,Full teardown - Claude + Codex scopes); \
status=0; \
$(MAKE) clear-claude || status=1; \
$(MAKE) clear-codex || status=1; \
echo "✓ Cleared (managed plugins uninstalled; marketplace registrations + caveman left in place)"; \
exit $$status
## Claude-side targets ---------------------------------------------------------
clear-claude:
@$(call section,Claude teardown - uninstall marketplace plugins); \
for p in $(PLUGINS); do \
claude plugin uninstall "$$p@$(MARKETPLACE)" 2>/dev/null && echo " ✓ uninstalled $$p" || echo " – $$p not installed, skipping"; \
done; \
echo "✓ Claude plugins cleared"
migrate-marketplace:
@echo "Migrating stale marketplace registrations..."; \
while IFS= read -r stale; do \
stale="$${stale%$$'\r'}"; \
[[ -z "$$stale" ]] && continue; \
echo "Migrating marketplace '$$stale' → '$(MARKETPLACE)'..."; \
if [[ -d "$(CACHE_DIR)/$$stale" && ! -d "$(CACHE_DIR)/$(MARKETPLACE)" ]]; then \
mv "$(CACHE_DIR)/$$stale" "$(CACHE_DIR)/$(MARKETPLACE)"; \
echo " ✓ cache dir renamed"; \
elif [[ -d "$(CACHE_DIR)/$$stale" ]]; then \
rm -rf "$(CACHE_DIR)/$$stale"; \
echo " ✓ stale cache dir removed"; \
fi; \
tmp=$$(mktemp); \
jq --arg old "$$stale" --arg new "$(MARKETPLACE)" '.[$$new] = .[$$old] | del(.[$$old])' "$(KNOWN_MARKETPLACES)" > "$$tmp" && mv "$$tmp" "$(KNOWN_MARKETPLACES)"; \
tmp=$$(mktemp); \
jq --arg old "$$stale" --arg new "$(MARKETPLACE)" '.plugins = (.plugins | with_entries(.key |= gsub($$old; $$new)) | walk(if type == "string" then gsub($$old; $$new) else . end))' "$(INSTALLED_PLUGINS)" > "$$tmp" && mv "$$tmp" "$(INSTALLED_PLUGINS)"; \
tmp=$$(mktemp); \
jq --arg old "$$stale" --arg new "$(MARKETPLACE)" 'del(.extraKnownMarketplaces[$$old]) | walk(if type == "string" then gsub($$old; $$new) elif type == "object" then with_entries(.key |= gsub($$old; $$new)) else . end)' "$(SETTINGS)" > "$$tmp" && mv "$$tmp" "$(SETTINGS)"; \
echo " ✓ registries updated ($$stale → $(MARKETPLACE))"; \
done < <(jq -r --arg path "$(PROJECT_DIR)" --arg new "$(MARKETPLACE)" 'to_entries | map(select(.value.source.path == $$path and .key != $$new)) | .[].key' "$(KNOWN_MARKETPLACES)")
uninstall-claude-plugins:
@$(call stage,Uninstall existing plugins); \
for p in $(PLUGINS); do \
claude plugin uninstall "$$p@$(MARKETPLACE)" 2>/dev/null && echo " ✓ uninstalled $$p" || echo " – $$p not installed, skipping"; \
done
refresh-ext-marketplace:
@echo "Refreshing external plugin marketplaces..."; \
if ! python3 "$(TIMEOUT_RUNNER)" --timeout-seconds "$(EXTERNAL_PLUGIN_TIMEOUT_SECONDS)" --label "caveman marketplace registration" -- claude plugin marketplace add JuliusBrussee/caveman 2>/dev/null; then \
echo " ⚠ caveman marketplace registration failed or timed out; trying the existing registration"; \
fi; \
if python3 "$(TIMEOUT_RUNNER)" --timeout-seconds "$(EXTERNAL_PLUGIN_TIMEOUT_SECONDS)" --label "caveman marketplace refresh" -- claude plugin marketplace update caveman 2>/dev/null; then \
echo " ✓ caveman refreshed"; \
else \
external_status=$$?; \
if [[ $$external_status -eq 124 ]]; then \
echo " ⚠ caveman refresh timed out after $(EXTERNAL_PLUGIN_TIMEOUT_SECONDS)s"; \
else \
echo " ⚠ caveman refresh failed (offline?)"; \
fi; \
fi
# Gate on marketplace-refresh success dropped intentionally (blueprint constraint 7,
# no test coverage found) — always attempts reinstall regardless of refresh outcome.
update-ext-plugins:
@echo "Updating external plugins..."; \
for p in $(EXTERNAL_PLUGINS); do \
if python3 "$(TIMEOUT_RUNNER)" --timeout-seconds "$(EXTERNAL_PLUGIN_TIMEOUT_SECONDS)" --label "$$p uninstall" -- claude plugin uninstall "$$p" 2>/dev/null; then \
echo " ✓ uninstalled $$p"; \
else \
external_status=$$?; \
if [[ $$external_status -eq 124 ]]; then \
echo " ⚠ $$p uninstall timed out; skipping reinstall"; \
continue; \
fi; \
echo " – $$p not installed, skipping uninstall"; \
fi; \
if python3 "$(TIMEOUT_RUNNER)" --timeout-seconds "$(EXTERNAL_PLUGIN_TIMEOUT_SECONDS)" --label "$$p install" -- claude plugin install "$$p"; then \
identity=$$(jq -r --arg plugin "$$p" '(.plugins[$$plugin] // []) | sort_by(.lastUpdated // "") | last // {} | [(.version // "unknown"), (.gitCommitSha // "unknown")] | @tsv' "$(INSTALLED_PLUGINS)" 2>/dev/null || printf 'unknown\tunknown'); \
version="$${identity%%$$'\t'*}"; \
revision="$${identity#*$$'\t'}"; \
[[ "$$revision" != "unknown" ]] && revision="$${revision:0:12}"; \
echo " ✓ $$p: version $$version, revision $$revision"; \
else \
external_status=$$?; \
if [[ $$external_status -eq 124 ]]; then \
echo " ✗ $$p install timed out after $(EXTERNAL_PLUGIN_TIMEOUT_SECONDS)s"; \
else \
echo " ✗ $$p install failed"; \
fi; \
fi; \
done
register-marketplace:
@$(call stage,Register marketplace - GitHub source to versioned cache); \
LOCAL_SHA=$$(git -C "$(PROJECT_DIR)" rev-parse HEAD 2>/dev/null); \
REMOTE_SHA=$$(git ls-remote "$(MARKETPLACE_REMOTE)" HEAD 2>/dev/null | awk '{print $$1}'); \
if [ -z "$$REMOTE_SHA" ]; then \
echo " ⚠ cannot reach $(MARKETPLACE_REMOTE) — skipping SHA check (offline or auth?)"; \
elif [ "$$LOCAL_SHA" != "$$REMOTE_SHA" ]; then \
echo " ┌──────────────────────────────────────────────────────────────"; \
echo " │ ⚠ LOCAL ≠ REMOTE — cache will install the REMOTE commit, not your local work"; \
echo " │ local HEAD: $$LOCAL_SHA"; \
echo " │ remote HEAD: $$REMOTE_SHA"; \
echo " │ → commit + push first, or these plugins install a stale/different version"; \
echo " └──────────────────────────────────────────────────────────────"; \
fi; \
claude plugin marketplace remove "$(MARKETPLACE)" 2>/dev/null || true; \
claude plugin marketplace add "$(MARKETPLACE_REMOTE)"
# `claude plugin uninstall` only marks a replaced version dir with `.orphaned_at`; nothing ever deletes it. A session
# started before the replacement still reads its skill files from that dir, so only markers older than
# ORPHAN_MIN_AGE_HOURS are pruned, and a dir any install record still names is always kept.
prune-claude-cache:
@$(call stage,Prune orphaned plugin cache versions); \
now_ms=$$(( $$(date +%s) * 1000 )); \
min_age_ms=$$(( $(ORPHAN_MIN_AGE_HOURS) * 3600 * 1000 )); \
if ! installed=$$(jq -r '.plugins // {} | to_entries[] | .value[]? | .installPath // empty' "$(INSTALLED_PLUGINS)" 2>/dev/null); then \
echo " ⚠ cannot read $(INSTALLED_PLUGINS); pruning skipped so no installed version dir is removed"; \
exit 0; \
fi; \
pruned=0; \
for marker in "$(CACHE_DIR)/$(MARKETPLACE)"/*/*/.orphaned_at; do \
[[ -f "$$marker" ]] || continue; \
dir="$${marker%/.orphaned_at}"; \
label="$${dir#$(CACHE_DIR)/}"; \
if grep -Fxq -- "$$dir" <<<"$$installed"; then echo " – kept $$label (still installed)"; continue; fi; \
stamp=$$(tr -cd '0-9' < "$$marker"); \
if [[ -z "$$stamp" ]]; then echo " – kept $$label (unreadable marker)"; continue; fi; \
if (( $${#stamp} <= 10 )); then stamp=$$(( stamp * 1000 )); fi; \
if (( now_ms - stamp < min_age_ms )); then echo " – kept $$label (orphaned under $(ORPHAN_MIN_AGE_HOURS)h ago)"; continue; fi; \
rm -rf -- "$$dir"; \
pruned=$$(( pruned + 1 )); \
echo " ✓ removed $$label"; \
done; \
echo " $$pruned orphaned version dir(s) removed"
# Folds install + purge + setup-skills into one target (blueprint constraint 4) — one
# shell invocation, so the bridge-purge guard (constraint 6) and the try-all-6-then-report
# contract (constraint 3) both stay local bash state, no relay file needed.
install-claude-plugins:
@$(call stage,Install marketplace plugins); \
BRIDGE_INSTALLED=false; \
FAILED_INSTALLS=0; \
for p in $(PLUGINS); do \
if claude plugin install "$$p@$(MARKETPLACE)"; then \
identity=$$(jq -r --arg plugin "$$p@$(MARKETPLACE)" '(.plugins[$$plugin] // []) | sort_by(.lastUpdated // "") | last // {} | [(.version // "unknown"), (.gitCommitSha // "unknown")] | @tsv' "$(INSTALLED_PLUGINS)" 2>/dev/null || printf 'unknown\tunknown'); \
version="$${identity%%$$'\t'*}"; \
revision="$${identity#*$$'\t'}"; \
[[ "$$revision" != "unknown" ]] && revision="$${revision:0:12}"; \
echo " ✓ $$p@$(MARKETPLACE): version $$version, revision $$revision"; \
if [[ "$$p" == "bridge" ]]; then \
BRIDGE_INSTALLED=true; \
fi; \
else \
echo " ✗ $$p@$(MARKETPLACE) install failed"; \
FAILED_INSTALLS=$$((FAILED_INSTALLS + 1)); \
fi; \
done; \
PURGE_PLUGINS=(ponytail@ponytail); \
if $$BRIDGE_INSTALLED; then \
PURGE_PLUGINS+=(codex@openai-codex); \
fi; \
echo "Purging retired plugins..."; \
for p in "$${PURGE_PLUGINS[@]}"; do \
if claude plugin uninstall "$$p" 2>/dev/null; then \
echo " ✓ purged $$p"; \
else \
echo " – $$p not installed, nothing to purge"; \
fi; \
done; \
$(call stage,Initialize installed plugin setup skills); \
for p in $(PLUGINS); do \
install_path=$$(jq -r --arg plugin "$$p@$(MARKETPLACE)" '(.plugins[$$plugin] // []) | map(select(.installPath?)) | sort_by(.installedAt // "") | last // {} | .installPath // ""' "$(INSTALLED_PLUGINS)"); \
if [[ -z "$$install_path" ]]; then \
echo " – $$p not installed, skipping setup"; \
continue; \
fi; \
if [[ "$$p" == "bridge" ]]; then \
bridge_doctor="$$install_path/bin/bridge_diagnose.py"; \
if [[ ! -f "$$bridge_doctor" || -L "$$bridge_doctor" ]]; then \
echo " ✗ bridge static diagnosis is incomplete or linked" >&2; \
exit 1; \
fi; \
if ! python_version=$$(python3 --version 2>&1); then \
echo " ✗ bridge requires Python 3.10 or newer" >&2; \
exit 1; \
fi; \
if [[ ! "$$python_version" =~ Python[[:space:]]+([0-9]+)\.([0-9]+) ]]; then \
echo " ✗ bridge requires Python 3.10 or newer" >&2; \
exit 1; \
fi; \
if (( BASH_REMATCH[1] < 3 || (BASH_REMATCH[1] == 3 && BASH_REMATCH[2] < 10) )); then \
echo " ✗ bridge requires Python 3.10 or newer; found $$python_version" >&2; \
exit 1; \
fi; \
if command -v codex >/dev/null 2>&1; then \
bridge_direction="codex"; \
else \
echo " – codex CLI not found; bridge diagnosis covers the claude direction only"; \
bridge_direction="claude"; \
fi; \
if ! bridge_diagnosis=$$(python3 "$$bridge_doctor" --direction "$$bridge_direction"); then \
echo " ✗ bridge static diagnosis command failed" >&2; \
exit 1; \
fi; \
if ! jq -e '(.ok == true and .live == false and .payload.complete == true)' <<<"$$bridge_diagnosis" >/dev/null; then \
echo " ✗ bridge static diagnosis failed" >&2; \
exit 1; \
fi; \
echo " ✓ bridge static diagnosis passed; no provider call made"; \
continue; \
fi; \
setup_skill=""; \
for candidate in "$$install_path/skills/setup/SKILL.md" "$$install_path/claude-skills/setup/SKILL.md"; do \
if [[ -f "$$candidate" ]]; then \
setup_skill="$$candidate"; \
break; \
fi; \
done; \
if [[ -z "$$setup_skill" ]]; then \
echo " – $$p has no setup skill, skipping"; \
continue; \
fi; \
echo " → $$p:setup"; \
CLAUDE_PLUGIN_ROOT="$$install_path" claude --print --output-format text \
--append-system-prompt "Your output is printed directly in a plain terminal. Format all reports as plain text with short labels and indented lists. Do not use Markdown headings, emphasis markers, backticks, fenced code blocks, tables, or Markdown links. Write paths, commands, and URLs literally. Preserve all required report content." \
"/$$p:setup --approve"; \
done; \
if [ "$$FAILED_INSTALLS" -gt 0 ]; then \
echo "⚠ Done with $$FAILED_INSTALLS failed install(s) — rerun after checking network and marketplace access"; \
exit 1; \
fi; \
echo "✓ Done"
# Several steps above tolerate failure on purpose (an uninstall reported as "not installed", an external refresh that
# may be offline), so a plugin could end on an older version while every step printed success. This compares every
# install record (user scope and any project/local scope) with the version the freshly registered marketplace clone
# declares, and that clone's HEAD with the remote's, and fails the sync on any mismatch. External plugins are
# report-only, keeping update-ext-plugins non-fatal for offline runs.
verify-claude-plugins:
@$(call stage,Verify installed versions match the marketplace); \
python3 "$(VERIFY_PLUGINS_SCRIPT)" verify \
--installed "$(INSTALLED_PLUGINS)" \
--known-marketplaces "$(KNOWN_MARKETPLACES)" \
--marketplace "$(MARKETPLACE)" \
--plugins $(PLUGINS) \
--report-only $(EXTERNAL_PLUGINS) \
--expect-remote "$(MARKETPLACE_REMOTE)"
# A Claude Code process keeps the plugin versions it loaded at start, so every session started before this install
# still runs the old copies until restarted. Warning only: the install itself succeeded, and only the user can restart
# those sessions. Hosts without `ps` (native Windows) print nothing.
warn-stale-claude-sessions:
@python3 "$(VERIFY_PLUGINS_SCRIPT)" sessions --installed "$(INSTALLED_PLUGINS)"
## Codex-side targets ----------------------------------------------------------
clear-codex:
@$(call section,Codex teardown - clear plugins); \
python3 "$(CODEX_SYNC_SCRIPT)" clear; \
echo "✓ Codex plugins cleared"
# --codex-ref/--no-clean/--no-codex-global-agents dropped (blueprint constraint 2):
# always tracks the default branch, always cleans before reinstalling, always
# installs the global-instructions block.
install-codex-plugins:
@# sync_codex.py uses a filtered sparse clone and migrates older full marketplace checkouts.
@$(call stage,Install Codex plugins from default branch); \
python3 "$(CODEX_SYNC_SCRIPT)" install
sync-codex-home-policy:
@python3 "$(CODEX_HOME_SYNC_SCRIPT)" \
--source-config "$(PROJECT_DIR)/.codex/config.toml" \
--source-policy "$(PROJECT_DIR)/.codex/global-session-policy.md" \
--codex-home "$${CODEX_HOME:-$$HOME/.codex}"
# Consent follows policy sync so its exact block survives the next ordinary sync.
# The grant needs operator proof (terminal or BRIDGE_OPERATOR_TOKEN from issue-token); without either the implicit
# full-sync default is skipped so a piped or scripted full sync still succeeds, while an explicit
# APPROVE_CLAUDE_CONSENT=1 fails so a requested grant never silently does nothing. The terminal probe runs the helper's own
# predicate through the launcher: bash `[ -t ]` can disagree with native Windows Python under mintty. Probe exit 3 =
# no terminal; any other failure falls through so the grant reports it.
sync-codex-bridge-consent:
@if [ "$(APPROVE_CLAUDE_CONSENT)" = "0" ]; then \
echo " – Claude transfer consent unchanged (opt in with APPROVE_CLAUDE_CONSENT=1)"; \
exit 0; \
fi; \
if [ -z "$${BRIDGE_OPERATOR_TOKEN:-}" ]; then \
probe=0; \
"$(BRIDGE_CONSENT_LAUNCHER)" -c 'import sys; sys.exit(0 if sys.stdin.isatty() and sys.stdout.isatty() else 3)' || probe=$$?; \
if [ "$$probe" = "3" ]; then \
if [ "$(CONSENT_EXPLICIT)" = "1" ]; then \
echo "✗ APPROVE_CLAUDE_CONSENT=1 needs operator proof: run from a terminal or pass BRIDGE_OPERATOR_TOKEN from bridge_consent.py issue-token" >&2; \
exit 1; \
fi; \
echo " – consent unchanged: needs operator terminal or BRIDGE_OPERATOR_TOKEN"; \
exit 0; \
fi; \
fi; \
$(call stage,Record explicit Claude transfer consent); \
"$(BRIDGE_CONSENT_LAUNCHER)" "$(BRIDGE_CONSENT_SCRIPT)" grant --approve && \
"$(BRIDGE_CONSENT_LAUNCHER)" "$(BRIDGE_CONSENT_SCRIPT)" check
## OpenCode plugin targets -----------------------------------------------------
OPENCODE_RIG_DIR := $(PROJECT_DIR)/plugins/opencode-rig
rig-install:
@npm --prefix "$(OPENCODE_RIG_DIR)" install --no-audit --no-fund
rig-typecheck:
@npm --prefix "$(OPENCODE_RIG_DIR)" run typecheck
rig-test:
@npm --prefix "$(OPENCODE_RIG_DIR)" test
rig-format:
@npm --prefix "$(OPENCODE_RIG_DIR)" run format
rig-pack:
@npm --prefix "$(OPENCODE_RIG_DIR)" run pack:check
# Mirrors the package's CI gate: typecheck, formatting, Vitest suites, version sources,
# and the published tarball contract.
rig-check:
@npm --prefix "$(OPENCODE_RIG_DIR)" run check
## Benchmark retention -------------------------------------------------------
# benchmarks/results/ is gitignored and nothing reclaims it, so every run accumulates.
# Deletions are unrecoverable; the bare target only reports, and -apply deletes.
# Override the window with DAYS=90.
DAYS ?= 30
prune-benchmarks:
@python3 "$(PROJECT_DIR)/benchmarks/prune_results.py" --days $(DAYS)
prune-benchmarks-apply:
@python3 "$(PROJECT_DIR)/benchmarks/prune_results.py" --days $(DAYS) --apply