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,754 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright autonomous — Master controller for AI-building-AI loop ║
4
+ # ║ Analyze → Create issues → Build → Learn → Repeat ║
5
+ # ║ Closes the loop: PM creates issues, daemon builds, system learns ║
6
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
7
+ set -euo pipefail
8
+ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
9
+
10
+ VERSION="2.1.0"
11
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
+ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
13
+
14
+ # ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
15
+ CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
16
+ PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
17
+ BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
18
+ GREEN='\033[38;2;74;222;128m' # success
19
+ YELLOW='\033[38;2;250;204;21m' # warning
20
+ RED='\033[38;2;248;113;113m' # error
21
+ DIM='\033[2m'
22
+ BOLD='\033[1m'
23
+ RESET='\033[0m'
24
+
25
+ # ─── Cross-platform compatibility ──────────────────────────────────────────
26
+ # shellcheck source=lib/compat.sh
27
+ [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
28
+
29
+ # ─── Output Helpers ─────────────────────────────────────────────────────────
30
+ info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
31
+ success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
32
+ warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
33
+ error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
34
+
35
+ now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
36
+ now_epoch() { date +%s; }
37
+
38
+ format_duration() {
39
+ local secs="$1"
40
+ if [[ "$secs" -ge 3600 ]]; then
41
+ printf "%dh %dm %ds" $((secs/3600)) $((secs%3600/60)) $((secs%60))
42
+ elif [[ "$secs" -ge 60 ]]; then
43
+ printf "%dm %ds" $((secs/60)) $((secs%60))
44
+ else
45
+ printf "%ds" "$secs"
46
+ fi
47
+ }
48
+
49
+ # ─── Structured Event Log ──────────────────────────────────────────────────
50
+ EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
51
+
52
+ emit_event() {
53
+ local event_type="$1"
54
+ shift
55
+ local json_fields=""
56
+ for kv in "$@"; do
57
+ local key="${kv%%=*}"
58
+ local val="${kv#*=}"
59
+ if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
60
+ json_fields="${json_fields},\"${key}\":${val}"
61
+ else
62
+ val="${val//\"/\\\"}"
63
+ json_fields="${json_fields},\"${key}\":\"${val}\""
64
+ fi
65
+ done
66
+ mkdir -p "${HOME}/.shipwright"
67
+ echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
68
+ }
69
+
70
+ # ─── State & Config Paths ─────────────────────────────────────────────────
71
+ STATE_DIR="${HOME}/.shipwright/autonomous"
72
+ STATE_FILE="${STATE_DIR}/state.json"
73
+ HISTORY_FILE="${STATE_DIR}/history.jsonl"
74
+ CONFIG_FILE="${STATE_DIR}/config.json"
75
+ CYCLE_COUNTER="${STATE_DIR}/cycle-counter.txt"
76
+
77
+ # Ensure directories exist
78
+ ensure_state_dir() {
79
+ mkdir -p "$STATE_DIR"
80
+ if [[ ! -f "$CONFIG_FILE" ]]; then
81
+ cat > "$CONFIG_FILE" << 'EOF'
82
+ {
83
+ "cycle_interval_minutes": 60,
84
+ "max_issues_per_cycle": 5,
85
+ "max_concurrent_pipelines": 2,
86
+ "enable_human_approval": false,
87
+ "approval_timeout_minutes": 30,
88
+ "rollback_on_failures": true,
89
+ "max_consecutive_failures": 3,
90
+ "learning_enabled": true,
91
+ "self_improvement_enabled": true,
92
+ "shipwright_self_improvement_threshold": 3
93
+ }
94
+ EOF
95
+ info "Created default config at ${CONFIG_FILE}"
96
+ fi
97
+ }
98
+
99
+ # Get config value
100
+ get_config() {
101
+ local key="$1"
102
+ local default="${2:-}"
103
+ jq -r ".${key} // \"${default}\"" "$CONFIG_FILE" 2>/dev/null || echo "$default"
104
+ }
105
+
106
+ # Set config value
107
+ set_config() {
108
+ local key="$1"
109
+ local value="$2"
110
+ local tmp_file
111
+ tmp_file=$(mktemp "${TMPDIR:-/tmp}/sw-autonomous-config.XXXXXX")
112
+ # Try to parse value as JSON first, otherwise treat as string
113
+ local json_value
114
+ if echo "$value" | jq . >/dev/null 2>&1; then
115
+ json_value="$value"
116
+ else
117
+ json_value="\"${value//\"/\\\"}\""
118
+ fi
119
+ jq ".\"$key\" = $json_value" "$CONFIG_FILE" > "$tmp_file" 2>/dev/null && \
120
+ mv "$tmp_file" "$CONFIG_FILE" || rm -f "$tmp_file"
121
+ }
122
+
123
+ # ─── State Management ───────────────────────────────────────────────────────
124
+
125
+ init_state() {
126
+ local tmp_file
127
+ tmp_file=$(mktemp "${TMPDIR:-/tmp}/sw-autonomous-state.XXXXXX")
128
+ jq -n \
129
+ --arg started "$(now_iso)" \
130
+ '{
131
+ status: "idle",
132
+ started: $started,
133
+ last_cycle: null,
134
+ cycles_completed: 0,
135
+ issues_created: 0,
136
+ issues_completed: 0,
137
+ pipelines_succeeded: 0,
138
+ pipelines_failed: 0,
139
+ consecutive_failures: 0,
140
+ paused_at: null
141
+ }' > "$tmp_file" && mv "$tmp_file" "$STATE_FILE" || rm -f "$tmp_file"
142
+ }
143
+
144
+ read_state() {
145
+ ensure_state_dir
146
+ if [[ ! -f "$STATE_FILE" ]]; then
147
+ init_state
148
+ fi
149
+ cat "$STATE_FILE"
150
+ }
151
+
152
+ update_state() {
153
+ local key="$1"
154
+ local value="$2"
155
+ local tmp_file
156
+ tmp_file=$(mktemp "${TMPDIR:-/tmp}/sw-autonomous-state.XXXXXX")
157
+ # Try to parse value as JSON first, otherwise treat as string
158
+ local json_value
159
+ if echo "$value" | jq . >/dev/null 2>&1; then
160
+ json_value="$value"
161
+ else
162
+ json_value="\"${value//\"/\\\"}\""
163
+ fi
164
+ read_state | jq ".\"$key\" = $json_value" > "$tmp_file" && \
165
+ mv "$tmp_file" "$STATE_FILE" || rm -f "$tmp_file"
166
+ }
167
+
168
+ increment_counter() {
169
+ local key="$1"
170
+ local tmp_file
171
+ tmp_file=$(mktemp "${TMPDIR:-/tmp}/sw-autonomous-state.XXXXXX")
172
+ read_state | jq ".\"$key\" += 1" > "$tmp_file" && \
173
+ mv "$tmp_file" "$STATE_FILE" || rm -f "$tmp_file"
174
+ }
175
+
176
+ record_cycle() {
177
+ local issues_found="$1"
178
+ local issues_created="$2"
179
+ local status="$3"
180
+ local tmp_file
181
+ tmp_file=$(mktemp "${TMPDIR:-/tmp}/sw-autonomous-cycle.XXXXXX")
182
+ jq -n \
183
+ --arg ts "$(now_iso)" \
184
+ --argjson found "$issues_found" \
185
+ --argjson created "$issues_created" \
186
+ --arg status "$status" \
187
+ '{ts: $ts, found: $found, created: $created, status: $status}' \
188
+ >> "$HISTORY_FILE"
189
+ }
190
+
191
+ # ─── Analysis Cycle ────────────────────────────────────────────────────────
192
+
193
+ run_analysis_cycle() {
194
+ info "Starting analysis cycle..."
195
+
196
+ ensure_state_dir
197
+ update_state "status" "analyzing"
198
+
199
+ local findings
200
+ findings=$(mktemp "${TMPDIR:-/tmp}/sw-autonomous-findings.XXXXXX")
201
+
202
+ # Use Claude to analyze the codebase
203
+ if command -v claude &>/dev/null; then
204
+ info "Running codebase analysis with Claude..."
205
+
206
+ claude -p 'You are Shipwright'"'"'s autonomous PM. Analyze this repository for:
207
+ 1. Bugs & Issues: Missing error handling, potential crashes, edge cases
208
+ 2. Performance: Bottlenecks, n+1 queries, memory leaks, unnecessary work
209
+ 3. Missing Tests: Uncovered code paths, critical scenarios
210
+ 4. Stale Documentation: Outdated guides, missing API docs
211
+ 5. Security: Input validation, injection risks, credential leaks
212
+ 6. Code Quality: Dead code, refactoring opportunities, tech debt
213
+ 7. Self-Improvement: How Shipwright itself could be enhanced
214
+
215
+ For each finding, output JSON with fields: title, description, priority (critical/high/medium/low), effort (S/M/L), labels (array), category.
216
+ Output ONLY a JSON array, no other text.' --max-turns 3 > "$findings" 2>/dev/null || true
217
+
218
+ else
219
+ warn "Claude CLI not available, using static heuristics..."
220
+
221
+ # Static heuristics for analysis
222
+ {
223
+ local has_tests=$(find . -type f -name "*test*" -o -name "*spec*" | wc -l || echo "0")
224
+ local shell_scripts=$(find scripts -type f -name "*.sh" | wc -l || echo "0")
225
+
226
+ jq -n \
227
+ --argjson test_count "$has_tests" \
228
+ --argjson script_count "$shell_scripts" \
229
+ '[
230
+ {
231
+ title: "Add comprehensive test coverage for critical paths",
232
+ description: "Several scripts lack unit test coverage",
233
+ priority: "high",
234
+ effort: "L",
235
+ labels: ["test", "quality"],
236
+ category: "test"
237
+ },
238
+ {
239
+ title: "Simplify error handling in daemon.sh",
240
+ description: "Daemon error handling could be more robust",
241
+ priority: "medium",
242
+ effort: "M",
243
+ labels: ["refactor", "self-improvement"],
244
+ category: "self-improvement"
245
+ }
246
+ ]'
247
+ } > "$findings"
248
+ fi
249
+
250
+ cat "$findings"
251
+ }
252
+
253
+ # ─── Issue Creation ────────────────────────────────────────────────────────
254
+
255
+ create_issue_from_finding() {
256
+ local title="$1"
257
+ local description="$2"
258
+ local priority="$3"
259
+ local effort="$4"
260
+ local labels="$5"
261
+
262
+ if [[ "$NO_GITHUB" == "true" ]]; then
263
+ warn "GitHub disabled, skipping issue creation for: $title"
264
+ return 1
265
+ fi
266
+
267
+ # Check if issue already exists
268
+ local existing
269
+ existing=$(gh issue list --search "$title" --json number -q 'length' 2>/dev/null || echo "0")
270
+ if [[ "${existing:-0}" -gt 0 ]]; then
271
+ warn "Issue already exists: $title"
272
+ return 1
273
+ fi
274
+
275
+ # Add shipwright label to auto-feed daemon
276
+ local all_labels="shipwright,$labels"
277
+
278
+ # Create GitHub issue; capture URL and parse issue number
279
+ local create_out
280
+ create_out=$(gh issue create \
281
+ --title "$title" \
282
+ --body "$description
283
+
284
+ ---
285
+ **Metadata:**
286
+ - Priority: \`${priority}\`
287
+ - Effort: \`${effort}\`
288
+ - Created: \`$(now_iso)\`
289
+ - By: Autonomous loop (sw-autonomous.sh)
290
+ " \
291
+ --label "$all_labels" 2>/dev/null) || {
292
+ warn "Failed to create issue: $title"
293
+ return 1
294
+ }
295
+ success "Created issue: $title"
296
+ local issue_num
297
+ issue_num=$(echo "$create_out" | sed -n 's|.*/issues/\([0-9]*\)|\1|p')
298
+ [[ -n "$issue_num" ]] && echo "$issue_num"
299
+ return 0
300
+ }
301
+
302
+ # ─── Issue Processing from Analysis ────────────────────────────────────────
303
+ # Trigger pipeline for a finding issue (daemon will also pick it up; this runs immediately)
304
+ trigger_pipeline_for_finding() {
305
+ local issue_num="$1"
306
+ local title="$2"
307
+ if [[ -z "$issue_num" || ! "$issue_num" =~ ^[0-9]+$ ]]; then
308
+ return 0
309
+ fi
310
+ if [[ ! -x "$SCRIPT_DIR/sw-pipeline.sh" ]]; then
311
+ return 0
312
+ fi
313
+ info "Triggering pipeline for finding issue #${issue_num}: $title"
314
+ (cd "$REPO_DIR" && export REPO_DIR SCRIPT_DIR && bash "$SCRIPT_DIR/sw-pipeline.sh" start --issue "$issue_num" 2>/dev/null) &
315
+ emit_event "autonomous.pipeline_triggered" "issue=$issue_num" "title=$title"
316
+ }
317
+
318
+ # Record finding outcome for learning (which findings led to successful fixes)
319
+ record_finding_pending() {
320
+ local issue_num="$1"
321
+ local finding_title="$2"
322
+ ensure_state_dir
323
+ local state_file="${STATE_DIR}/state.json"
324
+ local pending_file="${STATE_DIR}/pending_findings.jsonl"
325
+ [[ ! -f "$pending_file" ]] && touch "$pending_file"
326
+ echo "{\"issue_number\":$issue_num,\"finding_title\":\"${finding_title//\"/\\\"}\",\"created_at\":\"$(now_iso)\",\"outcome\":\"pending\"}" >> "$pending_file"
327
+ }
328
+
329
+ # Update outcomes for pending findings (called at start of each cycle)
330
+ update_finding_outcomes() {
331
+ [[ "$NO_GITHUB" == "true" ]] && return 0
332
+ local pending_file="${STATE_DIR}/pending_findings.jsonl"
333
+ [[ ! -f "$pending_file" ]] && return 0
334
+ local updated_file
335
+ updated_file=$(mktemp "${TMPDIR:-/tmp}/sw-autonomous-pending.XXXXXX")
336
+ while IFS= read -r line; do
337
+ [[ -z "$line" ]] && continue
338
+ local issue_num outcome
339
+ issue_num=$(echo "$line" | jq -r '.issue_number // empty')
340
+ outcome=$(echo "$line" | jq -r '.outcome // "pending"')
341
+ if [[ "$outcome" != "pending" || -z "$issue_num" ]]; then
342
+ echo "$line" >> "$updated_file"
343
+ continue
344
+ fi
345
+ local state
346
+ state=$(gh issue view "$issue_num" --json state 2>/dev/null | jq -r '.state // "OPEN"') || state="OPEN"
347
+ if [[ "$state" != "CLOSED" ]]; then
348
+ echo "$line" >> "$updated_file"
349
+ continue
350
+ fi
351
+ local merged=""
352
+ local merged_pipeline merged_daemon
353
+ merged_pipeline=$(gh pr list --head "pipeline/issue-${issue_num}" --json state -q '.[0].state' 2>/dev/null || echo "")
354
+ merged_daemon=$(gh pr list --head "daemon/issue-${issue_num}" --json state -q '.[0].state' 2>/dev/null || echo "")
355
+ [[ "$merged_pipeline" == "MERGED" || "$merged_daemon" == "MERGED" ]] && merged="MERGED"
356
+ if [[ "$merged" == "MERGED" ]]; then
357
+ outcome="success"
358
+ increment_counter "issues_completed"
359
+ emit_event "autonomous.finding_success" "issue=$issue_num"
360
+ else
361
+ outcome="failure"
362
+ emit_event "autonomous.finding_failure" "issue=$issue_num"
363
+ fi
364
+ echo "$line" | jq --arg o "$outcome" '.outcome = $o' >> "$updated_file"
365
+ done < "$pending_file"
366
+ mv "$updated_file" "$pending_file"
367
+ }
368
+
369
+ process_findings() {
370
+ local findings_file="$1"
371
+ local max_per_cycle
372
+ max_per_cycle=$(get_config "max_issues_per_cycle" "5")
373
+
374
+ info "Processing findings (max ${max_per_cycle} per cycle)..."
375
+
376
+ local created=0
377
+ local total=0
378
+
379
+ # Parse findings and create issues; trigger pipeline for each; record for outcome tracking
380
+ while IFS= read -r finding; do
381
+ [[ -z "$finding" ]] && continue
382
+
383
+ total=$((total + 1))
384
+ [[ "$created" -ge "$max_per_cycle" ]] && break
385
+
386
+ local title description priority effort labels category
387
+ title=$(echo "$finding" | jq -r '.title // ""')
388
+ description=$(echo "$finding" | jq -r '.description // ""')
389
+ priority=$(echo "$finding" | jq -r '.priority // "medium"')
390
+ effort=$(echo "$finding" | jq -r '.effort // "M"')
391
+ labels=$(echo "$finding" | jq -r '.labels | join(",") // ""')
392
+ category=$(echo "$finding" | jq -r '.category // ""')
393
+
394
+ if [[ -z "$title" ]]; then
395
+ continue
396
+ fi
397
+
398
+ # Add category to labels if not present
399
+ if [[ "$labels" != *"$category"* ]]; then
400
+ labels="${category}${labels:+,$labels}"
401
+ fi
402
+
403
+ local issue_num
404
+ issue_num=$(create_issue_from_finding "$title" "$description" "$priority" "$effort" "$labels")
405
+ if [[ $? -eq 0 && -n "$issue_num" ]]; then
406
+ created=$((created + 1))
407
+ increment_counter "issues_created"
408
+ emit_event "autonomous.issue_created" "title=$title" "priority=$priority" "effort=$effort" "issue=$issue_num"
409
+ trigger_pipeline_for_finding "$issue_num" "$title"
410
+ record_finding_pending "$issue_num" "$title"
411
+ fi
412
+ done < <(jq -c '.[]' "$findings_file" 2>/dev/null)
413
+
414
+ info "Created $created of $total findings as issues (pipelines triggered)"
415
+ echo "$created"
416
+ }
417
+
418
+ # ─── Learning & Feedback ───────────────────────────────────────────────────
419
+
420
+ analyze_pipeline_result() {
421
+ local pipeline_state="${1:-}"
422
+
423
+ if [[ -z "$pipeline_state" || ! -f "$pipeline_state" ]]; then
424
+ warn "Pipeline state file not found: ${pipeline_state:-<empty>}"
425
+ return 1
426
+ fi
427
+
428
+ info "Analyzing pipeline result..."
429
+
430
+ local status=""
431
+ status=$(sed -n 's/^status: *//p' "$pipeline_state" | head -1)
432
+
433
+ local goal=""
434
+ goal=$(sed -n 's/^goal: *"*\([^"]*\)"*/\1/p' "$pipeline_state" | head -1)
435
+
436
+ # Capture lessons learned
437
+ if [[ "$status" == "complete" || "$status" == "success" ]]; then
438
+ success "Pipeline completed successfully: $goal"
439
+ increment_counter "pipelines_succeeded"
440
+ update_state "consecutive_failures" "0"
441
+ emit_event "autonomous.pipeline_success" "goal=$goal"
442
+ return 0
443
+ else
444
+ warn "Pipeline failed: $goal (status: $status)"
445
+ increment_counter "pipelines_failed"
446
+ local failures
447
+ failures=$(read_state | jq '.consecutive_failures // 0')
448
+ update_state "consecutive_failures" "$((failures + 1))"
449
+ emit_event "autonomous.pipeline_failure" "goal=$goal" "status=$status"
450
+ return 1
451
+ fi
452
+ }
453
+
454
+ # ─── Status & Metrics ───────────────────────────────────────────────────────
455
+
456
+ show_status() {
457
+ ensure_state_dir
458
+
459
+ local state
460
+ state=$(read_state)
461
+
462
+ local status cycles issues_created issues_completed succeeded failed
463
+ status=$(echo "$state" | jq -r '.status')
464
+ cycles=$(echo "$state" | jq -r '.cycles_completed')
465
+ issues_created=$(echo "$state" | jq -r '.issues_created')
466
+ issues_completed=$(echo "$state" | jq -r '.issues_completed')
467
+ succeeded=$(echo "$state" | jq -r '.pipelines_succeeded')
468
+ failed=$(echo "$state" | jq -r '.pipelines_failed')
469
+
470
+ local cycle_interval
471
+ cycle_interval=$(get_config "cycle_interval_minutes" "60")
472
+
473
+ echo ""
474
+ echo -e "${CYAN}${BOLD}╔════════════════════════════════════════════════════╗${RESET}"
475
+ echo -e "${CYAN}${BOLD}║ Autonomous Loop Status${RESET}"
476
+ echo -e "${CYAN}${BOLD}╚════════════════════════════════════════════════════╝${RESET}"
477
+ echo ""
478
+ echo -e " ${BOLD}Status${RESET} ${CYAN}${status}${RESET}"
479
+ echo -e " ${BOLD}Cycles${RESET} ${GREEN}${cycles}${RESET}"
480
+ echo -e " ${BOLD}Issues Created${RESET} ${CYAN}${issues_created}${RESET}"
481
+ echo -e " ${BOLD}Issues Completed${RESET} ${GREEN}${issues_completed}${RESET}"
482
+ echo -e " ${BOLD}Pipelines Succeeded${RESET} ${GREEN}${succeeded}${RESET}"
483
+ echo -e " ${BOLD}Pipelines Failed${RESET} ${RED}${failed}${RESET}"
484
+ echo -e " ${BOLD}Cycle Interval${RESET} ${YELLOW}${cycle_interval}${RESET} minutes"
485
+ echo ""
486
+ }
487
+
488
+ show_history() {
489
+ ensure_state_dir
490
+
491
+ if [[ ! -f "$HISTORY_FILE" ]]; then
492
+ warn "No cycle history recorded yet"
493
+ return 0
494
+ fi
495
+
496
+ echo ""
497
+ echo -e "${CYAN}${BOLD}Recent Cycles${RESET}"
498
+ echo ""
499
+
500
+ tail -20 "$HISTORY_FILE" | while read -r line; do
501
+ local ts status created
502
+ ts=$(echo "$line" | jq -r '.ts')
503
+ status=$(echo "$line" | jq -r '.status')
504
+ created=$(echo "$line" | jq -r '.created')
505
+
506
+ local status_color="$GREEN"
507
+ [[ "$status" != "success" ]] && status_color="$RED"
508
+
509
+ echo -e " ${ts} Status: ${status_color}${status}${RESET} Created: ${CYAN}${created}${RESET}"
510
+ done
511
+ }
512
+
513
+ # ─── Loop Control ──────────────────────────────────────────────────────────
514
+
515
+ # Scheduler: run cycles at interval (real scheduler instead of manual cycle only)
516
+ run_scheduler() {
517
+ ensure_state_dir
518
+ init_state
519
+ update_state "status" "running"
520
+ info "Autonomous scheduler started (cycle every $(get_config "cycle_interval_minutes" "60") minutes)"
521
+ emit_event "autonomous.scheduler_started"
522
+
523
+ while true; do
524
+ local status
525
+ status=$(read_state | jq -r '.status // "running"')
526
+ if [[ "$status" != "running" ]]; then
527
+ info "Status is ${status} — exiting scheduler"
528
+ break
529
+ fi
530
+ run_single_cycle || true
531
+ local interval_mins
532
+ interval_mins=$(get_config "cycle_interval_minutes" "60")
533
+ info "Next cycle in ${interval_mins} minutes"
534
+ sleep $((interval_mins * 60))
535
+ done
536
+ }
537
+
538
+ start_loop() {
539
+ ensure_state_dir
540
+ init_state
541
+ update_state "status" "running"
542
+
543
+ info "Starting autonomous loop..."
544
+ success "Loop is now running in background"
545
+ success "Run '${CYAN}sw autonomous status${RESET}' to check progress"
546
+
547
+ emit_event "autonomous.started"
548
+ }
549
+
550
+ stop_loop() {
551
+ ensure_state_dir
552
+ update_state "status" "stopped"
553
+ success "Autonomous loop stopped"
554
+ emit_event "autonomous.stopped"
555
+ }
556
+
557
+ pause_loop() {
558
+ ensure_state_dir
559
+ update_state "status" "paused"
560
+ update_state "paused_at" "$(now_iso)"
561
+ success "Autonomous loop paused"
562
+ emit_event "autonomous.paused"
563
+ }
564
+
565
+ resume_loop() {
566
+ ensure_state_dir
567
+ update_state "status" "running"
568
+ update_state "paused_at" "null"
569
+ success "Autonomous loop resumed"
570
+ emit_event "autonomous.resumed"
571
+ }
572
+
573
+ run_single_cycle() {
574
+ ensure_state_dir
575
+ update_state "status" "analyzing"
576
+
577
+ # Update outcomes for pending findings (which led to successful fixes)
578
+ update_finding_outcomes
579
+
580
+ info "Running single analysis cycle..."
581
+
582
+ # Step 1: Analysis
583
+ local findings
584
+ findings=$(mktemp "${TMPDIR:-/tmp}/sw-autonomous-findings.XXXXXX")
585
+ run_analysis_cycle > "$findings" 2>&1 || {
586
+ warn "Analysis failed"
587
+ rm -f "$findings"
588
+ return 1
589
+ }
590
+
591
+ # Step 2: Create issues
592
+ local created
593
+ created=$(process_findings "$findings")
594
+
595
+ # Step 3: Record cycle
596
+ local total_findings
597
+ total_findings=$(jq -s 'length' "$findings" 2>/dev/null || echo "0")
598
+ record_cycle "$total_findings" "$created" "success"
599
+
600
+ increment_counter "cycles_completed"
601
+ update_state "status" "idle"
602
+ update_state "last_cycle" "$(now_iso)"
603
+
604
+ success "Cycle complete. Created $created issues"
605
+ rm -f "$findings"
606
+ }
607
+
608
+ show_config() {
609
+ ensure_state_dir
610
+ echo ""
611
+ echo -e "${CYAN}${BOLD}Autonomous Loop Configuration${RESET}"
612
+ echo ""
613
+ cat "$CONFIG_FILE" | jq '.' 2>/dev/null || cat "$CONFIG_FILE"
614
+ echo ""
615
+ }
616
+
617
+ set_cycle_config() {
618
+ local key="$1"
619
+ local value="$2"
620
+ ensure_state_dir
621
+
622
+ case "$key" in
623
+ interval|cycle-interval)
624
+ set_config "cycle_interval_minutes" "$value"
625
+ success "Cycle interval set to ${CYAN}${value}${RESET} minutes"
626
+ ;;
627
+ max-issues)
628
+ set_config "max_issues_per_cycle" "$value"
629
+ success "Max issues per cycle set to ${CYAN}${value}${RESET}"
630
+ ;;
631
+ max-pipelines|max-concurrent)
632
+ set_config "max_concurrent_pipelines" "$value"
633
+ success "Max concurrent pipelines set to ${CYAN}${value}${RESET}"
634
+ ;;
635
+ approval|human-approval)
636
+ local bool_val="true"
637
+ [[ "$value" == "false" || "$value" == "0" || "$value" == "no" ]] && bool_val="false"
638
+ set_config "enable_human_approval" "$bool_val"
639
+ success "Human approval set to ${CYAN}${bool_val}${RESET}"
640
+ ;;
641
+ rollback)
642
+ local bool_val="true"
643
+ [[ "$value" == "false" || "$value" == "0" || "$value" == "no" ]] && bool_val="false"
644
+ set_config "rollback_on_failures" "$bool_val"
645
+ success "Rollback on failures set to ${CYAN}${bool_val}${RESET}"
646
+ ;;
647
+ *)
648
+ error "Unknown config key: $key"
649
+ return 1
650
+ ;;
651
+ esac
652
+ }
653
+
654
+ # ─── Help ──────────────────────────────────────────────────────────────────
655
+
656
+ show_help() {
657
+ cat << 'EOF'
658
+
659
+ USAGE
660
+ sw autonomous <command> [options]
661
+
662
+ COMMANDS
663
+ start Set status to running (use with external scheduler)
664
+ run Run scheduler: cycle every N minutes until stopped
665
+ stop Stop the loop gracefully
666
+ pause Pause without losing state
667
+ resume Resume from pause
668
+ cycle Run one analysis cycle manually (create issues + trigger pipelines)
669
+ status Show loop status, recent cycles, issue creation stats
670
+ config [show|set] Show or set configuration
671
+ history Show past cycles and their outcomes
672
+ help Show this help message
673
+
674
+ OPTIONS (for config)
675
+ set interval <minutes> Set cycle interval (default 60)
676
+ set max-issues <num> Set max issues per cycle (default 5)
677
+ set max-pipelines <num> Set max concurrent pipelines (default 2)
678
+ set approval <bool> Enable human approval mode (default false)
679
+ set rollback <bool> Rollback on failures (default true)
680
+
681
+ EXAMPLES
682
+ sw autonomous start # Start the loop
683
+ sw autonomous cycle # Run one cycle immediately
684
+ sw autonomous config set interval 30 # Change cycle to 30 minutes
685
+ sw autonomous status # Check progress
686
+ sw autonomous history # View past cycles
687
+
688
+ EOF
689
+ }
690
+
691
+ # ─── Main ──────────────────────────────────────────────────────────────────
692
+
693
+ main() {
694
+ local cmd="${1:-help}"
695
+ shift 2>/dev/null || true
696
+
697
+ case "$cmd" in
698
+ start)
699
+ start_loop
700
+ ;;
701
+ run)
702
+ run_scheduler
703
+ ;;
704
+ stop)
705
+ stop_loop
706
+ ;;
707
+ pause)
708
+ pause_loop
709
+ ;;
710
+ resume)
711
+ resume_loop
712
+ ;;
713
+ cycle)
714
+ run_single_cycle
715
+ ;;
716
+ status)
717
+ show_status
718
+ show_history
719
+ ;;
720
+ config)
721
+ local subcmd="${1:-show}"
722
+ shift 2>/dev/null || true
723
+ case "$subcmd" in
724
+ show)
725
+ show_config
726
+ ;;
727
+ set)
728
+ set_cycle_config "$@"
729
+ ;;
730
+ *)
731
+ error "Unknown config subcommand: $subcmd"
732
+ show_help
733
+ exit 1
734
+ ;;
735
+ esac
736
+ ;;
737
+ history)
738
+ show_history
739
+ ;;
740
+ help|--help|-h)
741
+ show_help
742
+ ;;
743
+ *)
744
+ error "Unknown command: $cmd"
745
+ show_help
746
+ exit 1
747
+ ;;
748
+ esac
749
+ }
750
+
751
+ # ─── Source Guard ───────────────────────────────────────────────────────────
752
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
753
+ main "$@"
754
+ fi