@mmerterden/multi-agent-pipeline 11.5.0 → 12.0.0
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.
- package/CHANGELOG.md +54 -0
- package/docs/adr/0007-multi-tool-adapter-framework.md +2 -2
- package/install/claude.mjs +25 -1
- package/install/index.mjs +34 -1
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/analysis/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/autopilot/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/build-optimize/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/channels/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/create-jira/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/dev/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/dev-local/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/diff-explain/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/finish/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +29 -3
- package/pipeline/commands/multi-agent/help/SKILL.md +3 -2
- package/pipeline/commands/multi-agent/issue/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/jira/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/kill/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/language/SKILL.md +20 -1
- package/pipeline/commands/multi-agent/local/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/log/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/manual-test/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/prune-logs/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/purge/SKILL.md +27 -24
- package/pipeline/commands/multi-agent/refactor/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/resume/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/review/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/review-issue/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/review-jira/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/scan/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/search/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/setup/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/stack/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/status/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/sync/SKILL.md +20 -2
- package/pipeline/commands/multi-agent/test/SKILL.md +1 -0
- package/pipeline/commands/multi-agent/uninstall/SKILL.md +87 -0
- package/pipeline/commands/multi-agent/update/SKILL.md +1 -0
- package/pipeline/multi-agent-refs/cross-cli-contract.md +5 -5
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +12 -1
- package/pipeline/scripts/fixtures/install-layout.tsv +4 -2
- package/pipeline/scripts/gc-tmp.sh +67 -8
- package/pipeline/scripts/gc-worktrees.sh +140 -0
- package/pipeline/scripts/localize-commands.mjs +130 -0
- package/pipeline/scripts/prune-logs.sh +100 -15
- package/pipeline/scripts/purge.sh +235 -0
- package/pipeline/scripts/smoke-description-tr.sh +82 -0
- package/pipeline/scripts/smoke-gc-tmp.sh +50 -4
- package/pipeline/scripts/smoke-gc-worktrees.sh +125 -0
- package/pipeline/scripts/smoke-prune-logs.sh +57 -7
- package/pipeline/scripts/smoke-purge.sh +138 -0
- package/pipeline/scripts/smoke-update-check.sh +13 -0
- package/pipeline/scripts/uninstall.mjs +41 -6
- package/pipeline/scripts/update-check.sh +9 -0
- package/pipeline/skills/.skill-manifest.json +7 -7
- package/pipeline/skills/.skills-index.json +9 -9
- package/pipeline/skills/shared/README.md +19 -10
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +1 -0
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-uninstall/SKILL.md +74 -0
- package/pipeline/skills/skills-index.md +1 -1
- package/pipeline/commands/multi-agent/delete/SKILL.md +0 -67
- package/pipeline/skills/shared/core/multi-agent-delete/SKILL.md +0 -66
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: "Select the active stack for this repo by enabling the matching marketplace plugin(s) in .claude/settings.json (ios/android/mobile/backend/frontend/fullstack/all)."
|
|
3
|
+
description-tr: "Bu repo için aktif stack'i seçer: eşleşen marketplace plugin'lerini .claude/settings.json'da etkinleştirir (ios/android/mobile/backend/frontend/fullstack/all)."
|
|
3
4
|
allowed-tools: Bash, Read, Edit, Write
|
|
4
5
|
---
|
|
5
6
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: "One-shot sync of the entire multi-agent ecosystem: Claude Code, Copilot CLI, pipeline repo, and website."
|
|
3
|
+
description-tr: "Tüm multi-agent ekosisteminin tek atımlık senkronu: Claude Code, Copilot CLI, pipeline repo ve website."
|
|
3
4
|
argument-hint: "[release] [--platform=<macos|linux|windows>]"
|
|
4
5
|
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion, WebFetch
|
|
5
6
|
---
|
|
@@ -130,6 +131,23 @@ If nothing is stale → report "All targets up to date" and stop.
|
|
|
130
131
|
~/multi-agent-pipeline/pipeline/commands/ 2>/dev/null \
|
|
131
132
|
&& { echo "ABORT: corporate reference leaked into pipeline/commands"; exit 1; } || true
|
|
132
133
|
```
|
|
134
|
+
- **Localized picker descriptions are an installed-tree artifact.** When
|
|
135
|
+
`outputLanguage=tr`, the installed `description:` values are Turkish and a
|
|
136
|
+
`description-en:` sidecar holds the English original (see
|
|
137
|
+
`localize-commands.mjs`). The repo must ALWAYS receive the English form:
|
|
138
|
+
after copying commands into `pipeline/commands/`, normalize the copy and
|
|
139
|
+
verify no sidecar or Turkish description leaked:
|
|
140
|
+
```bash
|
|
141
|
+
# normalize: restore English descriptions, strip description-en sidecars
|
|
142
|
+
node ~/multi-agent-pipeline/pipeline/scripts/localize-commands.mjs restore \
|
|
143
|
+
--dir ~/multi-agent-pipeline/pipeline/commands/multi-agent
|
|
144
|
+
# backstop: no sidecar may survive into the repo
|
|
145
|
+
grep -rl "^description-en:" ~/multi-agent-pipeline/pipeline/commands/ 2>/dev/null \
|
|
146
|
+
&& { echo "ABORT: description-en sidecar leaked into pipeline/commands"; exit 1; } || true
|
|
147
|
+
# backstop: repo description lines must be English (Turkish-diacritics proxy)
|
|
148
|
+
grep -rn '^description:.*[çğıöşüÇĞİÖŞÜ]' ~/multi-agent-pipeline/pipeline/commands/ 2>/dev/null \
|
|
149
|
+
&& { echo "ABORT: localized description leaked into pipeline/commands"; exit 1; } || true
|
|
150
|
+
```
|
|
133
151
|
|
|
134
152
|
6. **Cross-platform smoke gate** (final step of the REPO sync, before push):
|
|
135
153
|
```bash
|
|
@@ -242,11 +260,11 @@ This runs on the Claude <-> Copilot axis — the two CLIs the pipeline supports
|
|
|
242
260
|
**38 commands are synced** (canonical inventory - must match `cross-cli-contract.md` section 1; drift = contract violation):
|
|
243
261
|
|
|
244
262
|
```
|
|
245
|
-
analysis, analysis-resolve, autopilot, build-optimize, channels, create-jira,
|
|
263
|
+
analysis, analysis-resolve, autopilot, build-optimize, channels, create-jira, dev,
|
|
246
264
|
dev-autopilot, dev-local, dev-local-autopilot, diff-explain, finish, garbage-collect,
|
|
247
265
|
help, issue, jira, kill, language, local,
|
|
248
266
|
local-autopilot, log, manual-test, prune-logs, purge, refactor, resume, review, review-issue, review-jira,
|
|
249
|
-
scan, search, setup, stack, status, sync, test, update
|
|
267
|
+
scan, search, setup, stack, status, sync, test, uninstall, update
|
|
250
268
|
```
|
|
251
269
|
|
|
252
270
|
**NOT synced**: `$HOME/.claude/multi-agent-refs/*` - lazy-load references, Claude Code specific
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: "UI Bug Hunter - iOS Simulator (simctl) + Android Emulator (adb). Auto-detects platform. Screenshot + tap + analyze on the booted device. The Phase 5 Manual Test flow lives at /multi-agent:manual-test."
|
|
3
|
+
description-tr: "UI Bug Hunter - iOS Simulator (simctl) + Android Emulator (adb). Platformu otomatik algılar. Açık cihazda screenshot + tap + analiz. Faz 5 Manuel Test akışı /multi-agent:manual-test'te."
|
|
3
4
|
argument-hint: '[scenario] - e.g. "dark mode" | "accessibility" | "dynamic type" | "screenshot tr" | "store-ready" | (empty = full sweep)'
|
|
4
5
|
---
|
|
5
6
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Uninstall the pipeline from Claude Code + Copilot CLI. Keychain access tokens are always left untouched; --all-data also clears pipeline settings and logs. Asks for double confirmation."
|
|
3
|
+
description-tr: "Pipeline'ı Claude Code + Copilot CLI'dan kaldırır. Keychain erişim token'larına asla dokunulmaz; --all-data ayrıca pipeline ayarlarını ve loglarını da siler. Çift onay ister."
|
|
4
|
+
argument-hint: "[--dry-run] [--all-data] [--claude] [--copilot] [--target=<path>]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# multi-agent uninstall - Token-Preserving Uninstall
|
|
8
|
+
|
|
9
|
+
Uninstalls the pipeline itself from the system. **This is different from `:purge`:**
|
|
10
|
+
|
|
11
|
+
| Command | What it removes |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `:purge` | Active tasks' worktrees / branches / logs / state (development data) |
|
|
14
|
+
| `:uninstall` | The pipeline itself: skills, commands, agents, scripts - from every CLI |
|
|
15
|
+
|
|
16
|
+
## Two modes
|
|
17
|
+
|
|
18
|
+
| Mode | Flag | Removes | Keeps |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
| **Standard** (default) | (none) | distributed files: commands, skills, agents, scripts, refs, schemas, lib | tokens, `CLAUDE.md`, `rules/`, `multi-agent-preferences.json`, all logs |
|
|
21
|
+
| **Full cleanup** | `--all-data` | standard PLUS `multi-agent-preferences.json` + `~/.claude/logs/multi-agent/` (task logs, state, metrics, audit) | tokens, `CLAUDE.md`, `rules/` (user-owned) |
|
|
22
|
+
|
|
23
|
+
Tokens in the OS credential store are NEVER removed by either mode.
|
|
24
|
+
|
|
25
|
+
## Never deleted (either mode)
|
|
26
|
+
|
|
27
|
+
- **Keychain / Credential Manager / libsecret** personal access tokens (GitHub, Jira, Bitbucket, Confluence, Telegram, Vercel, Firebase, etc.)
|
|
28
|
+
- `~/.claude/CLAUDE.md` (user customisations)
|
|
29
|
+
- `~/.claude/rules/` (user-owned; installed write-if-missing, never overwritten)
|
|
30
|
+
- User content OUTSIDE the `<!-- multi-agent-pipeline:begin/end -->` markers (`copilot-instructions.md`)
|
|
31
|
+
|
|
32
|
+
Settings (`multi-agent-preferences.json`) and the log root (`~/.claude/logs/multi-agent/`) are kept by default and removed only when the user picks full cleanup (`--all-data`).
|
|
33
|
+
|
|
34
|
+
## Steps
|
|
35
|
+
|
|
36
|
+
1. **Determine scope** - parse `$ARGUMENTS`:
|
|
37
|
+
- No target flag: every installed target (default)
|
|
38
|
+
- `--claude / --copilot`: selective
|
|
39
|
+
- `--all-data`: full cleanup (also removes settings + logs)
|
|
40
|
+
- `--target=<path>`: adapter target (default: cwd)
|
|
41
|
+
- `--dry-run`: report only, delete nothing
|
|
42
|
+
|
|
43
|
+
2. **Ask the cleanup depth** (skip if the user already passed `--all-data` or `--dry-run`) - native `AskUserQuestion` picker in `outputLanguage`:
|
|
44
|
+
- `question`: "How much should uninstall remove? Tokens are preserved either way."
|
|
45
|
+
- `header`: "Scope" · `options`:
|
|
46
|
+
`{ label: "Standard", description: "Remove pipeline files; keep settings + logs" }`,
|
|
47
|
+
`{ label: "Full cleanup", description: "Also remove settings + all task logs/state/metrics" }`
|
|
48
|
+
- **Full cleanup** adds `--all-data` to the flag set below.
|
|
49
|
+
|
|
50
|
+
3. **Show a preview** (run the script with `--dry-run`, carrying the chosen flags):
|
|
51
|
+
```bash
|
|
52
|
+
node "$HOME/.claude/scripts/uninstall.mjs" --dry-run --yes <flags>
|
|
53
|
+
```
|
|
54
|
+
Show the output raw to the user. In full-cleanup mode the preview lists the extra settings + log removals.
|
|
55
|
+
|
|
56
|
+
4. **First confirmation** - native `AskUserQuestion` picker (no typed y/N):
|
|
57
|
+
- `question`: "Remove the pipeline from the listed targets? Tokens are NOT touched." (`outputLanguage`)
|
|
58
|
+
- `header`: "Uninstall" · `options`: `{ label: "Continue", description: "Proceed to the final confirmation" }`, `{ label: "Cancel", description: "Keep everything installed" }`
|
|
59
|
+
- **Cancel** → exit.
|
|
60
|
+
|
|
61
|
+
5. **Second confirmation** (paranoia gate, same pattern as `:purge`) - a second `AskUserQuestion` picker:
|
|
62
|
+
- `question`: full cleanup -> "This also erases settings + all logs and is irreversible. Proceed?"; standard -> "This is irreversible. Permanently remove the pipeline files now?" (`outputLanguage`)
|
|
63
|
+
- `header`: "Confirm" · `options`: `{ label: "Remove permanently", description: "Run the uninstall; tokens are preserved" }`, `{ label: "Cancel", description: "Abort, change nothing" }`
|
|
64
|
+
- Anything other than **Remove permanently** → cancel.
|
|
65
|
+
|
|
66
|
+
6. **Remove** - run the real script with `--yes` (and `--all-data` if full cleanup was chosen):
|
|
67
|
+
```bash
|
|
68
|
+
node "$HOME/.claude/scripts/uninstall.mjs" --yes <flags>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
7. **Confirm**:
|
|
72
|
+
```
|
|
73
|
+
✓ multi-agent-pipeline removed.
|
|
74
|
+
ℹ Personal access tokens preserved in Keychain.
|
|
75
|
+
```
|
|
76
|
+
Remind the user how to inspect the tokens (cross-platform - same command on every backend):
|
|
77
|
+
- `~/.claude/lib/credential-store.sh get "<service>"`
|
|
78
|
+
|
|
79
|
+
8. **Reinstall** (if the user wants to):
|
|
80
|
+
```bash
|
|
81
|
+
npx @mmerterden/multi-agent-pipeline install
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Notes
|
|
85
|
+
|
|
86
|
+
- This command does NOT remove the *installed copy* of the npm package. It only removes the distributed files. `npm uninstall -g @mmerterden/multi-agent-pipeline` is a separate step.
|
|
87
|
+
- `--dry-run` is always safe - zero side effects.
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
## 1. Command Inventory (38 commands)
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
analysis, analysis-resolve, autopilot, build-optimize, channels, create-jira,
|
|
12
|
+
analysis, analysis-resolve, autopilot, build-optimize, channels, create-jira, dev,
|
|
13
13
|
dev-autopilot, dev-local, dev-local-autopilot, diff-explain, finish, garbage-collect,
|
|
14
14
|
help, issue, jira, kill, language, local,
|
|
15
15
|
local-autopilot, log, manual-test, prune-logs, purge, refactor, resume, review, review-issue, review-jira,
|
|
16
|
-
scan, search, setup, stack, status, sync, test, update
|
|
16
|
+
scan, search, setup, stack, status, sync, test, uninstall, update
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
Categories:
|
|
@@ -23,7 +23,7 @@ Categories:
|
|
|
23
23
|
- **Full 8-phase modes**: `autopilot`, `local`, `local-autopilot`
|
|
24
24
|
- **Fast modes** (Init -> Dev(Opus) -> Commit -> Report): `dev`, `dev-autopilot`, `dev-local`, `dev-local-autopilot`
|
|
25
25
|
- **Tail modes** (run the pipeline tail over already-done local work): `finish`
|
|
26
|
-
- **Ops commands** (one-shot, no worktree): `status`, `log`, `kill`, `purge`, `
|
|
26
|
+
- **Ops commands** (one-shot, no worktree): `status`, `log`, `kill`, `purge`, `uninstall`, `resume`, `review`, `review-jira`, `review-issue`, `analysis`, `analysis-resolve`, `build-optimize`, `channels`, `scan`, `search`, `diff-explain`, `garbage-collect`, `prune-logs`
|
|
27
27
|
- **Meta-ops**: `setup`, `sync`, `update`, `help`, `refactor`, `test`, `stack`, `manual-test`, `language`
|
|
28
28
|
|
|
29
29
|
> **Inventory drift is a contract violation.** Adding a slash command under `pipeline/commands/multi-agent/` without updating this list + its counterpart Copilot dir (`pipeline/skills/shared/core/multi-agent-<cmd>/`) is a merge blocker. `smoke-commands-skills-parity.sh` enforces command ↔ skill directory parity; `smoke-cross-cli-behavior.sh` enforces behavior parity. This doc is the authoritative command list - bump the count + table together.
|
|
@@ -163,10 +163,10 @@ argument-hint: "<input hint>"
|
|
|
163
163
|
|
|
164
164
|
| Direction | Action |
|
|
165
165
|
|---|---|
|
|
166
|
-
| Claude → Copilot | Add `name`, `user-invocable: true`, `argument-hint`; remove `allowed-tools`;
|
|
166
|
+
| Claude → Copilot | Add `name`, `user-invocable: true`, `argument-hint`; remove `allowed-tools`; `description` stays English (localization happens only in the installed Claude tree via `description-tr` + `localize-commands.mjs`, never in repo or Copilot files) |
|
|
167
167
|
| Copilot → Claude | Remove `name`, `user-invocable`, `argument-hint`; add `allowed-tools` (inferred from command's actual tool calls) |
|
|
168
168
|
|
|
169
|
-
**Invariant**: the `description`
|
|
169
|
+
**Invariant**: the English `description` must have the same meaning on both sides, and a `description-tr` line (when present) must be a faithful translation of it. Semantic drift is not allowed. `description-en` sidecars are an installed-tree artifact and must never appear in repo or Copilot files.
|
|
170
170
|
|
|
171
171
|
---
|
|
172
172
|
|
|
@@ -425,7 +425,7 @@ git -C $PROJECT_ROOT config user.email "{identity.email}"
|
|
|
425
425
|
|
|
426
426
|
`worktreePath` = `$PROJECT_ROOT`, `localMode` = `true`.
|
|
427
427
|
|
|
428
|
-
**If normal mode** (worktree - default): 2. Worktree path: Jira → `.worktrees/{jiraId}/`, GitHub → `.worktrees/GH{issueNo}/`, free-text → `.worktrees/task-{shortId}/` 3. **Heal stale admin state first** (see "Worktree stale-lock heal" below), then `git -C $PROJECT_ROOT worktree add {path} -b {branch} origin/{baseBranch}` (if exists: enter, pull) 4. Set identity: `git -C {worktree-path} config user.name/email` 5. Create log dir + `agent-log.md` + `agent-state.json`:
|
|
428
|
+
**If normal mode** (worktree - default): 2. Worktree path: Jira → `.worktrees/{jiraId}/`, GitHub → `.worktrees/GH{issueNo}/`, free-text → `.worktrees/task-{shortId}/` 3. **Heal stale admin state first** (see "Worktree stale-lock heal" below) and **apply the residue guard** (see "Worktree residue guard" below), then `git -C $PROJECT_ROOT worktree add {path} -b {branch} origin/{baseBranch}` (if exists: enter, pull) 4. Set identity: `git -C {worktree-path} config user.name/email` 5. Create log dir + `agent-log.md` + `agent-state.json`:
|
|
429
429
|
|
|
430
430
|
**Worktree stale-lock heal (required before every `worktree add`):** a run killed mid-`worktree add` (OOM, SIGTERM, disk full) leaves a locked or broken admin entry under `.git/worktrees/{id}/`, so the retry fails with `fatal: '<path>' already exists`. Always run the heal first - it is a no-op on a clean repo:
|
|
431
431
|
|
|
@@ -438,6 +438,14 @@ fi
|
|
|
438
438
|
|
|
439
439
|
If the path is still registered and healthy after the heal, enter + pull instead of re-adding (existing behavior). Only when `worktree add` still fails after the heal do the rollback / collision flow in the multi-repo block below.
|
|
440
440
|
|
|
441
|
+
**Worktree residue guard (required once per repo, idempotent):** every worktree dir contains a `.git` file, so a blanket `git add -A` / `git add .` in the parent tree records `.worktrees/{id}` as a gitlink ("Subproject commit ...") that pollutes the branch and later needs a manual removal commit. Keep `.worktrees/` out of the index via the clone-local exclude file (never committed, so no PR noise in shared repos):
|
|
442
|
+
|
|
443
|
+
```bash
|
|
444
|
+
ex="$(git -C "$PROJECT_ROOT" rev-parse --path-format=absolute --git-common-dir)/info/exclude"
|
|
445
|
+
mkdir -p "$(dirname "$ex")"
|
|
446
|
+
grep -qxF '.worktrees/' "$ex" 2>/dev/null || printf '.worktrees/\n' >> "$ex"
|
|
447
|
+
```
|
|
448
|
+
|
|
441
449
|
**v2.1.0+ Multi-Repo Worktree Setup**:
|
|
442
450
|
|
|
443
451
|
When `state.projects[].length > 1`, repeat steps 2-4 **serially per repo** (worktrees are cheap; serial keeps git index sane and surfaces collisions one at a time):
|
|
@@ -448,6 +456,9 @@ for proj in "${PROJECTS[@]}"; do
|
|
|
448
456
|
git -C "$proj" worktree prune 2>/dev/null || true # heal stale admin state
|
|
449
457
|
git -C "$proj" worktree list --porcelain | grep -qF "$WT_PATH" \
|
|
450
458
|
&& git -C "$proj" worktree unlock "$WT_PATH" 2>/dev/null || true
|
|
459
|
+
ex="$(git -C "$proj" rev-parse --path-format=absolute --git-common-dir)/info/exclude"
|
|
460
|
+
mkdir -p "$(dirname "$ex")"
|
|
461
|
+
grep -qxF '.worktrees/' "$ex" 2>/dev/null || printf '.worktrees/\n' >> "$ex" # residue guard
|
|
451
462
|
git -C "$proj" worktree add "$WT_PATH" -b "$BRANCH" "origin/$BASE_BRANCH"
|
|
452
463
|
git -C "$WT_PATH" config user.name "${proj_identity_name}"
|
|
453
464
|
git -C "$WT_PATH" config user.email "${proj_identity_email}"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
.claude/.pipeline-version 1
|
|
1
2
|
.claude/CLAUDE.md 1
|
|
2
3
|
.claude/agents 8
|
|
3
4
|
.claude/commands 44
|
|
@@ -6,12 +7,13 @@
|
|
|
6
7
|
.claude/multi-agent-refs 51
|
|
7
8
|
.claude/rules 12
|
|
8
9
|
.claude/schemas 23
|
|
9
|
-
.claude/scripts
|
|
10
|
+
.claude/scripts 190
|
|
10
11
|
.claude/settings.json 1
|
|
11
12
|
.claude/skills 428
|
|
13
|
+
.copilot/.pipeline-version 1
|
|
12
14
|
.copilot/agents 8
|
|
13
15
|
.copilot/copilot-instructions.md 1
|
|
14
16
|
.copilot/lib 25
|
|
15
17
|
.copilot/schemas 23
|
|
16
|
-
.copilot/scripts
|
|
18
|
+
.copilot/scripts 190
|
|
17
19
|
.copilot/skills 467
|
|
@@ -6,8 +6,15 @@
|
|
|
6
6
|
# never cleaned up automatically (review.md traps its own, the rest linger), so
|
|
7
7
|
# a long-lived machine accumulates them. This sweeps the known prefixes.
|
|
8
8
|
#
|
|
9
|
-
# SAFE BY DEFAULT:
|
|
10
|
-
#
|
|
9
|
+
# SAFE BY DEFAULT:
|
|
10
|
+
# - dry-run: lists what WOULD be removed and the space it would free, and
|
|
11
|
+
# deletes nothing until you pass --yes.
|
|
12
|
+
# - root guard: the scratch root must be an existing directory and is
|
|
13
|
+
# refused when it resolves to /, $HOME, or a git work tree, so a stray
|
|
14
|
+
# GC_TMP_ROOT can never sweep a home dir or a repo checkout.
|
|
15
|
+
# - fresh-scratch grace: items touched in the last GC_TMP_GRACE_MIN minutes
|
|
16
|
+
# (default 10) are spared, so a sweep during an in-flight run cannot pull
|
|
17
|
+
# scratch out from under it. GC_TMP_GRACE_MIN=0 disables the grace.
|
|
11
18
|
#
|
|
12
19
|
# Usage:
|
|
13
20
|
# gc-tmp.sh # dry-run: list matches + total size
|
|
@@ -16,13 +23,16 @@
|
|
|
16
23
|
# gc-tmp.sh --older-than=60 --yes
|
|
17
24
|
#
|
|
18
25
|
# Env:
|
|
19
|
-
# GC_TMP_ROOT
|
|
26
|
+
# GC_TMP_ROOT override the scratch root (default /tmp; tests set this)
|
|
27
|
+
# GC_TMP_GRACE_MIN fresh-scratch grace window in minutes (default 10, 0 = off)
|
|
20
28
|
#
|
|
21
|
-
# Exit: 0 on success, 2 on usage error. Always exits 0
|
|
29
|
+
# Exit: 0 on success, 2 on usage error or refused scratch root. Always exits 0
|
|
30
|
+
# when nothing matches.
|
|
22
31
|
|
|
23
32
|
set -uo pipefail
|
|
24
33
|
|
|
25
34
|
ROOT="${GC_TMP_ROOT:-/tmp}"
|
|
35
|
+
GRACE_MIN="${GC_TMP_GRACE_MIN:-10}"
|
|
26
36
|
DELETE=0
|
|
27
37
|
OLDER_MIN=0
|
|
28
38
|
|
|
@@ -51,9 +61,35 @@ for arg in "$@"; do
|
|
|
51
61
|
esac
|
|
52
62
|
done
|
|
53
63
|
|
|
64
|
+
if ! printf '%s' "$GRACE_MIN" | grep -qE '^[0-9]+$'; then
|
|
65
|
+
echo "gc-tmp: GC_TMP_GRACE_MIN needs a whole number of minutes, got: $GRACE_MIN" >&2
|
|
66
|
+
exit 2
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
if [ ! -d "$ROOT" ]; then
|
|
70
|
+
echo "gc-tmp: no scratch root at $ROOT - nothing to do"
|
|
71
|
+
exit 0
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# Root guard: resolve symlinks (macOS /tmp -> /private/tmp), then refuse
|
|
75
|
+
# roots that can never be a scratch root. Sweeping $HOME by mistake would
|
|
76
|
+
# match real dirs like ~/multi-agent-pipeline via the name prefixes below.
|
|
77
|
+
ROOT="$(cd "$ROOT" 2>/dev/null && pwd -P)"
|
|
78
|
+
if [ -z "$ROOT" ]; then
|
|
79
|
+
echo "gc-tmp: cannot resolve scratch root" >&2
|
|
80
|
+
exit 2
|
|
81
|
+
fi
|
|
82
|
+
HOME_REAL="$(cd "$HOME" 2>/dev/null && pwd -P || printf '%s' "$HOME")"
|
|
83
|
+
if [ "$ROOT" = "/" ] || [ "$ROOT" = "$HOME_REAL" ] || [ -e "$ROOT/.git" ]; then
|
|
84
|
+
echo "gc-tmp: refusing scratch root $ROOT (/, \$HOME, or a git work tree is never a scratch root)" >&2
|
|
85
|
+
exit 2
|
|
86
|
+
fi
|
|
87
|
+
|
|
54
88
|
# Scratch prefixes the pipeline writes under $ROOT (see jira.md, review.md,
|
|
55
89
|
# update-issue-progress.sh, channels/*.md, external-context-injection.md,
|
|
56
|
-
# analysis.md).
|
|
90
|
+
# analysis.md). mindepth 1 so the root itself is never a candidate even when
|
|
91
|
+
# its own name matches a prefix; maxdepth 1 so we never descend into
|
|
92
|
+
# unrelated trees.
|
|
57
93
|
NAME_ARGS=(
|
|
58
94
|
-name 'multi-agent-*'
|
|
59
95
|
-o -name 'issue-progress-*'
|
|
@@ -69,12 +105,24 @@ if [ "$OLDER_MIN" -gt 0 ]; then
|
|
|
69
105
|
fi
|
|
70
106
|
|
|
71
107
|
matches=()
|
|
108
|
+
skipped_active=0
|
|
72
109
|
while IFS= read -r -d '' p; do
|
|
110
|
+
# Fresh-scratch grace: spare items an in-flight run touched recently
|
|
111
|
+
# (checks the item and, for dirs, anything inside it).
|
|
112
|
+
if [ "$GRACE_MIN" -gt 0 ] &&
|
|
113
|
+
find "$p" -mmin "-$GRACE_MIN" -print 2>/dev/null | head -1 | grep -q .; then
|
|
114
|
+
skipped_active=$((skipped_active + 1))
|
|
115
|
+
continue
|
|
116
|
+
fi
|
|
73
117
|
matches+=("$p")
|
|
74
|
-
done < <(find "$ROOT" -maxdepth 1 \( "${NAME_ARGS[@]}" \) ${MTIME_ARGS[@]+"${MTIME_ARGS[@]}"} -print0 2>/dev/null)
|
|
118
|
+
done < <(find "$ROOT" -mindepth 1 -maxdepth 1 \( "${NAME_ARGS[@]}" \) ${MTIME_ARGS[@]+"${MTIME_ARGS[@]}"} -print0 2>/dev/null)
|
|
75
119
|
|
|
76
120
|
if [ "${#matches[@]}" -eq 0 ]; then
|
|
77
|
-
|
|
121
|
+
if [ "$skipped_active" -gt 0 ]; then
|
|
122
|
+
echo "gc-tmp: only fresh scratch matched (touched in the last ${GRACE_MIN}m) - spared, nothing to do"
|
|
123
|
+
else
|
|
124
|
+
echo "gc-tmp: no leftover scratch under $ROOT - nothing to do"
|
|
125
|
+
fi
|
|
78
126
|
exit 0
|
|
79
127
|
fi
|
|
80
128
|
|
|
@@ -90,13 +138,24 @@ human="${total_kb} KB"
|
|
|
90
138
|
if [ "$DELETE" -eq 0 ]; then
|
|
91
139
|
echo "DRY-RUN - would remove ${#matches[@]} item(s) under $ROOT (frees ~${human}):"
|
|
92
140
|
for p in "${matches[@]}"; do echo " $p"; done
|
|
141
|
+
[ "$skipped_active" -gt 0 ] && echo "Spared $skipped_active fresh item(s) touched in the last ${GRACE_MIN}m."
|
|
93
142
|
echo "Re-run with --yes to delete."
|
|
94
143
|
exit 0
|
|
95
144
|
fi
|
|
96
145
|
|
|
97
146
|
removed=0
|
|
98
147
|
for p in "${matches[@]}"; do
|
|
148
|
+
# Containment guard: only ever delete direct children of the resolved root.
|
|
149
|
+
case "$p" in
|
|
150
|
+
"$ROOT"/*) ;;
|
|
151
|
+
*)
|
|
152
|
+
echo "gc-tmp: skipping path outside scratch root: $p" >&2
|
|
153
|
+
continue
|
|
154
|
+
;;
|
|
155
|
+
esac
|
|
99
156
|
rm -rf "$p" 2>/dev/null && removed=$((removed + 1))
|
|
100
157
|
done
|
|
101
|
-
|
|
158
|
+
spared_note=""
|
|
159
|
+
[ "$skipped_active" -gt 0 ] && spared_note=", spared $skipped_active fresh"
|
|
160
|
+
echo "══ garbage-collect: removed ${removed}/${#matches[@]} item(s)${spared_note}, freed ~${human} ══"
|
|
102
161
|
exit 0
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# gc-worktrees.sh - sweep worktree residue in the current project repo.
|
|
3
|
+
#
|
|
4
|
+
# Complements gc-tmp.sh (which only touches /tmp scratch). This handles the
|
|
5
|
+
# leftovers a worktree lifecycle can strand inside the repo itself:
|
|
6
|
+
#
|
|
7
|
+
# 1. stale worktree admin entries -> git worktree prune
|
|
8
|
+
# 2. orphan .worktrees/* dirs -> dirs no longer registered as
|
|
9
|
+
# worktrees (killed mid-run, manual deletes). Listed in dry-run,
|
|
10
|
+
# removed only with --yes.
|
|
11
|
+
# 3. gitlink residue in the index -> .worktrees/{id} recorded as a
|
|
12
|
+
# "Subproject commit" entry by a blanket `git add -A` before the
|
|
13
|
+
# residue guard existed. --yes runs `git rm --cached` (the commit
|
|
14
|
+
# itself stays yours to make).
|
|
15
|
+
# 4. missing residue guard -> ensures `.worktrees/` is in
|
|
16
|
+
# .git/info/exclude so the gitlink class cannot re-occur.
|
|
17
|
+
#
|
|
18
|
+
# Registered, healthy worktrees are NEVER touched - finishing or killing a
|
|
19
|
+
# task is /multi-agent:finish / /multi-agent:kill territory.
|
|
20
|
+
#
|
|
21
|
+
# SAFE BY DEFAULT: dry-run. Deletes nothing until you pass --yes.
|
|
22
|
+
#
|
|
23
|
+
# Usage:
|
|
24
|
+
# gc-worktrees.sh # dry-run in the current repo
|
|
25
|
+
# gc-worktrees.sh --yes # apply
|
|
26
|
+
# gc-worktrees.sh --repo <path> # operate on another repo
|
|
27
|
+
# gc-worktrees.sh --older-than=60 # only orphan dirs idle > 60 min
|
|
28
|
+
#
|
|
29
|
+
# Exit: 0 on success (including "nothing to do"), 2 on usage error.
|
|
30
|
+
|
|
31
|
+
set -uo pipefail
|
|
32
|
+
|
|
33
|
+
REPO="$PWD"
|
|
34
|
+
DELETE=0
|
|
35
|
+
OLDER_MIN=0
|
|
36
|
+
|
|
37
|
+
usage() {
|
|
38
|
+
grep -E '^#( |$)' "$0" | sed -E 's/^# ?//'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
while [ $# -gt 0 ]; do
|
|
42
|
+
case "$1" in
|
|
43
|
+
--yes | --force) DELETE=1; shift ;;
|
|
44
|
+
--repo)
|
|
45
|
+
REPO="${2:-}"
|
|
46
|
+
[ -z "$REPO" ] && { echo "gc-worktrees: --repo needs a path" >&2; exit 2; }
|
|
47
|
+
shift 2
|
|
48
|
+
;;
|
|
49
|
+
--older-than=*)
|
|
50
|
+
OLDER_MIN="${1#*=}"
|
|
51
|
+
if ! printf '%s' "$OLDER_MIN" | grep -qE '^[0-9]+$'; then
|
|
52
|
+
echo "gc-worktrees: --older-than needs a whole number of minutes, got: $OLDER_MIN" >&2
|
|
53
|
+
exit 2
|
|
54
|
+
fi
|
|
55
|
+
shift
|
|
56
|
+
;;
|
|
57
|
+
-h | --help) usage; exit 0 ;;
|
|
58
|
+
*) echo "gc-worktrees: unknown argument: $1" >&2; exit 2 ;;
|
|
59
|
+
esac
|
|
60
|
+
done
|
|
61
|
+
|
|
62
|
+
if ! git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
63
|
+
echo "gc-worktrees: $REPO is not a git work tree - nothing to do"
|
|
64
|
+
exit 0
|
|
65
|
+
fi
|
|
66
|
+
REPO="$(cd "$REPO" && pwd -P)"
|
|
67
|
+
WT_ROOT="$REPO/.worktrees"
|
|
68
|
+
|
|
69
|
+
changed=0
|
|
70
|
+
|
|
71
|
+
# 1. Stale admin entries (safe on a clean repo, no confirmation needed:
|
|
72
|
+
# prune only drops bookkeeping for dirs that are already gone).
|
|
73
|
+
pruned=$(git -C "$REPO" worktree prune -v 2>&1 | grep -c . || true)
|
|
74
|
+
[ "$pruned" -gt 0 ] && echo "→ pruned $pruned stale worktree admin entr$( [ "$pruned" -eq 1 ] && echo y || echo ies)"
|
|
75
|
+
|
|
76
|
+
# 2. Orphan dirs: present under .worktrees/ but not registered as worktrees.
|
|
77
|
+
orphans=()
|
|
78
|
+
if [ -d "$WT_ROOT" ]; then
|
|
79
|
+
registered=$(git -C "$REPO" worktree list --porcelain 2>/dev/null | sed -n 's/^worktree //p')
|
|
80
|
+
for d in "$WT_ROOT"/*/; do
|
|
81
|
+
[ -d "$d" ] || continue
|
|
82
|
+
dir="$(cd "$d" && pwd -P)"
|
|
83
|
+
# Path guard: only ever consider dirs strictly inside <repo>/.worktrees/.
|
|
84
|
+
case "$dir" in "$WT_ROOT"/*) ;; *) continue ;; esac
|
|
85
|
+
if printf '%s\n' "$registered" | grep -qxF "$dir"; then
|
|
86
|
+
continue # healthy registered worktree - kill/finish territory
|
|
87
|
+
fi
|
|
88
|
+
if [ "$OLDER_MIN" -gt 0 ] && find "$dir" -type f -mmin "-$OLDER_MIN" 2>/dev/null | head -1 | grep -q .; then
|
|
89
|
+
continue # recently touched - spare it
|
|
90
|
+
fi
|
|
91
|
+
orphans+=("$dir")
|
|
92
|
+
done
|
|
93
|
+
fi
|
|
94
|
+
for dir in ${orphans+"${orphans[@]}"}; do
|
|
95
|
+
size=$(du -sh "$dir" 2>/dev/null | cut -f1)
|
|
96
|
+
if [ "$DELETE" -eq 1 ]; then
|
|
97
|
+
rm -rf "$dir" && { echo "→ removed orphan worktree dir: ${dir#"$REPO"/} (${size:-?})"; changed=$((changed + 1)); }
|
|
98
|
+
else
|
|
99
|
+
echo "would remove orphan worktree dir: ${dir#"$REPO"/} (${size:-?})"
|
|
100
|
+
fi
|
|
101
|
+
done
|
|
102
|
+
|
|
103
|
+
# 3. Gitlink residue in the index ("Subproject commit" entries under
|
|
104
|
+
# .worktrees/ from a pre-guard `git add -A`).
|
|
105
|
+
gitlinks=$(git -C "$REPO" ls-files -s -- .worktrees 2>/dev/null | awk '$1 == "160000" { print $4 }')
|
|
106
|
+
if [ -n "$gitlinks" ]; then
|
|
107
|
+
while IFS= read -r path; do
|
|
108
|
+
[ -z "$path" ] && continue
|
|
109
|
+
if [ "$DELETE" -eq 1 ]; then
|
|
110
|
+
git -C "$REPO" rm --cached --ignore-unmatch -q -- "$path" \
|
|
111
|
+
&& { echo "→ unstaged gitlink residue: $path (commit the removal yourself)"; changed=$((changed + 1)); }
|
|
112
|
+
else
|
|
113
|
+
echo "would unstage gitlink residue from the index: $path"
|
|
114
|
+
fi
|
|
115
|
+
done <<EOF
|
|
116
|
+
$gitlinks
|
|
117
|
+
EOF
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# 4. Residue guard: keep .worktrees/ out of the index from now on.
|
|
121
|
+
ex="$(git -C "$REPO" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)/info/exclude"
|
|
122
|
+
if [ -n "${ex%/info/exclude}" ] && ! grep -qxF '.worktrees/' "$ex" 2>/dev/null; then
|
|
123
|
+
if [ "$DELETE" -eq 1 ]; then
|
|
124
|
+
mkdir -p "$(dirname "$ex")" \
|
|
125
|
+
&& printf '.worktrees/\n' >> "$ex" \
|
|
126
|
+
&& { echo "→ added .worktrees/ to .git/info/exclude (residue guard)"; changed=$((changed + 1)); }
|
|
127
|
+
else
|
|
128
|
+
echo "would add .worktrees/ to .git/info/exclude (residue guard)"
|
|
129
|
+
fi
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
total=$(( ${#orphans[@]} + $(printf '%s' "$gitlinks" | grep -c . || true) ))
|
|
133
|
+
if [ "$DELETE" -eq 1 ]; then
|
|
134
|
+
echo "══ gc-worktrees: applied $changed change(s) in $REPO ══"
|
|
135
|
+
elif [ "$total" -eq 0 ] && [ "$pruned" -eq 0 ] && { [ -z "${ex%/info/exclude}" ] || grep -qxF '.worktrees/' "$ex" 2>/dev/null; }; then
|
|
136
|
+
echo "gc-worktrees: no worktree residue in $REPO - nothing to do"
|
|
137
|
+
else
|
|
138
|
+
echo "══ gc-worktrees: dry-run - re-run with --yes to apply ══"
|
|
139
|
+
fi
|
|
140
|
+
exit 0
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* localize-commands.mjs - swap slash-command picker descriptions between
|
|
4
|
+
* English and the outputLanguage variant in an INSTALLED command tree.
|
|
5
|
+
*
|
|
6
|
+
* The repo (and the npm package) always keeps English `description:` values;
|
|
7
|
+
* files that ship a `description-tr:` frontmatter line can be localized in
|
|
8
|
+
* place after install. The original English text is preserved in a
|
|
9
|
+
* `description-en:` sidecar line so the swap is fully reversible.
|
|
10
|
+
*
|
|
11
|
+
* apply description-en <- description, description <- description-tr
|
|
12
|
+
* restore description <- description-en, sidecar line removed
|
|
13
|
+
*
|
|
14
|
+
* Both modes are idempotent and only touch the three description* lines of
|
|
15
|
+
* the YAML frontmatter; every other byte is preserved. Files without a
|
|
16
|
+
* description-tr line are left untouched.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* node localize-commands.mjs apply [--dir <commandsDir>]
|
|
20
|
+
* node localize-commands.mjs restore [--dir <commandsDir>]
|
|
21
|
+
*
|
|
22
|
+
* Default dir: $HOME/.claude/commands/multi-agent
|
|
23
|
+
* Exit: 0 on success (including missing dir), 2 on usage error.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { readFileSync, writeFileSync, readdirSync, existsSync, statSync } from "fs";
|
|
27
|
+
import { join } from "path";
|
|
28
|
+
|
|
29
|
+
const DESC = /^description:[ \t]*(.*)$/;
|
|
30
|
+
const DESC_TR = /^description-tr:[ \t]*(.*)$/;
|
|
31
|
+
const DESC_EN = /^description-en:[ \t]*(.*)$/;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Swap the description of one SKILL.md. Returns true when the file changed.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} file
|
|
37
|
+
* @param {"apply"|"restore"} mode
|
|
38
|
+
*/
|
|
39
|
+
export function localizeFile(file, mode) {
|
|
40
|
+
const original = readFileSync(file, "utf-8");
|
|
41
|
+
const lines = original.split("\n");
|
|
42
|
+
// Only operate inside the first frontmatter block.
|
|
43
|
+
if (lines[0] !== "---") return false;
|
|
44
|
+
const end = lines.indexOf("---", 1);
|
|
45
|
+
if (end < 0) return false;
|
|
46
|
+
|
|
47
|
+
let descIdx = -1;
|
|
48
|
+
let trIdx = -1;
|
|
49
|
+
let enIdx = -1;
|
|
50
|
+
for (let i = 1; i < end; i++) {
|
|
51
|
+
if (descIdx < 0 && DESC.test(lines[i])) descIdx = i;
|
|
52
|
+
else if (trIdx < 0 && DESC_TR.test(lines[i])) trIdx = i;
|
|
53
|
+
else if (enIdx < 0 && DESC_EN.test(lines[i])) enIdx = i;
|
|
54
|
+
}
|
|
55
|
+
if (descIdx < 0) return false;
|
|
56
|
+
|
|
57
|
+
if (mode === "apply") {
|
|
58
|
+
if (trIdx < 0) return false;
|
|
59
|
+
const trValue = lines[trIdx].match(DESC_TR)[1];
|
|
60
|
+
const current = lines[descIdx].match(DESC)[1];
|
|
61
|
+
if (current === trValue) return false; // already applied
|
|
62
|
+
if (enIdx < 0) {
|
|
63
|
+
// Preserve the English original right after the description line.
|
|
64
|
+
lines.splice(descIdx + 1, 0, `description-en: ${current}`);
|
|
65
|
+
}
|
|
66
|
+
lines[descIdx] = `description: ${trValue}`;
|
|
67
|
+
} else {
|
|
68
|
+
if (enIdx < 0) return false;
|
|
69
|
+
const enValue = lines[enIdx].match(DESC_EN)[1];
|
|
70
|
+
lines[descIdx] = `description: ${enValue}`;
|
|
71
|
+
lines.splice(enIdx, 1);
|
|
72
|
+
}
|
|
73
|
+
const next = lines.join("\n");
|
|
74
|
+
if (next === original) return false;
|
|
75
|
+
writeFileSync(file, next);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Localize every SKILL.md directly in `dir` and one level below it.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} dir
|
|
83
|
+
* @param {"apply"|"restore"} mode
|
|
84
|
+
* @returns {number} count of changed files
|
|
85
|
+
*/
|
|
86
|
+
export function localizeCommands(dir, mode) {
|
|
87
|
+
if (!existsSync(dir)) return 0;
|
|
88
|
+
const files = [];
|
|
89
|
+
const rootSkill = join(dir, "SKILL.md");
|
|
90
|
+
if (existsSync(rootSkill)) files.push(rootSkill);
|
|
91
|
+
for (const entry of readdirSync(dir)) {
|
|
92
|
+
const sub = join(dir, entry, "SKILL.md");
|
|
93
|
+
try {
|
|
94
|
+
if (statSync(join(dir, entry)).isDirectory() && existsSync(sub)) files.push(sub);
|
|
95
|
+
} catch {
|
|
96
|
+
// unreadable entry - skip
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
let changed = 0;
|
|
100
|
+
for (const f of files) {
|
|
101
|
+
try {
|
|
102
|
+
if (localizeFile(f, mode)) changed++;
|
|
103
|
+
} catch {
|
|
104
|
+
// never fail the whole run over one file
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return changed;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const invokedDirectly =
|
|
111
|
+
process.argv[1] && import.meta.url.endsWith(process.argv[1].split("/").pop());
|
|
112
|
+
if (invokedDirectly) {
|
|
113
|
+
const args = process.argv.slice(2);
|
|
114
|
+
const mode = args[0];
|
|
115
|
+
if (mode !== "apply" && mode !== "restore") {
|
|
116
|
+
console.error("usage: localize-commands.mjs <apply|restore> [--dir <commandsDir>]");
|
|
117
|
+
process.exit(2);
|
|
118
|
+
}
|
|
119
|
+
let dir = join(process.env.HOME || process.env.USERPROFILE || "", ".claude", "commands", "multi-agent");
|
|
120
|
+
const dirFlag = args.indexOf("--dir");
|
|
121
|
+
if (dirFlag >= 0) {
|
|
122
|
+
if (!args[dirFlag + 1]) {
|
|
123
|
+
console.error("localize-commands: --dir needs a path");
|
|
124
|
+
process.exit(2);
|
|
125
|
+
}
|
|
126
|
+
dir = args[dirFlag + 1];
|
|
127
|
+
}
|
|
128
|
+
const changed = localizeCommands(dir, mode);
|
|
129
|
+
console.log(`localize-commands: ${mode} -> ${changed} file(s) changed in ${dir}`);
|
|
130
|
+
}
|