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.
Files changed (108) hide show
  1. package/README.md +114 -36
  2. package/completions/_shipwright +212 -32
  3. package/completions/shipwright.bash +97 -25
  4. package/docs/strategy/01-market-research.md +619 -0
  5. package/docs/strategy/02-mission-and-brand.md +587 -0
  6. package/docs/strategy/03-gtm-and-roadmap.md +759 -0
  7. package/docs/strategy/QUICK-START.txt +289 -0
  8. package/docs/strategy/README.md +172 -0
  9. package/package.json +4 -2
  10. package/scripts/sw +208 -1
  11. package/scripts/sw-activity.sh +500 -0
  12. package/scripts/sw-adaptive.sh +925 -0
  13. package/scripts/sw-adversarial.sh +1 -1
  14. package/scripts/sw-architecture-enforcer.sh +1 -1
  15. package/scripts/sw-auth.sh +613 -0
  16. package/scripts/sw-autonomous.sh +664 -0
  17. package/scripts/sw-changelog.sh +704 -0
  18. package/scripts/sw-checkpoint.sh +1 -1
  19. package/scripts/sw-ci.sh +602 -0
  20. package/scripts/sw-cleanup.sh +1 -1
  21. package/scripts/sw-code-review.sh +637 -0
  22. package/scripts/sw-connect.sh +1 -1
  23. package/scripts/sw-context.sh +605 -0
  24. package/scripts/sw-cost.sh +1 -1
  25. package/scripts/sw-daemon.sh +432 -130
  26. package/scripts/sw-dashboard.sh +1 -1
  27. package/scripts/sw-db.sh +540 -0
  28. package/scripts/sw-decompose.sh +539 -0
  29. package/scripts/sw-deps.sh +551 -0
  30. package/scripts/sw-developer-simulation.sh +1 -1
  31. package/scripts/sw-discovery.sh +412 -0
  32. package/scripts/sw-docs-agent.sh +539 -0
  33. package/scripts/sw-docs.sh +1 -1
  34. package/scripts/sw-doctor.sh +59 -1
  35. package/scripts/sw-dora.sh +615 -0
  36. package/scripts/sw-durable.sh +710 -0
  37. package/scripts/sw-e2e-orchestrator.sh +535 -0
  38. package/scripts/sw-eventbus.sh +393 -0
  39. package/scripts/sw-feedback.sh +471 -0
  40. package/scripts/sw-fix.sh +1 -1
  41. package/scripts/sw-fleet-discover.sh +567 -0
  42. package/scripts/sw-fleet-viz.sh +404 -0
  43. package/scripts/sw-fleet.sh +8 -1
  44. package/scripts/sw-github-app.sh +596 -0
  45. package/scripts/sw-github-checks.sh +1 -1
  46. package/scripts/sw-github-deploy.sh +1 -1
  47. package/scripts/sw-github-graphql.sh +1 -1
  48. package/scripts/sw-guild.sh +569 -0
  49. package/scripts/sw-heartbeat.sh +1 -1
  50. package/scripts/sw-hygiene.sh +559 -0
  51. package/scripts/sw-incident.sh +617 -0
  52. package/scripts/sw-init.sh +88 -1
  53. package/scripts/sw-instrument.sh +699 -0
  54. package/scripts/sw-intelligence.sh +1 -1
  55. package/scripts/sw-jira.sh +1 -1
  56. package/scripts/sw-launchd.sh +363 -28
  57. package/scripts/sw-linear.sh +1 -1
  58. package/scripts/sw-logs.sh +1 -1
  59. package/scripts/sw-loop.sh +64 -3
  60. package/scripts/sw-memory.sh +1 -1
  61. package/scripts/sw-mission-control.sh +487 -0
  62. package/scripts/sw-model-router.sh +545 -0
  63. package/scripts/sw-otel.sh +596 -0
  64. package/scripts/sw-oversight.sh +689 -0
  65. package/scripts/sw-pipeline-composer.sh +1 -1
  66. package/scripts/sw-pipeline-vitals.sh +1 -1
  67. package/scripts/sw-pipeline.sh +687 -24
  68. package/scripts/sw-pm.sh +693 -0
  69. package/scripts/sw-pr-lifecycle.sh +522 -0
  70. package/scripts/sw-predictive.sh +1 -1
  71. package/scripts/sw-prep.sh +1 -1
  72. package/scripts/sw-ps.sh +1 -1
  73. package/scripts/sw-public-dashboard.sh +798 -0
  74. package/scripts/sw-quality.sh +595 -0
  75. package/scripts/sw-reaper.sh +1 -1
  76. package/scripts/sw-recruit.sh +573 -0
  77. package/scripts/sw-regression.sh +642 -0
  78. package/scripts/sw-release-manager.sh +736 -0
  79. package/scripts/sw-release.sh +706 -0
  80. package/scripts/sw-remote.sh +1 -1
  81. package/scripts/sw-replay.sh +520 -0
  82. package/scripts/sw-retro.sh +691 -0
  83. package/scripts/sw-scale.sh +444 -0
  84. package/scripts/sw-security-audit.sh +505 -0
  85. package/scripts/sw-self-optimize.sh +1 -1
  86. package/scripts/sw-session.sh +1 -1
  87. package/scripts/sw-setup.sh +1 -1
  88. package/scripts/sw-standup.sh +712 -0
  89. package/scripts/sw-status.sh +1 -1
  90. package/scripts/sw-strategic.sh +658 -0
  91. package/scripts/sw-stream.sh +450 -0
  92. package/scripts/sw-swarm.sh +583 -0
  93. package/scripts/sw-team-stages.sh +511 -0
  94. package/scripts/sw-templates.sh +1 -1
  95. package/scripts/sw-testgen.sh +515 -0
  96. package/scripts/sw-tmux-pipeline.sh +554 -0
  97. package/scripts/sw-tmux.sh +1 -1
  98. package/scripts/sw-trace.sh +485 -0
  99. package/scripts/sw-tracker-github.sh +188 -0
  100. package/scripts/sw-tracker-jira.sh +172 -0
  101. package/scripts/sw-tracker-linear.sh +251 -0
  102. package/scripts/sw-tracker.sh +117 -2
  103. package/scripts/sw-triage.sh +603 -0
  104. package/scripts/sw-upgrade.sh +1 -1
  105. package/scripts/sw-ux.sh +677 -0
  106. package/scripts/sw-webhook.sh +627 -0
  107. package/scripts/sw-widgets.sh +530 -0
  108. package/scripts/sw-worktree.sh +1 -1
@@ -4,7 +4,7 @@
4
4
  # ║ ║
5
5
  # ║ Shows running teams, agent windows, and task progress. ║
6
6
  # ╚═══════════════════════════════════════════════════════════════════════════╝
7
- VERSION="1.10.0"
7
+ VERSION="2.0.0"
8
8
  set -euo pipefail
9
9
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
10
10
 
@@ -0,0 +1,658 @@
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.0.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=3
61
+ STRATEGIC_COOLDOWN_SECONDS=43200 # 12 hours
62
+ STRATEGIC_MODEL="claude-haiku-4-5-20251001"
63
+ STRATEGIC_MAX_TOKENS=2048
64
+ STRATEGIC_STRATEGY_LINES=200
65
+
66
+ # ─── Cooldown Check ──────────────────────────────────────────────────────────
67
+ strategic_check_cooldown() {
68
+ local events_file="${EVENTS_FILE:-${HOME}/.shipwright/events.jsonl}"
69
+ if [[ ! -f "$events_file" ]]; then
70
+ return 0 # No events file — no cooldown
71
+ fi
72
+
73
+ local now_e
74
+ now_e=$(now_epoch)
75
+ local last_run
76
+ last_run=$(grep '"strategic.cycle_complete"' "$events_file" 2>/dev/null | tail -1 | jq -r '.ts_epoch // 0' 2>/dev/null || echo "0")
77
+
78
+ local elapsed=$(( now_e - last_run ))
79
+ if [[ "$elapsed" -lt "$STRATEGIC_COOLDOWN_SECONDS" ]]; then
80
+ local remaining=$(( (STRATEGIC_COOLDOWN_SECONDS - elapsed) / 60 ))
81
+ info "Strategic cooldown active — ${remaining} minutes remaining"
82
+ return 1
83
+ fi
84
+ return 0
85
+ }
86
+
87
+ # ─── Gather Context ──────────────────────────────────────────────────────────
88
+ strategic_gather_context() {
89
+ local repo_dir="${REPO_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
90
+ local script_dir="${SCRIPT_DIR:-${repo_dir}/scripts}"
91
+ local events_file="${EVENTS_FILE:-${HOME}/.shipwright/events.jsonl}"
92
+
93
+ # 1. Read STRATEGY.md (truncated)
94
+ local strategy_content=""
95
+ if [[ -f "${repo_dir}/STRATEGY.md" ]]; then
96
+ strategy_content=$(head -n "$STRATEGIC_STRATEGY_LINES" "${repo_dir}/STRATEGY.md")
97
+ else
98
+ strategy_content="(No STRATEGY.md found)"
99
+ fi
100
+
101
+ # 2. Codebase stats
102
+ local total_scripts=0
103
+ local untested_scripts=""
104
+ local untested_count=0
105
+ local total_tests=0
106
+
107
+ for script in "$script_dir"/sw-*.sh; do
108
+ [[ ! -f "$script" ]] && continue
109
+ local base
110
+ base=$(basename "$script" .sh)
111
+ [[ "$base" == *-test ]] && continue
112
+ [[ "$base" == sw-tracker-linear ]] && continue
113
+ [[ "$base" == sw-tracker-jira ]] && continue
114
+ [[ "$base" == sw-patrol-meta ]] && continue
115
+ [[ "$base" == sw-strategic ]] && continue
116
+ total_scripts=$((total_scripts + 1))
117
+
118
+ local test_file="$script_dir/${base}-test.sh"
119
+ if [[ -f "$test_file" ]]; then
120
+ total_tests=$((total_tests + 1))
121
+ else
122
+ untested_count=$((untested_count + 1))
123
+ untested_scripts="${untested_scripts} - ${base}.sh\n"
124
+ fi
125
+ done
126
+
127
+ # 3. Pipeline performance (last 7 days)
128
+ local completed=0
129
+ local failed=0
130
+ local success_rate="N/A"
131
+ local common_failures=""
132
+
133
+ if [[ -f "$events_file" ]]; then
134
+ local now_e
135
+ now_e=$(now_epoch)
136
+ local seven_days_ago=$(( now_e - 604800 ))
137
+
138
+ 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")
139
+ 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")
140
+
141
+ local total_pipelines=$(( completed + failed ))
142
+ if [[ "$total_pipelines" -gt 0 ]]; then
143
+ success_rate=$(( completed * 100 / total_pipelines ))
144
+ success_rate="${success_rate}%"
145
+ fi
146
+
147
+ common_failures=$(jq -s "
148
+ [.[] | select(.type == \"pipeline.completed\" and .result != \"success\" and (.ts_epoch // 0) >= $seven_days_ago)]
149
+ | group_by(.failed_stage // \"unknown\")
150
+ | map({stage: .[0].failed_stage // \"unknown\", count: length})
151
+ | sort_by(-.count)
152
+ | .[0:5]
153
+ | map(\"\(.stage) (\(.count)x)\")
154
+ | join(\", \")
155
+ " "$events_file" 2>/dev/null || echo "none")
156
+ fi
157
+
158
+ # 4. Open issues
159
+ local open_issues=""
160
+ if [[ "${NO_GITHUB:-false}" != "true" ]]; then
161
+ 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)")
162
+ else
163
+ open_issues="(GitHub access disabled)"
164
+ fi
165
+
166
+ # Build the context output
167
+ printf '%s\n' "STRATEGY_CONTENT<<EOF"
168
+ printf '%s\n' "$strategy_content"
169
+ printf '%s\n' "EOF"
170
+ printf '%s\n' "TOTAL_SCRIPTS=${total_scripts}"
171
+ printf '%s\n' "TOTAL_TESTS=${total_tests}"
172
+ printf '%s\n' "UNTESTED_COUNT=${untested_count}"
173
+ printf '%s\n' "UNTESTED_SCRIPTS<<EOF"
174
+ printf '%b' "$untested_scripts"
175
+ printf '%s\n' "EOF"
176
+ printf '%s\n' "PIPELINES_COMPLETED=${completed}"
177
+ printf '%s\n' "PIPELINES_FAILED=${failed}"
178
+ printf '%s\n' "SUCCESS_RATE=${success_rate}"
179
+ printf '%s\n' "COMMON_FAILURES=${common_failures}"
180
+ printf '%s\n' "OPEN_ISSUES<<EOF"
181
+ printf '%s\n' "$open_issues"
182
+ printf '%s\n' "EOF"
183
+ }
184
+
185
+ # ─── Build Prompt ─────────────────────────────────────────────────────────────
186
+ strategic_build_prompt() {
187
+ local repo_dir="${REPO_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
188
+ local script_dir="${SCRIPT_DIR:-${repo_dir}/scripts}"
189
+ local events_file="${EVENTS_FILE:-${HOME}/.shipwright/events.jsonl}"
190
+
191
+ # Read STRATEGY.md
192
+ local strategy_content=""
193
+ if [[ -f "${repo_dir}/STRATEGY.md" ]]; then
194
+ strategy_content=$(head -n "$STRATEGIC_STRATEGY_LINES" "${repo_dir}/STRATEGY.md")
195
+ else
196
+ strategy_content="(No STRATEGY.md found)"
197
+ fi
198
+
199
+ # Codebase stats
200
+ local total_scripts=0
201
+ local untested_list=""
202
+ local untested_count=0
203
+ local total_tests=0
204
+
205
+ for script in "$script_dir"/sw-*.sh; do
206
+ [[ ! -f "$script" ]] && continue
207
+ local base
208
+ base=$(basename "$script" .sh)
209
+ [[ "$base" == *-test ]] && continue
210
+ [[ "$base" == sw-tracker-linear ]] && continue
211
+ [[ "$base" == sw-tracker-jira ]] && continue
212
+ [[ "$base" == sw-patrol-meta ]] && continue
213
+ [[ "$base" == sw-strategic ]] && continue
214
+ total_scripts=$((total_scripts + 1))
215
+
216
+ if [[ -f "$script_dir/${base}-test.sh" ]]; then
217
+ total_tests=$((total_tests + 1))
218
+ else
219
+ untested_count=$((untested_count + 1))
220
+ untested_list="${untested_list}\n - ${base}.sh"
221
+ fi
222
+ done
223
+
224
+ # Pipeline performance (last 7 days)
225
+ local completed=0 failed=0 success_rate="N/A" common_failures="none"
226
+ if [[ -f "$events_file" ]]; then
227
+ local now_e
228
+ now_e=$(now_epoch)
229
+ local seven_days_ago=$(( now_e - 604800 ))
230
+
231
+ 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")
232
+ 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")
233
+
234
+ local total_pipelines=$(( completed + failed ))
235
+ if [[ "$total_pipelines" -gt 0 ]]; then
236
+ success_rate="$(( completed * 100 / total_pipelines ))%"
237
+ fi
238
+
239
+ common_failures=$(jq -s "
240
+ [.[] | select(.type == \"pipeline.completed\" and .result != \"success\" and (.ts_epoch // 0) >= $seven_days_ago)]
241
+ | group_by(.failed_stage // \"unknown\")
242
+ | map({stage: .[0].failed_stage // \"unknown\", count: length})
243
+ | sort_by(-.count)
244
+ | .[0:5]
245
+ | map(\"\(.stage) (\(.count)x)\")
246
+ | join(\", \")
247
+ " "$events_file" 2>/dev/null || echo "none")
248
+ # Empty string → "none"
249
+ common_failures="${common_failures:-none}"
250
+ fi
251
+
252
+ # Open issues
253
+ local open_issues=""
254
+ if [[ "${NO_GITHUB:-false}" != "true" ]]; then
255
+ open_issues=$(gh issue list --state open --json number,title --jq '.[] | "#\(.number): \(.title)"' 2>/dev/null | head -50 || echo "(could not fetch)")
256
+ else
257
+ open_issues="(GitHub access disabled)"
258
+ fi
259
+
260
+ # Compose the prompt
261
+ cat <<PROMPT_EOF
262
+ 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.
263
+
264
+ ## Strategy (from STRATEGY.md)
265
+ ${strategy_content}
266
+
267
+ ## Current Codebase
268
+ - Total scripts: ${total_scripts}
269
+ - Scripts with tests: ${total_tests}
270
+ - Scripts without tests (${untested_count}):$(echo -e "$untested_list")
271
+
272
+ ## Recent Pipeline Performance (last 7 days)
273
+ - Pipelines completed successfully: ${completed}
274
+ - Pipelines failed: ${failed}
275
+ - Success rate: ${success_rate}
276
+ - Common failure stages: ${common_failures}
277
+
278
+ ## Open Issues (already in progress — do NOT duplicate these)
279
+ ${open_issues}
280
+
281
+ ## Your Task
282
+ 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.
283
+
284
+ For each recommendation, provide EXACTLY this format (no extra fields, no deviations):
285
+
286
+ ISSUE_TITLE: <concise, actionable title>
287
+ PRIORITY: <P0|P1|P2|P3|P4|P5>
288
+ COMPLEXITY: <fast|standard|full>
289
+ STRATEGY_AREA: <which priority area from strategy, e.g. "P0: Reliability">
290
+ DESCRIPTION: <2-3 sentences describing what to build and why it matters>
291
+ ACCEPTANCE: <bullet list of acceptance criteria, one per line starting with "- ">
292
+ ---
293
+
294
+ Rules:
295
+ - Do NOT duplicate any open issue listed above
296
+ - Prioritize based on STRATEGY.md priorities (P0 > P1 > P2 > ...)
297
+ - Focus on concrete, actionable improvements (not vague goals)
298
+ - Each issue should be completable by one autonomous pipeline run
299
+ - Prefer reliability and DX improvements over new features
300
+ - Maximum 3 issues
301
+ PROMPT_EOF
302
+ }
303
+
304
+ # ─── Call Anthropic API ───────────────────────────────────────────────────────
305
+ strategic_call_api() {
306
+ local prompt="$1"
307
+
308
+ if [[ -z "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]]; then
309
+ error "CLAUDE_CODE_OAUTH_TOKEN not set — cannot run strategic analysis"
310
+ return 1
311
+ fi
312
+
313
+ if ! command -v claude &>/dev/null; then
314
+ error "Claude Code CLI not found — install with: npm install -g @anthropic-ai/claude-code"
315
+ return 1
316
+ fi
317
+
318
+ local tmp_prompt
319
+ tmp_prompt=$(mktemp)
320
+ printf '%s' "$prompt" > "$tmp_prompt"
321
+
322
+ local response_text
323
+ response_text=$(claude -p "$(cat "$tmp_prompt")" --max-turns 1 --model "$STRATEGIC_MODEL" 2>/dev/null || echo "")
324
+ rm -f "$tmp_prompt"
325
+
326
+ if [[ -z "$response_text" ]]; then
327
+ error "Claude returned empty response"
328
+ return 1
329
+ fi
330
+
331
+ printf '%s' "$response_text"
332
+ }
333
+
334
+ # ─── Parse Response & Create Issues ──────────────────────────────────────────
335
+ strategic_parse_and_create() {
336
+ local response="$1"
337
+ local created=0
338
+ local skipped=0
339
+
340
+ # Split response into issue blocks by "---" delimiter
341
+ local current_title="" current_priority="" current_complexity=""
342
+ local current_strategy="" current_description="" current_acceptance=""
343
+ local in_acceptance=false
344
+
345
+ while IFS= read -r line; do
346
+ # Strip carriage returns
347
+ line="${line//$'\r'/}"
348
+
349
+ if [[ "$line" == "---" ]] || [[ "$line" == "---"* && ${#line} -le 5 ]]; then
350
+ # End of block — create issue if we have a title
351
+ if [[ -n "$current_title" ]]; then
352
+ strategic_create_issue \
353
+ "$current_title" "$current_priority" "$current_complexity" \
354
+ "$current_strategy" "$current_description" "$current_acceptance"
355
+ local rc=$?
356
+ if [[ $rc -eq 0 ]]; then
357
+ created=$((created + 1))
358
+ else
359
+ skipped=$((skipped + 1))
360
+ fi
361
+
362
+ if [[ "$created" -ge "$STRATEGIC_MAX_ISSUES" ]]; then
363
+ info "Reached max issues per cycle (${STRATEGIC_MAX_ISSUES})"
364
+ break
365
+ fi
366
+ fi
367
+
368
+ # Reset for next block
369
+ current_title="" current_priority="" current_complexity=""
370
+ current_strategy="" current_description="" current_acceptance=""
371
+ in_acceptance=false
372
+ continue
373
+ fi
374
+
375
+ # Parse fields
376
+ if [[ "$line" == ISSUE_TITLE:* ]]; then
377
+ current_title="${line#ISSUE_TITLE: }"
378
+ current_title="${current_title#ISSUE_TITLE:}"
379
+ current_title=$(echo "$current_title" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
380
+ in_acceptance=false
381
+ elif [[ "$line" == PRIORITY:* ]]; then
382
+ current_priority="${line#PRIORITY: }"
383
+ current_priority="${current_priority#PRIORITY:}"
384
+ current_priority=$(echo "$current_priority" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
385
+ in_acceptance=false
386
+ elif [[ "$line" == COMPLEXITY:* ]]; then
387
+ current_complexity="${line#COMPLEXITY: }"
388
+ current_complexity="${current_complexity#COMPLEXITY:}"
389
+ current_complexity=$(echo "$current_complexity" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
390
+ in_acceptance=false
391
+ elif [[ "$line" == STRATEGY_AREA:* ]]; then
392
+ current_strategy="${line#STRATEGY_AREA: }"
393
+ current_strategy="${current_strategy#STRATEGY_AREA:}"
394
+ current_strategy=$(echo "$current_strategy" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
395
+ in_acceptance=false
396
+ elif [[ "$line" == DESCRIPTION:* ]]; then
397
+ current_description="${line#DESCRIPTION: }"
398
+ current_description="${current_description#DESCRIPTION:}"
399
+ current_description=$(echo "$current_description" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
400
+ in_acceptance=false
401
+ elif [[ "$line" == ACCEPTANCE:* ]]; then
402
+ current_acceptance="${line#ACCEPTANCE: }"
403
+ current_acceptance="${current_acceptance#ACCEPTANCE:}"
404
+ current_acceptance=$(echo "$current_acceptance" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
405
+ in_acceptance=true
406
+ elif [[ "$in_acceptance" == true && "$line" == "- "* ]]; then
407
+ # Continuation of acceptance criteria
408
+ if [[ -n "$current_acceptance" ]]; then
409
+ current_acceptance="${current_acceptance}\n${line}"
410
+ else
411
+ current_acceptance="$line"
412
+ fi
413
+ fi
414
+ done <<< "$response"
415
+
416
+ # Handle last block (if no trailing ---)
417
+ if [[ -n "$current_title" && "$created" -lt "$STRATEGIC_MAX_ISSUES" ]]; then
418
+ strategic_create_issue \
419
+ "$current_title" "$current_priority" "$current_complexity" \
420
+ "$current_strategy" "$current_description" "$current_acceptance"
421
+ local rc=$?
422
+ if [[ $rc -eq 0 ]]; then
423
+ created=$((created + 1))
424
+ else
425
+ skipped=$((skipped + 1))
426
+ fi
427
+ fi
428
+
429
+ echo "${created}:${skipped}"
430
+ }
431
+
432
+ # ─── Create Single Issue ─────────────────────────────────────────────────────
433
+ strategic_create_issue() {
434
+ local title="$1"
435
+ local priority="${2:-P2}"
436
+ local complexity="${3:-standard}"
437
+ local strategy_area="${4:-}"
438
+ local description="${5:-}"
439
+ local acceptance="${6:-}"
440
+
441
+ if [[ -z "$title" ]]; then
442
+ return 1
443
+ fi
444
+
445
+ # Dry-run mode
446
+ if [[ "${NO_GITHUB:-false}" == "true" ]]; then
447
+ info " [dry-run] Would create: ${title}"
448
+ return 0
449
+ fi
450
+
451
+ # Dedup: check if an open issue with this exact title already exists
452
+ local existing
453
+ existing=$(gh issue list --state open --search "$title" --json number,title --jq ".[].title" 2>/dev/null || echo "")
454
+ if echo "$existing" | grep -qF "$title" 2>/dev/null; then
455
+ info " Skipping duplicate: ${title}"
456
+ return 1
457
+ fi
458
+
459
+ # Build issue body
460
+ local timestamp
461
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
462
+
463
+ local body
464
+ body=$(cat <<BODY_EOF
465
+ ## Strategic Improvement
466
+
467
+ $(echo -e "$description")
468
+
469
+ ### Acceptance Criteria
470
+ $(echo -e "$acceptance")
471
+
472
+ ### Context
473
+ - **Priority**: ${priority}
474
+ - **Complexity**: ${complexity}
475
+ - **Generated by**: Strategic Intelligence Agent
476
+ - **Strategy alignment**: ${strategy_area}
477
+
478
+ <!-- STRATEGIC-CYCLE: ${timestamp} -->
479
+ BODY_EOF
480
+ )
481
+
482
+ local labels="auto-patrol,ready-to-build,strategic"
483
+
484
+ gh issue create \
485
+ --title "$title" \
486
+ --body "$body" \
487
+ --label "$labels" 2>/dev/null || {
488
+ warn " Failed to create issue: ${title}"
489
+ return 1
490
+ }
491
+
492
+ emit_event "strategic.issue_created" "title=$title" "priority=$priority" "complexity=$complexity"
493
+ success " Created issue: ${title}"
494
+ return 0
495
+ }
496
+
497
+ # ─── Main Strategic Run ──────────────────────────────────────────────────────
498
+ strategic_run() {
499
+ echo -e "\n${PURPLE}${BOLD}━━━ Strategic Intelligence Agent ━━━${RESET}"
500
+ echo -e "${DIM} Analyzing codebase, strategy, and metrics...${RESET}\n"
501
+
502
+ # Check cooldown
503
+ if ! strategic_check_cooldown; then
504
+ return 0
505
+ fi
506
+
507
+ # Check auth token
508
+ if [[ -z "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]]; then
509
+ error "CLAUDE_CODE_OAUTH_TOKEN not set — strategic analysis requires Claude access"
510
+ return 1
511
+ fi
512
+
513
+ # Build prompt with all context
514
+ info "Gathering context..."
515
+ local prompt
516
+ prompt=$(strategic_build_prompt)
517
+
518
+ # Call Anthropic API
519
+ info "Calling ${STRATEGIC_MODEL} for strategic analysis..."
520
+ local response
521
+ response=$(strategic_call_api "$prompt") || {
522
+ error "Strategic analysis API call failed"
523
+ emit_event "strategic.cycle_failed" "reason=api_error"
524
+ return 1
525
+ }
526
+
527
+ # Parse and create issues
528
+ info "Processing recommendations..."
529
+ local result
530
+ result=$(strategic_parse_and_create "$response")
531
+
532
+ local created="${result%%:*}"
533
+ local skipped="${result##*:}"
534
+
535
+ # Summary
536
+ echo ""
537
+ echo -e "${PURPLE}${BOLD}━━━ Strategic Summary ━━━${RESET}"
538
+ echo -e " Issues created: ${created}"
539
+ echo -e " Issues skipped: ${skipped} (duplicates)"
540
+ echo ""
541
+
542
+ emit_event "strategic.cycle_complete" "issues_created=$created" "issues_skipped=$skipped"
543
+ }
544
+
545
+ # ─── Status Command ──────────────────────────────────────────────────────────
546
+ strategic_status() {
547
+ echo -e "\n${PURPLE}${BOLD}━━━ Strategic Agent Status ━━━${RESET}\n"
548
+
549
+ local events_file="${EVENTS_FILE:-${HOME}/.shipwright/events.jsonl}"
550
+
551
+ if [[ ! -f "$events_file" ]]; then
552
+ info "No events data available"
553
+ return 0
554
+ fi
555
+
556
+ # Last run
557
+ local last_run_line
558
+ last_run_line=$(grep '"strategic.cycle_complete"' "$events_file" 2>/dev/null | tail -1 || echo "")
559
+
560
+ if [[ -z "$last_run_line" ]]; then
561
+ info "No strategic cycles recorded yet"
562
+ return 0
563
+ fi
564
+
565
+ local last_ts last_created last_skipped
566
+ last_ts=$(echo "$last_run_line" | jq -r '.ts // "unknown"' 2>/dev/null || echo "unknown")
567
+ last_created=$(echo "$last_run_line" | jq -r '.issues_created // 0' 2>/dev/null || echo "0")
568
+ last_skipped=$(echo "$last_run_line" | jq -r '.issues_skipped // 0' 2>/dev/null || echo "0")
569
+
570
+ echo -e " Last run: ${last_ts}"
571
+ echo -e " Issues created: ${last_created}"
572
+ echo -e " Issues skipped: ${last_skipped}"
573
+
574
+ # Cooldown status
575
+ local last_epoch
576
+ last_epoch=$(echo "$last_run_line" | jq -r '.ts_epoch // 0' 2>/dev/null || echo "0")
577
+ local now_e
578
+ now_e=$(now_epoch)
579
+ local elapsed=$(( now_e - last_epoch ))
580
+
581
+ if [[ "$elapsed" -lt "$STRATEGIC_COOLDOWN_SECONDS" ]]; then
582
+ local remaining_min=$(( (STRATEGIC_COOLDOWN_SECONDS - elapsed) / 60 ))
583
+ echo -e " Cooldown: ${YELLOW}${remaining_min} min remaining${RESET}"
584
+ else
585
+ echo -e " Cooldown: ${GREEN}Ready${RESET}"
586
+ fi
587
+
588
+ # Total issues created
589
+ local total_created
590
+ total_created=$(grep '"strategic.issue_created"' "$events_file" 2>/dev/null | wc -l | tr -d ' ' || echo "0")
591
+ echo -e " Total created: ${total_created} issues (all time)"
592
+
593
+ # Total cycles
594
+ local total_cycles
595
+ total_cycles=$(grep '"strategic.cycle_complete"' "$events_file" 2>/dev/null | wc -l | tr -d ' ' || echo "0")
596
+ echo -e " Total cycles: ${total_cycles}"
597
+
598
+ echo ""
599
+ }
600
+
601
+ # ─── Help ─────────────────────────────────────────────────────────────────────
602
+ strategic_show_help() {
603
+ echo -e "${PURPLE}${BOLD}Shipwright Strategic Intelligence Agent${RESET} v${VERSION}\n"
604
+ echo -e "Reads strategy, metrics, and codebase state to create high-impact improvement issues.\n"
605
+ echo -e "${BOLD}Usage:${RESET}"
606
+ echo -e " sw-strategic.sh <command>\n"
607
+ echo -e "${BOLD}Commands:${RESET}"
608
+ echo -e " run Run a strategic analysis cycle"
609
+ echo -e " status Show last run stats and cooldown"
610
+ echo -e " help Show this help\n"
611
+ echo -e "${BOLD}Environment:${RESET}"
612
+ echo -e " CLAUDE_CODE_OAUTH_TOKEN Required for Claude access"
613
+ echo -e " NO_GITHUB=true Dry-run mode (no issue creation)\n"
614
+ echo -e "${BOLD}Cooldown:${RESET}"
615
+ echo -e " 12 hours between cycles (checks events.jsonl)\n"
616
+ }
617
+
618
+ # ─── Daemon Integration (sourced mode) ────────────────────────────────────────
619
+ strategic_patrol_run() {
620
+ # Called by daemon during patrol cycle
621
+ # Check cooldown (12h minimum between runs)
622
+ # Requires CLAUDE_CODE_OAUTH_TOKEN
623
+ if [[ -z "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]]; then
624
+ echo -e " ${DIM}●${RESET} Strategic patrol skipped (no CLAUDE_CODE_OAUTH_TOKEN)"
625
+ return 0
626
+ fi
627
+
628
+ if ! strategic_check_cooldown; then
629
+ return 0
630
+ fi
631
+
632
+ echo -e "\n ${BOLD}Strategic Intelligence Patrol${RESET}"
633
+ strategic_run
634
+ }
635
+
636
+ # ─── Source Guard ─────────────────────────────────────────────────────────────
637
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
638
+ set -euo pipefail
639
+ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
640
+
641
+ main() {
642
+ local cmd="${1:-help}"
643
+ shift 2>/dev/null || true
644
+
645
+ case "$cmd" in
646
+ run) strategic_run ;;
647
+ status) strategic_status ;;
648
+ help) strategic_show_help ;;
649
+ *)
650
+ error "Unknown command: $cmd"
651
+ strategic_show_help
652
+ exit 1
653
+ ;;
654
+ esac
655
+ }
656
+
657
+ main "$@"
658
+ fi