@mmerterden/multi-agent-pipeline 10.6.0 → 10.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +10 -39
  3. package/install/index.mjs +9 -101
  4. package/package.json +1 -1
  5. package/pipeline/commands/multi-agent/delete.md +4 -5
  6. package/pipeline/commands/multi-agent/refs/phases/phase-4-review.md +1 -11
  7. package/pipeline/commands/multi-agent/refs/picker-contract.md +6 -10
  8. package/pipeline/commands/multi-agent/setup.md +0 -1
  9. package/pipeline/commands/multi-agent/sync.md +4 -73
  10. package/pipeline/commands/multi-agent/update.md +9 -0
  11. package/pipeline/lib/ask-choice.sh +1 -1
  12. package/pipeline/scripts/smoke-cross-cli-behavior.sh +0 -7
  13. package/pipeline/scripts/smoke-install-layout.sh +1 -2
  14. package/pipeline/skills/.skills-index.json +65 -20
  15. package/pipeline/skills/shared/README.md +1 -1
  16. package/pipeline/skills/shared/core/multi-agent-delete/SKILL.md +4 -4
  17. package/pipeline/skills/skills-index.md +24 -19
  18. package/install/_adapters.mjs +0 -73
  19. package/pipeline/adapters/_base.mjs +0 -640
  20. package/pipeline/adapters/antigravity.mjs +0 -140
  21. package/pipeline/adapters/codex.mjs +0 -159
  22. package/pipeline/adapters/copilot-chat-orchestration.mjs +0 -148
  23. package/pipeline/adapters/copilot-chat.mjs +0 -124
  24. package/pipeline/adapters/cursor-orchestration.mjs +0 -152
  25. package/pipeline/adapters/cursor.mjs +0 -146
  26. package/pipeline/scripts/smoke-adapters.sh +0 -276
  27. package/pipeline/scripts/smoke-delete-flow.sh +0 -151
  28. package/pipeline/scripts/smoke-shared-runtime.sh +0 -108
  29. package/pipeline/scripts/smoke-sync-adapters.sh +0 -113
  30. package/pipeline/scripts/sync-adapters.mjs +0 -183
@@ -1,151 +0,0 @@
1
- #!/usr/bin/env bash
2
- # smoke-delete-flow.sh - v7.7.0 token-preserving uninstall.
3
- #
4
- # Round-trip integrity for `multi-agent uninstall`:
5
- # 1. --dry-run produces a preview without modifying the filesystem
6
- # 2. Real uninstall removes pipeline-managed files
7
- # 3. User-authored content survives (CLAUDE.md, preferences, marker-wrapped blocks)
8
- # 4. Selective targets (--cursor, --copilot-chat) only touch the requested tree
9
- # 5. Adapter `uninstall()` is invoked and its result reflects in the output
10
- # 6. (Bonus) Slash command + Copilot skill exist with byte-eq description (parity)
11
- #
12
- # We never invoke the uninstaller against $HOME - the test scaffolds an
13
- # isolated `--target` tree and exercises the adapter portion only. Claude /
14
- # Copilot uninstall paths are exercised via dry-run (which reports them).
15
-
16
- set -uo pipefail
17
-
18
- REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
19
- INSTALL="$REPO_ROOT/install.js"
20
- UNINSTALL="$REPO_ROOT/pipeline/scripts/uninstall.mjs"
21
-
22
- PASS=0
23
- FAIL=0
24
- pass() { PASS=$((PASS + 1)); echo " ✓ $1"; }
25
- fail() { FAIL=$((FAIL + 1)); echo " ✗ $1"; }
26
-
27
- cleanup() {
28
- [ -n "${TMP:-}" ] && rm -rf "$TMP"
29
- }
30
- trap cleanup EXIT
31
-
32
- # ──────────────────────────────────────────────────────────────────────────
33
- echo "→ 1. Slash command + Copilot peer exist"
34
- if [ -f "$REPO_ROOT/pipeline/commands/multi-agent/delete.md" ]; then
35
- pass "pipeline/commands/multi-agent/delete.md present"
36
- else
37
- fail "delete.md missing"
38
- fi
39
- if [ -f "$REPO_ROOT/pipeline/skills/shared/core/multi-agent-delete/SKILL.md" ]; then
40
- pass "pipeline/skills/shared/core/multi-agent-delete/SKILL.md present"
41
- else
42
- fail "multi-agent-delete SKILL.md missing"
43
- fi
44
-
45
- # ──────────────────────────────────────────────────────────────────────────
46
- echo "→ 2. --dry-run produces preview without side effects"
47
- TMP=$(mktemp -d)
48
- node "$INSTALL" --cursor --target="$TMP" >/dev/null 2>&1
49
- pre_count=$(find "$TMP" -type f | wc -l | tr -d ' ')
50
-
51
- OUTPUT=$(node "$UNINSTALL" --dry-run --yes --cursor --target="$TMP" 2>&1)
52
- post_count=$(find "$TMP" -type f | wc -l | tr -d ' ')
53
-
54
- if [ "$pre_count" = "$post_count" ] && [ "$post_count" -gt 100 ]; then
55
- pass "--dry-run preserved file tree ($post_count files)"
56
- else
57
- fail "--dry-run changed file tree (pre=$pre_count post=$post_count)"
58
- fi
59
-
60
- if echo "$OUTPUT" | grep -q "DRY-RUN"; then
61
- pass "--dry-run output mentions DRY-RUN banner"
62
- else
63
- fail "--dry-run banner missing from output"
64
- fi
65
-
66
- if echo "$OUTPUT" | grep -q "Personal access tokens were NOT touched"; then
67
- pass "--dry-run output reaffirms token preservation"
68
- else
69
- fail "token-preservation message missing from --dry-run output"
70
- fi
71
-
72
- # ──────────────────────────────────────────────────────────────────────────
73
- echo "→ 3. Real uninstall removes pipeline files"
74
- node "$UNINSTALL" --yes --cursor --target="$TMP" >/dev/null 2>&1
75
- remaining=$(find "$TMP" -type f | wc -l | tr -d ' ')
76
- if [ "$remaining" = "0" ]; then
77
- pass "real uninstall: 0 pipeline files remain (was $pre_count)"
78
- else
79
- fail "real uninstall left $remaining file(s)"
80
- fi
81
-
82
- # ──────────────────────────────────────────────────────────────────────────
83
- echo "→ 4. User content outside markers survives uninstall (copilot-chat)"
84
- rm -rf "$TMP" && TMP=$(mktemp -d)
85
- mkdir -p "$TMP/.github"
86
- USER_LINE="DO_NOT_TOUCH_$(date +%s)"
87
- echo "$USER_LINE" > "$TMP/.github/copilot-instructions.md"
88
- node "$INSTALL" --copilot-chat --target="$TMP" >/dev/null 2>&1
89
- node "$UNINSTALL" --yes --copilot-chat --target="$TMP" >/dev/null 2>&1
90
-
91
- if grep -q "$USER_LINE" "$TMP/.github/copilot-instructions.md" 2>/dev/null; then
92
- pass "user content outside markers survived uninstall"
93
- else
94
- fail "user content lost during uninstall"
95
- fi
96
-
97
- # ──────────────────────────────────────────────────────────────────────────
98
- echo "→ 5. Selective target - --copilot-chat doesn't touch cursor tree"
99
- rm -rf "$TMP" && TMP=$(mktemp -d)
100
- node "$INSTALL" --cursor --target="$TMP" >/dev/null 2>&1
101
- node "$INSTALL" --copilot-chat --target="$TMP" >/dev/null 2>&1
102
-
103
- # Now uninstall ONLY copilot-chat; cursor tree should remain
104
- node "$UNINSTALL" --yes --copilot-chat --target="$TMP" >/dev/null 2>&1
105
-
106
- cursor_remaining=$(find "$TMP/.cursor" -type f 2>/dev/null | wc -l | tr -d ' ')
107
- copilot_remaining=$(find "$TMP/.github/instructions" -name "multi-agent-*" 2>/dev/null | wc -l | tr -d ' ')
108
-
109
- if [ "$cursor_remaining" -gt 0 ]; then
110
- pass "selective uninstall: cursor tree untouched ($cursor_remaining files)"
111
- else
112
- fail "selective uninstall: cursor tree was deleted"
113
- fi
114
-
115
- if [ "$copilot_remaining" = "0" ]; then
116
- pass "selective uninstall: copilot-chat per-skill files cleaned"
117
- else
118
- fail "selective uninstall: $copilot_remaining copilot-chat file(s) remain"
119
- fi
120
-
121
- # ──────────────────────────────────────────────────────────────────────────
122
- echo "→ 6. CLI router - 'uninstall' and 'delete' both reach the script"
123
- OUT_UNINSTALL=$(node "$REPO_ROOT/index.js" uninstall --dry-run --yes --cursor --target=/tmp/fake 2>&1 || true)
124
- OUT_DELETE=$(node "$REPO_ROOT/index.js" delete --dry-run --yes --cursor --target=/tmp/fake 2>&1 || true)
125
-
126
- if echo "$OUT_UNINSTALL" | grep -q "multi-agent-pipeline uninstaller"; then
127
- pass "'index.js uninstall' invokes the uninstaller"
128
- else
129
- fail "'index.js uninstall' did not reach uninstaller"
130
- fi
131
-
132
- if echo "$OUT_DELETE" | grep -q "multi-agent-pipeline uninstaller"; then
133
- pass "'index.js delete' alias also invokes the uninstaller"
134
- else
135
- fail "'index.js delete' alias did not reach uninstaller"
136
- fi
137
-
138
- # ──────────────────────────────────────────────────────────────────────────
139
- echo "→ 7. Token preservation - uninstaller never invokes credential-store"
140
- # Static check: uninstall.mjs must not import credential-store.sh or call
141
- # `security delete-generic-password` / `cmdkey /delete` / `secret-tool clear`.
142
- if grep -qE "credential-store|delete-generic-password|cmdkey /delete|secret-tool clear|secret-tool del" "$UNINSTALL"; then
143
- fail "uninstall.mjs references credential-store deletion APIs (token leak risk)"
144
- else
145
- pass "uninstall.mjs has no credential-store deletion references"
146
- fi
147
-
148
- # ──────────────────────────────────────────────────────────────────────────
149
- echo ""
150
- echo "══ delete-flow smoke: $PASS passed, $FAIL failed ══"
151
- [ "$FAIL" -eq 0 ]
@@ -1,108 +0,0 @@
1
- #!/usr/bin/env bash
2
- # smoke-shared-runtime.sh - the 3 adapter platforms (Cursor, Antigravity, VS
3
- # Code Copilot Chat) must install the gate scripts to a shared ~/.multi-agent/
4
- # runtime AND rewrite their emitted agent/command references to that absolute
5
- # path, so the deterministic gates can actually execute (not just be referenced).
6
- #
7
- # Runs in a sandbox HOME so the developer's real ~/.multi-agent is untouched.
8
- # Exit 0 = all pass, 1 = any failure.
9
-
10
- set -uo pipefail
11
-
12
- ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
13
- PASS=0; FAIL=0
14
- pass() { PASS=$((PASS+1)); echo " ✓ $1"; }
15
- fail() { FAIL=$((FAIL+1)); echo " ✗ $1"; }
16
- command -v node >/dev/null 2>&1 || { echo "error: node required" >&2; exit 127; }
17
-
18
- run_adapter() {
19
- # $1 = adapter module path, $2 = sandbox HOME, $3 = target
20
- HOME="$2" TARGET="$3" node -e '
21
- import(process.argv[1]).then(m => {
22
- m.default.install({ pipelineSrc: process.cwd()+"/pipeline", target: process.env.TARGET, platformFilter: "all" });
23
- }).catch(e => { console.error(e.message); process.exit(1); });
24
- ' "$1" >/dev/null 2>&1
25
- }
26
- uninstall_adapter() {
27
- HOME="$2" TARGET="$3" node -e '
28
- import(process.argv[1]).then(m => { m.default.uninstall({ target: process.env.TARGET }); }).catch(()=>process.exit(1));
29
- ' "$1" >/dev/null 2>&1
30
- }
31
-
32
- for adapter in cursor antigravity copilot-chat; do
33
- echo "→ $adapter"
34
- SBHOME=$(mktemp -d); TGT=$(mktemp -d); git init -q "$TGT" 2>/dev/null
35
- run_adapter "$ROOT/pipeline/adapters/$adapter.mjs" "$SBHOME" "$TGT"
36
-
37
- # 1. shared runtime populated with the gate scripts + lib + schemas
38
- if [ -f "$SBHOME/.multi-agent/scripts/evidence-gate.mjs" ] \
39
- && [ -f "$SBHOME/.multi-agent/scripts/learnings-ledger.mjs" ] \
40
- && [ -f "$SBHOME/.multi-agent/lib/classify-intent.sh" ] \
41
- && [ -f "$SBHOME/.multi-agent/schemas/triage-output.schema.json" ]; then
42
- pass "$adapter: gate scripts installed to shared runtime"
43
- else
44
- fail "$adapter: shared runtime missing gate scripts"
45
- fi
46
-
47
- # 2. dev-only / PII files NOT copied into the shared runtime
48
- if [ -f "$SBHOME/.multi-agent/scripts/figma-placeholder-map.json" ] \
49
- || [ -f "$SBHOME/.multi-agent/scripts/smoke-personal-data.sh" ]; then
50
- fail "$adapter: dev-only/PII file leaked into shared runtime"
51
- else
52
- pass "$adapter: dev-only/PII files excluded from shared runtime"
53
- fi
54
-
55
- # 3. The ORCHESTRATION files (agents + command/prompt/workflow - NOT the
56
- # knowledge-layer rules, which legitimately mention pipeline/scripts in
57
- # prose) reference the absolute runtime path and carry no bare,
58
- # non-resolving pipeline/scripts invocation.
59
- case "$adapter" in
60
- cursor) orch=$(ls "$TGT/.cursor/agents/"*.md "$TGT/.cursor/commands/multi-agent.md" 2>/dev/null) ;;
61
- antigravity) orch=$(ls "$TGT/.agent/workflows/multi-agent.md" 2>/dev/null) ;;
62
- copilot-chat) orch=$(ls "$TGT/.github/agents/"*.agent.md "$TGT/.github/prompts/multi-agent.prompt.md" 2>/dev/null) ;;
63
- esac
64
- if [ -n "$orch" ]; then
65
- if grep -qF '$HOME/.multi-agent/' $orch 2>/dev/null; then
66
- pass "$adapter: orchestration files reference the shared runtime path"
67
- else
68
- fail "$adapter: no shared-runtime reference in orchestration files"
69
- fi
70
- if grep -qE '[^.a-z/]pipeline/scripts/' $orch 2>/dev/null; then
71
- fail "$adapter: a bare pipeline/scripts/ invocation survived (won't resolve)"
72
- else
73
- pass "$adapter: no unresolved bare pipeline/scripts/ invocation"
74
- fi
75
- else
76
- fail "$adapter: no orchestration files found"
77
- fi
78
-
79
- # 3b. cross-vendor 2-model review: a second reviewer pinned to a different
80
- # model (cursor + copilot-chat; antigravity is dropdown-selected so it
81
- # documents the pair in the workflow instead).
82
- case "$adapter" in
83
- cursor)
84
- r1=$(grep '^model:' "$TGT/.cursor/agents/ma-code-reviewer.md" 2>/dev/null)
85
- r2=$(grep '^model:' "$TGT/.cursor/agents/ma-code-reviewer-x.md" 2>/dev/null)
86
- if [ -n "$r2" ] && [ "$r1" != "$r2" ]; then pass "cursor: two reviewers on distinct models ($r1 vs $r2)"; else fail "cursor: missing distinct cross-model reviewer"; fi ;;
87
- copilot-chat)
88
- r1=$(grep '^model:' "$TGT/.github/agents/ma-code-reviewer.agent.md" 2>/dev/null)
89
- r2=$(grep '^model:' "$TGT/.github/agents/ma-code-reviewer-x.agent.md" 2>/dev/null)
90
- if [ -n "$r2" ] && [ "$r1" != "$r2" ]; then pass "copilot-chat: two reviewers on distinct models"; else fail "copilot-chat: missing distinct cross-model reviewer"; fi ;;
91
- antigravity)
92
- if grep -qi "different model" "$TGT/.agent/workflows/multi-agent.md" 2>/dev/null; then pass "antigravity: workflow documents diverse-model parallel reviewers"; else fail "antigravity: workflow missing diverse-model instruction"; fi ;;
93
- esac
94
-
95
- # 4. uninstall removes the shared runtime
96
- uninstall_adapter "$ROOT/pipeline/adapters/$adapter.mjs" "$SBHOME" "$TGT"
97
- if [ -d "$SBHOME/.multi-agent" ]; then
98
- fail "$adapter: uninstall left the shared runtime behind"
99
- else
100
- pass "$adapter: uninstall removed the shared runtime"
101
- fi
102
-
103
- rm -rf "$SBHOME" "$TGT"
104
- done
105
-
106
- echo ""
107
- echo "══ shared-runtime smoke: $PASS passed, $FAIL failed ══"
108
- [ "$FAIL" -eq 0 ]
@@ -1,113 +0,0 @@
1
- #!/usr/bin/env bash
2
- # smoke-sync-adapters.sh
3
- #
4
- # Verifies the per-project adapter sync runner:
5
- # 1. pipeline/scripts/sync-adapters.mjs exists and is executable
6
- # 2. node sync-adapters.mjs --help exits 0
7
- # 3. --doctor on empty prefs reports a clean no-op
8
- # 4. --target=<empty-dir> reports [skip] (no adapter markers)
9
- # 5. --target=. on the pipeline repo itself triggers cursor adapter
10
- # (pipeline + .git makes cwd the canonical cursor consumer)
11
- # 6. After running on pipeline repo, .cursor/rules/ exists and has
12
- # >100 .mdc files
13
- # 7. .cursorrules legacy digest exists
14
- # 8. Unknown arg exits non-zero
15
- # 9. sync.md references sync-adapters.mjs (so the docs and the runtime agree)
16
- #
17
- # Exit 0 = all pass, 1 = any failure.
18
-
19
- set -uo pipefail
20
-
21
- ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
22
- RUNNER="$ROOT/pipeline/scripts/sync-adapters.mjs"
23
- SYNC_MD="$ROOT/pipeline/commands/multi-agent/sync.md"
24
-
25
- pass=0
26
- fail=0
27
- failures=()
28
- record_pass() { pass=$((pass + 1)); printf ' \033[0;32mPASS\033[0m %s\n' "$1"; }
29
- record_fail() { fail=$((fail + 1)); failures+=("$1"); printf ' \033[0;31mFAIL\033[0m %s\n' "$1"; }
30
-
31
- printf '→ smoke-sync-adapters: per-project adapter sync runner contract\n'
32
-
33
- # 1. Runner exists + executable
34
- if [ ! -f "$RUNNER" ]; then
35
- record_fail "pipeline/scripts/sync-adapters.mjs missing"
36
- elif [ ! -x "$RUNNER" ]; then
37
- record_fail "sync-adapters.mjs not executable"
38
- else
39
- record_pass "sync-adapters.mjs exists and is executable"
40
- fi
41
-
42
- # 2. --help exits 0
43
- if node "$RUNNER" --help >/dev/null 2>&1; then
44
- record_pass "--help exits 0"
45
- else
46
- record_fail "--help should exit 0"
47
- fi
48
-
49
- # 3. --doctor on empty env runs without error
50
- if node "$RUNNER" --doctor 2>&1 | grep -qE "projectsTouched is empty|sync-adapters: 0 ok"; then
51
- record_pass "--doctor reports clean state when projectsTouched is empty"
52
- else
53
- record_pass "--doctor exits cleanly" # doctor on a populated env is also fine
54
- fi
55
-
56
- # 4. --target=<empty-dir>
57
- EMPTY=$(mktemp -d)
58
- out=$(node "$RUNNER" --target="$EMPTY" 2>&1)
59
- if echo "$out" | grep -q "\[skip\]"; then
60
- record_pass "empty target reports [skip]"
61
- else
62
- record_fail "empty target should report [skip] (got: $out)"
63
- fi
64
- rm -rf "$EMPTY"
65
-
66
- # 5. Pipeline repo cursor sync - must produce output mentioning cursor
67
- out=$(node "$RUNNER" --target="$ROOT" 2>&1)
68
- if echo "$out" | grep -qE "\[cursor\]"; then
69
- record_pass "pipeline repo target triggers cursor adapter"
70
- else
71
- record_fail "pipeline repo target did NOT trigger cursor adapter (got: $out)"
72
- fi
73
-
74
- # 6. .cursor/rules/ exists with >100 mdc
75
- if [ -d "$ROOT/.cursor/rules" ]; then
76
- count=$(ls "$ROOT/.cursor/rules"/*.mdc 2>/dev/null | wc -l | tr -d ' ')
77
- if [ "$count" -gt 100 ]; then
78
- record_pass ".cursor/rules contains $count .mdc files (>100 expected)"
79
- else
80
- record_fail ".cursor/rules has only $count files (expected >100)"
81
- fi
82
- else
83
- record_fail ".cursor/rules dir missing"
84
- fi
85
-
86
- # 7. Legacy .cursorrules
87
- if [ -f "$ROOT/.cursorrules" ]; then
88
- record_pass ".cursorrules legacy digest exists"
89
- else
90
- record_fail ".cursorrules legacy digest missing"
91
- fi
92
-
93
- # 8. Unknown arg
94
- if node "$RUNNER" --garbage >/dev/null 2>&1; then
95
- record_fail "unknown arg should exit non-zero"
96
- else
97
- record_pass "unknown arg rejected"
98
- fi
99
-
100
- # 9. sync.md references the runner
101
- if grep -qF "sync-adapters.mjs" "$SYNC_MD"; then
102
- record_pass "sync.md references sync-adapters.mjs"
103
- else
104
- record_fail "sync.md missing sync-adapters.mjs reference"
105
- fi
106
-
107
- printf '\n══ sync-adapters smoke: %d passed, %d failed ══\n' "$pass" "$fail"
108
- if [ "$fail" -gt 0 ]; then
109
- printf '\nFailures:\n'
110
- for msg in "${failures[@]}"; do printf ' - %s\n' "$msg"; done
111
- exit 1
112
- fi
113
- exit 0
@@ -1,183 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * sync-adapters.mjs - wire the orphaned per-project adapter installers
4
- * (cursor / copilot-chat) into the sync flow.
5
- *
6
- * Three call shapes:
7
- *
8
- * 1. Single target (this repo, or any project root):
9
- * node sync-adapters.mjs --target=. [--platform=ios|android|all]
10
- *
11
- * 2. All projects registered in prefs.global.projectsTouched[]:
12
- * node sync-adapters.mjs --all
13
- *
14
- * 3. Doctor - show what would happen, no writes:
15
- * node sync-adapters.mjs --doctor
16
- *
17
- * Discovery contract:
18
- * - --target=. is the cwd; --target=<path> overrides.
19
- * - --all reads prefs.global.projectsTouched[] (LRU). Skips entries
20
- * whose path no longer exists.
21
- * - For each target, the script inspects the per-project marker files
22
- * (.cursor/, .cursorrules, .copilot/, .github/copilot-instructions.md)
23
- * and only runs the matching adapter if the marker is present -
24
- * a project that never set up Cursor won't get .cursor/rules/ filled
25
- * in by sync. This mirrors Step 2b's "setup-then-sync" contract.
26
- *
27
- * Source layout: this script is the bridge between sync.md Step 2b
28
- * (which used to reference a non-existent template path) and the
29
- * adapters under pipeline/adapters/{cursor,copilot-chat}.mjs.
30
- */
31
-
32
- import { existsSync, readFileSync } from "node:fs";
33
- import { dirname, join, resolve } from "node:path";
34
- import { fileURLToPath } from "node:url";
35
-
36
- const __dirname = dirname(fileURLToPath(import.meta.url));
37
- const PIPELINE_ROOT = resolve(__dirname, "..");
38
-
39
- // --- arg parser ------------------------------------------------------------
40
-
41
- const argv = process.argv.slice(2);
42
- const args = { target: null, all: false, doctor: false, platform: "all" };
43
- for (const a of argv) {
44
- if (a === "--all") args.all = true;
45
- else if (a === "--doctor") args.doctor = true;
46
- else if (a.startsWith("--target=")) args.target = a.slice("--target=".length);
47
- else if (a.startsWith("--platform=")) args.platform = a.slice("--platform=".length);
48
- else if (a === "--help" || a === "-h") {
49
- console.log(
50
- "usage: sync-adapters.mjs --target=<path> single project\n" +
51
- " sync-adapters.mjs --all every project in projectsTouched\n" +
52
- " sync-adapters.mjs --doctor dry-run summary",
53
- );
54
- process.exit(0);
55
- } else {
56
- console.error(`sync-adapters: unknown arg ${a}`);
57
- process.exit(2);
58
- }
59
- }
60
-
61
- if (!args.target && !args.all && !args.doctor) {
62
- // Default to current dir - most common case (sync running from the
63
- // pipeline repo itself).
64
- args.target = ".";
65
- }
66
-
67
- // --- helpers ---------------------------------------------------------------
68
-
69
- function loadPrefs() {
70
- const home = process.env.HOME || process.env.USERPROFILE;
71
- if (!home) return { global: { projectsTouched: [] } };
72
- const p = join(home, ".claude", "multi-agent-preferences.json");
73
- if (!existsSync(p)) return { global: { projectsTouched: [] } };
74
- try {
75
- return JSON.parse(readFileSync(p, "utf-8"));
76
- } catch {
77
- return { global: { projectsTouched: [] } };
78
- }
79
- }
80
-
81
- function detectAdapters(target) {
82
- const out = [];
83
- if (existsSync(join(target, ".cursor")) || existsSync(join(target, ".cursorrules"))) out.push("cursor");
84
- if (existsSync(join(target, ".copilot")) || existsSync(join(target, ".github/copilot-instructions.md"))) out.push("copilot-chat");
85
- if (existsSync(join(target, ".agent")) || existsSync(join(target, "AGENTS.md"))) out.push("antigravity");
86
- return out;
87
- }
88
-
89
- async function runAdapter(name, target) {
90
- const adapterPath = join(PIPELINE_ROOT, "adapters", `${name}.mjs`);
91
- if (!existsSync(adapterPath)) {
92
- console.error(`sync-adapters: adapter not found at ${adapterPath}`);
93
- return false;
94
- }
95
- const mod = await import(adapterPath);
96
- if (typeof mod.install !== "function") {
97
- console.error(`sync-adapters: adapter ${name} has no install() export`);
98
- return false;
99
- }
100
- mod.install({ pipelineSrc: PIPELINE_ROOT, target, platformFilter: args.platform });
101
- return true;
102
- }
103
-
104
- async function syncTarget(target) {
105
- const absTarget = resolve(target);
106
- if (!existsSync(absTarget)) {
107
- console.error(`sync-adapters: target ${absTarget} does not exist`);
108
- return false;
109
- }
110
- // Always-on first-time install signal: if the target is the pipeline
111
- // repo itself (has pipeline/ + .git/), we ALWAYS install Cursor since
112
- // the maintainer is the canonical cursor consumer.
113
- const isPipelineRepo =
114
- existsSync(join(absTarget, "pipeline")) && existsSync(join(absTarget, ".git"));
115
- const adapters = detectAdapters(absTarget);
116
- if (isPipelineRepo && !adapters.includes("cursor")) adapters.push("cursor");
117
- if (adapters.length === 0) {
118
- console.log(` [skip] ${absTarget} - no adapter markers (.cursor/.cursorrules/.copilot)`);
119
- return true;
120
- }
121
- console.log(`→ ${absTarget}`);
122
- for (const a of adapters) {
123
- if (args.doctor) {
124
- console.log(` [doctor] would run ${a} adapter`);
125
- } else {
126
- try {
127
- await runAdapter(a, absTarget);
128
- } catch (e) {
129
- console.error(` [error] ${a}: ${e.message}`);
130
- }
131
- }
132
- }
133
- return true;
134
- }
135
-
136
- // --- main ------------------------------------------------------------------
137
-
138
- const targets = [];
139
- if (args.all || args.doctor) {
140
- const prefs = loadPrefs();
141
- for (const entry of prefs.global?.projectsTouched ?? []) {
142
- if (entry && entry.path) targets.push(entry.path);
143
- }
144
- if (targets.length === 0) {
145
- console.log(
146
- "sync-adapters: prefs.global.projectsTouched is empty - run multi-agent:setup in a project first",
147
- );
148
- }
149
- }
150
- if (args.target) targets.push(args.target);
151
-
152
- let ok = 0;
153
- let failed = 0;
154
- for (const t of targets) {
155
- if (await syncTarget(t)) ok++;
156
- else failed++;
157
- }
158
-
159
- // Codex CLI is a GLOBAL-config tool (prompts + MCP under ~/.codex, no per-project
160
- // prompt dir), so it is synced once - not per target. Run it when Codex is
161
- // present on this machine.
162
- const codexHome = (() => {
163
- const home = process.env.HOME || process.env.USERPROFILE;
164
- return home ? join(home, ".codex") : null;
165
- })();
166
- if (codexHome && existsSync(codexHome)) {
167
- if (args.doctor) {
168
- console.log("→ ~/.codex (global)\n [doctor] would run codex adapter (global)");
169
- } else {
170
- console.log("→ ~/.codex (global)");
171
- try {
172
- const mod = await import(join(PIPELINE_ROOT, "adapters", "codex.mjs"));
173
- mod.install({ pipelineSrc: PIPELINE_ROOT, platformFilter: args.platform });
174
- ok++;
175
- } catch (e) {
176
- console.error(` [error] codex: ${e.message}`);
177
- failed++;
178
- }
179
- }
180
- }
181
-
182
- console.log(`sync-adapters: ${ok} ok · ${failed} failed`);
183
- process.exit(failed > 0 ? 1 : 0);