shipwright-cli 2.0.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 (113) hide show
  1. package/README.md +160 -72
  2. package/completions/_shipwright +59 -7
  3. package/completions/shipwright.bash +24 -4
  4. package/completions/shipwright.fish +80 -2
  5. package/dashboard/server.ts +208 -0
  6. package/docs/tmux-research/TMUX-ARCHITECTURE.md +567 -0
  7. package/docs/tmux-research/TMUX-AUDIT.md +925 -0
  8. package/docs/tmux-research/TMUX-BEST-PRACTICES-2025-2026.md +829 -0
  9. package/docs/tmux-research/TMUX-QUICK-REFERENCE.md +543 -0
  10. package/docs/tmux-research/TMUX-RESEARCH-INDEX.md +438 -0
  11. package/package.json +2 -2
  12. package/scripts/lib/helpers.sh +7 -0
  13. package/scripts/sw +116 -2
  14. package/scripts/sw-activity.sh +1 -1
  15. package/scripts/sw-adaptive.sh +1 -1
  16. package/scripts/sw-adversarial.sh +1 -1
  17. package/scripts/sw-architecture-enforcer.sh +1 -1
  18. package/scripts/sw-auth.sh +1 -1
  19. package/scripts/sw-autonomous.sh +128 -38
  20. package/scripts/sw-changelog.sh +1 -1
  21. package/scripts/sw-checkpoint.sh +1 -1
  22. package/scripts/sw-ci.sh +1 -1
  23. package/scripts/sw-cleanup.sh +1 -1
  24. package/scripts/sw-code-review.sh +62 -1
  25. package/scripts/sw-connect.sh +1 -1
  26. package/scripts/sw-context.sh +1 -1
  27. package/scripts/sw-cost.sh +44 -3
  28. package/scripts/sw-daemon.sh +155 -27
  29. package/scripts/sw-dashboard.sh +1 -1
  30. package/scripts/sw-db.sh +958 -118
  31. package/scripts/sw-decompose.sh +1 -1
  32. package/scripts/sw-deps.sh +1 -1
  33. package/scripts/sw-developer-simulation.sh +1 -1
  34. package/scripts/sw-discovery.sh +1 -1
  35. package/scripts/sw-docs-agent.sh +1 -1
  36. package/scripts/sw-docs.sh +1 -1
  37. package/scripts/sw-doctor.sh +49 -1
  38. package/scripts/sw-dora.sh +1 -1
  39. package/scripts/sw-durable.sh +1 -1
  40. package/scripts/sw-e2e-orchestrator.sh +1 -1
  41. package/scripts/sw-eventbus.sh +1 -1
  42. package/scripts/sw-feedback.sh +23 -15
  43. package/scripts/sw-fix.sh +1 -1
  44. package/scripts/sw-fleet-discover.sh +1 -1
  45. package/scripts/sw-fleet-viz.sh +1 -1
  46. package/scripts/sw-fleet.sh +1 -1
  47. package/scripts/sw-github-app.sh +1 -1
  48. package/scripts/sw-github-checks.sh +4 -4
  49. package/scripts/sw-github-deploy.sh +1 -1
  50. package/scripts/sw-github-graphql.sh +1 -1
  51. package/scripts/sw-guild.sh +1 -1
  52. package/scripts/sw-heartbeat.sh +1 -1
  53. package/scripts/sw-hygiene.sh +1 -1
  54. package/scripts/sw-incident.sh +45 -6
  55. package/scripts/sw-init.sh +150 -24
  56. package/scripts/sw-instrument.sh +1 -1
  57. package/scripts/sw-intelligence.sh +1 -1
  58. package/scripts/sw-jira.sh +1 -1
  59. package/scripts/sw-launchd.sh +1 -1
  60. package/scripts/sw-linear.sh +1 -1
  61. package/scripts/sw-logs.sh +1 -1
  62. package/scripts/sw-loop.sh +204 -19
  63. package/scripts/sw-memory.sh +18 -1
  64. package/scripts/sw-mission-control.sh +1 -1
  65. package/scripts/sw-model-router.sh +1 -1
  66. package/scripts/sw-otel.sh +1 -1
  67. package/scripts/sw-oversight.sh +76 -1
  68. package/scripts/sw-pipeline-composer.sh +1 -1
  69. package/scripts/sw-pipeline-vitals.sh +1 -1
  70. package/scripts/sw-pipeline.sh +261 -12
  71. package/scripts/sw-pm.sh +70 -5
  72. package/scripts/sw-pr-lifecycle.sh +1 -1
  73. package/scripts/sw-predictive.sh +8 -1
  74. package/scripts/sw-prep.sh +1 -1
  75. package/scripts/sw-ps.sh +1 -1
  76. package/scripts/sw-public-dashboard.sh +1 -1
  77. package/scripts/sw-quality.sh +1 -1
  78. package/scripts/sw-reaper.sh +1 -1
  79. package/scripts/sw-recruit.sh +1853 -178
  80. package/scripts/sw-regression.sh +1 -1
  81. package/scripts/sw-release-manager.sh +1 -1
  82. package/scripts/sw-release.sh +1 -1
  83. package/scripts/sw-remote.sh +1 -1
  84. package/scripts/sw-replay.sh +1 -1
  85. package/scripts/sw-retro.sh +1 -1
  86. package/scripts/sw-scale.sh +1 -1
  87. package/scripts/sw-security-audit.sh +1 -1
  88. package/scripts/sw-self-optimize.sh +1 -1
  89. package/scripts/sw-session.sh +1 -1
  90. package/scripts/sw-setup.sh +263 -127
  91. package/scripts/sw-standup.sh +1 -1
  92. package/scripts/sw-status.sh +44 -2
  93. package/scripts/sw-strategic.sh +189 -41
  94. package/scripts/sw-stream.sh +1 -1
  95. package/scripts/sw-swarm.sh +42 -5
  96. package/scripts/sw-team-stages.sh +1 -1
  97. package/scripts/sw-templates.sh +4 -4
  98. package/scripts/sw-testgen.sh +66 -15
  99. package/scripts/sw-tmux-pipeline.sh +1 -1
  100. package/scripts/sw-tmux-role-color.sh +58 -0
  101. package/scripts/sw-tmux-status.sh +128 -0
  102. package/scripts/sw-tmux.sh +1 -1
  103. package/scripts/sw-trace.sh +1 -1
  104. package/scripts/sw-tracker.sh +1 -1
  105. package/scripts/sw-triage.sh +61 -37
  106. package/scripts/sw-upgrade.sh +1 -1
  107. package/scripts/sw-ux.sh +1 -1
  108. package/scripts/sw-webhook.sh +1 -1
  109. package/scripts/sw-widgets.sh +1 -1
  110. package/scripts/sw-worktree.sh +1 -1
  111. package/templates/pipelines/autonomous.json +2 -2
  112. package/tmux/shipwright-overlay.conf +35 -17
  113. package/tmux/tmux.conf +23 -21
@@ -4,7 +4,7 @@
4
4
  # ║ ║
5
5
  # ║ Shows running teams, agent windows, and task progress. ║
6
6
  # ╚═══════════════════════════════════════════════════════════════════════════╝
7
- VERSION="2.0.0"
7
+ VERSION="2.1.0"
8
8
  set -euo pipefail
9
9
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
10
10
 
@@ -188,6 +188,26 @@ if [[ "$JSON_OUTPUT" == "true" ]]; then
188
188
  fi
189
189
  fi
190
190
 
191
+ # -- database --
192
+ DATABASE_JSON="null"
193
+ _db_file="${HOME}/.shipwright/shipwright.db"
194
+ if command -v sqlite3 &>/dev/null && [[ -f "$_db_file" ]]; then
195
+ _db_ver=$(sqlite3 "$_db_file" "SELECT MAX(version) FROM _schema;" 2>/dev/null || echo "0")
196
+ _db_wal=$(sqlite3 "$_db_file" "PRAGMA journal_mode;" 2>/dev/null || echo "unknown")
197
+ _db_events=$(sqlite3 "$_db_file" "SELECT COUNT(*) FROM events;" 2>/dev/null || echo "0")
198
+ _db_runs=$(sqlite3 "$_db_file" "SELECT COUNT(*) FROM pipeline_runs;" 2>/dev/null || echo "0")
199
+ _db_costs=$(sqlite3 "$_db_file" "SELECT COUNT(*) FROM cost_entries;" 2>/dev/null || echo "0")
200
+ _db_size=$(ls -l "$_db_file" 2>/dev/null | awk '{print $5}')
201
+ DATABASE_JSON=$(jq -n \
202
+ --argjson schema_version "${_db_ver:-0}" \
203
+ --arg wal_mode "$_db_wal" \
204
+ --argjson events "${_db_events:-0}" \
205
+ --argjson runs "${_db_runs:-0}" \
206
+ --argjson costs "${_db_costs:-0}" \
207
+ --argjson size_bytes "${_db_size:-0}" \
208
+ '{schema_version:$schema_version, wal_mode:$wal_mode, events:$events, runs:$runs, costs:$costs, size_bytes:$size_bytes}') || DATABASE_JSON="null"
209
+ fi
210
+
191
211
  # -- assemble and output --
192
212
  jq -n \
193
213
  --arg version "$VERSION" \
@@ -200,6 +220,7 @@ if [[ "$JSON_OUTPUT" == "true" ]]; then
200
220
  --argjson heartbeats "$HEARTBEATS_JSON" \
201
221
  --argjson remote_machines "$MACHINES_JSON" \
202
222
  --argjson connected_developers "$DEVELOPERS_JSON" \
223
+ --argjson database "$DATABASE_JSON" \
203
224
  '{
204
225
  version: $version,
205
226
  timestamp: $timestamp,
@@ -210,7 +231,8 @@ if [[ "$JSON_OUTPUT" == "true" ]]; then
210
231
  issue_tracker: $issue_tracker,
211
232
  heartbeats: $heartbeats,
212
233
  remote_machines: $remote_machines,
213
- connected_developers: $connected_developers
234
+ connected_developers: $connected_developers,
235
+ database: $database
214
236
  }'
215
237
  exit 0
216
238
  fi
@@ -708,6 +730,26 @@ if [[ -f "$MACHINES_FILE" ]]; then
708
730
  fi
709
731
  fi
710
732
 
733
+ # ─── Database ────────────────────────────────────────────────────────────
734
+
735
+ _DB_FILE="${HOME}/.shipwright/shipwright.db"
736
+ if command -v sqlite3 &>/dev/null && [[ -f "$_DB_FILE" ]]; then
737
+ echo ""
738
+ echo -e "${PURPLE}${BOLD} DATABASE${RESET} ${DIM}~/.shipwright/shipwright.db${RESET}"
739
+ echo -e "${DIM} ──────────────────────────────────────────${RESET}"
740
+
741
+ _db_wal=$(sqlite3 "$_DB_FILE" "PRAGMA journal_mode;" 2>/dev/null || echo "?")
742
+ _db_ver=$(sqlite3 "$_DB_FILE" "SELECT MAX(version) FROM _schema;" 2>/dev/null || echo "?")
743
+ _db_size_bytes=$(ls -l "$_DB_FILE" 2>/dev/null | awk '{print $5}')
744
+ _db_size_mb=$(awk -v s="${_db_size_bytes:-0}" 'BEGIN { printf "%.1f", s / 1048576 }')
745
+ _db_events=$(sqlite3 "$_DB_FILE" "SELECT COUNT(*) FROM events;" 2>/dev/null || echo "0")
746
+ _db_runs=$(sqlite3 "$_DB_FILE" "SELECT COUNT(*) FROM pipeline_runs;" 2>/dev/null || echo "0")
747
+ _db_costs=$(sqlite3 "$_DB_FILE" "SELECT COUNT(*) FROM cost_entries;" 2>/dev/null || echo "0")
748
+
749
+ echo -e " ${GREEN}●${RESET} ${BOLD}SQLite${RESET} ${DIM}v${_db_ver} WAL=${_db_wal} ${_db_size_mb}MB${RESET}"
750
+ echo -e " ${DIM}events:${_db_events} runs:${_db_runs} costs:${_db_costs}${RESET}"
751
+ fi
752
+
711
753
  # ─── Connected Developers ─────────────────────────────────────────────────
712
754
 
713
755
  # Check if curl and jq are available
@@ -7,7 +7,7 @@
7
7
  # When sourced, do NOT add set -euo pipefail — the parent handles that.
8
8
  # When run directly, main() sets up the error handling.
9
9
 
10
- VERSION="2.0.0"
10
+ VERSION="2.1.0"
11
11
 
12
12
  # ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
13
13
  CYAN='\033[38;2;0;212;255m'
@@ -57,11 +57,92 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
57
57
  fi
58
58
 
59
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
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
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
+ }
65
146
 
66
147
  # ─── Cooldown Check ──────────────────────────────────────────────────────────
67
148
  strategic_check_cooldown() {
@@ -257,6 +338,14 @@ strategic_build_prompt() {
257
338
  open_issues="(GitHub access disabled)"
258
339
  fi
259
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
+
260
349
  # Compose the prompt
261
350
  cat <<PROMPT_EOF
262
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.
@@ -278,6 +367,9 @@ ${strategy_content}
278
367
  ## Open Issues (already in progress — do NOT duplicate these)
279
368
  ${open_issues}
280
369
 
370
+ ## Recently Completed (already built — do NOT recreate these)
371
+ ${recent_closed}
372
+
281
373
  ## Your Task
282
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.
283
375
 
@@ -292,12 +384,14 @@ ACCEPTANCE: <bullet list of acceptance criteria, one per line starting with "- "
292
384
  ---
293
385
 
294
386
  Rules:
295
- - Do NOT duplicate any open issue listed above
387
+ - Do NOT duplicate any open issue OR any recently completed issue
296
388
  - Prioritize based on STRATEGY.md priorities (P0 > P1 > P2 > ...)
297
389
  - Focus on concrete, actionable improvements (not vague goals)
298
390
  - Each issue should be completable by one autonomous pipeline run
299
- - Prefer reliability and DX improvements over new features
300
- - Maximum 3 issues
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
301
395
  PROMPT_EOF
302
396
  }
303
397
 
@@ -320,7 +414,7 @@ strategic_call_api() {
320
414
  printf '%s' "$prompt" > "$tmp_prompt"
321
415
 
322
416
  local response_text
323
- response_text=$(claude -p "$(cat "$tmp_prompt")" --max-turns 1 --model "$STRATEGIC_MODEL" 2>/dev/null || echo "")
417
+ response_text=$(cat "$tmp_prompt" | claude -p --max-turns 1 --model "$STRATEGIC_MODEL" 2>/dev/null || echo "")
324
418
  rm -f "$tmp_prompt"
325
419
 
326
420
  if [[ -z "$response_text" ]]; then
@@ -328,6 +422,14 @@ strategic_call_api() {
328
422
  return 1
329
423
  fi
330
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
+
331
433
  printf '%s' "$response_text"
332
434
  }
333
435
 
@@ -360,7 +462,7 @@ strategic_parse_and_create() {
360
462
  fi
361
463
 
362
464
  if [[ "$created" -ge "$STRATEGIC_MAX_ISSUES" ]]; then
363
- info "Reached max issues per cycle (${STRATEGIC_MAX_ISSUES})"
465
+ info "Reached max issues per cycle (${STRATEGIC_MAX_ISSUES})" >&2
364
466
  break
365
467
  fi
366
468
  fi
@@ -372,34 +474,38 @@ strategic_parse_and_create() {
372
474
  continue
373
475
  fi
374
476
 
375
- # Parse fields
376
- if [[ "$line" == ISSUE_TITLE:* ]]; then
377
- current_title="${line#ISSUE_TITLE: }"
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: }"
378
484
  current_title="${current_title#ISSUE_TITLE:}"
379
485
  current_title=$(echo "$current_title" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
380
486
  in_acceptance=false
381
- elif [[ "$line" == PRIORITY:* ]]; then
382
- current_priority="${line#PRIORITY: }"
487
+ elif [[ "$clean_line" == PRIORITY:* ]]; then
488
+ current_priority="${clean_line#PRIORITY: }"
383
489
  current_priority="${current_priority#PRIORITY:}"
384
490
  current_priority=$(echo "$current_priority" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
385
491
  in_acceptance=false
386
- elif [[ "$line" == COMPLEXITY:* ]]; then
387
- current_complexity="${line#COMPLEXITY: }"
492
+ elif [[ "$clean_line" == COMPLEXITY:* ]]; then
493
+ current_complexity="${clean_line#COMPLEXITY: }"
388
494
  current_complexity="${current_complexity#COMPLEXITY:}"
389
495
  current_complexity=$(echo "$current_complexity" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
390
496
  in_acceptance=false
391
- elif [[ "$line" == STRATEGY_AREA:* ]]; then
392
- current_strategy="${line#STRATEGY_AREA: }"
497
+ elif [[ "$clean_line" == STRATEGY_AREA:* ]]; then
498
+ current_strategy="${clean_line#STRATEGY_AREA: }"
393
499
  current_strategy="${current_strategy#STRATEGY_AREA:}"
394
500
  current_strategy=$(echo "$current_strategy" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
395
501
  in_acceptance=false
396
- elif [[ "$line" == DESCRIPTION:* ]]; then
397
- current_description="${line#DESCRIPTION: }"
502
+ elif [[ "$clean_line" == DESCRIPTION:* ]]; then
503
+ current_description="${clean_line#DESCRIPTION: }"
398
504
  current_description="${current_description#DESCRIPTION:}"
399
505
  current_description=$(echo "$current_description" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
400
506
  in_acceptance=false
401
- elif [[ "$line" == ACCEPTANCE:* ]]; then
402
- current_acceptance="${line#ACCEPTANCE: }"
507
+ elif [[ "$clean_line" == ACCEPTANCE:* ]]; then
508
+ current_acceptance="${clean_line#ACCEPTANCE: }"
403
509
  current_acceptance="${current_acceptance#ACCEPTANCE:}"
404
510
  current_acceptance=$(echo "$current_acceptance" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
405
511
  in_acceptance=true
@@ -442,9 +548,15 @@ strategic_create_issue() {
442
548
  return 1
443
549
  fi
444
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
+
445
557
  # Dry-run mode
446
558
  if [[ "${NO_GITHUB:-false}" == "true" ]]; then
447
- info " [dry-run] Would create: ${title}"
559
+ info " [dry-run] Would create: ${title}" >&2
448
560
  return 0
449
561
  fi
450
562
 
@@ -452,7 +564,7 @@ strategic_create_issue() {
452
564
  local existing
453
565
  existing=$(gh issue list --state open --search "$title" --json number,title --jq ".[].title" 2>/dev/null || echo "")
454
566
  if echo "$existing" | grep -qF "$title" 2>/dev/null; then
455
- info " Skipping duplicate: ${title}"
567
+ info " Skipping duplicate: ${title}" >&2
456
568
  return 1
457
569
  fi
458
570
 
@@ -479,29 +591,53 @@ $(echo -e "$acceptance")
479
591
  BODY_EOF
480
592
  )
481
593
 
482
- local labels="auto-patrol,ready-to-build,strategic"
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
483
602
 
484
- gh issue create \
603
+ local issue_url
604
+ issue_url=$(gh issue create \
485
605
  --title "$title" \
486
606
  --body "$body" \
487
- --label "$labels" 2>/dev/null || {
488
- warn " Failed to create issue: ${title}"
607
+ --label "$labels" 2>/dev/null) || {
608
+ warn " Failed to create issue: ${title}" >&2
489
609
  return 1
490
610
  }
491
611
 
492
612
  emit_event "strategic.issue_created" "title=$title" "priority=$priority" "complexity=$complexity"
493
- success " Created issue: ${title}"
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
494
618
  return 0
495
619
  }
496
620
 
497
621
  # ─── Main Strategic Run ──────────────────────────────────────────────────────
498
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
+
499
631
  echo -e "\n${PURPLE}${BOLD}━━━ Strategic Intelligence Agent ━━━${RESET}"
500
632
  echo -e "${DIM} Analyzing codebase, strategy, and metrics...${RESET}\n"
501
633
 
502
- # Check cooldown
503
- if ! strategic_check_cooldown; then
504
- return 0
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)"
505
641
  fi
506
642
 
507
643
  # Check auth token
@@ -510,6 +646,10 @@ strategic_run() {
510
646
  return 1
511
647
  fi
512
648
 
649
+ # Load existing issue titles for semantic dedup
650
+ info "Loading issue title cache for dedup..."
651
+ strategic_load_title_cache
652
+
513
653
  # Build prompt with all context
514
654
  info "Gathering context..."
515
655
  local prompt
@@ -539,7 +679,13 @@ strategic_run() {
539
679
  echo -e " Issues skipped: ${skipped} (duplicates)"
540
680
  echo ""
541
681
 
542
- emit_event "strategic.cycle_complete" "issues_created=$created" "issues_skipped=$skipped"
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
543
689
  }
544
690
 
545
691
  # ─── Status Command ──────────────────────────────────────────────────────────
@@ -605,14 +751,16 @@ strategic_show_help() {
605
751
  echo -e "${BOLD}Usage:${RESET}"
606
752
  echo -e " sw-strategic.sh <command>\n"
607
753
  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"
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"
611
757
  echo -e "${BOLD}Environment:${RESET}"
612
758
  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"
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"
616
764
  }
617
765
 
618
766
  # ─── Daemon Integration (sourced mode) ────────────────────────────────────────
@@ -643,7 +791,7 @@ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
643
791
  shift 2>/dev/null || true
644
792
 
645
793
  case "$cmd" in
646
- run) strategic_run ;;
794
+ run) strategic_run "$@" ;;
647
795
  status) strategic_status ;;
648
796
  help) strategic_show_help ;;
649
797
  *)
@@ -5,7 +5,7 @@
5
5
  # ║ Streams tmux pane output in real-time to the dashboard or CLI. ║
6
6
  # ║ Captures output periodically, tags by agent/team, supports replay. ║
7
7
  # ╚═══════════════════════════════════════════════════════════════════════════╝
8
- VERSION="2.0.0"
8
+ VERSION="2.1.0"
9
9
  set -euo pipefail
10
10
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
11
11
 
@@ -6,8 +6,9 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="2.0.0"
9
+ VERSION="2.1.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
11
12
 
12
13
  # ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
13
14
  CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
@@ -113,13 +114,33 @@ record_metric() {
113
114
 
114
115
  # ─── Spawn a new agent ────────────────────────────────────────────────────
115
116
  cmd_spawn() {
116
- local agent_type="${1:-standard}"
117
+ local agent_type="${1:-}"
118
+ shift 2>/dev/null || true
119
+ local task_desc="${1:-}"
117
120
  shift 2>/dev/null || true
118
121
 
119
122
  ensure_dirs
120
123
  init_registry
121
124
  init_config
122
125
 
126
+ # Recruit-powered type selection when task description given but no explicit type
127
+ if [[ -z "$agent_type" || "$agent_type" == "--task" ]] && [[ -n "$task_desc" ]]; then
128
+ if [[ -x "${SCRIPT_DIR:-}/sw-recruit.sh" ]]; then
129
+ local _recruit_match
130
+ _recruit_match=$(bash "$SCRIPT_DIR/sw-recruit.sh" match --json "$task_desc" 2>/dev/null) || true
131
+ if [[ -n "$_recruit_match" ]]; then
132
+ local _role
133
+ _role=$(echo "$_recruit_match" | jq -r '.primary_role // ""' 2>/dev/null) || true
134
+ case "$_role" in
135
+ architect|security-auditor|incident-responder) agent_type="powerful" ;;
136
+ docs-writer) agent_type="fast" ;;
137
+ *) agent_type="standard" ;;
138
+ esac
139
+ fi
140
+ fi
141
+ fi
142
+ [[ -z "$agent_type" ]] && agent_type="standard"
143
+
123
144
  local agent_id
124
145
  agent_id=$(gen_agent_id)
125
146
 
@@ -165,6 +186,16 @@ cmd_spawn() {
165
186
  mv "$tmp_file" "$REGISTRY_FILE"
166
187
  record_metric "$agent_id" "spawn" "1" "$agent_type"
167
188
 
189
+ # Create real tmux session for the agent (so scale/loop can send commands)
190
+ if command -v tmux &>/dev/null; then
191
+ local session_name="swarm-${agent_id}"
192
+ if ! tmux has-session -t "$session_name" 2>/dev/null; then
193
+ tmux new-session -d -s "$session_name" -c "$REPO_DIR" \
194
+ "echo \"Agent $agent_id ready (type: $agent_type)\"; while true; do sleep 3600; done" 2>/dev/null && \
195
+ info "Tmux session created: $session_name" || warn "Tmux session creation failed (agent still in registry)"
196
+ fi
197
+ fi
198
+
168
199
  success "Spawned agent: ${CYAN}${agent_id}${RESET} (type: ${agent_type})"
169
200
  echo ""
170
201
  echo -e " Agent ID: ${CYAN}${agent_id}${RESET}"
@@ -206,18 +237,24 @@ cmd_retire() {
206
237
  echo ""
207
238
  fi
208
239
 
209
- # Mark as retiring
240
+ # Kill real tmux session if present
241
+ local session_name="swarm-${agent_id}"
242
+ if command -v tmux &>/dev/null && tmux has-session -t "$session_name" 2>/dev/null; then
243
+ tmux kill-session -t "$session_name" 2>/dev/null && info "Tmux session killed: $session_name" || warn "Tmux kill failed for $session_name"
244
+ fi
245
+
246
+ # Mark as retiring / remove from registry
210
247
  local tmp_file
211
248
  tmp_file=$(mktemp)
212
249
 
213
250
  jq --arg aid "$agent_id" \
214
- '.agents |= map(if .id == $aid then .status = "retiring" else . end) | .last_updated = "'$(now_iso)'"' \
251
+ '.agents |= map(select(.id != $aid)) | .active_count = ([.agents[] | select(.status == "active")] | length) | .last_updated = "'$(now_iso)'"' \
215
252
  "$REGISTRY_FILE" > "$tmp_file"
216
253
 
217
254
  mv "$tmp_file" "$REGISTRY_FILE"
218
255
  record_metric "$agent_id" "retire" "1" "graceful_shutdown"
219
256
 
220
- success "Retiring agent: ${CYAN}${agent_id}${RESET}"
257
+ success "Retired agent: ${CYAN}${agent_id}${RESET}"
221
258
  echo ""
222
259
  }
223
260
 
@@ -6,7 +6,7 @@
6
6
  set -euo pipefail
7
7
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
8
 
9
- VERSION="2.0.0"
9
+ VERSION="2.1.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -5,7 +5,7 @@
5
5
  # ║ Templates define reusable agent team configurations (roles, layout, ║
6
6
  # ║ focus areas) that shipwright session --template can use to scaffold teams. ║
7
7
  # ╚═══════════════════════════════════════════════════════════════════════════╝
8
- VERSION="2.0.0"
8
+ VERSION="2.1.0"
9
9
  set -euo pipefail
10
10
  trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
11
11
 
@@ -20,6 +20,8 @@ DIM='\033[2m'
20
20
  BOLD='\033[1m'
21
21
  RESET='\033[0m'
22
22
 
23
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24
+
23
25
  # ─── Cross-platform compatibility ──────────────────────────────────────────
24
26
  # shellcheck source=lib/compat.sh
25
27
  [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
@@ -29,8 +31,6 @@ warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
29
31
  error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
30
32
 
31
33
  # ─── Template Discovery ─────────────────────────────────────────────────────
32
-
33
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
34
34
  REPO_TEMPLATES_DIR="$(cd "$SCRIPT_DIR/../tmux/templates" 2>/dev/null && pwd)" || REPO_TEMPLATES_DIR=""
35
35
  USER_TEMPLATES_DIR="${HOME}/.shipwright/templates"
36
36
 
@@ -77,7 +77,7 @@ json_agent_count() {
77
77
  if command -v jq &>/dev/null; then
78
78
  jq -r '.agents // [] | length' "$file" 2>/dev/null
79
79
  else
80
- grep -c '"name"' "$file" 2>/dev/null | head -1
80
+ grep -c '"name"' "$file" 2>/dev/null || echo "0"
81
81
  fi
82
82
  }
83
83