shipwright-cli 1.7.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/LICENSE +21 -0
- package/README.md +926 -0
- package/claude-code/CLAUDE.md.shipwright +125 -0
- package/claude-code/hooks/notify-idle.sh +35 -0
- package/claude-code/hooks/pre-compact-save.sh +57 -0
- package/claude-code/hooks/task-completed.sh +170 -0
- package/claude-code/hooks/teammate-idle.sh +68 -0
- package/claude-code/settings.json.template +184 -0
- package/completions/_shipwright +140 -0
- package/completions/shipwright.bash +89 -0
- package/completions/shipwright.fish +107 -0
- package/docs/KNOWN-ISSUES.md +199 -0
- package/docs/TIPS.md +331 -0
- package/docs/definition-of-done.example.md +16 -0
- package/docs/patterns/README.md +139 -0
- package/docs/patterns/audit-loop.md +149 -0
- package/docs/patterns/bug-hunt.md +183 -0
- package/docs/patterns/feature-implementation.md +159 -0
- package/docs/patterns/refactoring.md +183 -0
- package/docs/patterns/research-exploration.md +144 -0
- package/docs/patterns/test-generation.md +173 -0
- package/package.json +49 -0
- package/scripts/adapters/docker-deploy.sh +50 -0
- package/scripts/adapters/fly-deploy.sh +41 -0
- package/scripts/adapters/iterm2-adapter.sh +122 -0
- package/scripts/adapters/railway-deploy.sh +34 -0
- package/scripts/adapters/tmux-adapter.sh +87 -0
- package/scripts/adapters/vercel-deploy.sh +35 -0
- package/scripts/adapters/wezterm-adapter.sh +103 -0
- package/scripts/cct +242 -0
- package/scripts/cct-cleanup.sh +172 -0
- package/scripts/cct-cost.sh +590 -0
- package/scripts/cct-daemon.sh +3189 -0
- package/scripts/cct-doctor.sh +328 -0
- package/scripts/cct-fix.sh +478 -0
- package/scripts/cct-fleet.sh +904 -0
- package/scripts/cct-init.sh +282 -0
- package/scripts/cct-logs.sh +273 -0
- package/scripts/cct-loop.sh +1332 -0
- package/scripts/cct-memory.sh +1148 -0
- package/scripts/cct-pipeline.sh +3844 -0
- package/scripts/cct-prep.sh +1352 -0
- package/scripts/cct-ps.sh +168 -0
- package/scripts/cct-reaper.sh +390 -0
- package/scripts/cct-session.sh +284 -0
- package/scripts/cct-status.sh +169 -0
- package/scripts/cct-templates.sh +242 -0
- package/scripts/cct-upgrade.sh +422 -0
- package/scripts/cct-worktree.sh +405 -0
- package/scripts/postinstall.mjs +96 -0
- package/templates/pipelines/autonomous.json +71 -0
- package/templates/pipelines/cost-aware.json +95 -0
- package/templates/pipelines/deployed.json +79 -0
- package/templates/pipelines/enterprise.json +114 -0
- package/templates/pipelines/fast.json +63 -0
- package/templates/pipelines/full.json +104 -0
- package/templates/pipelines/hotfix.json +63 -0
- package/templates/pipelines/standard.json +91 -0
- package/tmux/claude-teams-overlay.conf +109 -0
- package/tmux/templates/architecture.json +19 -0
- package/tmux/templates/bug-fix.json +24 -0
- package/tmux/templates/code-review.json +24 -0
- package/tmux/templates/devops.json +19 -0
- package/tmux/templates/documentation.json +19 -0
- package/tmux/templates/exploration.json +19 -0
- package/tmux/templates/feature-dev.json +24 -0
- package/tmux/templates/full-stack.json +24 -0
- package/tmux/templates/migration.json +24 -0
- package/tmux/templates/refactor.json +19 -0
- package/tmux/templates/security-audit.json +24 -0
- package/tmux/templates/testing.json +24 -0
- package/tmux/tmux.conf +167 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ shipwright fix — Bulk Fix Across Multiple Repos ║
|
|
4
|
+
# ║ Clone a goal across repos · Run pipelines in parallel · Collect PRs ║
|
|
5
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
VERSION="1.7.0"
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
|
|
11
|
+
# ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
|
|
12
|
+
CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
|
|
13
|
+
PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
|
|
14
|
+
BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
|
|
15
|
+
GREEN='\033[38;2;74;222;128m' # success
|
|
16
|
+
YELLOW='\033[38;2;250;204;21m' # warning
|
|
17
|
+
RED='\033[38;2;248;113;113m' # error
|
|
18
|
+
DIM='\033[2m'
|
|
19
|
+
BOLD='\033[1m'
|
|
20
|
+
RESET='\033[0m'
|
|
21
|
+
|
|
22
|
+
# ─── Output Helpers ─────────────────────────────────────────────────────────
|
|
23
|
+
info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
|
|
24
|
+
success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
|
|
25
|
+
warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
|
|
26
|
+
error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
|
|
27
|
+
|
|
28
|
+
now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
29
|
+
now_epoch() { date +%s; }
|
|
30
|
+
|
|
31
|
+
format_duration() {
|
|
32
|
+
local secs="$1"
|
|
33
|
+
if [[ "$secs" -ge 3600 ]]; then
|
|
34
|
+
printf "%dh %dm %ds" $((secs/3600)) $((secs%3600/60)) $((secs%60))
|
|
35
|
+
elif [[ "$secs" -ge 60 ]]; then
|
|
36
|
+
printf "%dm %ds" $((secs/60)) $((secs%60))
|
|
37
|
+
else
|
|
38
|
+
printf "%ds" "$secs"
|
|
39
|
+
fi
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# ─── Structured Event Log ──────────────────────────────────────────────────
|
|
43
|
+
EVENTS_FILE="${HOME}/.claude-teams/events.jsonl"
|
|
44
|
+
|
|
45
|
+
emit_event() {
|
|
46
|
+
local event_type="$1"
|
|
47
|
+
shift
|
|
48
|
+
local json_fields=""
|
|
49
|
+
for kv in "$@"; do
|
|
50
|
+
local key="${kv%%=*}"
|
|
51
|
+
local val="${kv#*=}"
|
|
52
|
+
if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
|
|
53
|
+
json_fields="${json_fields},\"${key}\":${val}"
|
|
54
|
+
else
|
|
55
|
+
val="${val//\"/\\\"}"
|
|
56
|
+
json_fields="${json_fields},\"${key}\":\"${val}\""
|
|
57
|
+
fi
|
|
58
|
+
done
|
|
59
|
+
mkdir -p "${HOME}/.claude-teams"
|
|
60
|
+
echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# ─── Defaults ───────────────────────────────────────────────────────────────
|
|
64
|
+
FIX_DIR="${HOME}/.claude-teams"
|
|
65
|
+
GOAL=""
|
|
66
|
+
REPOS=()
|
|
67
|
+
REPOS_FROM=""
|
|
68
|
+
TEMPLATE="fast"
|
|
69
|
+
MODEL=""
|
|
70
|
+
MAX_PARALLEL=3
|
|
71
|
+
DRY_RUN=false
|
|
72
|
+
BRANCH_PREFIX="fix/"
|
|
73
|
+
|
|
74
|
+
# ─── Help ───────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
show_help() {
|
|
77
|
+
echo -e "${CYAN}${BOLD}shipwright fix${RESET} — Bulk fix across multiple repos"
|
|
78
|
+
echo ""
|
|
79
|
+
echo -e "${BOLD}USAGE${RESET}"
|
|
80
|
+
echo -e " ${CYAN}shipwright fix${RESET} \"goal\" [options]"
|
|
81
|
+
echo ""
|
|
82
|
+
echo -e "${BOLD}OPTIONS${RESET}"
|
|
83
|
+
echo -e " ${DIM}--repos dir1,dir2,...${RESET} Comma-separated repo paths"
|
|
84
|
+
echo -e " ${DIM}--repos-from file${RESET} Read repo paths from file (one per line)"
|
|
85
|
+
echo -e " ${DIM}--pipeline template${RESET} Pipeline template (default: fast)"
|
|
86
|
+
echo -e " ${DIM}--model model${RESET} Model to use (default: auto)"
|
|
87
|
+
echo -e " ${DIM}--max-parallel N${RESET} Max concurrent pipelines (default: 3)"
|
|
88
|
+
echo -e " ${DIM}--branch-prefix prefix${RESET} Branch name prefix (default: \"fix/\")"
|
|
89
|
+
echo -e " ${DIM}--dry-run${RESET} Show what would happen without executing"
|
|
90
|
+
echo -e " ${DIM}--status${RESET} Show running fix sessions"
|
|
91
|
+
echo ""
|
|
92
|
+
echo -e "${BOLD}EXAMPLES${RESET}"
|
|
93
|
+
echo -e " ${DIM}shipwright fix \"Update lodash to 4.17.21\" --repos ~/api,~/web,~/mobile${RESET}"
|
|
94
|
+
echo -e " ${DIM}shipwright fix \"Fix SQL injection in auth\" --repos ~/api --pipeline fast${RESET}"
|
|
95
|
+
echo -e " ${DIM}shipwright fix \"Bump Node to 22\" --repos-from repos.txt --pipeline hotfix${RESET}"
|
|
96
|
+
echo -e " ${DIM}shipwright fix --status${RESET}"
|
|
97
|
+
echo ""
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# ─── Argument Parsing ───────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
parse_args() {
|
|
103
|
+
while [[ $# -gt 0 ]]; do
|
|
104
|
+
case "$1" in
|
|
105
|
+
--repos)
|
|
106
|
+
IFS=',' read -ra REPOS <<< "$2"
|
|
107
|
+
shift 2
|
|
108
|
+
;;
|
|
109
|
+
--repos-from)
|
|
110
|
+
REPOS_FROM="$2"
|
|
111
|
+
shift 2
|
|
112
|
+
;;
|
|
113
|
+
--pipeline)
|
|
114
|
+
TEMPLATE="$2"
|
|
115
|
+
shift 2
|
|
116
|
+
;;
|
|
117
|
+
--model)
|
|
118
|
+
MODEL="$2"
|
|
119
|
+
shift 2
|
|
120
|
+
;;
|
|
121
|
+
--max-parallel)
|
|
122
|
+
MAX_PARALLEL="$2"
|
|
123
|
+
shift 2
|
|
124
|
+
;;
|
|
125
|
+
--branch-prefix)
|
|
126
|
+
BRANCH_PREFIX="$2"
|
|
127
|
+
shift 2
|
|
128
|
+
;;
|
|
129
|
+
--dry-run)
|
|
130
|
+
DRY_RUN=true
|
|
131
|
+
shift
|
|
132
|
+
;;
|
|
133
|
+
--status)
|
|
134
|
+
fix_status
|
|
135
|
+
exit 0
|
|
136
|
+
;;
|
|
137
|
+
help|--help|-h)
|
|
138
|
+
show_help
|
|
139
|
+
exit 0
|
|
140
|
+
;;
|
|
141
|
+
*)
|
|
142
|
+
if [[ -z "$GOAL" && ! "$1" =~ ^-- ]]; then
|
|
143
|
+
GOAL="$1"
|
|
144
|
+
fi
|
|
145
|
+
shift
|
|
146
|
+
;;
|
|
147
|
+
esac
|
|
148
|
+
done
|
|
149
|
+
|
|
150
|
+
# Load repos from file if specified
|
|
151
|
+
if [[ -n "$REPOS_FROM" ]]; then
|
|
152
|
+
if [[ ! -f "$REPOS_FROM" ]]; then
|
|
153
|
+
error "Repos file not found: $REPOS_FROM"
|
|
154
|
+
exit 1
|
|
155
|
+
fi
|
|
156
|
+
while IFS= read -r line; do
|
|
157
|
+
line="${line%%#*}" # strip comments
|
|
158
|
+
line="${line// /}" # strip whitespace
|
|
159
|
+
if [[ -n "$line" ]]; then
|
|
160
|
+
REPOS+=("$line")
|
|
161
|
+
fi
|
|
162
|
+
done < "$REPOS_FROM"
|
|
163
|
+
fi
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# ─── Sanitize Goal for Branch Names ────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
sanitize_branch() {
|
|
169
|
+
local raw="$1"
|
|
170
|
+
# Lowercase, replace spaces/special chars with hyphens, truncate
|
|
171
|
+
echo "$raw" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-50
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# ─── Fix Status ─────────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
fix_status() {
|
|
177
|
+
local fix_files
|
|
178
|
+
fix_files=$(find "$FIX_DIR" -name 'fix-*.json' -maxdepth 1 2>/dev/null | sort -r) || true
|
|
179
|
+
|
|
180
|
+
if [[ -z "$fix_files" ]]; then
|
|
181
|
+
info "No fix sessions found."
|
|
182
|
+
return
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
echo -e "${CYAN}${BOLD}═══ Fix Sessions ═══${RESET}"
|
|
186
|
+
echo ""
|
|
187
|
+
|
|
188
|
+
while IFS= read -r f; do
|
|
189
|
+
[[ -z "$f" ]] && continue
|
|
190
|
+
local goal status repo_count started
|
|
191
|
+
goal=$(jq -r '.goal // "unknown"' "$f" 2>/dev/null) || goal="unknown"
|
|
192
|
+
status=$(jq -r '.status // "unknown"' "$f" 2>/dev/null) || status="unknown"
|
|
193
|
+
repo_count=$(jq -r '.repos | length // 0' "$f" 2>/dev/null) || repo_count=0
|
|
194
|
+
started=$(jq -r '.started // "unknown"' "$f" 2>/dev/null) || started="unknown"
|
|
195
|
+
|
|
196
|
+
local status_color="$YELLOW"
|
|
197
|
+
[[ "$status" == "completed" ]] && status_color="$GREEN"
|
|
198
|
+
[[ "$status" == "failed" ]] && status_color="$RED"
|
|
199
|
+
|
|
200
|
+
echo -e " ${BOLD}${goal}${RESET}"
|
|
201
|
+
echo -e " Status: ${status_color}${status}${RESET} | Repos: ${repo_count} | Started: ${DIM}${started}${RESET}"
|
|
202
|
+
|
|
203
|
+
# Show per-repo status
|
|
204
|
+
local repo_statuses
|
|
205
|
+
repo_statuses=$(jq -r '.repos[]? | "\(.name)|\(.status // "pending")|\(.pr_url // "-")|\(.duration // "-")"' "$f" 2>/dev/null) || true
|
|
206
|
+
if [[ -n "$repo_statuses" ]]; then
|
|
207
|
+
while IFS='|' read -r rname rstatus rpr rdur; do
|
|
208
|
+
local ricon="⋯"
|
|
209
|
+
[[ "$rstatus" == "pass" ]] && ricon="${GREEN}✓${RESET}"
|
|
210
|
+
[[ "$rstatus" == "fail" ]] && ricon="${RED}✗${RESET}"
|
|
211
|
+
echo -e " ${ricon} ${rname} ${DIM}${rstatus}${RESET} ${DIM}${rpr}${RESET}"
|
|
212
|
+
done <<< "$repo_statuses"
|
|
213
|
+
fi
|
|
214
|
+
echo ""
|
|
215
|
+
done <<< "$fix_files"
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# ─── Fix Start ──────────────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
fix_start() {
|
|
221
|
+
# Validate
|
|
222
|
+
if [[ -z "$GOAL" ]]; then
|
|
223
|
+
error "Goal is required."
|
|
224
|
+
echo -e " Example: ${DIM}shipwright fix \"Update lodash to 4.17.21\" --repos ~/api,~/web${RESET}"
|
|
225
|
+
exit 1
|
|
226
|
+
fi
|
|
227
|
+
|
|
228
|
+
if [[ ${#REPOS[@]} -eq 0 ]]; then
|
|
229
|
+
error "No repos specified. Use --repos or --repos-from."
|
|
230
|
+
exit 1
|
|
231
|
+
fi
|
|
232
|
+
|
|
233
|
+
# Validate repos exist
|
|
234
|
+
for repo in "${REPOS[@]}"; do
|
|
235
|
+
local expanded
|
|
236
|
+
expanded=$(eval echo "$repo")
|
|
237
|
+
if [[ ! -d "$expanded" ]]; then
|
|
238
|
+
error "Repo directory not found: $expanded"
|
|
239
|
+
exit 1
|
|
240
|
+
fi
|
|
241
|
+
if [[ ! -d "$expanded/.git" ]]; then
|
|
242
|
+
warn "Not a git repo: $expanded (skipping)"
|
|
243
|
+
fi
|
|
244
|
+
done
|
|
245
|
+
|
|
246
|
+
local sanitized
|
|
247
|
+
sanitized=$(sanitize_branch "$GOAL")
|
|
248
|
+
local branch_name="${BRANCH_PREFIX}${sanitized}"
|
|
249
|
+
local session_id="fix-$(date +%s)"
|
|
250
|
+
local state_file="$FIX_DIR/${session_id}.json"
|
|
251
|
+
local log_dir="$FIX_DIR/${session_id}-logs"
|
|
252
|
+
local start_epoch
|
|
253
|
+
start_epoch=$(now_epoch)
|
|
254
|
+
|
|
255
|
+
mkdir -p "$FIX_DIR" "$log_dir"
|
|
256
|
+
|
|
257
|
+
# ─── Header ─────────────────────────────────────────────────────────────
|
|
258
|
+
echo ""
|
|
259
|
+
echo -e "${CYAN}${BOLD}╔═══════════════════════════════════════════════════════════════╗${RESET}"
|
|
260
|
+
echo -e "${CYAN}${BOLD}║ Shipwright Fix ║${RESET}"
|
|
261
|
+
echo -e "${CYAN}${BOLD}╚═══════════════════════════════════════════════════════════════╝${RESET}"
|
|
262
|
+
echo ""
|
|
263
|
+
echo -e " ${BOLD}Goal:${RESET} $GOAL"
|
|
264
|
+
echo -e " ${BOLD}Repos:${RESET} ${#REPOS[@]}"
|
|
265
|
+
echo -e " ${BOLD}Pipeline:${RESET} $TEMPLATE"
|
|
266
|
+
echo -e " ${BOLD}Branch:${RESET} $branch_name"
|
|
267
|
+
echo -e " ${BOLD}Parallel:${RESET} $MAX_PARALLEL"
|
|
268
|
+
[[ -n "$MODEL" ]] && echo -e " ${BOLD}Model:${RESET} $MODEL"
|
|
269
|
+
echo ""
|
|
270
|
+
|
|
271
|
+
# ─── Dry Run ────────────────────────────────────────────────────────────
|
|
272
|
+
if [[ "$DRY_RUN" == "true" ]]; then
|
|
273
|
+
info "Dry run — would execute:"
|
|
274
|
+
for repo in "${REPOS[@]}"; do
|
|
275
|
+
local expanded
|
|
276
|
+
expanded=$(eval echo "$repo")
|
|
277
|
+
local rname
|
|
278
|
+
rname=$(basename "$expanded")
|
|
279
|
+
echo -e " ${DIM}cd $expanded && git checkout -b $branch_name${RESET}"
|
|
280
|
+
echo -e " ${DIM}cct-pipeline.sh start --goal \"$GOAL\" --pipeline $TEMPLATE --skip-gates${RESET}"
|
|
281
|
+
echo ""
|
|
282
|
+
done
|
|
283
|
+
return
|
|
284
|
+
fi
|
|
285
|
+
|
|
286
|
+
# Build initial state JSON using jq
|
|
287
|
+
local repos_json="[]"
|
|
288
|
+
for repo in "${REPOS[@]}"; do
|
|
289
|
+
local expanded
|
|
290
|
+
expanded=$(eval echo "$repo")
|
|
291
|
+
local rname
|
|
292
|
+
rname=$(basename "$expanded")
|
|
293
|
+
repos_json=$(echo "$repos_json" | jq --arg name "$rname" --arg path "$expanded" \
|
|
294
|
+
'. + [{"name": $name, "path": $path, "status": "pending", "pr_url": "-", "duration": "-", "pid": 0}]')
|
|
295
|
+
done
|
|
296
|
+
|
|
297
|
+
# Atomic write initial state
|
|
298
|
+
local tmp_state
|
|
299
|
+
tmp_state=$(mktemp)
|
|
300
|
+
jq -n \
|
|
301
|
+
--arg goal "$GOAL" \
|
|
302
|
+
--arg branch "$branch_name" \
|
|
303
|
+
--arg template "$TEMPLATE" \
|
|
304
|
+
--arg started "$(now_iso)" \
|
|
305
|
+
--arg session_id "$session_id" \
|
|
306
|
+
--argjson repos "$repos_json" \
|
|
307
|
+
'{goal: $goal, branch: $branch, template: $template, started: $started, session_id: $session_id, status: "running", repos: $repos}' \
|
|
308
|
+
> "$tmp_state"
|
|
309
|
+
mv "$tmp_state" "$state_file"
|
|
310
|
+
|
|
311
|
+
emit_event "fix.started" "goal=$GOAL" "repos=${#REPOS[@]}" "template=$TEMPLATE" "session=$session_id"
|
|
312
|
+
|
|
313
|
+
# ─── Parallel Execution ─────────────────────────────────────────────────
|
|
314
|
+
local pids=()
|
|
315
|
+
local pid_to_idx=()
|
|
316
|
+
local idx=0
|
|
317
|
+
|
|
318
|
+
for repo in "${REPOS[@]}"; do
|
|
319
|
+
local expanded
|
|
320
|
+
expanded=$(eval echo "$repo")
|
|
321
|
+
local rname
|
|
322
|
+
rname=$(basename "$expanded")
|
|
323
|
+
|
|
324
|
+
# Throttle: wait for a slot if at max parallel
|
|
325
|
+
while [[ ${#pids[@]} -ge $MAX_PARALLEL ]]; do
|
|
326
|
+
# Wait for any one to finish
|
|
327
|
+
wait -n "${pids[@]}" 2>/dev/null || true
|
|
328
|
+
# Rebuild pids array — remove finished ones
|
|
329
|
+
local new_pids=()
|
|
330
|
+
for p in "${pids[@]}"; do
|
|
331
|
+
if kill -0 "$p" 2>/dev/null; then
|
|
332
|
+
new_pids+=("$p")
|
|
333
|
+
fi
|
|
334
|
+
done
|
|
335
|
+
pids=("${new_pids[@]}")
|
|
336
|
+
done
|
|
337
|
+
|
|
338
|
+
info "Starting: ${BOLD}${rname}${RESET}"
|
|
339
|
+
|
|
340
|
+
emit_event "fix.repo.started" "repo=$rname" "session=$session_id"
|
|
341
|
+
|
|
342
|
+
# Update state to running
|
|
343
|
+
tmp_state=$(mktemp)
|
|
344
|
+
jq --arg name "$rname" '(.repos[] | select(.name == $name)).status = "running"' "$state_file" > "$tmp_state"
|
|
345
|
+
mv "$tmp_state" "$state_file"
|
|
346
|
+
|
|
347
|
+
# Spawn pipeline in subshell
|
|
348
|
+
(
|
|
349
|
+
cd "$expanded"
|
|
350
|
+
|
|
351
|
+
# Determine base branch
|
|
352
|
+
local base
|
|
353
|
+
base=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@') || base="main"
|
|
354
|
+
|
|
355
|
+
# Create fix branch
|
|
356
|
+
git checkout -b "$branch_name" "origin/$base" 2>/dev/null || git checkout -b "$branch_name" "$base" 2>/dev/null || true
|
|
357
|
+
|
|
358
|
+
# Build pipeline command
|
|
359
|
+
local cmd=("$SCRIPT_DIR/cct-pipeline.sh" start --goal "$GOAL" --pipeline "$TEMPLATE" --skip-gates)
|
|
360
|
+
[[ -n "$MODEL" ]] && cmd+=(--model "$MODEL")
|
|
361
|
+
|
|
362
|
+
# Run pipeline
|
|
363
|
+
"${cmd[@]}"
|
|
364
|
+
) > "$log_dir/${rname}.log" 2>&1 &
|
|
365
|
+
|
|
366
|
+
local pid=$!
|
|
367
|
+
pids+=("$pid")
|
|
368
|
+
|
|
369
|
+
# Track PID → repo index via temp file (subshell-safe)
|
|
370
|
+
echo "$rname" > "$log_dir/.pid-${pid}"
|
|
371
|
+
|
|
372
|
+
idx=$((idx + 1))
|
|
373
|
+
done
|
|
374
|
+
|
|
375
|
+
# ─── Wait for All ───────────────────────────────────────────────────────
|
|
376
|
+
info "Waiting for ${#REPOS[@]} pipelines to complete..."
|
|
377
|
+
echo ""
|
|
378
|
+
|
|
379
|
+
local success_count=0
|
|
380
|
+
local fail_count=0
|
|
381
|
+
|
|
382
|
+
# Wait for remaining PIDs and collect results
|
|
383
|
+
for pid in "${pids[@]}"; do
|
|
384
|
+
local rname=""
|
|
385
|
+
if [[ -f "$log_dir/.pid-${pid}" ]]; then
|
|
386
|
+
rname=$(< "$log_dir/.pid-${pid}")
|
|
387
|
+
fi
|
|
388
|
+
|
|
389
|
+
local repo_start
|
|
390
|
+
repo_start=$(now_epoch)
|
|
391
|
+
|
|
392
|
+
if wait "$pid" 2>/dev/null; then
|
|
393
|
+
local repo_end
|
|
394
|
+
repo_end=$(now_epoch)
|
|
395
|
+
local repo_dur=$((repo_end - repo_start))
|
|
396
|
+
|
|
397
|
+
# Try to extract PR URL from log
|
|
398
|
+
local pr_url="-"
|
|
399
|
+
if [[ -f "$log_dir/${rname}.log" ]]; then
|
|
400
|
+
pr_url=$(grep -oE 'https://github\.com/[^ ]+/pull/[0-9]+' "$log_dir/${rname}.log" 2>/dev/null | tail -1) || pr_url="-"
|
|
401
|
+
[[ -z "$pr_url" ]] && pr_url="-"
|
|
402
|
+
fi
|
|
403
|
+
|
|
404
|
+
success " ${rname}: pass"
|
|
405
|
+
success_count=$((success_count + 1))
|
|
406
|
+
|
|
407
|
+
emit_event "fix.repo.completed" "repo=$rname" "status=pass" "pr_url=$pr_url" "session=$session_id"
|
|
408
|
+
|
|
409
|
+
# Update state
|
|
410
|
+
tmp_state=$(mktemp)
|
|
411
|
+
jq --arg name "$rname" --arg pr "$pr_url" --arg dur "$(format_duration $repo_dur)" \
|
|
412
|
+
'(.repos[] | select(.name == $name)) |= (.status = "pass" | .pr_url = $pr | .duration = $dur)' \
|
|
413
|
+
"$state_file" > "$tmp_state"
|
|
414
|
+
mv "$tmp_state" "$state_file"
|
|
415
|
+
else
|
|
416
|
+
local repo_end
|
|
417
|
+
repo_end=$(now_epoch)
|
|
418
|
+
local repo_dur=$((repo_end - repo_start))
|
|
419
|
+
|
|
420
|
+
error " ${rname}: fail"
|
|
421
|
+
fail_count=$((fail_count + 1))
|
|
422
|
+
|
|
423
|
+
emit_event "fix.repo.completed" "repo=$rname" "status=fail" "session=$session_id"
|
|
424
|
+
|
|
425
|
+
tmp_state=$(mktemp)
|
|
426
|
+
jq --arg name "$rname" --arg dur "$(format_duration $repo_dur)" \
|
|
427
|
+
'(.repos[] | select(.name == $name)) |= (.status = "fail" | .duration = $dur)' \
|
|
428
|
+
"$state_file" > "$tmp_state"
|
|
429
|
+
mv "$tmp_state" "$state_file"
|
|
430
|
+
fi
|
|
431
|
+
done
|
|
432
|
+
|
|
433
|
+
# ─── Summary ────────────────────────────────────────────────────────────
|
|
434
|
+
local end_epoch
|
|
435
|
+
end_epoch=$(now_epoch)
|
|
436
|
+
local total_dur=$((end_epoch - start_epoch))
|
|
437
|
+
local final_status="completed"
|
|
438
|
+
[[ $fail_count -gt 0 ]] && final_status="partial"
|
|
439
|
+
[[ $success_count -eq 0 ]] && final_status="failed"
|
|
440
|
+
|
|
441
|
+
# Update final state
|
|
442
|
+
tmp_state=$(mktemp)
|
|
443
|
+
jq --arg status "$final_status" --arg dur "$(format_duration $total_dur)" \
|
|
444
|
+
'.status = $status | .total_duration = $dur' "$state_file" > "$tmp_state"
|
|
445
|
+
mv "$tmp_state" "$state_file"
|
|
446
|
+
|
|
447
|
+
emit_event "fix.completed" "goal=$GOAL" "session=$session_id" \
|
|
448
|
+
"success=$success_count" "fail=$fail_count" "total=${#REPOS[@]}" \
|
|
449
|
+
"duration=$total_dur" "status=$final_status"
|
|
450
|
+
|
|
451
|
+
echo ""
|
|
452
|
+
echo -e "${CYAN}${BOLD}═══ Fix Complete: \"${GOAL}\" ═══${RESET}"
|
|
453
|
+
echo ""
|
|
454
|
+
printf " ${BOLD}%-16s %-10s %-30s %s${RESET}\n" "Repo" "Status" "PR" "Duration"
|
|
455
|
+
echo -e " ${DIM}────────────────────────────────────────────────────────────────${RESET}"
|
|
456
|
+
|
|
457
|
+
while IFS='|' read -r rname rstatus rpr rdur; do
|
|
458
|
+
[[ -z "$rname" ]] && continue
|
|
459
|
+
local icon="${YELLOW}⋯${RESET}"
|
|
460
|
+
[[ "$rstatus" == "pass" ]] && icon="${GREEN}✓${RESET}"
|
|
461
|
+
[[ "$rstatus" == "fail" ]] && icon="${RED}✗${RESET}"
|
|
462
|
+
printf " %-16s ${icon} %-8s %-30s %s\n" "$rname" "$rstatus" "$rpr" "$rdur"
|
|
463
|
+
done < <(jq -r '.repos[] | "\(.name)|\(.status)|\(.pr_url)|\(.duration)"' "$state_file" 2>/dev/null)
|
|
464
|
+
|
|
465
|
+
echo ""
|
|
466
|
+
echo -e " ${BOLD}Success:${RESET} ${success_count}/${#REPOS[@]} | ${BOLD}Duration:${RESET} $(format_duration $total_dur) (parallel)"
|
|
467
|
+
|
|
468
|
+
if [[ $fail_count -gt 0 ]]; then
|
|
469
|
+
echo ""
|
|
470
|
+
echo -e " ${DIM}Logs: $log_dir${RESET}"
|
|
471
|
+
fi
|
|
472
|
+
echo ""
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
# ─── Main ───────────────────────────────────────────────────────────────────
|
|
476
|
+
|
|
477
|
+
parse_args "$@"
|
|
478
|
+
fix_start
|