@mmerterden/multi-agent-pipeline 11.4.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.
Files changed (117) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/README.md +1 -0
  3. package/docs/adr/0007-multi-tool-adapter-framework.md +2 -2
  4. package/index.js +9 -2
  5. package/install/_common.mjs +107 -5
  6. package/install/_telemetry.mjs +5 -23
  7. package/install/claude.mjs +95 -11
  8. package/install/copilot.mjs +75 -6
  9. package/install/index.mjs +34 -1
  10. package/package.json +4 -10
  11. package/pipeline/commands/multi-agent/SKILL.md +1 -0
  12. package/pipeline/commands/multi-agent/analysis/SKILL.md +1 -0
  13. package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +1 -0
  14. package/pipeline/commands/multi-agent/autopilot/SKILL.md +1 -0
  15. package/pipeline/commands/multi-agent/build-optimize/SKILL.md +1 -0
  16. package/pipeline/commands/multi-agent/channels/SKILL.md +1 -0
  17. package/pipeline/commands/multi-agent/create-jira/SKILL.md +1 -0
  18. package/pipeline/commands/multi-agent/dev/SKILL.md +1 -0
  19. package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +1 -0
  20. package/pipeline/commands/multi-agent/dev-local/SKILL.md +2 -1
  21. package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +1 -0
  22. package/pipeline/commands/multi-agent/diff-explain/SKILL.md +1 -0
  23. package/pipeline/commands/multi-agent/finish/SKILL.md +1 -0
  24. package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +29 -3
  25. package/pipeline/commands/multi-agent/help/SKILL.md +3 -2
  26. package/pipeline/commands/multi-agent/issue/SKILL.md +1 -0
  27. package/pipeline/commands/multi-agent/jira/SKILL.md +1 -0
  28. package/pipeline/commands/multi-agent/kill/SKILL.md +1 -0
  29. package/pipeline/commands/multi-agent/language/SKILL.md +20 -1
  30. package/pipeline/commands/multi-agent/local/SKILL.md +1 -0
  31. package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +1 -0
  32. package/pipeline/commands/multi-agent/log/SKILL.md +1 -0
  33. package/pipeline/commands/multi-agent/manual-test/SKILL.md +1 -0
  34. package/pipeline/commands/multi-agent/prune-logs/SKILL.md +1 -0
  35. package/pipeline/commands/multi-agent/purge/SKILL.md +27 -24
  36. package/pipeline/commands/multi-agent/refactor/SKILL.md +1 -0
  37. package/pipeline/commands/multi-agent/resume/SKILL.md +1 -0
  38. package/pipeline/commands/multi-agent/review/SKILL.md +1 -0
  39. package/pipeline/commands/multi-agent/review-issue/SKILL.md +1 -0
  40. package/pipeline/commands/multi-agent/review-jira/SKILL.md +1 -0
  41. package/pipeline/commands/multi-agent/scan/SKILL.md +1 -0
  42. package/pipeline/commands/multi-agent/search/SKILL.md +1 -0
  43. package/pipeline/commands/multi-agent/setup/SKILL.md +1 -0
  44. package/pipeline/commands/multi-agent/stack/SKILL.md +1 -0
  45. package/pipeline/commands/multi-agent/status/SKILL.md +1 -0
  46. package/pipeline/commands/multi-agent/sync/SKILL.md +20 -2
  47. package/pipeline/commands/multi-agent/test/SKILL.md +1 -0
  48. package/pipeline/commands/multi-agent/uninstall/SKILL.md +87 -0
  49. package/pipeline/commands/multi-agent/update/SKILL.md +1 -0
  50. package/pipeline/lib/account-resolver.sh +61 -23
  51. package/pipeline/lib/channels-multi-repo.sh +7 -2
  52. package/pipeline/lib/count-lib.sh +27 -0
  53. package/pipeline/lib/credential-store.sh +23 -6
  54. package/pipeline/lib/extract-conventions.sh +27 -21
  55. package/pipeline/lib/fetch-confluence.sh +25 -13
  56. package/pipeline/lib/fetch-crashlytics.sh +30 -8
  57. package/pipeline/lib/fetch-fortify.sh +11 -2
  58. package/pipeline/lib/fetch-swagger.sh +18 -7
  59. package/pipeline/lib/figma-mcp-refresh.sh +26 -11
  60. package/pipeline/lib/figma-screenshot.sh +11 -2
  61. package/pipeline/lib/issue-fetcher.sh +31 -6
  62. package/pipeline/lib/multi-repo-pipeline.sh +84 -20
  63. package/pipeline/lib/plan-todos.sh +12 -3
  64. package/pipeline/lib/post-pr-review.sh +5 -3
  65. package/pipeline/lib/repo-cache.sh +53 -9
  66. package/pipeline/lib/review-watch.sh +26 -6
  67. package/pipeline/lib/shadow-git.sh +45 -4
  68. package/pipeline/lib/vercel-deploy.sh +10 -1
  69. package/pipeline/multi-agent-refs/cross-cli-contract.md +5 -5
  70. package/pipeline/multi-agent-refs/phases/phase-0-init.md +12 -1
  71. package/pipeline/scripts/audit-log-rotate.sh +38 -10
  72. package/pipeline/scripts/check-md-links.mjs +34 -9
  73. package/pipeline/scripts/diff-risk-score.mjs +4 -1
  74. package/pipeline/scripts/evidence-gate.mjs +10 -1
  75. package/pipeline/scripts/fixtures/install-layout.tsv +6 -4
  76. package/pipeline/scripts/fixtures/pack-expected-count.txt +1 -0
  77. package/pipeline/scripts/gc-tmp.sh +67 -8
  78. package/pipeline/scripts/gc-worktrees.sh +140 -0
  79. package/pipeline/scripts/keychain-save.sh +13 -9
  80. package/pipeline/scripts/lint-skills.mjs +40 -2
  81. package/pipeline/scripts/localize-commands.mjs +130 -0
  82. package/pipeline/scripts/migrate-prefs.mjs +26 -7
  83. package/pipeline/scripts/phase-tracker.sh +71 -0
  84. package/pipeline/scripts/pre-commit-check.sh +39 -0
  85. package/pipeline/scripts/prune-logs.sh +100 -15
  86. package/pipeline/scripts/purge.sh +235 -0
  87. package/pipeline/scripts/scan-skills.sh +217 -127
  88. package/pipeline/scripts/smoke-bitbucket-contract.sh +21 -5
  89. package/pipeline/scripts/smoke-description-tr.sh +82 -0
  90. package/pipeline/scripts/smoke-fetchers-offline.sh +382 -0
  91. package/pipeline/scripts/smoke-gc-tmp.sh +50 -4
  92. package/pipeline/scripts/smoke-gc-worktrees.sh +125 -0
  93. package/pipeline/scripts/smoke-install-layout.sh +11 -3
  94. package/pipeline/scripts/smoke-lib-scripts.sh +54 -1
  95. package/pipeline/scripts/smoke-pack-contents.sh +140 -0
  96. package/pipeline/scripts/smoke-plugin-validate.sh +64 -0
  97. package/pipeline/scripts/smoke-prune-logs.sh +57 -7
  98. package/pipeline/scripts/smoke-purge.sh +138 -0
  99. package/pipeline/scripts/smoke-shadow-git.sh +48 -1
  100. package/pipeline/scripts/smoke-skill-authoring.sh +1 -1
  101. package/pipeline/scripts/smoke-update-check.sh +13 -0
  102. package/pipeline/scripts/smoke-workflow-audit.sh +69 -0
  103. package/pipeline/scripts/test-gap-scan.mjs +12 -1
  104. package/pipeline/scripts/triage-memory.mjs +10 -1
  105. package/pipeline/scripts/uninstall.mjs +146 -42
  106. package/pipeline/scripts/update-check.sh +9 -0
  107. package/pipeline/scripts/update-issue-progress.sh +60 -41
  108. package/pipeline/scripts/write-state.mjs +14 -4
  109. package/pipeline/skills/.skill-manifest.json +7 -7
  110. package/pipeline/skills/.skills-index.json +9 -9
  111. package/pipeline/skills/shared/README.md +19 -10
  112. package/pipeline/skills/shared/core/multi-agent/SKILL.md +1 -0
  113. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +2 -2
  114. package/pipeline/skills/shared/core/multi-agent-uninstall/SKILL.md +74 -0
  115. package/pipeline/skills/skills-index.md +1 -1
  116. package/pipeline/commands/multi-agent/delete/SKILL.md +0 -67
  117. package/pipeline/skills/shared/core/multi-agent-delete/SKILL.md +0 -66
@@ -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: dry-run. It lists what WOULD be removed and the space it
10
- # would free, and deletes nothing until you pass --yes.
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 override the scratch root (default /tmp; tests set this)
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 when nothing matches.
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). maxdepth 1 so we never descend into unrelated trees.
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
- echo "gc-tmp: no leftover scratch under $ROOT - nothing to do"
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
- echo "══ garbage-collect: removed ${removed}/${#matches[@]} item(s), freed ~${human} ══"
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
@@ -17,11 +17,11 @@ fi
17
17
  echo "Ne tür bir secret kaydedeceksin?"
18
18
  echo " 1) Personal Access Token / API Key"
19
19
  echo " 2) JSON dosyası (service account vb.)"
20
- read -p "Seçim (1/2): " CHOICE
20
+ read -rp "Seçim (1/2): " CHOICE
21
21
 
22
22
  case "$CHOICE" in
23
23
  1)
24
- read -sp "Token'ı yapıştır (gizli yazılır): " SECRET
24
+ read -rsp "Token'ı yapıştır (gizli yazılır): " SECRET
25
25
  echo ""
26
26
  if [ -z "$SECRET" ]; then
27
27
  echo "Hata: Token boş olamaz."
@@ -29,7 +29,7 @@ case "$CHOICE" in
29
29
  fi
30
30
  ;;
31
31
  2)
32
- read -p "JSON dosya yolunu gir: " JSON_PATH
32
+ read -rp "JSON dosya yolunu gir: " JSON_PATH
33
33
  # Kullanıcının girdiği tırnak işaretlerini temizle
34
34
  JSON_PATH="${JSON_PATH//\'/}"
35
35
  JSON_PATH="${JSON_PATH//\"/}"
@@ -53,20 +53,24 @@ case "$CHOICE" in
53
53
  ;;
54
54
  esac
55
55
 
56
- CRED="$HOME/.claude/lib/credential-store.sh"
56
+ # Locate the credential helper via the resolver so Copilot-only installs work.
57
+ # shellcheck disable=SC1090,SC1091
58
+ . "$HOME/.claude/lib/credential-store-resolver.sh" 2>/dev/null \
59
+ || . "$HOME/.copilot/lib/credential-store-resolver.sh" 2>/dev/null \
60
+ || { echo "Hata: credential-store bulunamadi. Kurulum: npx @mmerterden/multi-agent-pipeline install"; exit 1; }
61
+ CRED="$CRED_STORE"
57
62
 
58
63
  "$CRED" delete "$SERVICE_NAME" >/dev/null 2>&1 || true
59
- "$CRED" set "$SERVICE_NAME" "$SECRET"
60
-
61
- if [ $? -eq 0 ]; then
64
+ # Secret goes through stdin ("set <key> -") so it never appears on argv.
65
+ if printf '%s' "$SECRET" | "$CRED" set "$SERVICE_NAME" -; then
62
66
  echo "Kaydedildi: $SERVICE_NAME (platform: $("$CRED" platform))"
63
67
  echo ""
64
68
  echo "Okumak icin:"
65
- echo " ~/.claude/lib/credential-store.sh get \"$SERVICE_NAME\""
69
+ echo " $CRED get \"$SERVICE_NAME\""
66
70
  if [ "$CHOICE" = "2" ]; then
67
71
  echo ""
68
72
  echo "JSON'a geri cevirmek icin:"
69
- echo " ~/.claude/lib/credential-store.sh get \"$SERVICE_NAME\" | base64 -d"
73
+ echo " $CRED get \"$SERVICE_NAME\" | base64 -d"
70
74
  fi
71
75
  else
72
76
  echo "Hata: Kaydedilemedi."
@@ -1,19 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  // Frontmatter linter for every SKILL.md shipped by the pipeline.
3
3
  //
4
- // Until v10.0.0 the 217 SKILL.md files were counted by smokes but never
4
+ // Until v10.0.0 the shipped SKILL.md files were counted by smokes but never
5
5
  // content-checked, so a skill could ship without the frontmatter that
6
6
  // Claude Code / Copilot CLI need to register it. This gate enforces the
7
7
  // minimal contract; it is zero-dependency (line-based YAML subset) so it
8
8
  // runs inside `npm test` and ci-lite without installs.
9
9
  //
10
- // Contract per SKILL.md:
10
+ // Contract per SKILL.md (errors - fail the gate):
11
11
  // - frontmatter block present (--- ... ---) starting at line 1
12
12
  // - name: required, lowercase kebab-case (underscores tolerated for
13
13
  // legacy skills already published under a snake_case name)
14
14
  // - description: required, non-empty, single line, <= 1024 chars
15
15
  // - user-invocable: when present, must be true or false
16
16
  // - platform: when present, must be ios | android | common | all
17
+ //
18
+ // Spec-conformance signals (warnings - never fail the gate):
19
+ // - name should match the agent-skills spec pattern
20
+ // (lowercase letters/digits/hyphens, <= 64 chars)
21
+ // - description should carry a routing/trigger clause ("use when ...")
22
+ // so the model can decide when to load the skill
23
+ // - a body over ~500 lines with no companion-file reference should be
24
+ // split into references/ files loaded on demand
17
25
 
18
26
  import { readFileSync } from "node:fs";
19
27
  import { execSync } from "node:child_process";
@@ -61,6 +69,16 @@ for (const file of files) {
61
69
  fail(file, "missing required field: name");
62
70
  } else if (!/^[a-z0-9]+([-_][a-z0-9]+)*$/.test(fields.name)) {
63
71
  fail(file, `name is not lowercase kebab/snake-case: "${fields.name}"`);
72
+ } else {
73
+ // Spec conformance (warn-only): the agent-skills spec allows lowercase
74
+ // letters, digits, and hyphens, max 64 chars. Snake_case passes the
75
+ // legacy error check above but is out of spec.
76
+ if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(fields.name)) {
77
+ warn(file, `name is outside the agent-skills spec pattern (hyphens only): "${fields.name}"`);
78
+ }
79
+ if (fields.name.length > 64) {
80
+ warn(file, `name exceeds the spec's 64-char limit (${fields.name.length})`);
81
+ }
64
82
  }
65
83
 
66
84
  if (!("description" in fields)) {
@@ -81,6 +99,26 @@ for (const file of files) {
81
99
  if (!isBlockScalar && words < 6) {
82
100
  warn(file, `description is terse (${words} words) - say what it does AND when to use it`);
83
101
  }
102
+ // Routing quality (warn-only): without a trigger clause the model has no
103
+ // signal for WHEN to load the skill. Heuristic phrase match; block
104
+ // scalars are skipped for the same parser reason as above.
105
+ const ROUTING_RE = /\b(use (this|it|when|for|whenever)|when (the|a|an|you|asked|working|building|implementing|writing|reviewing)|triggers?|invoked?( when| for)?|applies when|activated?)\b/i;
106
+ if (!isBlockScalar && !ROUTING_RE.test(desc)) {
107
+ warn(file, "description has no routing/trigger clause (e.g. \"use when ...\")");
108
+ }
109
+ }
110
+
111
+ // Body size (warn-only): the agent-skills guidance is to keep SKILL.md
112
+ // lean and push detail into companion files loaded on demand. A ~500+
113
+ // line body that references no companion file loads whole every time.
114
+ const body = raw.slice(end + 4);
115
+ const bodyLines = body.split("\n").length;
116
+ if (bodyLines > 500) {
117
+ const referencesCompanion =
118
+ /\]\((?!https?:|mailto:|#)[^)]+\)/.test(body) || /`[^`\n]*\/[^`\n]*\.(md|sh|mjs|json|py|txt)`/.test(body);
119
+ if (!referencesCompanion) {
120
+ warn(file, `body is ${bodyLines} lines with no companion-file reference - consider splitting into references/`);
121
+ }
84
122
  }
85
123
 
86
124
  if ("user-invocable" in fields && !["true", "false"].includes(fields["user-invocable"])) {
@@ -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
+ }
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
- // migrate-prefs.mjs - migrates multi-agent-preferences.json v2.0.0 v2.1.0
2
+ // migrate-prefs.mjs - migrates multi-agent-preferences.json to the current
3
+ // target schema (see TARGET_VERSION below).
3
4
  //
4
5
  // Usage:
5
6
  // node migrate-prefs.mjs [--dry-run] [--file <path>]
6
7
  //
7
- // - Idempotent: safe to run multiple times. v2.1.0 files pass through unchanged.
8
+ // - Idempotent: safe to run multiple times. Target-version files pass through unchanged.
8
9
  // - Atomic: writes to a .tmp file and rename()s. No partial writes.
9
10
  // - Non-destructive: renames gitIdentities → identities, preserves everything else.
10
11
  // - Fills defaults for new v2.1.0 fields.
@@ -15,15 +16,33 @@ import os from "node:os";
15
16
 
16
17
  const DEFAULT_FILE = path.join(os.homedir(), ".claude", "multi-agent-preferences.json");
17
18
 
19
+ // Single source of truth for the migration target version. Referenced by the
20
+ // migrate() bump chain AND every user-facing message, so they cannot drift.
21
+ const TARGET_VERSION = "2.3.0";
22
+
23
+ const USAGE = "Usage: migrate-prefs.mjs [--dry-run] [--file <path>]";
24
+
18
25
  function parseArgs(argv) {
19
26
  const args = { file: DEFAULT_FILE, dryRun: false };
20
27
  for (let i = 2; i < argv.length; i++) {
21
28
  const a = argv[i];
22
29
  if (a === "--dry-run") args.dryRun = true;
23
- else if (a === "--file") args.file = argv[++i];
24
- else if (a === "--help" || a === "-h") {
25
- console.log("Usage: migrate-prefs.mjs [--dry-run] [--file <path>]");
30
+ else if (a === "--file") {
31
+ const v = argv[++i];
32
+ if (v === undefined) {
33
+ console.error("ERROR: --file requires a path argument");
34
+ console.error(USAGE);
35
+ process.exit(64);
36
+ }
37
+ args.file = v;
38
+ } else if (a === "--help" || a === "-h") {
39
+ console.log(USAGE);
26
40
  process.exit(0);
41
+ } else {
42
+ // A typo like --dry-rnu must never fall through to a real write.
43
+ console.error(`ERROR: unknown argument: ${a}`);
44
+ console.error(USAGE);
45
+ process.exit(64);
27
46
  }
28
47
  }
29
48
  return args;
@@ -142,7 +161,7 @@ function migrate(prefs) {
142
161
  // v2.3.0 is the v7.4.0 target - adds optional `accounts[]` (per-account host
143
162
  // overrides) and `recentAccounts[]` (LRU for autopilot account picker).
144
163
  // Both default to empty arrays; old prefs continue to validate without them.
145
- const TARGET = "2.3.0";
164
+ const TARGET = TARGET_VERSION;
146
165
  if (out.schemaVersion === TARGET) {
147
166
  // already on target - skip version bump, but still run sub-migrations below
148
167
  } else if (out.schemaVersion === "2.2.0") {
@@ -371,7 +390,7 @@ function main() {
371
390
  const { out, changes, alreadyMigrated } = migrate(prefs);
372
391
 
373
392
  if (alreadyMigrated) {
374
- console.log(`✓ already v2.1.0 - no changes: ${args.file}`);
393
+ console.log(`✓ already v${TARGET_VERSION} - no changes: ${args.file}`);
375
394
  process.exit(0);
376
395
  }
377
396