shipwright-cli 1.10.0 → 2.1.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 (121) hide show
  1. package/README.md +221 -55
  2. package/completions/_shipwright +264 -32
  3. package/completions/shipwright.bash +118 -26
  4. package/completions/shipwright.fish +80 -2
  5. package/dashboard/server.ts +208 -0
  6. package/docs/strategy/01-market-research.md +619 -0
  7. package/docs/strategy/02-mission-and-brand.md +587 -0
  8. package/docs/strategy/03-gtm-and-roadmap.md +759 -0
  9. package/docs/strategy/QUICK-START.txt +289 -0
  10. package/docs/strategy/README.md +172 -0
  11. package/docs/tmux-research/TMUX-ARCHITECTURE.md +567 -0
  12. package/docs/tmux-research/TMUX-AUDIT.md +925 -0
  13. package/docs/tmux-research/TMUX-BEST-PRACTICES-2025-2026.md +829 -0
  14. package/docs/tmux-research/TMUX-QUICK-REFERENCE.md +543 -0
  15. package/docs/tmux-research/TMUX-RESEARCH-INDEX.md +438 -0
  16. package/package.json +4 -2
  17. package/scripts/lib/helpers.sh +7 -0
  18. package/scripts/sw +323 -2
  19. package/scripts/sw-activity.sh +500 -0
  20. package/scripts/sw-adaptive.sh +925 -0
  21. package/scripts/sw-adversarial.sh +1 -1
  22. package/scripts/sw-architecture-enforcer.sh +1 -1
  23. package/scripts/sw-auth.sh +613 -0
  24. package/scripts/sw-autonomous.sh +754 -0
  25. package/scripts/sw-changelog.sh +704 -0
  26. package/scripts/sw-checkpoint.sh +1 -1
  27. package/scripts/sw-ci.sh +602 -0
  28. package/scripts/sw-cleanup.sh +1 -1
  29. package/scripts/sw-code-review.sh +698 -0
  30. package/scripts/sw-connect.sh +1 -1
  31. package/scripts/sw-context.sh +605 -0
  32. package/scripts/sw-cost.sh +44 -3
  33. package/scripts/sw-daemon.sh +568 -138
  34. package/scripts/sw-dashboard.sh +1 -1
  35. package/scripts/sw-db.sh +1380 -0
  36. package/scripts/sw-decompose.sh +539 -0
  37. package/scripts/sw-deps.sh +551 -0
  38. package/scripts/sw-developer-simulation.sh +1 -1
  39. package/scripts/sw-discovery.sh +412 -0
  40. package/scripts/sw-docs-agent.sh +539 -0
  41. package/scripts/sw-docs.sh +1 -1
  42. package/scripts/sw-doctor.sh +107 -1
  43. package/scripts/sw-dora.sh +615 -0
  44. package/scripts/sw-durable.sh +710 -0
  45. package/scripts/sw-e2e-orchestrator.sh +535 -0
  46. package/scripts/sw-eventbus.sh +393 -0
  47. package/scripts/sw-feedback.sh +479 -0
  48. package/scripts/sw-fix.sh +1 -1
  49. package/scripts/sw-fleet-discover.sh +567 -0
  50. package/scripts/sw-fleet-viz.sh +404 -0
  51. package/scripts/sw-fleet.sh +8 -1
  52. package/scripts/sw-github-app.sh +596 -0
  53. package/scripts/sw-github-checks.sh +4 -4
  54. package/scripts/sw-github-deploy.sh +1 -1
  55. package/scripts/sw-github-graphql.sh +1 -1
  56. package/scripts/sw-guild.sh +569 -0
  57. package/scripts/sw-heartbeat.sh +1 -1
  58. package/scripts/sw-hygiene.sh +559 -0
  59. package/scripts/sw-incident.sh +656 -0
  60. package/scripts/sw-init.sh +237 -24
  61. package/scripts/sw-instrument.sh +699 -0
  62. package/scripts/sw-intelligence.sh +1 -1
  63. package/scripts/sw-jira.sh +1 -1
  64. package/scripts/sw-launchd.sh +363 -28
  65. package/scripts/sw-linear.sh +1 -1
  66. package/scripts/sw-logs.sh +1 -1
  67. package/scripts/sw-loop.sh +267 -21
  68. package/scripts/sw-memory.sh +18 -1
  69. package/scripts/sw-mission-control.sh +487 -0
  70. package/scripts/sw-model-router.sh +545 -0
  71. package/scripts/sw-otel.sh +596 -0
  72. package/scripts/sw-oversight.sh +764 -0
  73. package/scripts/sw-pipeline-composer.sh +1 -1
  74. package/scripts/sw-pipeline-vitals.sh +1 -1
  75. package/scripts/sw-pipeline.sh +947 -35
  76. package/scripts/sw-pm.sh +758 -0
  77. package/scripts/sw-pr-lifecycle.sh +522 -0
  78. package/scripts/sw-predictive.sh +8 -1
  79. package/scripts/sw-prep.sh +1 -1
  80. package/scripts/sw-ps.sh +1 -1
  81. package/scripts/sw-public-dashboard.sh +798 -0
  82. package/scripts/sw-quality.sh +595 -0
  83. package/scripts/sw-reaper.sh +1 -1
  84. package/scripts/sw-recruit.sh +2248 -0
  85. package/scripts/sw-regression.sh +642 -0
  86. package/scripts/sw-release-manager.sh +736 -0
  87. package/scripts/sw-release.sh +706 -0
  88. package/scripts/sw-remote.sh +1 -1
  89. package/scripts/sw-replay.sh +520 -0
  90. package/scripts/sw-retro.sh +691 -0
  91. package/scripts/sw-scale.sh +444 -0
  92. package/scripts/sw-security-audit.sh +505 -0
  93. package/scripts/sw-self-optimize.sh +1 -1
  94. package/scripts/sw-session.sh +1 -1
  95. package/scripts/sw-setup.sh +263 -127
  96. package/scripts/sw-standup.sh +712 -0
  97. package/scripts/sw-status.sh +44 -2
  98. package/scripts/sw-strategic.sh +806 -0
  99. package/scripts/sw-stream.sh +450 -0
  100. package/scripts/sw-swarm.sh +620 -0
  101. package/scripts/sw-team-stages.sh +511 -0
  102. package/scripts/sw-templates.sh +4 -4
  103. package/scripts/sw-testgen.sh +566 -0
  104. package/scripts/sw-tmux-pipeline.sh +554 -0
  105. package/scripts/sw-tmux-role-color.sh +58 -0
  106. package/scripts/sw-tmux-status.sh +128 -0
  107. package/scripts/sw-tmux.sh +1 -1
  108. package/scripts/sw-trace.sh +485 -0
  109. package/scripts/sw-tracker-github.sh +188 -0
  110. package/scripts/sw-tracker-jira.sh +172 -0
  111. package/scripts/sw-tracker-linear.sh +251 -0
  112. package/scripts/sw-tracker.sh +117 -2
  113. package/scripts/sw-triage.sh +627 -0
  114. package/scripts/sw-upgrade.sh +1 -1
  115. package/scripts/sw-ux.sh +677 -0
  116. package/scripts/sw-webhook.sh +627 -0
  117. package/scripts/sw-widgets.sh +530 -0
  118. package/scripts/sw-worktree.sh +1 -1
  119. package/templates/pipelines/autonomous.json +2 -2
  120. package/tmux/shipwright-overlay.conf +35 -17
  121. package/tmux/tmux.conf +23 -21
@@ -0,0 +1,806 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ sw-strategic.sh — Strategic Intelligence Agent ║
4
+ # ║ Reads strategy, metrics, and codebase to create high-impact issues ║
5
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
6
+ # This file can be BOTH sourced (by sw-daemon.sh) and run standalone.
7
+ # When sourced, do NOT add set -euo pipefail — the parent handles that.
8
+ # When run directly, main() sets up the error handling.
9
+
10
+ VERSION="2.1.0"
11
+
12
+ # ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
13
+ CYAN='\033[38;2;0;212;255m'
14
+ PURPLE='\033[38;2;124;58;237m'
15
+ BLUE='\033[38;2;0;102;255m'
16
+ GREEN='\033[38;2;74;222;128m'
17
+ YELLOW='\033[38;2;250;204;21m'
18
+ RED='\033[38;2;248;113;113m'
19
+ DIM='\033[2m'
20
+ BOLD='\033[1m'
21
+ RESET='\033[0m'
22
+
23
+ # ─── Helpers (define fallbacks if not provided by parent) ─────────────────────
24
+ # When sourced by sw-daemon.sh, these are already defined. When run standalone
25
+ # or sourced by tests, we define them here.
26
+ [[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
27
+ [[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
28
+ [[ "$(type -t warn 2>/dev/null)" == "function" ]] || warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
29
+ [[ "$(type -t error 2>/dev/null)" == "function" ]] || error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
30
+ [[ "$(type -t now_epoch 2>/dev/null)" == "function" ]] || now_epoch() { date +%s; }
31
+ [[ "$(type -t now_iso 2>/dev/null)" == "function" ]] || now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
32
+
33
+ # ─── Paths (set defaults if not provided by parent) ──────────────────────────
34
+ SCRIPT_DIR="${SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
35
+ REPO_DIR="${REPO_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}"
36
+ EVENTS_FILE="${EVENTS_FILE:-${HOME}/.shipwright/events.jsonl}"
37
+
38
+ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
39
+ emit_event() {
40
+ local event_type="$1"
41
+ shift
42
+ local json_fields=""
43
+ for kv in "$@"; do
44
+ local key="${kv%%=*}"
45
+ local val="${kv#*=}"
46
+ if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
47
+ json_fields="${json_fields},\"${key}\":${val}"
48
+ else
49
+ local escaped_val
50
+ escaped_val=$(printf '%s' "$val" | jq -Rs '.' 2>/dev/null || printf '"%s"' "${val//\"/\\\"}")
51
+ json_fields="${json_fields},\"${key}\":${escaped_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
+ fi
58
+
59
+ # ─── Constants ────────────────────────────────────────────────────────────────
60
+ STRATEGIC_MAX_ISSUES=5
61
+ STRATEGIC_COOLDOWN_SECONDS=14400 # 4 hours
62
+ STRATEGIC_MODEL="claude-sonnet-4-5-20250929"
63
+ STRATEGIC_MAX_TOKENS=4096
64
+ STRATEGIC_STRATEGY_LINES=200
65
+ STRATEGIC_LABELS="auto-patrol,ready-to-build,strategic,shipwright"
66
+
67
+ # ─── Semantic Dedup ─────────────────────────────────────────────────────────
68
+ # Cache of existing issue titles (open + recently closed) loaded at cycle start.
69
+ STRATEGIC_TITLE_CACHE=""
70
+ STRATEGIC_OVERLAP_THRESHOLD=60 # Skip if >60% word overlap
71
+
72
+ # Compute word-overlap similarity between two titles (0-100).
73
+ # Uses lowercase word sets, ignoring common stop words.
74
+ strategic_word_overlap() {
75
+ local title_a="$1"
76
+ local title_b="$2"
77
+
78
+ # Normalize: lowercase, strip punctuation, split to words, basic stemming
79
+ local words_a words_b
80
+ words_a=$(printf '%s' "$title_a" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alnum:]' '\n' | \
81
+ sed -E 's/ations?$//; s/tions?$//; s/ments?$//; s/ings?$//; s/ness$//; s/ies$/y/; s/([^s])s$/\1/' | \
82
+ sort -u | grep -vE '^(a|an|the|and|or|for|to|in|of|is|it|by|on|at|with|from|based)$' | grep -v '^$' || true)
83
+ words_b=$(printf '%s' "$title_b" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alnum:]' '\n' | \
84
+ sed -E 's/ations?$//; s/tions?$//; s/ments?$//; s/ings?$//; s/ness$//; s/ies$/y/; s/([^s])s$/\1/' | \
85
+ sort -u | grep -vE '^(a|an|the|and|or|for|to|in|of|is|it|by|on|at|with|from|based)$' | grep -v '^$' || true)
86
+
87
+ [[ -z "$words_a" || -z "$words_b" ]] && echo "0" && return 0
88
+
89
+ # Count words in each set
90
+ local count_a count_b
91
+ count_a=$(printf '%s\n' "$words_a" | wc -l | tr -d ' ')
92
+ count_b=$(printf '%s\n' "$words_b" | wc -l | tr -d ' ')
93
+
94
+ # Count shared words (intersection)
95
+ local shared
96
+ shared=$(comm -12 <(printf '%s\n' "$words_a") <(printf '%s\n' "$words_b") | wc -l | tr -d ' ')
97
+
98
+ # Overlap = shared / min(count_a, count_b) * 100
99
+ local min_count
100
+ if [[ "$count_a" -le "$count_b" ]]; then
101
+ min_count="$count_a"
102
+ else
103
+ min_count="$count_b"
104
+ fi
105
+
106
+ [[ "$min_count" -eq 0 ]] && echo "0" && return 0
107
+
108
+ echo $(( shared * 100 / min_count ))
109
+ }
110
+
111
+ # Load all open + recently closed issue titles into cache.
112
+ strategic_load_title_cache() {
113
+ STRATEGIC_TITLE_CACHE=""
114
+
115
+ if [[ "${NO_GITHUB:-false}" == "true" ]]; then
116
+ return 0
117
+ fi
118
+
119
+ local open_titles closed_titles
120
+ open_titles=$(gh issue list --state open --json title --jq '.[].title' 2>/dev/null || echo "")
121
+ closed_titles=$(gh issue list --state closed --limit 30 --json title --jq '.[].title' 2>/dev/null || echo "")
122
+
123
+ STRATEGIC_TITLE_CACHE="${open_titles}
124
+ ${closed_titles}"
125
+ }
126
+
127
+ # Check if a title has >threshold% overlap with any cached title.
128
+ # Returns 0 (true) if a near-duplicate is found, 1 (false) otherwise.
129
+ strategic_is_near_duplicate() {
130
+ local new_title="$1"
131
+
132
+ [[ -z "$STRATEGIC_TITLE_CACHE" ]] && return 1
133
+
134
+ while IFS= read -r existing_title; do
135
+ [[ -z "$existing_title" ]] && continue
136
+ local overlap
137
+ overlap=$(strategic_word_overlap "$new_title" "$existing_title")
138
+ if [[ "$overlap" -gt "$STRATEGIC_OVERLAP_THRESHOLD" ]]; then
139
+ info " Near-duplicate (${overlap}% overlap): \"${existing_title}\"" >&2
140
+ return 0
141
+ fi
142
+ done <<< "$STRATEGIC_TITLE_CACHE"
143
+
144
+ return 1
145
+ }
146
+
147
+ # ─── Cooldown Check ──────────────────────────────────────────────────────────
148
+ strategic_check_cooldown() {
149
+ local events_file="${EVENTS_FILE:-${HOME}/.shipwright/events.jsonl}"
150
+ if [[ ! -f "$events_file" ]]; then
151
+ return 0 # No events file — no cooldown
152
+ fi
153
+
154
+ local now_e
155
+ now_e=$(now_epoch)
156
+ local last_run
157
+ last_run=$(grep '"strategic.cycle_complete"' "$events_file" 2>/dev/null | tail -1 | jq -r '.ts_epoch // 0' 2>/dev/null || echo "0")
158
+
159
+ local elapsed=$(( now_e - last_run ))
160
+ if [[ "$elapsed" -lt "$STRATEGIC_COOLDOWN_SECONDS" ]]; then
161
+ local remaining=$(( (STRATEGIC_COOLDOWN_SECONDS - elapsed) / 60 ))
162
+ info "Strategic cooldown active — ${remaining} minutes remaining"
163
+ return 1
164
+ fi
165
+ return 0
166
+ }
167
+
168
+ # ─── Gather Context ──────────────────────────────────────────────────────────
169
+ strategic_gather_context() {
170
+ local repo_dir="${REPO_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
171
+ local script_dir="${SCRIPT_DIR:-${repo_dir}/scripts}"
172
+ local events_file="${EVENTS_FILE:-${HOME}/.shipwright/events.jsonl}"
173
+
174
+ # 1. Read STRATEGY.md (truncated)
175
+ local strategy_content=""
176
+ if [[ -f "${repo_dir}/STRATEGY.md" ]]; then
177
+ strategy_content=$(head -n "$STRATEGIC_STRATEGY_LINES" "${repo_dir}/STRATEGY.md")
178
+ else
179
+ strategy_content="(No STRATEGY.md found)"
180
+ fi
181
+
182
+ # 2. Codebase stats
183
+ local total_scripts=0
184
+ local untested_scripts=""
185
+ local untested_count=0
186
+ local total_tests=0
187
+
188
+ for script in "$script_dir"/sw-*.sh; do
189
+ [[ ! -f "$script" ]] && continue
190
+ local base
191
+ base=$(basename "$script" .sh)
192
+ [[ "$base" == *-test ]] && continue
193
+ [[ "$base" == sw-tracker-linear ]] && continue
194
+ [[ "$base" == sw-tracker-jira ]] && continue
195
+ [[ "$base" == sw-patrol-meta ]] && continue
196
+ [[ "$base" == sw-strategic ]] && continue
197
+ total_scripts=$((total_scripts + 1))
198
+
199
+ local test_file="$script_dir/${base}-test.sh"
200
+ if [[ -f "$test_file" ]]; then
201
+ total_tests=$((total_tests + 1))
202
+ else
203
+ untested_count=$((untested_count + 1))
204
+ untested_scripts="${untested_scripts} - ${base}.sh\n"
205
+ fi
206
+ done
207
+
208
+ # 3. Pipeline performance (last 7 days)
209
+ local completed=0
210
+ local failed=0
211
+ local success_rate="N/A"
212
+ local common_failures=""
213
+
214
+ if [[ -f "$events_file" ]]; then
215
+ local now_e
216
+ now_e=$(now_epoch)
217
+ local seven_days_ago=$(( now_e - 604800 ))
218
+
219
+ completed=$(jq -s "[.[] | select(.type == \"pipeline.completed\" and .result == \"success\" and (.ts_epoch // 0) >= $seven_days_ago)] | length" "$events_file" 2>/dev/null || echo "0")
220
+ failed=$(jq -s "[.[] | select(.type == \"pipeline.completed\" and .result != \"success\" and (.ts_epoch // 0) >= $seven_days_ago)] | length" "$events_file" 2>/dev/null || echo "0")
221
+
222
+ local total_pipelines=$(( completed + failed ))
223
+ if [[ "$total_pipelines" -gt 0 ]]; then
224
+ success_rate=$(( completed * 100 / total_pipelines ))
225
+ success_rate="${success_rate}%"
226
+ fi
227
+
228
+ common_failures=$(jq -s "
229
+ [.[] | select(.type == \"pipeline.completed\" and .result != \"success\" and (.ts_epoch // 0) >= $seven_days_ago)]
230
+ | group_by(.failed_stage // \"unknown\")
231
+ | map({stage: .[0].failed_stage // \"unknown\", count: length})
232
+ | sort_by(-.count)
233
+ | .[0:5]
234
+ | map(\"\(.stage) (\(.count)x)\")
235
+ | join(\", \")
236
+ " "$events_file" 2>/dev/null || echo "none")
237
+ fi
238
+
239
+ # 4. Open issues
240
+ local open_issues=""
241
+ if [[ "${NO_GITHUB:-false}" != "true" ]]; then
242
+ open_issues=$(gh issue list --state open --json number,title,labels --jq '.[] | "#\(.number): \(.title) [\(.labels | map(.name) | join(","))]"' 2>/dev/null | head -50 || echo "(could not fetch issues)")
243
+ else
244
+ open_issues="(GitHub access disabled)"
245
+ fi
246
+
247
+ # Build the context output
248
+ printf '%s\n' "STRATEGY_CONTENT<<EOF"
249
+ printf '%s\n' "$strategy_content"
250
+ printf '%s\n' "EOF"
251
+ printf '%s\n' "TOTAL_SCRIPTS=${total_scripts}"
252
+ printf '%s\n' "TOTAL_TESTS=${total_tests}"
253
+ printf '%s\n' "UNTESTED_COUNT=${untested_count}"
254
+ printf '%s\n' "UNTESTED_SCRIPTS<<EOF"
255
+ printf '%b' "$untested_scripts"
256
+ printf '%s\n' "EOF"
257
+ printf '%s\n' "PIPELINES_COMPLETED=${completed}"
258
+ printf '%s\n' "PIPELINES_FAILED=${failed}"
259
+ printf '%s\n' "SUCCESS_RATE=${success_rate}"
260
+ printf '%s\n' "COMMON_FAILURES=${common_failures}"
261
+ printf '%s\n' "OPEN_ISSUES<<EOF"
262
+ printf '%s\n' "$open_issues"
263
+ printf '%s\n' "EOF"
264
+ }
265
+
266
+ # ─── Build Prompt ─────────────────────────────────────────────────────────────
267
+ strategic_build_prompt() {
268
+ local repo_dir="${REPO_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
269
+ local script_dir="${SCRIPT_DIR:-${repo_dir}/scripts}"
270
+ local events_file="${EVENTS_FILE:-${HOME}/.shipwright/events.jsonl}"
271
+
272
+ # Read STRATEGY.md
273
+ local strategy_content=""
274
+ if [[ -f "${repo_dir}/STRATEGY.md" ]]; then
275
+ strategy_content=$(head -n "$STRATEGIC_STRATEGY_LINES" "${repo_dir}/STRATEGY.md")
276
+ else
277
+ strategy_content="(No STRATEGY.md found)"
278
+ fi
279
+
280
+ # Codebase stats
281
+ local total_scripts=0
282
+ local untested_list=""
283
+ local untested_count=0
284
+ local total_tests=0
285
+
286
+ for script in "$script_dir"/sw-*.sh; do
287
+ [[ ! -f "$script" ]] && continue
288
+ local base
289
+ base=$(basename "$script" .sh)
290
+ [[ "$base" == *-test ]] && continue
291
+ [[ "$base" == sw-tracker-linear ]] && continue
292
+ [[ "$base" == sw-tracker-jira ]] && continue
293
+ [[ "$base" == sw-patrol-meta ]] && continue
294
+ [[ "$base" == sw-strategic ]] && continue
295
+ total_scripts=$((total_scripts + 1))
296
+
297
+ if [[ -f "$script_dir/${base}-test.sh" ]]; then
298
+ total_tests=$((total_tests + 1))
299
+ else
300
+ untested_count=$((untested_count + 1))
301
+ untested_list="${untested_list}\n - ${base}.sh"
302
+ fi
303
+ done
304
+
305
+ # Pipeline performance (last 7 days)
306
+ local completed=0 failed=0 success_rate="N/A" common_failures="none"
307
+ if [[ -f "$events_file" ]]; then
308
+ local now_e
309
+ now_e=$(now_epoch)
310
+ local seven_days_ago=$(( now_e - 604800 ))
311
+
312
+ completed=$(jq -s "[.[] | select(.type == \"pipeline.completed\" and .result == \"success\" and (.ts_epoch // 0) >= $seven_days_ago)] | length" "$events_file" 2>/dev/null || echo "0")
313
+ failed=$(jq -s "[.[] | select(.type == \"pipeline.completed\" and .result != \"success\" and (.ts_epoch // 0) >= $seven_days_ago)] | length" "$events_file" 2>/dev/null || echo "0")
314
+
315
+ local total_pipelines=$(( completed + failed ))
316
+ if [[ "$total_pipelines" -gt 0 ]]; then
317
+ success_rate="$(( completed * 100 / total_pipelines ))%"
318
+ fi
319
+
320
+ common_failures=$(jq -s "
321
+ [.[] | select(.type == \"pipeline.completed\" and .result != \"success\" and (.ts_epoch // 0) >= $seven_days_ago)]
322
+ | group_by(.failed_stage // \"unknown\")
323
+ | map({stage: .[0].failed_stage // \"unknown\", count: length})
324
+ | sort_by(-.count)
325
+ | .[0:5]
326
+ | map(\"\(.stage) (\(.count)x)\")
327
+ | join(\", \")
328
+ " "$events_file" 2>/dev/null || echo "none")
329
+ # Empty string → "none"
330
+ common_failures="${common_failures:-none}"
331
+ fi
332
+
333
+ # Open issues
334
+ local open_issues=""
335
+ if [[ "${NO_GITHUB:-false}" != "true" ]]; then
336
+ open_issues=$(gh issue list --state open --json number,title --jq '.[] | "#\(.number): \(.title)"' 2>/dev/null | head -50 || echo "(could not fetch)")
337
+ else
338
+ open_issues="(GitHub access disabled)"
339
+ fi
340
+
341
+ # Recently closed issues (last 20) — so we don't rebuild what was just shipped
342
+ local recent_closed=""
343
+ if [[ "${NO_GITHUB:-false}" != "true" ]]; then
344
+ recent_closed=$(gh issue list --state closed --limit 20 --json number,title --jq '.[] | "#\(.number): \(.title)"' 2>/dev/null || echo "(could not fetch)")
345
+ else
346
+ recent_closed="(GitHub access disabled)"
347
+ fi
348
+
349
+ # Compose the prompt
350
+ cat <<PROMPT_EOF
351
+ You are the Strategic PM for Shipwright — an autonomous software delivery system. Your job is to analyze the current state and recommend 1-3 high-impact improvements to build next.
352
+
353
+ ## Strategy (from STRATEGY.md)
354
+ ${strategy_content}
355
+
356
+ ## Current Codebase
357
+ - Total scripts: ${total_scripts}
358
+ - Scripts with tests: ${total_tests}
359
+ - Scripts without tests (${untested_count}):$(echo -e "$untested_list")
360
+
361
+ ## Recent Pipeline Performance (last 7 days)
362
+ - Pipelines completed successfully: ${completed}
363
+ - Pipelines failed: ${failed}
364
+ - Success rate: ${success_rate}
365
+ - Common failure stages: ${common_failures}
366
+
367
+ ## Open Issues (already in progress — do NOT duplicate these)
368
+ ${open_issues}
369
+
370
+ ## Recently Completed (already built — do NOT recreate these)
371
+ ${recent_closed}
372
+
373
+ ## Your Task
374
+ Based on the strategy priorities and current data, recommend 1-3 concrete improvements to build next. Each should be a single, well-scoped task completable by one autonomous pipeline run.
375
+
376
+ For each recommendation, provide EXACTLY this format (no extra fields, no deviations):
377
+
378
+ ISSUE_TITLE: <concise, actionable title>
379
+ PRIORITY: <P0|P1|P2|P3|P4|P5>
380
+ COMPLEXITY: <fast|standard|full>
381
+ STRATEGY_AREA: <which priority area from strategy, e.g. "P0: Reliability">
382
+ DESCRIPTION: <2-3 sentences describing what to build and why it matters>
383
+ ACCEPTANCE: <bullet list of acceptance criteria, one per line starting with "- ">
384
+ ---
385
+
386
+ Rules:
387
+ - Do NOT duplicate any open issue OR any recently completed issue
388
+ - Prioritize based on STRATEGY.md priorities (P0 > P1 > P2 > ...)
389
+ - Focus on concrete, actionable improvements (not vague goals)
390
+ - Each issue should be completable by one autonomous pipeline run
391
+ - Balance: reliability/DX fixes AND strategic new capabilities
392
+ - Think about what would make the biggest impact on success rate, developer experience, and system intelligence
393
+ - Be ambitious — push the platform forward, don't just maintain it
394
+ - Maximum ${STRATEGIC_MAX_ISSUES} issues
395
+ PROMPT_EOF
396
+ }
397
+
398
+ # ─── Call Anthropic API ───────────────────────────────────────────────────────
399
+ strategic_call_api() {
400
+ local prompt="$1"
401
+
402
+ if [[ -z "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]]; then
403
+ error "CLAUDE_CODE_OAUTH_TOKEN not set — cannot run strategic analysis"
404
+ return 1
405
+ fi
406
+
407
+ if ! command -v claude &>/dev/null; then
408
+ error "Claude Code CLI not found — install with: npm install -g @anthropic-ai/claude-code"
409
+ return 1
410
+ fi
411
+
412
+ local tmp_prompt
413
+ tmp_prompt=$(mktemp)
414
+ printf '%s' "$prompt" > "$tmp_prompt"
415
+
416
+ local response_text
417
+ response_text=$(cat "$tmp_prompt" | claude -p --max-turns 1 --model "$STRATEGIC_MODEL" 2>/dev/null || echo "")
418
+ rm -f "$tmp_prompt"
419
+
420
+ if [[ -z "$response_text" ]]; then
421
+ error "Claude returned empty response"
422
+ return 1
423
+ fi
424
+
425
+ # Strip markdown code fences if present (Sonnet sometimes wraps output)
426
+ response_text=$(printf '%s' "$response_text" | sed '/^```/d')
427
+
428
+ # Debug: show first 200 chars of response
429
+ local preview
430
+ preview=$(printf '%s' "$response_text" | head -c 200)
431
+ info "Response preview: ${preview}..." >&2
432
+
433
+ printf '%s' "$response_text"
434
+ }
435
+
436
+ # ─── Parse Response & Create Issues ──────────────────────────────────────────
437
+ strategic_parse_and_create() {
438
+ local response="$1"
439
+ local created=0
440
+ local skipped=0
441
+
442
+ # Split response into issue blocks by "---" delimiter
443
+ local current_title="" current_priority="" current_complexity=""
444
+ local current_strategy="" current_description="" current_acceptance=""
445
+ local in_acceptance=false
446
+
447
+ while IFS= read -r line; do
448
+ # Strip carriage returns
449
+ line="${line//$'\r'/}"
450
+
451
+ if [[ "$line" == "---" ]] || [[ "$line" == "---"* && ${#line} -le 5 ]]; then
452
+ # End of block — create issue if we have a title
453
+ if [[ -n "$current_title" ]]; then
454
+ strategic_create_issue \
455
+ "$current_title" "$current_priority" "$current_complexity" \
456
+ "$current_strategy" "$current_description" "$current_acceptance"
457
+ local rc=$?
458
+ if [[ $rc -eq 0 ]]; then
459
+ created=$((created + 1))
460
+ else
461
+ skipped=$((skipped + 1))
462
+ fi
463
+
464
+ if [[ "$created" -ge "$STRATEGIC_MAX_ISSUES" ]]; then
465
+ info "Reached max issues per cycle (${STRATEGIC_MAX_ISSUES})" >&2
466
+ break
467
+ fi
468
+ fi
469
+
470
+ # Reset for next block
471
+ current_title="" current_priority="" current_complexity=""
472
+ current_strategy="" current_description="" current_acceptance=""
473
+ in_acceptance=false
474
+ continue
475
+ fi
476
+
477
+ # Strip leading markdown bold/italic markers for field matching
478
+ local clean_line
479
+ clean_line=$(echo "$line" | sed 's/^\*\*//;s/\*\*$//' | sed 's/^__//;s/__$//' | sed 's/^[[:space:]]*//')
480
+
481
+ # Parse fields (match with and without markdown formatting)
482
+ if [[ "$clean_line" == ISSUE_TITLE:* ]]; then
483
+ current_title="${clean_line#ISSUE_TITLE: }"
484
+ current_title="${current_title#ISSUE_TITLE:}"
485
+ current_title=$(echo "$current_title" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
486
+ in_acceptance=false
487
+ elif [[ "$clean_line" == PRIORITY:* ]]; then
488
+ current_priority="${clean_line#PRIORITY: }"
489
+ current_priority="${current_priority#PRIORITY:}"
490
+ current_priority=$(echo "$current_priority" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
491
+ in_acceptance=false
492
+ elif [[ "$clean_line" == COMPLEXITY:* ]]; then
493
+ current_complexity="${clean_line#COMPLEXITY: }"
494
+ current_complexity="${current_complexity#COMPLEXITY:}"
495
+ current_complexity=$(echo "$current_complexity" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
496
+ in_acceptance=false
497
+ elif [[ "$clean_line" == STRATEGY_AREA:* ]]; then
498
+ current_strategy="${clean_line#STRATEGY_AREA: }"
499
+ current_strategy="${current_strategy#STRATEGY_AREA:}"
500
+ current_strategy=$(echo "$current_strategy" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
501
+ in_acceptance=false
502
+ elif [[ "$clean_line" == DESCRIPTION:* ]]; then
503
+ current_description="${clean_line#DESCRIPTION: }"
504
+ current_description="${current_description#DESCRIPTION:}"
505
+ current_description=$(echo "$current_description" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
506
+ in_acceptance=false
507
+ elif [[ "$clean_line" == ACCEPTANCE:* ]]; then
508
+ current_acceptance="${clean_line#ACCEPTANCE: }"
509
+ current_acceptance="${current_acceptance#ACCEPTANCE:}"
510
+ current_acceptance=$(echo "$current_acceptance" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
511
+ in_acceptance=true
512
+ elif [[ "$in_acceptance" == true && "$line" == "- "* ]]; then
513
+ # Continuation of acceptance criteria
514
+ if [[ -n "$current_acceptance" ]]; then
515
+ current_acceptance="${current_acceptance}\n${line}"
516
+ else
517
+ current_acceptance="$line"
518
+ fi
519
+ fi
520
+ done <<< "$response"
521
+
522
+ # Handle last block (if no trailing ---)
523
+ if [[ -n "$current_title" && "$created" -lt "$STRATEGIC_MAX_ISSUES" ]]; then
524
+ strategic_create_issue \
525
+ "$current_title" "$current_priority" "$current_complexity" \
526
+ "$current_strategy" "$current_description" "$current_acceptance"
527
+ local rc=$?
528
+ if [[ $rc -eq 0 ]]; then
529
+ created=$((created + 1))
530
+ else
531
+ skipped=$((skipped + 1))
532
+ fi
533
+ fi
534
+
535
+ echo "${created}:${skipped}"
536
+ }
537
+
538
+ # ─── Create Single Issue ─────────────────────────────────────────────────────
539
+ strategic_create_issue() {
540
+ local title="$1"
541
+ local priority="${2:-P2}"
542
+ local complexity="${3:-standard}"
543
+ local strategy_area="${4:-}"
544
+ local description="${5:-}"
545
+ local acceptance="${6:-}"
546
+
547
+ if [[ -z "$title" ]]; then
548
+ return 1
549
+ fi
550
+
551
+ # Semantic dedup: check word overlap against cached titles
552
+ if strategic_is_near_duplicate "$title"; then
553
+ info " Skipping near-duplicate: ${title}" >&2
554
+ return 1
555
+ fi
556
+
557
+ # Dry-run mode
558
+ if [[ "${NO_GITHUB:-false}" == "true" ]]; then
559
+ info " [dry-run] Would create: ${title}" >&2
560
+ return 0
561
+ fi
562
+
563
+ # Dedup: check if an open issue with this exact title already exists
564
+ local existing
565
+ existing=$(gh issue list --state open --search "$title" --json number,title --jq ".[].title" 2>/dev/null || echo "")
566
+ if echo "$existing" | grep -qF "$title" 2>/dev/null; then
567
+ info " Skipping duplicate: ${title}" >&2
568
+ return 1
569
+ fi
570
+
571
+ # Build issue body
572
+ local timestamp
573
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
574
+
575
+ local body
576
+ body=$(cat <<BODY_EOF
577
+ ## Strategic Improvement
578
+
579
+ $(echo -e "$description")
580
+
581
+ ### Acceptance Criteria
582
+ $(echo -e "$acceptance")
583
+
584
+ ### Context
585
+ - **Priority**: ${priority}
586
+ - **Complexity**: ${complexity}
587
+ - **Generated by**: Strategic Intelligence Agent
588
+ - **Strategy alignment**: ${strategy_area}
589
+
590
+ <!-- STRATEGIC-CYCLE: ${timestamp} -->
591
+ BODY_EOF
592
+ )
593
+
594
+ local labels="${STRATEGIC_LABELS}"
595
+
596
+ # Ensure all labels exist (create if missing)
597
+ local IFS=','
598
+ for lbl in $labels; do
599
+ gh label create "$lbl" --color "7c3aed" 2>/dev/null || true
600
+ done
601
+ unset IFS
602
+
603
+ local issue_url
604
+ issue_url=$(gh issue create \
605
+ --title "$title" \
606
+ --body "$body" \
607
+ --label "$labels" 2>/dev/null) || {
608
+ warn " Failed to create issue: ${title}" >&2
609
+ return 1
610
+ }
611
+
612
+ emit_event "strategic.issue_created" "title=$title" "priority=$priority" "complexity=$complexity"
613
+ # Add to title cache so subsequent issues in this cycle don't duplicate
614
+ STRATEGIC_TITLE_CACHE="${STRATEGIC_TITLE_CACHE}
615
+ ${title}"
616
+ # Output to stderr so it doesn't pollute the parse_and_create return value
617
+ success " Created issue: ${title} (${issue_url})" >&2
618
+ return 0
619
+ }
620
+
621
+ # ─── Main Strategic Run ──────────────────────────────────────────────────────
622
+ strategic_run() {
623
+ local force=false
624
+ while [[ $# -gt 0 ]]; do
625
+ case "$1" in
626
+ --force|-f) force=true; shift ;;
627
+ *) shift ;;
628
+ esac
629
+ done
630
+
631
+ echo -e "\n${PURPLE}${BOLD}━━━ Strategic Intelligence Agent ━━━${RESET}"
632
+ echo -e "${DIM} Analyzing codebase, strategy, and metrics...${RESET}\n"
633
+
634
+ # Check cooldown (skip if --force)
635
+ if [[ "$force" != true ]]; then
636
+ if ! strategic_check_cooldown; then
637
+ return 0
638
+ fi
639
+ else
640
+ info "Cooldown bypassed (--force)"
641
+ fi
642
+
643
+ # Check auth token
644
+ if [[ -z "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]]; then
645
+ error "CLAUDE_CODE_OAUTH_TOKEN not set — strategic analysis requires Claude access"
646
+ return 1
647
+ fi
648
+
649
+ # Load existing issue titles for semantic dedup
650
+ info "Loading issue title cache for dedup..."
651
+ strategic_load_title_cache
652
+
653
+ # Build prompt with all context
654
+ info "Gathering context..."
655
+ local prompt
656
+ prompt=$(strategic_build_prompt)
657
+
658
+ # Call Anthropic API
659
+ info "Calling ${STRATEGIC_MODEL} for strategic analysis..."
660
+ local response
661
+ response=$(strategic_call_api "$prompt") || {
662
+ error "Strategic analysis API call failed"
663
+ emit_event "strategic.cycle_failed" "reason=api_error"
664
+ return 1
665
+ }
666
+
667
+ # Parse and create issues
668
+ info "Processing recommendations..."
669
+ local result
670
+ result=$(strategic_parse_and_create "$response")
671
+
672
+ local created="${result%%:*}"
673
+ local skipped="${result##*:}"
674
+
675
+ # Summary
676
+ echo ""
677
+ echo -e "${PURPLE}${BOLD}━━━ Strategic Summary ━━━${RESET}"
678
+ echo -e " Issues created: ${created}"
679
+ echo -e " Issues skipped: ${skipped} (duplicates)"
680
+ echo ""
681
+
682
+ # Only record cycle completion if we actually ran analysis (for cooldown tracking)
683
+ # This prevents a "0 issues" run from burning the cooldown timer
684
+ if [[ "$created" -gt 0 ]] || [[ "$skipped" -gt 0 ]]; then
685
+ emit_event "strategic.cycle_complete" "issues_created=$created" "issues_skipped=$skipped"
686
+ else
687
+ info "No issues produced — cooldown NOT reset (will retry next cycle)"
688
+ fi
689
+ }
690
+
691
+ # ─── Status Command ──────────────────────────────────────────────────────────
692
+ strategic_status() {
693
+ echo -e "\n${PURPLE}${BOLD}━━━ Strategic Agent Status ━━━${RESET}\n"
694
+
695
+ local events_file="${EVENTS_FILE:-${HOME}/.shipwright/events.jsonl}"
696
+
697
+ if [[ ! -f "$events_file" ]]; then
698
+ info "No events data available"
699
+ return 0
700
+ fi
701
+
702
+ # Last run
703
+ local last_run_line
704
+ last_run_line=$(grep '"strategic.cycle_complete"' "$events_file" 2>/dev/null | tail -1 || echo "")
705
+
706
+ if [[ -z "$last_run_line" ]]; then
707
+ info "No strategic cycles recorded yet"
708
+ return 0
709
+ fi
710
+
711
+ local last_ts last_created last_skipped
712
+ last_ts=$(echo "$last_run_line" | jq -r '.ts // "unknown"' 2>/dev/null || echo "unknown")
713
+ last_created=$(echo "$last_run_line" | jq -r '.issues_created // 0' 2>/dev/null || echo "0")
714
+ last_skipped=$(echo "$last_run_line" | jq -r '.issues_skipped // 0' 2>/dev/null || echo "0")
715
+
716
+ echo -e " Last run: ${last_ts}"
717
+ echo -e " Issues created: ${last_created}"
718
+ echo -e " Issues skipped: ${last_skipped}"
719
+
720
+ # Cooldown status
721
+ local last_epoch
722
+ last_epoch=$(echo "$last_run_line" | jq -r '.ts_epoch // 0' 2>/dev/null || echo "0")
723
+ local now_e
724
+ now_e=$(now_epoch)
725
+ local elapsed=$(( now_e - last_epoch ))
726
+
727
+ if [[ "$elapsed" -lt "$STRATEGIC_COOLDOWN_SECONDS" ]]; then
728
+ local remaining_min=$(( (STRATEGIC_COOLDOWN_SECONDS - elapsed) / 60 ))
729
+ echo -e " Cooldown: ${YELLOW}${remaining_min} min remaining${RESET}"
730
+ else
731
+ echo -e " Cooldown: ${GREEN}Ready${RESET}"
732
+ fi
733
+
734
+ # Total issues created
735
+ local total_created
736
+ total_created=$(grep '"strategic.issue_created"' "$events_file" 2>/dev/null | wc -l | tr -d ' ' || echo "0")
737
+ echo -e " Total created: ${total_created} issues (all time)"
738
+
739
+ # Total cycles
740
+ local total_cycles
741
+ total_cycles=$(grep '"strategic.cycle_complete"' "$events_file" 2>/dev/null | wc -l | tr -d ' ' || echo "0")
742
+ echo -e " Total cycles: ${total_cycles}"
743
+
744
+ echo ""
745
+ }
746
+
747
+ # ─── Help ─────────────────────────────────────────────────────────────────────
748
+ strategic_show_help() {
749
+ echo -e "${PURPLE}${BOLD}Shipwright Strategic Intelligence Agent${RESET} v${VERSION}\n"
750
+ echo -e "Reads strategy, metrics, and codebase state to create high-impact improvement issues.\n"
751
+ echo -e "${BOLD}Usage:${RESET}"
752
+ echo -e " sw-strategic.sh <command>\n"
753
+ echo -e "${BOLD}Commands:${RESET}"
754
+ echo -e " run [--force] Run a strategic analysis cycle (--force bypasses cooldown)"
755
+ echo -e " status Show last run stats and cooldown"
756
+ echo -e " help Show this help\n"
757
+ echo -e "${BOLD}Environment:${RESET}"
758
+ echo -e " CLAUDE_CODE_OAUTH_TOKEN Required for Claude access"
759
+ echo -e " NO_GITHUB=true Dry-run mode (no issue creation)\n"
760
+ echo -e "${BOLD}Configuration:${RESET}"
761
+ echo -e " Max issues/cycle: ${STRATEGIC_MAX_ISSUES}"
762
+ echo -e " Cooldown: $(( STRATEGIC_COOLDOWN_SECONDS / 3600 )) hours"
763
+ echo -e " Model: ${STRATEGIC_MODEL}\n"
764
+ }
765
+
766
+ # ─── Daemon Integration (sourced mode) ────────────────────────────────────────
767
+ strategic_patrol_run() {
768
+ # Called by daemon during patrol cycle
769
+ # Check cooldown (12h minimum between runs)
770
+ # Requires CLAUDE_CODE_OAUTH_TOKEN
771
+ if [[ -z "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]]; then
772
+ echo -e " ${DIM}●${RESET} Strategic patrol skipped (no CLAUDE_CODE_OAUTH_TOKEN)"
773
+ return 0
774
+ fi
775
+
776
+ if ! strategic_check_cooldown; then
777
+ return 0
778
+ fi
779
+
780
+ echo -e "\n ${BOLD}Strategic Intelligence Patrol${RESET}"
781
+ strategic_run
782
+ }
783
+
784
+ # ─── Source Guard ─────────────────────────────────────────────────────────────
785
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
786
+ set -euo pipefail
787
+ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
788
+
789
+ main() {
790
+ local cmd="${1:-help}"
791
+ shift 2>/dev/null || true
792
+
793
+ case "$cmd" in
794
+ run) strategic_run "$@" ;;
795
+ status) strategic_status ;;
796
+ help) strategic_show_help ;;
797
+ *)
798
+ error "Unknown command: $cmd"
799
+ strategic_show_help
800
+ exit 1
801
+ ;;
802
+ esac
803
+ }
804
+
805
+ main "$@"
806
+ fi