shipwright-cli 1.10.0 → 2.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/README.md +114 -36
- package/completions/_shipwright +212 -32
- package/completions/shipwright.bash +97 -25
- package/docs/strategy/01-market-research.md +619 -0
- package/docs/strategy/02-mission-and-brand.md +587 -0
- package/docs/strategy/03-gtm-and-roadmap.md +759 -0
- package/docs/strategy/QUICK-START.txt +289 -0
- package/docs/strategy/README.md +172 -0
- package/package.json +4 -2
- package/scripts/sw +208 -1
- package/scripts/sw-activity.sh +500 -0
- package/scripts/sw-adaptive.sh +925 -0
- package/scripts/sw-adversarial.sh +1 -1
- package/scripts/sw-architecture-enforcer.sh +1 -1
- package/scripts/sw-auth.sh +613 -0
- package/scripts/sw-autonomous.sh +664 -0
- package/scripts/sw-changelog.sh +704 -0
- package/scripts/sw-checkpoint.sh +1 -1
- package/scripts/sw-ci.sh +602 -0
- package/scripts/sw-cleanup.sh +1 -1
- package/scripts/sw-code-review.sh +637 -0
- package/scripts/sw-connect.sh +1 -1
- package/scripts/sw-context.sh +605 -0
- package/scripts/sw-cost.sh +1 -1
- package/scripts/sw-daemon.sh +432 -130
- package/scripts/sw-dashboard.sh +1 -1
- package/scripts/sw-db.sh +540 -0
- package/scripts/sw-decompose.sh +539 -0
- package/scripts/sw-deps.sh +551 -0
- package/scripts/sw-developer-simulation.sh +1 -1
- package/scripts/sw-discovery.sh +412 -0
- package/scripts/sw-docs-agent.sh +539 -0
- package/scripts/sw-docs.sh +1 -1
- package/scripts/sw-doctor.sh +59 -1
- package/scripts/sw-dora.sh +615 -0
- package/scripts/sw-durable.sh +710 -0
- package/scripts/sw-e2e-orchestrator.sh +535 -0
- package/scripts/sw-eventbus.sh +393 -0
- package/scripts/sw-feedback.sh +471 -0
- package/scripts/sw-fix.sh +1 -1
- package/scripts/sw-fleet-discover.sh +567 -0
- package/scripts/sw-fleet-viz.sh +404 -0
- package/scripts/sw-fleet.sh +8 -1
- package/scripts/sw-github-app.sh +596 -0
- package/scripts/sw-github-checks.sh +1 -1
- package/scripts/sw-github-deploy.sh +1 -1
- package/scripts/sw-github-graphql.sh +1 -1
- package/scripts/sw-guild.sh +569 -0
- package/scripts/sw-heartbeat.sh +1 -1
- package/scripts/sw-hygiene.sh +559 -0
- package/scripts/sw-incident.sh +617 -0
- package/scripts/sw-init.sh +88 -1
- package/scripts/sw-instrument.sh +699 -0
- package/scripts/sw-intelligence.sh +1 -1
- package/scripts/sw-jira.sh +1 -1
- package/scripts/sw-launchd.sh +363 -28
- package/scripts/sw-linear.sh +1 -1
- package/scripts/sw-logs.sh +1 -1
- package/scripts/sw-loop.sh +64 -3
- package/scripts/sw-memory.sh +1 -1
- package/scripts/sw-mission-control.sh +487 -0
- package/scripts/sw-model-router.sh +545 -0
- package/scripts/sw-otel.sh +596 -0
- package/scripts/sw-oversight.sh +689 -0
- package/scripts/sw-pipeline-composer.sh +1 -1
- package/scripts/sw-pipeline-vitals.sh +1 -1
- package/scripts/sw-pipeline.sh +687 -24
- package/scripts/sw-pm.sh +693 -0
- package/scripts/sw-pr-lifecycle.sh +522 -0
- package/scripts/sw-predictive.sh +1 -1
- package/scripts/sw-prep.sh +1 -1
- package/scripts/sw-ps.sh +1 -1
- package/scripts/sw-public-dashboard.sh +798 -0
- package/scripts/sw-quality.sh +595 -0
- package/scripts/sw-reaper.sh +1 -1
- package/scripts/sw-recruit.sh +573 -0
- package/scripts/sw-regression.sh +642 -0
- package/scripts/sw-release-manager.sh +736 -0
- package/scripts/sw-release.sh +706 -0
- package/scripts/sw-remote.sh +1 -1
- package/scripts/sw-replay.sh +520 -0
- package/scripts/sw-retro.sh +691 -0
- package/scripts/sw-scale.sh +444 -0
- package/scripts/sw-security-audit.sh +505 -0
- package/scripts/sw-self-optimize.sh +1 -1
- package/scripts/sw-session.sh +1 -1
- package/scripts/sw-setup.sh +1 -1
- package/scripts/sw-standup.sh +712 -0
- package/scripts/sw-status.sh +1 -1
- package/scripts/sw-strategic.sh +658 -0
- package/scripts/sw-stream.sh +450 -0
- package/scripts/sw-swarm.sh +583 -0
- package/scripts/sw-team-stages.sh +511 -0
- package/scripts/sw-templates.sh +1 -1
- package/scripts/sw-testgen.sh +515 -0
- package/scripts/sw-tmux-pipeline.sh +554 -0
- package/scripts/sw-tmux.sh +1 -1
- package/scripts/sw-trace.sh +485 -0
- package/scripts/sw-tracker-github.sh +188 -0
- package/scripts/sw-tracker-jira.sh +172 -0
- package/scripts/sw-tracker-linear.sh +251 -0
- package/scripts/sw-tracker.sh +117 -2
- package/scripts/sw-triage.sh +603 -0
- package/scripts/sw-upgrade.sh +1 -1
- package/scripts/sw-ux.sh +677 -0
- package/scripts/sw-webhook.sh +627 -0
- package/scripts/sw-widgets.sh +530 -0
- package/scripts/sw-worktree.sh +1 -1
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ shipwright decompose — Intelligent Issue Decomposition ║
|
|
4
|
+
# ║ Analyze complexity · Auto-create subtasks · Track progress ║
|
|
5
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
8
|
+
|
|
9
|
+
VERSION="2.0.0"
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
12
|
+
|
|
13
|
+
# ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
|
|
14
|
+
CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
|
|
15
|
+
PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
|
|
16
|
+
BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
|
|
17
|
+
GREEN='\033[38;2;74;222;128m' # success
|
|
18
|
+
YELLOW='\033[38;2;250;204;21m' # warning
|
|
19
|
+
RED='\033[38;2;248;113;113m' # error
|
|
20
|
+
DIM='\033[2m'
|
|
21
|
+
BOLD='\033[1m'
|
|
22
|
+
RESET='\033[0m'
|
|
23
|
+
|
|
24
|
+
# ─── Cross-platform compatibility ──────────────────────────────────────────
|
|
25
|
+
# shellcheck source=lib/compat.sh
|
|
26
|
+
[[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
|
|
27
|
+
|
|
28
|
+
# ─── Output Helpers ─────────────────────────────────────────────────────────
|
|
29
|
+
info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
|
|
30
|
+
success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
|
|
31
|
+
warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
|
|
32
|
+
error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
|
|
33
|
+
|
|
34
|
+
now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
35
|
+
now_epoch() { date +%s; }
|
|
36
|
+
|
|
37
|
+
# ─── Structured Event Log ──────────────────────────────────────────────────
|
|
38
|
+
EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
|
|
39
|
+
|
|
40
|
+
emit_event() {
|
|
41
|
+
local event_type="$1"
|
|
42
|
+
shift
|
|
43
|
+
local json_fields=""
|
|
44
|
+
for kv in "$@"; do
|
|
45
|
+
local key="${kv%%=*}"
|
|
46
|
+
local val="${kv#*=}"
|
|
47
|
+
if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
|
|
48
|
+
json_fields="${json_fields},\"${key}\":${val}"
|
|
49
|
+
else
|
|
50
|
+
val="${val//\"/\\\"}"
|
|
51
|
+
json_fields="${json_fields},\"${key}\":\"${val}\""
|
|
52
|
+
fi
|
|
53
|
+
done
|
|
54
|
+
mkdir -p "${HOME}/.shipwright"
|
|
55
|
+
echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# ─── Configuration ─────────────────────────────────────────────────────────
|
|
59
|
+
COMPLEXITY_THRESHOLD=70 # Decompose if complexity > this
|
|
60
|
+
HOURS_THRESHOLD=8 # Decompose if estimated hours > this
|
|
61
|
+
MAX_SUBTASKS=5
|
|
62
|
+
MIN_SUBTASKS=3
|
|
63
|
+
DECOMPOSE_LABEL="subtask"
|
|
64
|
+
DECOMPOSED_MARKER_LABEL="decomposed"
|
|
65
|
+
|
|
66
|
+
# ─── Helper: Check if issue has label ──────────────────────────────────────
|
|
67
|
+
_has_label() {
|
|
68
|
+
local issue_num="$1"
|
|
69
|
+
local label="$2"
|
|
70
|
+
|
|
71
|
+
if [[ "$NO_GITHUB" == "true" ]]; then
|
|
72
|
+
return 1
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
local labels
|
|
76
|
+
labels=$(gh issue view "$issue_num" --json labels --jq '.labels[].name' 2>/dev/null || echo "")
|
|
77
|
+
[[ "$labels" =~ $label ]]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# ─── Helper: Call Claude for complexity analysis ──────────────────────────
|
|
81
|
+
_decompose_call_claude() {
|
|
82
|
+
local prompt="$1"
|
|
83
|
+
|
|
84
|
+
# Verify claude CLI is available
|
|
85
|
+
if ! command -v claude >/dev/null 2>&1; then
|
|
86
|
+
error "claude CLI not found"
|
|
87
|
+
echo '{"error":"claude_cli_not_found"}'
|
|
88
|
+
return 1
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# Call Claude (--print mode returns raw text response, max-turns 1)
|
|
92
|
+
local response
|
|
93
|
+
if ! response=$(claude --print --max-turns 1 "$prompt" 2>/dev/null); then
|
|
94
|
+
error "Claude call failed"
|
|
95
|
+
echo '{"error":"claude_call_failed"}'
|
|
96
|
+
return 1
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# Extract JSON from the response
|
|
100
|
+
local result
|
|
101
|
+
result=$(echo "$response" | jq -c . 2>/dev/null || echo "")
|
|
102
|
+
|
|
103
|
+
if [[ -z "$result" || "$result" == "null" ]]; then
|
|
104
|
+
error "Failed to parse Claude response as JSON"
|
|
105
|
+
echo '{"error":"parse_failed"}'
|
|
106
|
+
return 1
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
echo "$result"
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# ─── Analyze Issue Complexity ──────────────────────────────────────────────
|
|
113
|
+
decompose_analyze() {
|
|
114
|
+
local issue_num="$1"
|
|
115
|
+
|
|
116
|
+
if [[ "$NO_GITHUB" == "true" ]]; then
|
|
117
|
+
# Mock data for testing (JSON only, no messages)
|
|
118
|
+
echo '{
|
|
119
|
+
"issue_number": '$issue_num',
|
|
120
|
+
"complexity_score": 85,
|
|
121
|
+
"estimated_hours": 12,
|
|
122
|
+
"should_decompose": true,
|
|
123
|
+
"reasoning": "Issue involves major architectural changes",
|
|
124
|
+
"subtasks": [
|
|
125
|
+
{
|
|
126
|
+
"title": "Subtask 1: Design phase",
|
|
127
|
+
"description": "Plan and document the new architecture"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"title": "Subtask 2: Implementation phase",
|
|
131
|
+
"description": "Implement core changes"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"title": "Subtask 3: Integration & testing",
|
|
135
|
+
"description": "Integrate changes and add tests"
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
}'
|
|
139
|
+
return 0
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
# Fetch issue details
|
|
143
|
+
local issue_json
|
|
144
|
+
issue_json=$(gh issue view "$issue_num" --json number,title,body,labels 2>/dev/null || echo "")
|
|
145
|
+
|
|
146
|
+
if [[ -z "$issue_json" ]]; then
|
|
147
|
+
error "Could not fetch issue #${issue_num}"
|
|
148
|
+
return 1
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
local issue_title
|
|
152
|
+
issue_title=$(echo "$issue_json" | jq -r '.title' 2>/dev/null || echo "")
|
|
153
|
+
|
|
154
|
+
local issue_body
|
|
155
|
+
issue_body=$(echo "$issue_json" | jq -r '.body // ""' 2>/dev/null | head -500 || echo "")
|
|
156
|
+
|
|
157
|
+
local issue_labels
|
|
158
|
+
issue_labels=$(echo "$issue_json" | jq -r '.labels[].name' 2>/dev/null | tr '\n' ',' || echo "")
|
|
159
|
+
|
|
160
|
+
# Build prompt for Claude
|
|
161
|
+
local prompt
|
|
162
|
+
read -r -d '' prompt <<'PROMPT' || true
|
|
163
|
+
You are an issue complexity analyzer. Analyze the GitHub issue below and determine:
|
|
164
|
+
1. Complexity score (1-100): How intricate/multi-faceted is the work?
|
|
165
|
+
2. Estimated hours (1-100): How long would this realistically take?
|
|
166
|
+
3. Should decompose: Is complexity > 70 OR hours > 8?
|
|
167
|
+
4. If should decompose: Generate 3-5 focused, independent subtasks
|
|
168
|
+
|
|
169
|
+
Each subtask should be:
|
|
170
|
+
- Self-contained (can be worked on independently)
|
|
171
|
+
- Completable in one pipeline run (~20 iterations max)
|
|
172
|
+
- Have clear acceptance criteria
|
|
173
|
+
- Include test strategy
|
|
174
|
+
|
|
175
|
+
Return ONLY valid JSON (no markdown, no explanation):
|
|
176
|
+
{
|
|
177
|
+
"issue_number": <number>,
|
|
178
|
+
"complexity_score": <1-100>,
|
|
179
|
+
"estimated_hours": <1-100>,
|
|
180
|
+
"should_decompose": <true|false>,
|
|
181
|
+
"reasoning": "<brief explanation>",
|
|
182
|
+
"subtasks": [
|
|
183
|
+
{
|
|
184
|
+
"title": "Subtask N: <clear title>",
|
|
185
|
+
"description": "<1-2 sentences describing the work>",
|
|
186
|
+
"acceptance_criteria": ["criterion 1", "criterion 2"],
|
|
187
|
+
"test_approach": "<how to validate this subtask>"
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
ISSUE #<issue_number>:
|
|
193
|
+
Title: <issue_title>
|
|
194
|
+
Body:
|
|
195
|
+
<issue_body>
|
|
196
|
+
Labels: <issue_labels>
|
|
197
|
+
PROMPT
|
|
198
|
+
|
|
199
|
+
# Replace placeholders
|
|
200
|
+
prompt="${prompt//<issue_number>/$issue_num}"
|
|
201
|
+
prompt="${prompt//<issue_title>/$issue_title}"
|
|
202
|
+
prompt="${prompt//<issue_body>/$issue_body}"
|
|
203
|
+
prompt="${prompt//<issue_labels>/$issue_labels}"
|
|
204
|
+
|
|
205
|
+
# Call Claude
|
|
206
|
+
local result
|
|
207
|
+
result=$(_decompose_call_claude "$prompt")
|
|
208
|
+
|
|
209
|
+
if [[ "$result" == *"error"* ]]; then
|
|
210
|
+
error "Claude analysis failed"
|
|
211
|
+
return 1
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
echo "$result"
|
|
215
|
+
emit_event "decompose.analyzed" "issue=$issue_num" "result=$result"
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# ─── Create Subtask Issues ──────────────────────────────────────────────────
|
|
219
|
+
decompose_create_subtasks() {
|
|
220
|
+
local issue_num="$1"
|
|
221
|
+
local analysis_json="$2"
|
|
222
|
+
|
|
223
|
+
if [[ "$NO_GITHUB" == "true" ]]; then
|
|
224
|
+
# Return mock subtask numbers (JSON-clean output only)
|
|
225
|
+
echo "123 124 125"
|
|
226
|
+
return 0
|
|
227
|
+
fi
|
|
228
|
+
|
|
229
|
+
# Fetch parent issue details for label inheritance
|
|
230
|
+
local parent_labels parent_title
|
|
231
|
+
parent_labels=$(gh issue view "$issue_num" --json labels --jq '.labels[].name' 2>/dev/null | tr '\n' ',' | sed 's/,$//' || echo "")
|
|
232
|
+
parent_title=$(gh issue view "$issue_num" --json title --jq '.title' 2>/dev/null || echo "")
|
|
233
|
+
|
|
234
|
+
# Extract subtasks from analysis
|
|
235
|
+
local subtask_count
|
|
236
|
+
subtask_count=$(echo "$analysis_json" | jq '.subtasks | length' 2>/dev/null || echo "0")
|
|
237
|
+
|
|
238
|
+
if [[ "$subtask_count" -eq 0 ]]; then
|
|
239
|
+
return 1
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
local created_issue_nums=""
|
|
243
|
+
local idx=1
|
|
244
|
+
|
|
245
|
+
while [[ "$idx" -le "$subtask_count" ]]; do
|
|
246
|
+
local subtask
|
|
247
|
+
subtask=$(echo "$analysis_json" | jq ".subtasks[$((idx - 1))]" 2>/dev/null || echo "{}")
|
|
248
|
+
|
|
249
|
+
local subtask_title
|
|
250
|
+
subtask_title=$(echo "$subtask" | jq -r '.title // ""' 2>/dev/null || echo "")
|
|
251
|
+
|
|
252
|
+
if [[ -z "$subtask_title" ]]; then
|
|
253
|
+
error " Subtask #$idx: missing title"
|
|
254
|
+
idx=$((idx + 1))
|
|
255
|
+
continue
|
|
256
|
+
fi
|
|
257
|
+
|
|
258
|
+
# Build subtask description with acceptance criteria and test approach
|
|
259
|
+
local subtask_description
|
|
260
|
+
local acceptance_criteria
|
|
261
|
+
local test_approach
|
|
262
|
+
|
|
263
|
+
acceptance_criteria=$(echo "$subtask" | jq -r '.acceptance_criteria[]? // empty' 2>/dev/null | sed 's/^/- /' || echo "")
|
|
264
|
+
test_approach=$(echo "$subtask" | jq -r '.test_approach // ""' 2>/dev/null || echo "")
|
|
265
|
+
|
|
266
|
+
read -r -d '' subtask_description <<SUBEOF || true
|
|
267
|
+
## Description
|
|
268
|
+
$(echo "$subtask" | jq -r '.description // ""' 2>/dev/null)
|
|
269
|
+
|
|
270
|
+
## Part of
|
|
271
|
+
Issue #${issue_num}: ${parent_title}
|
|
272
|
+
|
|
273
|
+
## Acceptance Criteria
|
|
274
|
+
${acceptance_criteria:-None specified}
|
|
275
|
+
|
|
276
|
+
## Test Approach
|
|
277
|
+
${test_approach:-Run standard test suite}
|
|
278
|
+
SUBEOF
|
|
279
|
+
|
|
280
|
+
# Create the subtask issue
|
|
281
|
+
local create_labels="${parent_labels}"
|
|
282
|
+
if [[ -n "$create_labels" ]]; then
|
|
283
|
+
create_labels="${create_labels},${DECOMPOSE_LABEL}"
|
|
284
|
+
else
|
|
285
|
+
create_labels="$DECOMPOSE_LABEL"
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
local subtask_issue_num
|
|
289
|
+
if subtask_issue_num=$(gh issue create \
|
|
290
|
+
--title "$subtask_title" \
|
|
291
|
+
--body "$subtask_description" \
|
|
292
|
+
--label "$create_labels" 2>/dev/null); then
|
|
293
|
+
|
|
294
|
+
created_issue_nums="${created_issue_nums}${subtask_issue_num} "
|
|
295
|
+
emit_event "decompose.subtask_created" "parent=$issue_num" "subtask=$subtask_issue_num"
|
|
296
|
+
fi
|
|
297
|
+
|
|
298
|
+
idx=$((idx + 1))
|
|
299
|
+
done
|
|
300
|
+
|
|
301
|
+
echo "${created_issue_nums% }"
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
# ─── Add Comment to Parent Issue ────────────────────────────────────────────
|
|
305
|
+
decompose_add_parent_comment() {
|
|
306
|
+
local issue_num="$1"
|
|
307
|
+
local subtask_nums="$2"
|
|
308
|
+
|
|
309
|
+
if [[ "$NO_GITHUB" == "true" ]]; then
|
|
310
|
+
return 0
|
|
311
|
+
fi
|
|
312
|
+
|
|
313
|
+
# Build comment with subtask links
|
|
314
|
+
local comment_body="## 🔄 Decomposed into subtasks
|
|
315
|
+
|
|
316
|
+
This issue was too ambitious for a single pipeline run. It has been decomposed into smaller, focused subtasks:
|
|
317
|
+
|
|
318
|
+
"
|
|
319
|
+
|
|
320
|
+
for subtask_num in $subtask_nums; do
|
|
321
|
+
comment_body="${comment_body}- #${subtask_num}
|
|
322
|
+
"
|
|
323
|
+
done
|
|
324
|
+
|
|
325
|
+
comment_body="${comment_body}
|
|
326
|
+
Each subtask can be completed independently and merged gradually. Close this issue once all subtasks are complete."
|
|
327
|
+
|
|
328
|
+
if gh issue comment "$issue_num" --body "$comment_body" 2>/dev/null; then
|
|
329
|
+
success "Added decomposition comment to issue #$issue_num"
|
|
330
|
+
return 0
|
|
331
|
+
else
|
|
332
|
+
warn "Failed to add comment to issue #$issue_num"
|
|
333
|
+
return 1
|
|
334
|
+
fi
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
# ─── Add Decomposed Label ───────────────────────────────────────────────────
|
|
338
|
+
decompose_mark_decomposed() {
|
|
339
|
+
local issue_num="$1"
|
|
340
|
+
|
|
341
|
+
if [[ "$NO_GITHUB" == "true" ]]; then
|
|
342
|
+
return 0
|
|
343
|
+
fi
|
|
344
|
+
|
|
345
|
+
if gh issue edit "$issue_num" --add-label "$DECOMPOSED_MARKER_LABEL" 2>/dev/null; then
|
|
346
|
+
success "Marked issue #$issue_num as decomposed"
|
|
347
|
+
return 0
|
|
348
|
+
else
|
|
349
|
+
warn "Failed to add decomposed label to issue #$issue_num"
|
|
350
|
+
return 1
|
|
351
|
+
fi
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
# ─── Main: Analyze Only ─────────────────────────────────────────────────────
|
|
355
|
+
cmd_analyze() {
|
|
356
|
+
local issue_num="${1:-}"
|
|
357
|
+
|
|
358
|
+
if [[ -z "$issue_num" ]]; then
|
|
359
|
+
error "Usage: sw-decompose.sh analyze <issue-number>"
|
|
360
|
+
return 1
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
echo ""
|
|
364
|
+
info "Issue Complexity Analysis"
|
|
365
|
+
info "Analyzing issue #${issue_num}..."
|
|
366
|
+
echo ""
|
|
367
|
+
|
|
368
|
+
local analysis
|
|
369
|
+
analysis=$(decompose_analyze "$issue_num") || return 1
|
|
370
|
+
|
|
371
|
+
# Pretty-print the JSON result
|
|
372
|
+
echo "$analysis" | jq '.' 2>/dev/null || echo "$analysis"
|
|
373
|
+
|
|
374
|
+
echo ""
|
|
375
|
+
local should_decompose
|
|
376
|
+
should_decompose=$(echo "$analysis" | jq '.should_decompose' 2>/dev/null || echo "false")
|
|
377
|
+
|
|
378
|
+
if [[ "$should_decompose" == "true" ]]; then
|
|
379
|
+
local complexity
|
|
380
|
+
complexity=$(echo "$analysis" | jq '.complexity_score' 2>/dev/null || echo "0")
|
|
381
|
+
local hours
|
|
382
|
+
hours=$(echo "$analysis" | jq '.estimated_hours' 2>/dev/null || echo "0")
|
|
383
|
+
warn "Issue is too ambitious (complexity=${complexity}, hours=${hours})"
|
|
384
|
+
echo "Run 'sw decompose $issue_num' to auto-create subtasks"
|
|
385
|
+
else
|
|
386
|
+
success "Issue is simple enough for a single pipeline run"
|
|
387
|
+
fi
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
# ─── Main: Decompose & Create Subtasks ──────────────────────────────────────
|
|
391
|
+
cmd_decompose() {
|
|
392
|
+
local issue_num="${1:-}"
|
|
393
|
+
|
|
394
|
+
if [[ -z "$issue_num" ]]; then
|
|
395
|
+
error "Usage: sw-decompose.sh decompose <issue-number>"
|
|
396
|
+
return 1
|
|
397
|
+
fi
|
|
398
|
+
|
|
399
|
+
echo ""
|
|
400
|
+
info "Decomposing Issue #${issue_num}"
|
|
401
|
+
echo ""
|
|
402
|
+
|
|
403
|
+
# Check if already decomposed
|
|
404
|
+
if _has_label "$issue_num" "$DECOMPOSED_MARKER_LABEL"; then
|
|
405
|
+
warn "Issue #$issue_num is already marked as decomposed"
|
|
406
|
+
return 0
|
|
407
|
+
fi
|
|
408
|
+
|
|
409
|
+
# Analyze
|
|
410
|
+
local analysis
|
|
411
|
+
analysis=$(decompose_analyze "$issue_num") || return 1
|
|
412
|
+
|
|
413
|
+
local should_decompose
|
|
414
|
+
should_decompose=$(echo "$analysis" | jq '.should_decompose' 2>/dev/null || echo "false")
|
|
415
|
+
|
|
416
|
+
if [[ "$should_decompose" != "true" ]]; then
|
|
417
|
+
success "Issue #$issue_num is simple enough — no decomposition needed"
|
|
418
|
+
emit_event "decompose.skipped" "issue=$issue_num" "reason=simple"
|
|
419
|
+
return 0
|
|
420
|
+
fi
|
|
421
|
+
|
|
422
|
+
# Create subtasks
|
|
423
|
+
info "Creating subtask issues..."
|
|
424
|
+
local subtask_nums
|
|
425
|
+
subtask_nums=$(decompose_create_subtasks "$issue_num" "$analysis") || return 1
|
|
426
|
+
|
|
427
|
+
if [[ -z "$subtask_nums" ]]; then
|
|
428
|
+
error "No subtasks were created"
|
|
429
|
+
return 1
|
|
430
|
+
fi
|
|
431
|
+
|
|
432
|
+
# Add parent comment
|
|
433
|
+
decompose_add_parent_comment "$issue_num" "$subtask_nums"
|
|
434
|
+
|
|
435
|
+
# Mark as decomposed
|
|
436
|
+
decompose_mark_decomposed "$issue_num"
|
|
437
|
+
|
|
438
|
+
echo ""
|
|
439
|
+
local subtask_count
|
|
440
|
+
subtask_count=$(echo "$subtask_nums" | wc -w)
|
|
441
|
+
success "Issue #$issue_num decomposed into $subtask_count subtasks:"
|
|
442
|
+
for subtask_num in $subtask_nums; do
|
|
443
|
+
echo " - #$subtask_num"
|
|
444
|
+
done
|
|
445
|
+
|
|
446
|
+
emit_event "decompose.completed" "issue=$issue_num" "subtask_count=$(echo $subtask_nums | wc -w)"
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
# ─── Main: Auto (for daemon) ────────────────────────────────────────────────
|
|
450
|
+
cmd_auto() {
|
|
451
|
+
local issue_num="${1:-}"
|
|
452
|
+
|
|
453
|
+
if [[ -z "$issue_num" ]]; then
|
|
454
|
+
error "Usage: sw-decompose.sh auto <issue-number>"
|
|
455
|
+
return 1
|
|
456
|
+
fi
|
|
457
|
+
|
|
458
|
+
# Check if already decomposed
|
|
459
|
+
if _has_label "$issue_num" "$DECOMPOSED_MARKER_LABEL"; then
|
|
460
|
+
return 0
|
|
461
|
+
fi
|
|
462
|
+
|
|
463
|
+
# Analyze
|
|
464
|
+
local analysis
|
|
465
|
+
analysis=$(decompose_analyze "$issue_num") || return 1
|
|
466
|
+
|
|
467
|
+
local should_decompose
|
|
468
|
+
should_decompose=$(echo "$analysis" | jq '.should_decompose' 2>/dev/null || echo "false")
|
|
469
|
+
|
|
470
|
+
if [[ "$should_decompose" != "true" ]]; then
|
|
471
|
+
return 0
|
|
472
|
+
fi
|
|
473
|
+
|
|
474
|
+
# Create subtasks
|
|
475
|
+
local subtask_nums
|
|
476
|
+
subtask_nums=$(decompose_create_subtasks "$issue_num" "$analysis") || return 1
|
|
477
|
+
|
|
478
|
+
if [[ -z "$subtask_nums" ]]; then
|
|
479
|
+
return 1
|
|
480
|
+
fi
|
|
481
|
+
|
|
482
|
+
# Add parent comment
|
|
483
|
+
decompose_add_parent_comment "$issue_num" "$subtask_nums"
|
|
484
|
+
|
|
485
|
+
# Mark as decomposed
|
|
486
|
+
decompose_mark_decomposed "$issue_num"
|
|
487
|
+
|
|
488
|
+
emit_event "decompose.auto_completed" "issue=$issue_num" "subtask_count=$(echo $subtask_nums | wc -w)"
|
|
489
|
+
|
|
490
|
+
return 0
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
# ─── CLI Router ──────────────────────────────────────────────────────────────
|
|
494
|
+
main() {
|
|
495
|
+
local cmd="${1:-help}"
|
|
496
|
+
|
|
497
|
+
case "$cmd" in
|
|
498
|
+
analyze)
|
|
499
|
+
cmd_analyze "${2:-}"
|
|
500
|
+
;;
|
|
501
|
+
decompose)
|
|
502
|
+
cmd_decompose "${2:-}"
|
|
503
|
+
;;
|
|
504
|
+
auto)
|
|
505
|
+
cmd_auto "${2:-}"
|
|
506
|
+
;;
|
|
507
|
+
help|--help|-h)
|
|
508
|
+
echo ""
|
|
509
|
+
echo -e "${CYAN}${BOLD}shipwright decompose${RESET} — Issue Complexity Analysis & Decomposition"
|
|
510
|
+
echo ""
|
|
511
|
+
echo -e "${BOLD}USAGE${RESET}"
|
|
512
|
+
echo -e " ${CYAN}sw decompose${RESET} <command> <issue-number>"
|
|
513
|
+
echo ""
|
|
514
|
+
echo -e "${BOLD}COMMANDS${RESET}"
|
|
515
|
+
echo -e " ${CYAN}analyze${RESET} <num> Analyze complexity without creating issues"
|
|
516
|
+
echo -e " ${CYAN}decompose${RESET} <num> Analyze + create subtask issues if needed"
|
|
517
|
+
echo -e " ${CYAN}auto${RESET} <num> Daemon mode: silent decomposition (returns 0)"
|
|
518
|
+
echo ""
|
|
519
|
+
echo -e "${BOLD}EXAMPLES${RESET}"
|
|
520
|
+
echo -e " ${DIM}sw decompose analyze 42${RESET} # See complexity score and reasoning"
|
|
521
|
+
echo -e " ${DIM}sw decompose decompose 42${RESET} # Create subtasks for issue #42"
|
|
522
|
+
echo -e " ${DIM}sw decompose auto 42${RESET} # Used by daemon (no output)"
|
|
523
|
+
echo ""
|
|
524
|
+
;;
|
|
525
|
+
--version|-v)
|
|
526
|
+
echo "sw-decompose $VERSION"
|
|
527
|
+
;;
|
|
528
|
+
*)
|
|
529
|
+
error "Unknown command: $cmd"
|
|
530
|
+
echo "Run 'sw decompose help' for usage"
|
|
531
|
+
exit 1
|
|
532
|
+
;;
|
|
533
|
+
esac
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
# ─── Guard: only run main if not sourced ──────────────────────────────────
|
|
537
|
+
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
538
|
+
main "$@"
|
|
539
|
+
fi
|