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.
- package/README.md +221 -55
- package/completions/_shipwright +264 -32
- package/completions/shipwright.bash +118 -26
- package/completions/shipwright.fish +80 -2
- package/dashboard/server.ts +208 -0
- 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/docs/tmux-research/TMUX-ARCHITECTURE.md +567 -0
- package/docs/tmux-research/TMUX-AUDIT.md +925 -0
- package/docs/tmux-research/TMUX-BEST-PRACTICES-2025-2026.md +829 -0
- package/docs/tmux-research/TMUX-QUICK-REFERENCE.md +543 -0
- package/docs/tmux-research/TMUX-RESEARCH-INDEX.md +438 -0
- package/package.json +4 -2
- package/scripts/lib/helpers.sh +7 -0
- package/scripts/sw +323 -2
- 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 +754 -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 +698 -0
- package/scripts/sw-connect.sh +1 -1
- package/scripts/sw-context.sh +605 -0
- package/scripts/sw-cost.sh +44 -3
- package/scripts/sw-daemon.sh +568 -138
- package/scripts/sw-dashboard.sh +1 -1
- package/scripts/sw-db.sh +1380 -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 +107 -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 +479 -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 +4 -4
- 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 +656 -0
- package/scripts/sw-init.sh +237 -24
- 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 +267 -21
- package/scripts/sw-memory.sh +18 -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 +764 -0
- package/scripts/sw-pipeline-composer.sh +1 -1
- package/scripts/sw-pipeline-vitals.sh +1 -1
- package/scripts/sw-pipeline.sh +947 -35
- package/scripts/sw-pm.sh +758 -0
- package/scripts/sw-pr-lifecycle.sh +522 -0
- package/scripts/sw-predictive.sh +8 -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 +2248 -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 +263 -127
- package/scripts/sw-standup.sh +712 -0
- package/scripts/sw-status.sh +44 -2
- package/scripts/sw-strategic.sh +806 -0
- package/scripts/sw-stream.sh +450 -0
- package/scripts/sw-swarm.sh +620 -0
- package/scripts/sw-team-stages.sh +511 -0
- package/scripts/sw-templates.sh +4 -4
- package/scripts/sw-testgen.sh +566 -0
- package/scripts/sw-tmux-pipeline.sh +554 -0
- package/scripts/sw-tmux-role-color.sh +58 -0
- package/scripts/sw-tmux-status.sh +128 -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 +627 -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
- package/templates/pipelines/autonomous.json +2 -2
- package/tmux/shipwright-overlay.conf +35 -17
- 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
|