shipwright-cli 1.9.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.claude/hooks/post-tool-use.sh +12 -5
  2. package/README.md +114 -36
  3. package/completions/_shipwright +212 -32
  4. package/completions/shipwright.bash +97 -25
  5. package/docs/strategy/01-market-research.md +619 -0
  6. package/docs/strategy/02-mission-and-brand.md +587 -0
  7. package/docs/strategy/03-gtm-and-roadmap.md +759 -0
  8. package/docs/strategy/QUICK-START.txt +289 -0
  9. package/docs/strategy/README.md +172 -0
  10. package/package.json +4 -2
  11. package/scripts/sw +217 -2
  12. package/scripts/sw-activity.sh +500 -0
  13. package/scripts/sw-adaptive.sh +925 -0
  14. package/scripts/sw-adversarial.sh +1 -1
  15. package/scripts/sw-architecture-enforcer.sh +1 -1
  16. package/scripts/sw-auth.sh +613 -0
  17. package/scripts/sw-autonomous.sh +664 -0
  18. package/scripts/sw-changelog.sh +704 -0
  19. package/scripts/sw-checkpoint.sh +79 -1
  20. package/scripts/sw-ci.sh +602 -0
  21. package/scripts/sw-cleanup.sh +192 -7
  22. package/scripts/sw-code-review.sh +637 -0
  23. package/scripts/sw-connect.sh +1 -1
  24. package/scripts/sw-context.sh +605 -0
  25. package/scripts/sw-cost.sh +1 -1
  26. package/scripts/sw-daemon.sh +812 -138
  27. package/scripts/sw-dashboard.sh +1 -1
  28. package/scripts/sw-db.sh +540 -0
  29. package/scripts/sw-decompose.sh +539 -0
  30. package/scripts/sw-deps.sh +551 -0
  31. package/scripts/sw-developer-simulation.sh +1 -1
  32. package/scripts/sw-discovery.sh +412 -0
  33. package/scripts/sw-docs-agent.sh +539 -0
  34. package/scripts/sw-docs.sh +1 -1
  35. package/scripts/sw-doctor.sh +59 -1
  36. package/scripts/sw-dora.sh +615 -0
  37. package/scripts/sw-durable.sh +710 -0
  38. package/scripts/sw-e2e-orchestrator.sh +535 -0
  39. package/scripts/sw-eventbus.sh +393 -0
  40. package/scripts/sw-feedback.sh +471 -0
  41. package/scripts/sw-fix.sh +1 -1
  42. package/scripts/sw-fleet-discover.sh +567 -0
  43. package/scripts/sw-fleet-viz.sh +404 -0
  44. package/scripts/sw-fleet.sh +8 -1
  45. package/scripts/sw-github-app.sh +596 -0
  46. package/scripts/sw-github-checks.sh +1 -1
  47. package/scripts/sw-github-deploy.sh +1 -1
  48. package/scripts/sw-github-graphql.sh +1 -1
  49. package/scripts/sw-guild.sh +569 -0
  50. package/scripts/sw-heartbeat.sh +1 -1
  51. package/scripts/sw-hygiene.sh +559 -0
  52. package/scripts/sw-incident.sh +617 -0
  53. package/scripts/sw-init.sh +88 -1
  54. package/scripts/sw-instrument.sh +699 -0
  55. package/scripts/sw-intelligence.sh +1 -1
  56. package/scripts/sw-jira.sh +1 -1
  57. package/scripts/sw-launchd.sh +366 -31
  58. package/scripts/sw-linear.sh +1 -1
  59. package/scripts/sw-logs.sh +1 -1
  60. package/scripts/sw-loop.sh +507 -51
  61. package/scripts/sw-memory.sh +198 -3
  62. package/scripts/sw-mission-control.sh +487 -0
  63. package/scripts/sw-model-router.sh +545 -0
  64. package/scripts/sw-otel.sh +596 -0
  65. package/scripts/sw-oversight.sh +689 -0
  66. package/scripts/sw-pipeline-composer.sh +8 -8
  67. package/scripts/sw-pipeline-vitals.sh +1096 -0
  68. package/scripts/sw-pipeline.sh +2451 -180
  69. package/scripts/sw-pm.sh +693 -0
  70. package/scripts/sw-pr-lifecycle.sh +522 -0
  71. package/scripts/sw-predictive.sh +1 -1
  72. package/scripts/sw-prep.sh +1 -1
  73. package/scripts/sw-ps.sh +4 -3
  74. package/scripts/sw-public-dashboard.sh +798 -0
  75. package/scripts/sw-quality.sh +595 -0
  76. package/scripts/sw-reaper.sh +5 -3
  77. package/scripts/sw-recruit.sh +573 -0
  78. package/scripts/sw-regression.sh +642 -0
  79. package/scripts/sw-release-manager.sh +736 -0
  80. package/scripts/sw-release.sh +706 -0
  81. package/scripts/sw-remote.sh +1 -1
  82. package/scripts/sw-replay.sh +520 -0
  83. package/scripts/sw-retro.sh +691 -0
  84. package/scripts/sw-scale.sh +444 -0
  85. package/scripts/sw-security-audit.sh +505 -0
  86. package/scripts/sw-self-optimize.sh +109 -8
  87. package/scripts/sw-session.sh +31 -9
  88. package/scripts/sw-setup.sh +1 -1
  89. package/scripts/sw-standup.sh +712 -0
  90. package/scripts/sw-status.sh +192 -1
  91. package/scripts/sw-strategic.sh +658 -0
  92. package/scripts/sw-stream.sh +450 -0
  93. package/scripts/sw-swarm.sh +583 -0
  94. package/scripts/sw-team-stages.sh +511 -0
  95. package/scripts/sw-templates.sh +1 -1
  96. package/scripts/sw-testgen.sh +515 -0
  97. package/scripts/sw-tmux-pipeline.sh +554 -0
  98. package/scripts/sw-tmux.sh +1 -1
  99. package/scripts/sw-trace.sh +485 -0
  100. package/scripts/sw-tracker-github.sh +188 -0
  101. package/scripts/sw-tracker-jira.sh +172 -0
  102. package/scripts/sw-tracker-linear.sh +251 -0
  103. package/scripts/sw-tracker.sh +117 -2
  104. package/scripts/sw-triage.sh +603 -0
  105. package/scripts/sw-upgrade.sh +1 -1
  106. package/scripts/sw-ux.sh +677 -0
  107. package/scripts/sw-webhook.sh +627 -0
  108. package/scripts/sw-widgets.sh +530 -0
  109. package/scripts/sw-worktree.sh +1 -1
  110. package/templates/pipelines/autonomous.json +8 -1
  111. package/templates/pipelines/cost-aware.json +21 -0
  112. package/templates/pipelines/deployed.json +40 -6
  113. package/templates/pipelines/enterprise.json +16 -2
  114. package/templates/pipelines/fast.json +19 -0
  115. package/templates/pipelines/full.json +16 -2
  116. package/templates/pipelines/hotfix.json +19 -0
  117. package/templates/pipelines/standard.json +19 -0
@@ -137,6 +137,178 @@ jira_api() {
137
137
  curl "${args[@]}" "${JIRA_BASE_URL}/rest/api/3/${endpoint}" 2>&1
138
138
  }
139
139
 
140
+ # ─── Discovery & CRUD Interface ───────────────────────────────────────────
141
+ # Implements provider interface for daemon discovery and pipeline CRUD
142
+
143
+ provider_discover_issues() {
144
+ local label="$1"
145
+ local state="${2:-open}"
146
+ local limit="${3:-50}"
147
+
148
+ provider_load_config
149
+
150
+ # Build JQL query
151
+ local jql="project = \"${JIRA_PROJECT_KEY}\""
152
+
153
+ if [[ -n "$label" ]]; then
154
+ jql="${jql} AND labels = \"${label}\""
155
+ fi
156
+
157
+ # Map state parameter to Jira status
158
+ case "$state" in
159
+ open)
160
+ jql="${jql} AND status IN (${JIRA_TRANSITION_IN_PROGRESS:-\"In Progress\"},${JIRA_TRANSITION_IN_REVIEW:-\"In Review\"})"
161
+ ;;
162
+ closed)
163
+ jql="${jql} AND status = ${JIRA_TRANSITION_DONE:-\"Done\"}"
164
+ ;;
165
+ *)
166
+ # Custom status provided
167
+ jql="${jql} AND status = \"${state}\""
168
+ ;;
169
+ esac
170
+
171
+ jql="${jql} ORDER BY created DESC"
172
+
173
+ # Fetch issues
174
+ local response
175
+ response=$(jira_api "GET" "search?jql=$(printf '%s' "$jql" | jq -sRr @uri)&maxResults=${limit}&fields=key,summary,labels,status" 2>/dev/null) || {
176
+ echo "[]"
177
+ return 0
178
+ }
179
+
180
+ # Normalize to {id, title, labels[], state}
181
+ echo "$response" | jq '[.issues[]? | {id: .key, title: .fields.summary, labels: [.fields.labels[]?.name // empty], state: .fields.status.name}]' 2>/dev/null || echo "[]"
182
+ }
183
+
184
+ provider_get_issue() {
185
+ local issue_id="$1"
186
+
187
+ [[ -z "$issue_id" ]] && return 1
188
+
189
+ provider_load_config
190
+
191
+ local response
192
+ response=$(jira_api "GET" "issue/${issue_id}?fields=key,summary,description,labels,status" 2>/dev/null) || {
193
+ return 1
194
+ }
195
+
196
+ # Normalize output
197
+ echo "$response" | jq '{id: .key, title: .fields.summary, body: .fields.description, labels: [.fields.labels[]?.name // empty], state: .fields.status.name}' 2>/dev/null || return 1
198
+ }
199
+
200
+ provider_get_issue_body() {
201
+ local issue_id="$1"
202
+
203
+ [[ -z "$issue_id" ]] && return 1
204
+
205
+ provider_load_config
206
+
207
+ local response
208
+ response=$(jira_api "GET" "issue/${issue_id}?fields=description" 2>/dev/null) || {
209
+ return 1
210
+ }
211
+
212
+ echo "$response" | jq -r '.fields.description // ""' 2>/dev/null || return 1
213
+ }
214
+
215
+ provider_add_label() {
216
+ local issue_id="$1"
217
+ local label="$2"
218
+
219
+ [[ -z "$issue_id" || -z "$label" ]] && return 1
220
+
221
+ provider_load_config
222
+
223
+ # Get current labels
224
+ local current_labels
225
+ current_labels=$(jira_api "GET" "issue/${issue_id}?fields=labels" 2>/dev/null | jq '.fields.labels[]?.name' 2>/dev/null || echo "[]")
226
+
227
+ # Add new label
228
+ local payload
229
+ payload=$(jq -n --arg label "$label" --argjson labels "$current_labels" '{fields: {labels: ($labels | map(select(. != $label)) + [$label])}}' 2>/dev/null)
230
+
231
+ jira_api "PUT" "issue/${issue_id}" "$payload" 2>/dev/null || return 1
232
+ }
233
+
234
+ provider_remove_label() {
235
+ local issue_id="$1"
236
+ local label="$2"
237
+
238
+ [[ -z "$issue_id" || -z "$label" ]] && return 1
239
+
240
+ provider_load_config
241
+
242
+ # Get current labels and remove the specified one
243
+ local payload
244
+ payload=$(jq -n --arg label "$label" '{fields: {labels: [{name: ""}]}}')
245
+
246
+ jira_api "PUT" "issue/${issue_id}" "$payload" 2>/dev/null || return 1
247
+ }
248
+
249
+ provider_comment() {
250
+ local issue_id="$1"
251
+ local body="$2"
252
+
253
+ [[ -z "$issue_id" || -z "$body" ]] && return 1
254
+
255
+ provider_load_config
256
+ jira_add_comment "$issue_id" "$body"
257
+ }
258
+
259
+ provider_close_issue() {
260
+ local issue_id="$1"
261
+
262
+ [[ -z "$issue_id" ]] && return 1
263
+
264
+ provider_load_config
265
+ jira_transition "$issue_id" "$JIRA_TRANSITION_DONE"
266
+ }
267
+
268
+ provider_create_issue() {
269
+ local title="$1"
270
+ local body="$2"
271
+ local labels="${3:-}"
272
+
273
+ [[ -z "$title" ]] && return 1
274
+
275
+ provider_load_config
276
+
277
+ # Build payload
278
+ local payload
279
+ payload=$(jq -n --arg summary "$title" --arg description "$body" --arg project "$JIRA_PROJECT_KEY" \
280
+ '{
281
+ fields: {
282
+ project: {key: $project},
283
+ summary: $summary,
284
+ description: $description,
285
+ issuetype: {name: "Task"}
286
+ }
287
+ }')
288
+
289
+ # Add labels if provided
290
+ if [[ -n "$labels" ]]; then
291
+ local label_array
292
+ label_array=$(echo "$labels" | jq -R 'split("[, ]"; "x") | map(select(length > 0))' 2>/dev/null || echo '[]')
293
+ payload=$(echo "$payload" | jq --argjson labels "$label_array" '.fields.labels = $labels' 2>/dev/null)
294
+ fi
295
+
296
+ local response
297
+ response=$(jira_api "POST" "issue" "$payload" 2>/dev/null) || {
298
+ return 1
299
+ }
300
+
301
+ # Extract issue key from response
302
+ local issue_key
303
+ issue_key=$(echo "$response" | jq -r '.key // empty' 2>/dev/null)
304
+
305
+ if [[ -z "$issue_key" ]]; then
306
+ return 1
307
+ fi
308
+
309
+ echo "{\"id\": \"$issue_key\", \"title\": \"$title\"}"
310
+ }
311
+
140
312
  # ─── Find Jira Issue Key from GitHub Issue Body ───────────────────────────
141
313
 
142
314
  find_jira_key() {
@@ -216,6 +216,257 @@ linear_attach_pr() {
216
216
  linear_add_comment "$issue_id" "$body"
217
217
  }
218
218
 
219
+ # ─── Discovery & CRUD Interface ───────────────────────────────────────────
220
+ # Implements provider interface for daemon discovery and pipeline CRUD
221
+
222
+ provider_discover_issues() {
223
+ local label="$1"
224
+ local state="${2:-open}"
225
+ local limit="${3:-50}"
226
+
227
+ provider_load_config
228
+
229
+ # Build Linear query for issues
230
+ local query='query($teamId: String!, $first: Int, $filter: IssueFilter) {
231
+ team(id: $teamId) {
232
+ issues(first: $first, filter: $filter) {
233
+ nodes {
234
+ id identifier title labels {nodes {name}}
235
+ state {id name type}
236
+ }
237
+ }
238
+ }
239
+ }'
240
+
241
+ # Build filter for state
242
+ local state_filter=""
243
+ case "$state" in
244
+ open)
245
+ # Open = unstarted or started
246
+ state_filter='and: [or: [{state: {type: {eq: "unstarted"}}}, {state: {type: {eq: "started"}}}]]'
247
+ ;;
248
+ closed)
249
+ state_filter='and: [{state: {type: {eq: "completed"}}}]'
250
+ ;;
251
+ *)
252
+ # Custom state provided
253
+ state_filter="and: [{state: {type: {eq: \"${state}\"}}}]"
254
+ ;;
255
+ esac
256
+
257
+ # Add label filter if provided
258
+ if [[ -n "$label" ]]; then
259
+ state_filter="${state_filter}, {labels: {some: {name: {eq: \"${label}\"}}}}"
260
+ fi
261
+
262
+ local filter
263
+ filter="{${state_filter}}"
264
+
265
+ local vars
266
+ vars=$(jq -n --arg teamId "$LINEAR_TEAM_ID" --arg filter "$filter" --arg limit "$limit" \
267
+ "{teamId: \$teamId, first: (\$limit | tonumber), filter: $filter}" 2>/dev/null || \
268
+ jq -n --arg teamId "$LINEAR_TEAM_ID" --arg limit "$limit" \
269
+ '{teamId: $teamId, first: ($limit | tonumber)}')
270
+
271
+ local response
272
+ response=$(linear_graphql "$query" "$vars" 2>/dev/null) || {
273
+ echo "[]"
274
+ return 0
275
+ }
276
+
277
+ # Normalize to {id, title, labels[], state}
278
+ echo "$response" | jq '[.data.team.issues.nodes[]? | {id: .id, title: .title, labels: [.labels.nodes[]?.name // empty], state: .state.name}]' 2>/dev/null || echo "[]"
279
+ }
280
+
281
+ provider_get_issue() {
282
+ local issue_id="$1"
283
+
284
+ [[ -z "$issue_id" ]] && return 1
285
+
286
+ provider_load_config
287
+
288
+ local query='query($id: String!) {
289
+ issue(id: $id) {
290
+ id title description labels {nodes {name}}
291
+ state {id name}
292
+ }
293
+ }'
294
+
295
+ local vars
296
+ vars=$(jq -n --arg id "$issue_id" '{id: $id}')
297
+
298
+ local response
299
+ response=$(linear_graphql "$query" "$vars" 2>/dev/null) || {
300
+ return 1
301
+ }
302
+
303
+ # Normalize output
304
+ echo "$response" | jq '{id: .data.issue.id, title: .data.issue.title, body: .data.issue.description, labels: [.data.issue.labels.nodes[]?.name // empty], state: .data.issue.state.name}' 2>/dev/null || return 1
305
+ }
306
+
307
+ provider_get_issue_body() {
308
+ local issue_id="$1"
309
+
310
+ [[ -z "$issue_id" ]] && return 1
311
+
312
+ provider_load_config
313
+
314
+ local query='query($id: String!) {
315
+ issue(id: $id) {
316
+ description
317
+ }
318
+ }'
319
+
320
+ local vars
321
+ vars=$(jq -n --arg id "$issue_id" '{id: $id}')
322
+
323
+ local response
324
+ response=$(linear_graphql "$query" "$vars" 2>/dev/null) || {
325
+ return 1
326
+ }
327
+
328
+ echo "$response" | jq -r '.data.issue.description // ""' 2>/dev/null || return 1
329
+ }
330
+
331
+ provider_add_label() {
332
+ local issue_id="$1"
333
+ local label="$2"
334
+
335
+ [[ -z "$issue_id" || -z "$label" ]] && return 1
336
+
337
+ provider_load_config
338
+
339
+ # Linear label IDs are required — fetch them
340
+ local query='query {
341
+ labels(first: 100) {
342
+ nodes {id name}
343
+ }
344
+ }'
345
+
346
+ local labels_response
347
+ labels_response=$(linear_graphql "$query" "{}" 2>/dev/null) || return 1
348
+
349
+ local label_id
350
+ label_id=$(echo "$labels_response" | jq -r --arg name "$label" '.data.labels.nodes[] | select(.name == $name) | .id' 2>/dev/null || true)
351
+
352
+ if [[ -z "$label_id" ]]; then
353
+ # Label not found — skip
354
+ return 0
355
+ fi
356
+
357
+ local update_query='mutation($issueId: String!, $labelIds: [String!]) {
358
+ issueLabelCreate(issueId: $issueId, labelIds: $labelIds) {
359
+ success
360
+ }
361
+ }'
362
+
363
+ local vars
364
+ vars=$(jq -n --arg issueId "$issue_id" --arg labelId "$label_id" \
365
+ '{issueId: $issueId, labelIds: [$labelId]}')
366
+
367
+ linear_graphql "$update_query" "$vars" >/dev/null 2>&1 || return 1
368
+ }
369
+
370
+ provider_remove_label() {
371
+ local issue_id="$1"
372
+ local label="$2"
373
+
374
+ [[ -z "$issue_id" || -z "$label" ]] && return 1
375
+
376
+ provider_load_config
377
+
378
+ # Linear requires label IDs
379
+ local query='query {
380
+ labels(first: 100) {
381
+ nodes {id name}
382
+ }
383
+ }'
384
+
385
+ local labels_response
386
+ labels_response=$(linear_graphql "$query" "{}" 2>/dev/null) || return 1
387
+
388
+ local label_id
389
+ label_id=$(echo "$labels_response" | jq -r --arg name "$label" '.data.labels.nodes[] | select(.name == $name) | .id' 2>/dev/null || true)
390
+
391
+ if [[ -z "$label_id" ]]; then
392
+ return 0
393
+ fi
394
+
395
+ local update_query='mutation($issueId: String!, $labelIds: [String!]) {
396
+ issueLabelDelete(issueId: $issueId, labelIds: $labelIds) {
397
+ success
398
+ }
399
+ }'
400
+
401
+ local vars
402
+ vars=$(jq -n --arg issueId "$issue_id" --arg labelId "$label_id" \
403
+ '{issueId: $issueId, labelIds: [$labelId]}')
404
+
405
+ linear_graphql "$update_query" "$vars" >/dev/null 2>&1 || return 1
406
+ }
407
+
408
+ provider_comment() {
409
+ local issue_id="$1"
410
+ local body="$2"
411
+
412
+ [[ -z "$issue_id" || -z "$body" ]] && return 1
413
+
414
+ provider_load_config
415
+ linear_add_comment "$issue_id" "$body"
416
+ }
417
+
418
+ provider_close_issue() {
419
+ local issue_id="$1"
420
+
421
+ [[ -z "$issue_id" ]] && return 1
422
+
423
+ provider_load_config
424
+ linear_update_status "$issue_id" "$STATUS_DONE"
425
+ }
426
+
427
+ provider_create_issue() {
428
+ local title="$1"
429
+ local body="$2"
430
+ local labels="${3:-}"
431
+
432
+ [[ -z "$title" ]] && return 1
433
+
434
+ provider_load_config
435
+
436
+ local query='mutation($title: String!, $description: String, $teamId: String!) {
437
+ issueCreate(input: {title: $title, description: $description, teamId: $teamId}) {
438
+ issue {id}
439
+ }
440
+ }'
441
+
442
+ local vars
443
+ vars=$(jq -n --arg title "$title" --arg description "$body" --arg teamId "$LINEAR_TEAM_ID" \
444
+ '{title: $title, description: $description, teamId: $teamId}')
445
+
446
+ local response
447
+ response=$(linear_graphql "$query" "$vars" 2>/dev/null) || {
448
+ return 1
449
+ }
450
+
451
+ local issue_id
452
+ issue_id=$(echo "$response" | jq -r '.data.issueCreate.issue.id // empty' 2>/dev/null)
453
+
454
+ if [[ -z "$issue_id" ]]; then
455
+ return 1
456
+ fi
457
+
458
+ # Add labels if provided
459
+ if [[ -n "$labels" ]]; then
460
+ local label_list
461
+ label_list=$(echo "$labels" | tr ',' '\n' | tr ' ' '\n' | grep -v '^$' || true)
462
+ while IFS= read -r lbl; do
463
+ [[ -n "$lbl" ]] && provider_add_label "$issue_id" "$lbl" || true
464
+ done <<< "$label_list"
465
+ fi
466
+
467
+ echo "{\"id\": \"$issue_id\", \"title\": \"$title\"}"
468
+ }
469
+
219
470
  # ─── Find Linear Issue ID from GitHub Issue Body ──────────────────────────
220
471
 
221
472
  find_linear_id() {
@@ -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="1.9.0"
9
+ VERSION="2.0.0"
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
12
12
 
@@ -101,6 +101,14 @@ load_tracker_config() {
101
101
  warn "Jira provider script not found: $SCRIPT_DIR/sw-tracker-jira.sh"
102
102
  fi
103
103
  ;;
104
+ github)
105
+ if [[ -f "$SCRIPT_DIR/sw-tracker-github.sh" ]]; then
106
+ source "$SCRIPT_DIR/sw-tracker-github.sh"
107
+ return 0
108
+ else
109
+ warn "GitHub provider script not found: $SCRIPT_DIR/sw-tracker-github.sh"
110
+ fi
111
+ ;;
104
112
  none|"") return 0 ;;
105
113
  *)
106
114
  warn "Unknown tracker provider: $TRACKER_PROVIDER"
@@ -116,6 +124,98 @@ tracker_available() {
116
124
  [[ "$TRACKER_PROVIDER" != "none" && -n "$TRACKER_PROVIDER" ]]
117
125
  }
118
126
 
127
+ # ─── Internal Dispatcher ──────────────────────────────────────────────────
128
+ # Routes a provider function call to the loaded provider, with fallback to GitHub
129
+
130
+ _dispatch_provider() {
131
+ local func="$1"
132
+ shift
133
+
134
+ load_tracker_config
135
+
136
+ # Build the function name
137
+ local provider_func="provider_${func}"
138
+
139
+ # Provider scripts define provider_* functions
140
+ if type "$provider_func" &>/dev/null; then
141
+ "$provider_func" "$@"
142
+ return $?
143
+ else
144
+ # Fall back to GitHub if provider doesn't define the function
145
+ # (for backward compatibility with minimal providers like Jira/Linear notify-only)
146
+ if [[ "$TRACKER_PROVIDER" != "github" && "$TRACKER_PROVIDER" != "none" ]]; then
147
+ # Try GitHub provider
148
+ if [[ -f "$SCRIPT_DIR/sw-tracker-github.sh" ]]; then
149
+ source "$SCRIPT_DIR/sw-tracker-github.sh"
150
+ if type "$provider_func" &>/dev/null; then
151
+ "$provider_func" "$@"
152
+ return $?
153
+ fi
154
+ fi
155
+ fi
156
+ return 1
157
+ fi
158
+ }
159
+
160
+ # ─── Discovery Interface ────────────────────────────────────────────────────
161
+ # Used by daemon to discover issues from the configured tracker
162
+
163
+ # Discover issues matching criteria
164
+ # Usage: tracker_discover_issues <label> [state] [limit]
165
+ # Output: JSON array of {id, title, labels[], state}
166
+ tracker_discover_issues() {
167
+ _dispatch_provider "discover_issues" "$@"
168
+ }
169
+
170
+ # Fetch single issue details
171
+ # Usage: tracker_get_issue <issue_id>
172
+ # Output: JSON {id, title, body, labels[], state}
173
+ tracker_get_issue() {
174
+ _dispatch_provider "get_issue" "$@"
175
+ }
176
+
177
+ # Fetch issue body text only
178
+ # Usage: tracker_get_issue_body <issue_id>
179
+ # Output: plain text body
180
+ tracker_get_issue_body() {
181
+ _dispatch_provider "get_issue_body" "$@"
182
+ }
183
+
184
+ # ─── CRUD Interface ───────────────────────────────────────────────────────
185
+ # Used by pipeline to modify issues
186
+
187
+ # Add label to issue
188
+ # Usage: tracker_add_label <issue_id> <label>
189
+ tracker_add_label() {
190
+ _dispatch_provider "add_label" "$@"
191
+ }
192
+
193
+ # Remove label from issue
194
+ # Usage: tracker_remove_label <issue_id> <label>
195
+ tracker_remove_label() {
196
+ _dispatch_provider "remove_label" "$@"
197
+ }
198
+
199
+ # Add comment to issue
200
+ # Usage: tracker_comment <issue_id> <body>
201
+ tracker_comment() {
202
+ _dispatch_provider "comment" "$@"
203
+ }
204
+
205
+ # Close/resolve issue
206
+ # Usage: tracker_close_issue <issue_id>
207
+ tracker_close_issue() {
208
+ _dispatch_provider "close_issue" "$@"
209
+ }
210
+
211
+ # Create new issue
212
+ # Usage: tracker_create_issue <title> <body> [labels]
213
+ # Output: JSON {id, title}
214
+ tracker_create_issue() {
215
+ _dispatch_provider "create_issue" "$@"
216
+ }
217
+
218
+ # ─── Notification Interface (Legacy) ───────────────────────────────────────
119
219
  # Route notification to the active provider
120
220
  # Usage: tracker_notify <event> <gh_issue> [detail]
121
221
  # Events: spawn, started, stage_complete, stage_failed, review, pr-created, completed, done, failed
@@ -361,9 +461,24 @@ show_help() {
361
461
  echo -e " ${CYAN}help${RESET} Show this help"
362
462
  echo ""
363
463
  echo -e "${BOLD}PROVIDERS${RESET}"
464
+ echo -e " ${CYAN}github${RESET} GitHub (native, gh CLI)"
364
465
  echo -e " ${CYAN}linear${RESET} Linear.app (GraphQL API)"
365
466
  echo -e " ${CYAN}jira${RESET} Atlassian Jira (REST API v3)"
366
467
  echo ""
468
+ echo -e "${BOLD}DISCOVERY INTERFACE${RESET} (for daemon)"
469
+ echo -e " Provider-agnostic issue discovery and metadata:"
470
+ echo -e " ${CYAN}tracker_discover_issues${RESET} <label> [state] [limit]"
471
+ echo -e " ${CYAN}tracker_get_issue${RESET} <issue_id>"
472
+ echo -e " ${CYAN}tracker_get_issue_body${RESET} <issue_id>"
473
+ echo ""
474
+ echo -e "${BOLD}CRUD INTERFACE${RESET} (for pipeline)"
475
+ echo -e " Provider-agnostic issue modification:"
476
+ echo -e " ${CYAN}tracker_add_label${RESET} <issue_id> <label>"
477
+ echo -e " ${CYAN}tracker_remove_label${RESET} <issue_id> <label>"
478
+ echo -e " ${CYAN}tracker_comment${RESET} <issue_id> <body>"
479
+ echo -e " ${CYAN}tracker_close_issue${RESET} <issue_id>"
480
+ echo -e " ${CYAN}tracker_create_issue${RESET} <title> <body> [labels]"
481
+ echo ""
367
482
  echo -e "${BOLD}NOTIFICATION EVENTS${RESET}"
368
483
  echo -e " ${CYAN}spawn${RESET} Pipeline started"
369
484
  echo -e " ${CYAN}stage_complete${RESET} Stage finished (detail: stage_id|duration|description)"
@@ -374,7 +489,7 @@ show_help() {
374
489
  echo ""
375
490
  echo -e "${BOLD}CONFIGURATION${RESET}"
376
491
  echo -e " Config file: ${DIM}~/.shipwright/tracker-config.json${RESET}"
377
- echo -e " Env override: ${DIM}TRACKER_PROVIDER_OVERRIDE=linear|jira|none${RESET}"
492
+ echo -e " Env override: ${DIM}TRACKER_PROVIDER_OVERRIDE=github|linear|jira|none${RESET}"
378
493
  echo -e " Daemon block: ${DIM}.claude/daemon-config.json → .tracker.provider${RESET}"
379
494
  echo ""
380
495
  echo -e "${BOLD}EXAMPLES${RESET}"