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,405 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ shipwright worktree — Git worktree management for multi-agent isolation ║
|
|
4
|
+
# ║ ║
|
|
5
|
+
# ║ Each agent gets its own worktree so parallel agents don't clobber ║
|
|
6
|
+
# ║ each other's files. Worktrees live in .worktrees/ relative to root. ║
|
|
7
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
# ─── Colors ────────────────────────────────────────────────────────────────
|
|
11
|
+
CYAN='\033[38;2;0;212;255m'
|
|
12
|
+
PURPLE='\033[38;2;124;58;237m'
|
|
13
|
+
GREEN='\033[38;2;74;222;128m'
|
|
14
|
+
YELLOW='\033[38;2;250;204;21m'
|
|
15
|
+
RED='\033[38;2;248;113;113m'
|
|
16
|
+
DIM='\033[2m'
|
|
17
|
+
BOLD='\033[1m'
|
|
18
|
+
RESET='\033[0m'
|
|
19
|
+
|
|
20
|
+
info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
|
|
21
|
+
success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
|
|
22
|
+
warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
|
|
23
|
+
error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
|
|
24
|
+
|
|
25
|
+
# ─── Repo root ─────────────────────────────────────────────────────────────
|
|
26
|
+
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || {
|
|
27
|
+
error "Not inside a git repository."
|
|
28
|
+
exit 1
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
WORKTREE_DIR="$REPO_ROOT/.worktrees"
|
|
32
|
+
|
|
33
|
+
# ─── .gitignore helper ────────────────────────────────────────────────────
|
|
34
|
+
ensure_gitignore() {
|
|
35
|
+
local gitignore="$REPO_ROOT/.gitignore"
|
|
36
|
+
if ! grep -q '^\.worktrees/' "$gitignore" 2>/dev/null; then
|
|
37
|
+
echo ".worktrees/" >> "$gitignore"
|
|
38
|
+
info "Added .worktrees/ to .gitignore"
|
|
39
|
+
fi
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# ─── Commands ──────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
worktree_create() {
|
|
45
|
+
local name="$1"
|
|
46
|
+
local branch="${2:-loop/$name}"
|
|
47
|
+
local worktree_path="$WORKTREE_DIR/$name"
|
|
48
|
+
|
|
49
|
+
if [[ -d "$worktree_path" ]]; then
|
|
50
|
+
warn "Worktree '$name' already exists at $worktree_path"
|
|
51
|
+
return 0
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
ensure_gitignore
|
|
55
|
+
mkdir -p "$WORKTREE_DIR"
|
|
56
|
+
|
|
57
|
+
# Create branch from current HEAD if it doesn't exist
|
|
58
|
+
git branch "$branch" HEAD 2>/dev/null || true
|
|
59
|
+
|
|
60
|
+
# Create worktree
|
|
61
|
+
git worktree add "$worktree_path" "$branch"
|
|
62
|
+
|
|
63
|
+
success "Created worktree: ${BOLD}$name${RESET} → $worktree_path ${DIM}(branch: $branch)${RESET}"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
worktree_list() {
|
|
67
|
+
if [[ ! -d "$WORKTREE_DIR" ]]; then
|
|
68
|
+
echo -e " ${DIM}No worktrees found.${RESET}"
|
|
69
|
+
return 0
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
local found=0
|
|
73
|
+
echo ""
|
|
74
|
+
echo -e "${BOLD}AGENT WORKTREES${RESET}"
|
|
75
|
+
echo -e "${DIM}───────────────────────────────────────────────────────────────${RESET}"
|
|
76
|
+
|
|
77
|
+
for dir in "$WORKTREE_DIR"/*/; do
|
|
78
|
+
[[ -d "$dir" ]] || continue
|
|
79
|
+
local name
|
|
80
|
+
name="$(basename "$dir")"
|
|
81
|
+
local branch="loop/$name"
|
|
82
|
+
local main_branch
|
|
83
|
+
main_branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "main")"
|
|
84
|
+
|
|
85
|
+
local ahead behind
|
|
86
|
+
ahead="$(git rev-list "$main_branch".."$branch" --count 2>/dev/null || echo "?")"
|
|
87
|
+
behind="$(git rev-list "$branch".."$main_branch" --count 2>/dev/null || echo "?")"
|
|
88
|
+
|
|
89
|
+
local status_str=""
|
|
90
|
+
if [[ "$ahead" != "?" && "$behind" != "?" ]]; then
|
|
91
|
+
if [[ "$ahead" -gt 0 && "$behind" -gt 0 ]]; then
|
|
92
|
+
status_str="${GREEN}${ahead} ahead${RESET}, ${YELLOW}${behind} behind${RESET}"
|
|
93
|
+
elif [[ "$ahead" -gt 0 ]]; then
|
|
94
|
+
status_str="${GREEN}${ahead} ahead${RESET}"
|
|
95
|
+
elif [[ "$behind" -gt 0 ]]; then
|
|
96
|
+
status_str="${YELLOW}${behind} behind${RESET}"
|
|
97
|
+
else
|
|
98
|
+
status_str="${DIM}up to date${RESET}"
|
|
99
|
+
fi
|
|
100
|
+
else
|
|
101
|
+
status_str="${DIM}?${RESET}"
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
printf " ${CYAN}%-16s${RESET} ${PURPLE}%-22s${RESET} %b ${DIM}.worktrees/%s/${RESET}\n" \
|
|
105
|
+
"$name" "$branch" "$status_str" "$name"
|
|
106
|
+
((found++))
|
|
107
|
+
done
|
|
108
|
+
|
|
109
|
+
if [[ $found -eq 0 ]]; then
|
|
110
|
+
echo -e " ${DIM}No worktrees found.${RESET}"
|
|
111
|
+
fi
|
|
112
|
+
echo ""
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
worktree_sync() {
|
|
116
|
+
local name="$1"
|
|
117
|
+
local worktree_path="$WORKTREE_DIR/$name"
|
|
118
|
+
|
|
119
|
+
if [[ ! -d "$worktree_path" ]]; then
|
|
120
|
+
error "Worktree '$name' does not exist."
|
|
121
|
+
return 1
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
info "Syncing ${BOLD}$name${RESET} with main..."
|
|
125
|
+
|
|
126
|
+
(
|
|
127
|
+
cd "$worktree_path"
|
|
128
|
+
git fetch origin main 2>/dev/null || true
|
|
129
|
+
git merge origin/main --no-edit 2>/dev/null || {
|
|
130
|
+
warn "Merge conflict in $name — resolve manually in $worktree_path"
|
|
131
|
+
return 1
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
success "Synced $name with latest main"
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
worktree_sync_all() {
|
|
139
|
+
if [[ ! -d "$WORKTREE_DIR" ]]; then
|
|
140
|
+
warn "No worktrees found."
|
|
141
|
+
return 0
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
local count=0
|
|
145
|
+
local failed=0
|
|
146
|
+
|
|
147
|
+
for dir in "$WORKTREE_DIR"/*/; do
|
|
148
|
+
[[ -d "$dir" ]] || continue
|
|
149
|
+
local name
|
|
150
|
+
name="$(basename "$dir")"
|
|
151
|
+
worktree_sync "$name" || ((failed++))
|
|
152
|
+
((count++))
|
|
153
|
+
done
|
|
154
|
+
|
|
155
|
+
echo ""
|
|
156
|
+
if [[ $failed -gt 0 ]]; then
|
|
157
|
+
warn "Synced $count worktrees, $failed had conflicts"
|
|
158
|
+
else
|
|
159
|
+
success "Synced $count worktrees"
|
|
160
|
+
fi
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
worktree_merge() {
|
|
164
|
+
local name="$1"
|
|
165
|
+
local branch="loop/$name"
|
|
166
|
+
local current_branch
|
|
167
|
+
current_branch="$(git branch --show-current)"
|
|
168
|
+
|
|
169
|
+
if ! git rev-parse --verify "$branch" &>/dev/null; then
|
|
170
|
+
error "Branch '$branch' does not exist."
|
|
171
|
+
return 1
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
info "Merging ${BOLD}$branch${RESET} into ${BOLD}$current_branch${RESET}..."
|
|
175
|
+
|
|
176
|
+
git merge "$branch" --no-edit || {
|
|
177
|
+
error "Merge conflict merging $branch"
|
|
178
|
+
echo -e " ${DIM}Resolve conflicts, then run: git merge --continue${RESET}"
|
|
179
|
+
return 1
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
success "Merged $branch into $current_branch"
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
worktree_merge_all() {
|
|
186
|
+
if [[ ! -d "$WORKTREE_DIR" ]]; then
|
|
187
|
+
warn "No worktrees found."
|
|
188
|
+
return 0
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
local count=0
|
|
192
|
+
|
|
193
|
+
for dir in "$WORKTREE_DIR"/*/; do
|
|
194
|
+
[[ -d "$dir" ]] || continue
|
|
195
|
+
local name
|
|
196
|
+
name="$(basename "$dir")"
|
|
197
|
+
|
|
198
|
+
worktree_merge "$name" || {
|
|
199
|
+
error "Stopping merge-all due to conflict in $name"
|
|
200
|
+
echo -e " ${DIM}Resolve the conflict, then re-run: shipwright worktree merge-all${RESET}"
|
|
201
|
+
return 1
|
|
202
|
+
}
|
|
203
|
+
((count++))
|
|
204
|
+
done
|
|
205
|
+
|
|
206
|
+
echo ""
|
|
207
|
+
success "Merged $count worktree branches"
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
worktree_remove() {
|
|
211
|
+
local name="$1"
|
|
212
|
+
local worktree_path="$WORKTREE_DIR/$name"
|
|
213
|
+
local branch="loop/$name"
|
|
214
|
+
|
|
215
|
+
if [[ -d "$worktree_path" ]]; then
|
|
216
|
+
git worktree remove "$worktree_path" --force 2>/dev/null || {
|
|
217
|
+
warn "Could not cleanly remove worktree $name, forcing..."
|
|
218
|
+
rm -rf "$worktree_path"
|
|
219
|
+
}
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
git branch -D "$branch" 2>/dev/null || true
|
|
223
|
+
|
|
224
|
+
success "Removed worktree: ${BOLD}$name${RESET}"
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
worktree_cleanup() {
|
|
228
|
+
if [[ ! -d "$WORKTREE_DIR" ]]; then
|
|
229
|
+
success "Nothing to clean up — no .worktrees/ directory."
|
|
230
|
+
return 0
|
|
231
|
+
fi
|
|
232
|
+
|
|
233
|
+
local count=0
|
|
234
|
+
|
|
235
|
+
for dir in "$WORKTREE_DIR"/*/; do
|
|
236
|
+
[[ -d "$dir" ]] || continue
|
|
237
|
+
local name
|
|
238
|
+
name="$(basename "$dir")"
|
|
239
|
+
worktree_remove "$name"
|
|
240
|
+
((count++))
|
|
241
|
+
done
|
|
242
|
+
|
|
243
|
+
# Prune stale worktree references
|
|
244
|
+
git worktree prune
|
|
245
|
+
|
|
246
|
+
# Remove .worktrees directory if empty
|
|
247
|
+
rmdir "$WORKTREE_DIR" 2>/dev/null || true
|
|
248
|
+
|
|
249
|
+
echo ""
|
|
250
|
+
success "Cleaned up $count worktrees"
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
worktree_status() {
|
|
254
|
+
if [[ ! -d "$WORKTREE_DIR" ]]; then
|
|
255
|
+
echo -e " ${DIM}No worktrees found.${RESET}"
|
|
256
|
+
return 0
|
|
257
|
+
fi
|
|
258
|
+
|
|
259
|
+
local main_branch
|
|
260
|
+
main_branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "main")"
|
|
261
|
+
local found=0
|
|
262
|
+
|
|
263
|
+
echo ""
|
|
264
|
+
echo -e "${BOLD}WORKTREE STATUS${RESET} ${DIM}(relative to ${main_branch})${RESET}"
|
|
265
|
+
echo -e "${DIM}───────────────────────────────────────────────────────────────${RESET}"
|
|
266
|
+
|
|
267
|
+
for dir in "$WORKTREE_DIR"/*/; do
|
|
268
|
+
[[ -d "$dir" ]] || continue
|
|
269
|
+
local name
|
|
270
|
+
name="$(basename "$dir")"
|
|
271
|
+
local branch="loop/$name"
|
|
272
|
+
|
|
273
|
+
local ahead behind
|
|
274
|
+
ahead="$(git rev-list "$main_branch".."$branch" --count 2>/dev/null || echo "?")"
|
|
275
|
+
behind="$(git rev-list "$branch".."$main_branch" --count 2>/dev/null || echo "?")"
|
|
276
|
+
|
|
277
|
+
# Check for uncommitted changes in the worktree
|
|
278
|
+
local dirty=""
|
|
279
|
+
if (cd "$dir" && [[ -n "$(git status --porcelain 2>/dev/null)" ]]); then
|
|
280
|
+
dirty=" ${RED}(dirty)${RESET}"
|
|
281
|
+
fi
|
|
282
|
+
|
|
283
|
+
printf " ${CYAN}%-16s${RESET} ${PURPLE}%-22s${RESET} ${GREEN}%s ahead${RESET}, ${YELLOW}%s behind${RESET}%b\n" \
|
|
284
|
+
"$name" "$branch" "$ahead" "$behind" "$dirty"
|
|
285
|
+
((found++))
|
|
286
|
+
done
|
|
287
|
+
|
|
288
|
+
if [[ $found -eq 0 ]]; then
|
|
289
|
+
echo -e " ${DIM}No worktrees found.${RESET}"
|
|
290
|
+
fi
|
|
291
|
+
echo ""
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
worktree_help() {
|
|
295
|
+
echo ""
|
|
296
|
+
echo -e "${CYAN}${BOLD}shipwright worktree${RESET} — Git worktree management for multi-agent isolation"
|
|
297
|
+
echo ""
|
|
298
|
+
echo -e "${BOLD}USAGE${RESET}"
|
|
299
|
+
echo -e " shipwright worktree <command> [options]"
|
|
300
|
+
echo ""
|
|
301
|
+
echo -e "${BOLD}COMMANDS${RESET}"
|
|
302
|
+
echo -e " ${GREEN}create${RESET} <name> [--branch <branch>] Create a worktree for an agent"
|
|
303
|
+
echo -e " ${GREEN}list${RESET} List active agent worktrees"
|
|
304
|
+
echo -e " ${GREEN}sync${RESET} <name> Pull latest main into a worktree"
|
|
305
|
+
echo -e " ${GREEN}sync-all${RESET} Sync all worktrees with main"
|
|
306
|
+
echo -e " ${GREEN}merge${RESET} <name> Merge worktree branch back to main"
|
|
307
|
+
echo -e " ${GREEN}merge-all${RESET} Merge all worktree branches sequentially"
|
|
308
|
+
echo -e " ${GREEN}remove${RESET} <name> Remove a single worktree"
|
|
309
|
+
echo -e " ${GREEN}cleanup${RESET} Remove ALL worktrees and branches"
|
|
310
|
+
echo -e " ${GREEN}status${RESET} Show status of all worktrees"
|
|
311
|
+
echo -e " ${GREEN}help${RESET} Show this help"
|
|
312
|
+
echo ""
|
|
313
|
+
echo -e "${BOLD}EXAMPLES${RESET}"
|
|
314
|
+
echo -e " ${DIM}shipwright worktree create agent-1${RESET} # Create worktree on branch loop/agent-1"
|
|
315
|
+
echo -e " ${DIM}shipwright worktree create agent-1 --branch feat${RESET} # Custom branch name"
|
|
316
|
+
echo -e " ${DIM}shipwright worktree merge-all${RESET} # Merge all agent work back to main"
|
|
317
|
+
echo -e " ${DIM}shipwright worktree cleanup${RESET} # Remove all worktrees when done"
|
|
318
|
+
echo ""
|
|
319
|
+
echo -e "${BOLD}DIRECTORY STRUCTURE${RESET}"
|
|
320
|
+
echo -e " ${DIM}project-root/${RESET}"
|
|
321
|
+
echo -e " ${DIM}├── .worktrees/ # All agent worktrees${RESET}"
|
|
322
|
+
echo -e " ${DIM}│ ├── agent-1/ # Full repo copy for agent 1${RESET}"
|
|
323
|
+
echo -e " ${DIM}│ ├── agent-2/ # Full repo copy for agent 2${RESET}"
|
|
324
|
+
echo -e " ${DIM}│ └── agent-3/ # Full repo copy for agent 3${RESET}"
|
|
325
|
+
echo -e " ${DIM}└── .gitignore # Includes .worktrees/${RESET}"
|
|
326
|
+
echo ""
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
# ─── Main dispatch ─────────────────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
COMMAND="${1:-help}"
|
|
332
|
+
shift || true
|
|
333
|
+
|
|
334
|
+
case "$COMMAND" in
|
|
335
|
+
create)
|
|
336
|
+
if [[ $# -lt 1 ]]; then
|
|
337
|
+
error "Usage: shipwright worktree create <name> [--branch <branch>]"
|
|
338
|
+
exit 1
|
|
339
|
+
fi
|
|
340
|
+
NAME="$1"
|
|
341
|
+
shift
|
|
342
|
+
BRANCH=""
|
|
343
|
+
while [[ $# -gt 0 ]]; do
|
|
344
|
+
case "$1" in
|
|
345
|
+
--branch)
|
|
346
|
+
BRANCH="${2:-}"
|
|
347
|
+
shift 2 || { error "--branch requires a value"; exit 1; }
|
|
348
|
+
;;
|
|
349
|
+
*)
|
|
350
|
+
error "Unknown option: $1"
|
|
351
|
+
exit 1
|
|
352
|
+
;;
|
|
353
|
+
esac
|
|
354
|
+
done
|
|
355
|
+
if [[ -n "$BRANCH" ]]; then
|
|
356
|
+
worktree_create "$NAME" "$BRANCH"
|
|
357
|
+
else
|
|
358
|
+
worktree_create "$NAME"
|
|
359
|
+
fi
|
|
360
|
+
;;
|
|
361
|
+
list)
|
|
362
|
+
worktree_list
|
|
363
|
+
;;
|
|
364
|
+
sync)
|
|
365
|
+
if [[ $# -lt 1 ]]; then
|
|
366
|
+
error "Usage: shipwright worktree sync <name>"
|
|
367
|
+
exit 1
|
|
368
|
+
fi
|
|
369
|
+
worktree_sync "$1"
|
|
370
|
+
;;
|
|
371
|
+
sync-all)
|
|
372
|
+
worktree_sync_all
|
|
373
|
+
;;
|
|
374
|
+
merge)
|
|
375
|
+
if [[ $# -lt 1 ]]; then
|
|
376
|
+
error "Usage: shipwright worktree merge <name>"
|
|
377
|
+
exit 1
|
|
378
|
+
fi
|
|
379
|
+
worktree_merge "$1"
|
|
380
|
+
;;
|
|
381
|
+
merge-all)
|
|
382
|
+
worktree_merge_all
|
|
383
|
+
;;
|
|
384
|
+
remove)
|
|
385
|
+
if [[ $# -lt 1 ]]; then
|
|
386
|
+
error "Usage: shipwright worktree remove <name>"
|
|
387
|
+
exit 1
|
|
388
|
+
fi
|
|
389
|
+
worktree_remove "$1"
|
|
390
|
+
;;
|
|
391
|
+
cleanup)
|
|
392
|
+
worktree_cleanup
|
|
393
|
+
;;
|
|
394
|
+
status)
|
|
395
|
+
worktree_status
|
|
396
|
+
;;
|
|
397
|
+
help|--help|-h)
|
|
398
|
+
worktree_help
|
|
399
|
+
;;
|
|
400
|
+
*)
|
|
401
|
+
error "Unknown command: $COMMAND"
|
|
402
|
+
echo -e " ${DIM}Run 'shipwright worktree help' for usage.${RESET}"
|
|
403
|
+
exit 1
|
|
404
|
+
;;
|
|
405
|
+
esac
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
// ║ Shipwright — npm postinstall ║
|
|
4
|
+
// ║ Copies templates and migrates legacy config directories ║
|
|
5
|
+
// ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
6
|
+
|
|
7
|
+
import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync, appendFileSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
|
|
10
|
+
const HOME = process.env.HOME || process.env.USERPROFILE;
|
|
11
|
+
const PKG_DIR = join(import.meta.dirname, "..");
|
|
12
|
+
const SHIPWRIGHT_DIR = join(HOME, ".shipwright");
|
|
13
|
+
const LEGACY_DIR = join(HOME, ".claude-teams");
|
|
14
|
+
const CLAUDE_DIR = join(HOME, ".claude");
|
|
15
|
+
|
|
16
|
+
const CYAN = "\x1b[38;2;0;212;255m";
|
|
17
|
+
const GREEN = "\x1b[38;2;74;222;128m";
|
|
18
|
+
const YELLOW = "\x1b[38;2;250;204;21m";
|
|
19
|
+
const DIM = "\x1b[2m";
|
|
20
|
+
const BOLD = "\x1b[1m";
|
|
21
|
+
const RESET = "\x1b[0m";
|
|
22
|
+
|
|
23
|
+
function info(msg) { console.log(`${CYAN}${BOLD}▸${RESET} ${msg}`); }
|
|
24
|
+
function success(msg) { console.log(`${GREEN}${BOLD}✓${RESET} ${msg}`); }
|
|
25
|
+
function warn(msg) { console.log(`${YELLOW}${BOLD}⚠${RESET} ${msg}`); }
|
|
26
|
+
|
|
27
|
+
function ensureDir(dir) {
|
|
28
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function copyDir(src, dest) {
|
|
32
|
+
if (!existsSync(src)) return;
|
|
33
|
+
ensureDir(dest);
|
|
34
|
+
cpSync(src, dest, { recursive: true, force: false });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Copy team templates → ~/.shipwright/templates/
|
|
39
|
+
copyDir(join(PKG_DIR, "tmux", "templates"), join(SHIPWRIGHT_DIR, "templates"));
|
|
40
|
+
success("Installed team templates");
|
|
41
|
+
|
|
42
|
+
// Copy pipeline templates → ~/.shipwright/pipelines/
|
|
43
|
+
copyDir(join(PKG_DIR, "templates", "pipelines"), join(SHIPWRIGHT_DIR, "pipelines"));
|
|
44
|
+
success("Installed pipeline templates");
|
|
45
|
+
|
|
46
|
+
// Copy settings template → ~/.claude/settings.json.template (if missing)
|
|
47
|
+
const settingsTemplate = join(PKG_DIR, "claude-code", "settings.json");
|
|
48
|
+
const settingsDest = join(CLAUDE_DIR, "settings.json.template");
|
|
49
|
+
if (existsSync(settingsTemplate) && !existsSync(settingsDest)) {
|
|
50
|
+
ensureDir(CLAUDE_DIR);
|
|
51
|
+
cpSync(settingsTemplate, settingsDest);
|
|
52
|
+
success("Installed settings template");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Install CLAUDE.md agent instructions → ~/.claude/CLAUDE.md (idempotent)
|
|
56
|
+
const claudeMdSrc = join(PKG_DIR, "claude-code", "CLAUDE.md.shipwright");
|
|
57
|
+
const claudeMdDest = join(CLAUDE_DIR, "CLAUDE.md");
|
|
58
|
+
if (existsSync(claudeMdSrc)) {
|
|
59
|
+
ensureDir(CLAUDE_DIR);
|
|
60
|
+
if (existsSync(claudeMdDest)) {
|
|
61
|
+
const existing = readFileSync(claudeMdDest, "utf8");
|
|
62
|
+
if (!existing.includes("Shipwright")) {
|
|
63
|
+
appendFileSync(claudeMdDest, "\n---\n\n" + readFileSync(claudeMdSrc, "utf8"));
|
|
64
|
+
success("Appended Shipwright instructions to ~/.claude/CLAUDE.md");
|
|
65
|
+
} else {
|
|
66
|
+
success("~/.claude/CLAUDE.md already contains Shipwright instructions");
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
cpSync(claudeMdSrc, claudeMdDest);
|
|
70
|
+
success("Installed ~/.claude/CLAUDE.md");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Migrate ~/.claude-teams/ → ~/.shipwright/ (non-destructive)
|
|
75
|
+
if (existsSync(LEGACY_DIR) && !existsSync(join(SHIPWRIGHT_DIR, ".migrated"))) {
|
|
76
|
+
info("Migrating legacy ~/.claude-teams/ config...");
|
|
77
|
+
copyDir(LEGACY_DIR, SHIPWRIGHT_DIR);
|
|
78
|
+
writeFileSync(join(SHIPWRIGHT_DIR, ".migrated"), new Date().toISOString());
|
|
79
|
+
success("Migrated legacy config (originals preserved)");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Print success banner
|
|
83
|
+
const version = JSON.parse(readFileSync(join(PKG_DIR, "package.json"), "utf8")).version;
|
|
84
|
+
console.log();
|
|
85
|
+
console.log(`${CYAN}${BOLD} ⚓ Shipwright v${version} installed${RESET}`);
|
|
86
|
+
console.log();
|
|
87
|
+
console.log(` Next steps:`);
|
|
88
|
+
console.log(` ${DIM}$${RESET} shipwright doctor ${DIM}# Verify your setup${RESET}`);
|
|
89
|
+
console.log(` ${DIM}$${RESET} shipwright session ${DIM}# Launch an agent team${RESET}`);
|
|
90
|
+
console.log(` ${DIM}$${RESET} shipwright pipeline ${DIM}# Run a delivery pipeline${RESET}`);
|
|
91
|
+
console.log();
|
|
92
|
+
} catch (err) {
|
|
93
|
+
warn(`Postinstall encountered an issue: ${err.message}`);
|
|
94
|
+
warn("Shipwright is installed — some templates may need manual setup.");
|
|
95
|
+
warn(`Run: shipwright doctor`);
|
|
96
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "autonomous",
|
|
3
|
+
"description": "Fully autonomous pipeline with compound quality — zero human gates",
|
|
4
|
+
"defaults": {
|
|
5
|
+
"test_cmd": "npm test",
|
|
6
|
+
"model": "opus",
|
|
7
|
+
"agents": 1
|
|
8
|
+
},
|
|
9
|
+
"stages": [
|
|
10
|
+
{ "id": "intake", "enabled": true, "gate": "auto", "config": {} },
|
|
11
|
+
{ "id": "plan", "enabled": true, "gate": "auto", "config": { "model": "opus" } },
|
|
12
|
+
{ "id": "design", "enabled": true, "gate": "auto", "config": { "model": "opus" } },
|
|
13
|
+
{
|
|
14
|
+
"id": "build",
|
|
15
|
+
"enabled": true,
|
|
16
|
+
"gate": "auto",
|
|
17
|
+
"config": { "max_iterations": 20, "audit": true, "quality_gates": true }
|
|
18
|
+
},
|
|
19
|
+
{ "id": "test", "enabled": true, "gate": "auto", "config": { "coverage_min": 80 } },
|
|
20
|
+
{ "id": "review", "enabled": true, "gate": "auto", "config": {} },
|
|
21
|
+
{
|
|
22
|
+
"id": "compound_quality",
|
|
23
|
+
"enabled": true,
|
|
24
|
+
"gate": "auto",
|
|
25
|
+
"config": {
|
|
26
|
+
"adversarial": true,
|
|
27
|
+
"negative": true,
|
|
28
|
+
"e2e": true,
|
|
29
|
+
"dod_audit": true,
|
|
30
|
+
"max_cycles": 3
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{ "id": "pr", "enabled": true, "gate": "auto", "config": { "wait_ci": false } },
|
|
34
|
+
{
|
|
35
|
+
"id": "merge",
|
|
36
|
+
"enabled": true,
|
|
37
|
+
"gate": "auto",
|
|
38
|
+
"config": {
|
|
39
|
+
"merge_method": "squash",
|
|
40
|
+
"wait_ci_timeout_s": 600,
|
|
41
|
+
"auto_delete_branch": true
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "deploy",
|
|
46
|
+
"enabled": false,
|
|
47
|
+
"gate": "approve",
|
|
48
|
+
"config": { "staging_cmd": "", "production_cmd": "", "rollback_cmd": "" }
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "validate",
|
|
52
|
+
"enabled": false,
|
|
53
|
+
"gate": "auto",
|
|
54
|
+
"config": { "smoke_cmd": "", "health_url": "", "close_issue": true }
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"id": "monitor",
|
|
58
|
+
"enabled": true,
|
|
59
|
+
"gate": "auto",
|
|
60
|
+
"config": {
|
|
61
|
+
"duration_minutes": 5,
|
|
62
|
+
"health_url": "",
|
|
63
|
+
"error_threshold": 5,
|
|
64
|
+
"log_pattern": "ERROR|FATAL|PANIC",
|
|
65
|
+
"log_cmd": "",
|
|
66
|
+
"rollback_cmd": "",
|
|
67
|
+
"auto_rollback": false
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cost-aware",
|
|
3
|
+
"description": "Cost-optimized pipeline: budget limits, model routing (haiku→sonnet→opus), per-stage tracking",
|
|
4
|
+
"defaults": {
|
|
5
|
+
"test_cmd": "npm test",
|
|
6
|
+
"model": "sonnet",
|
|
7
|
+
"agents": 1,
|
|
8
|
+
"cost_tracking": true,
|
|
9
|
+
"budget_check": true
|
|
10
|
+
},
|
|
11
|
+
"stages": [
|
|
12
|
+
{
|
|
13
|
+
"id": "intake",
|
|
14
|
+
"enabled": true,
|
|
15
|
+
"gate": "auto",
|
|
16
|
+
"config": {
|
|
17
|
+
"model": "haiku",
|
|
18
|
+
"cost_tracking": true
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "plan",
|
|
23
|
+
"enabled": true,
|
|
24
|
+
"gate": "approve",
|
|
25
|
+
"config": {
|
|
26
|
+
"model": "sonnet",
|
|
27
|
+
"cost_tracking": true
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "build",
|
|
32
|
+
"enabled": true,
|
|
33
|
+
"gate": "auto",
|
|
34
|
+
"config": {
|
|
35
|
+
"model": "sonnet",
|
|
36
|
+
"max_iterations": 20,
|
|
37
|
+
"audit": true,
|
|
38
|
+
"quality_gates": true,
|
|
39
|
+
"cost_tracking": true,
|
|
40
|
+
"abort_on_budget": true
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "test",
|
|
45
|
+
"enabled": true,
|
|
46
|
+
"gate": "auto",
|
|
47
|
+
"config": {
|
|
48
|
+
"coverage_min": 80,
|
|
49
|
+
"model": "haiku",
|
|
50
|
+
"cost_tracking": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "review",
|
|
55
|
+
"enabled": true,
|
|
56
|
+
"gate": "approve",
|
|
57
|
+
"config": {
|
|
58
|
+
"model": "opus",
|
|
59
|
+
"cost_tracking": true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"id": "pr",
|
|
64
|
+
"enabled": true,
|
|
65
|
+
"gate": "approve",
|
|
66
|
+
"config": {
|
|
67
|
+
"wait_ci": false,
|
|
68
|
+
"model": "haiku",
|
|
69
|
+
"cost_tracking": true
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": "deploy",
|
|
74
|
+
"enabled": false,
|
|
75
|
+
"gate": "approve",
|
|
76
|
+
"config": {
|
|
77
|
+
"staging_cmd": "",
|
|
78
|
+
"production_cmd": "",
|
|
79
|
+
"rollback_cmd": "",
|
|
80
|
+
"cost_tracking": true
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"id": "validate",
|
|
85
|
+
"enabled": false,
|
|
86
|
+
"gate": "auto",
|
|
87
|
+
"config": {
|
|
88
|
+
"smoke_cmd": "",
|
|
89
|
+
"health_url": "",
|
|
90
|
+
"close_issue": true,
|
|
91
|
+
"cost_tracking": true
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
}
|