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,188 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright tracker: GitHub Provider ║
4
+ # ║ Sourced by sw-tracker.sh — do not call directly ║
5
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
6
+ # This file is sourced by sw-tracker.sh.
7
+ # It defines provider_* functions used by the tracker router.
8
+ # Do NOT add set -euo pipefail or a main() function here.
9
+
10
+ # ─── Discovery & CRUD Interface ────────────────────────────────────────────
11
+ # All functions output normalized JSON (or plain text where specified).
12
+ # Input: normalized arguments (label, state, issue_id, etc.)
13
+ # Output: JSON matching common schema across all providers
14
+
15
+ # Discover issues from GitHub using gh CLI
16
+ # Input: label, state, limit
17
+ # Output: JSON array of {id, title, labels[], state, body}
18
+ provider_discover_issues() {
19
+ local label="$1"
20
+ local state="${2:-open}"
21
+ local limit="${3:-50}"
22
+
23
+ # Check $NO_GITHUB env var
24
+ [[ "${NO_GITHUB:-}" == "1" ]] && return 0
25
+
26
+ local gh_args=()
27
+ gh_args+=(issue list)
28
+ gh_args+=(--state "$state")
29
+ gh_args+=(--limit "$limit")
30
+
31
+ if [[ -n "$label" ]]; then
32
+ gh_args+=(--label "$label")
33
+ fi
34
+
35
+ # Fetch as JSON: number, title, labels, state
36
+ gh_args+=(--json number,title,labels,state)
37
+
38
+ local response
39
+ response=$(gh "${gh_args[@]}" 2>/dev/null) || {
40
+ echo "[]"
41
+ return 0
42
+ }
43
+
44
+ # Normalize to {id, title, labels[], state}
45
+ echo "$response" | jq '[.[] | {id: .number, title: .title, labels: [.labels[].name], state: .state}]' 2>/dev/null || echo "[]"
46
+ }
47
+
48
+ # Fetch single issue details
49
+ # Input: issue_id (number or identifier like "123" or "OWNER/REPO#123")
50
+ # Output: JSON {id, title, body, labels[], state}
51
+ provider_get_issue() {
52
+ local issue_id="$1"
53
+
54
+ [[ -z "$issue_id" ]] && return 1
55
+ [[ "${NO_GITHUB:-}" == "1" ]] && return 0
56
+
57
+ local response
58
+ response=$(gh issue view "$issue_id" --json number,title,body,labels,state 2>/dev/null) || {
59
+ return 1
60
+ }
61
+
62
+ # Normalize output
63
+ echo "$response" | jq '{id: .number, title: .title, body: .body, labels: [.labels[].name], state: .state}' 2>/dev/null || return 1
64
+ }
65
+
66
+ # Fetch issue body text only
67
+ # Input: issue_id
68
+ # Output: plain text body
69
+ provider_get_issue_body() {
70
+ local issue_id="$1"
71
+
72
+ [[ -z "$issue_id" ]] && return 1
73
+ [[ "${NO_GITHUB:-}" == "1" ]] && return 0
74
+
75
+ gh issue view "$issue_id" --json body --jq '.body' 2>/dev/null || return 1
76
+ }
77
+
78
+ # Add label to issue
79
+ # Input: issue_id, label
80
+ # Output: none (stdout on success, nothing on failure)
81
+ provider_add_label() {
82
+ local issue_id="$1"
83
+ local label="$2"
84
+
85
+ [[ -z "$issue_id" || -z "$label" ]] && return 1
86
+ [[ "${NO_GITHUB:-}" == "1" ]] && return 0
87
+
88
+ gh issue edit "$issue_id" --add-label "$label" 2>/dev/null || return 1
89
+ }
90
+
91
+ # Remove label from issue
92
+ # Input: issue_id, label
93
+ # Output: none
94
+ provider_remove_label() {
95
+ local issue_id="$1"
96
+ local label="$2"
97
+
98
+ [[ -z "$issue_id" || -z "$label" ]] && return 1
99
+ [[ "${NO_GITHUB:-}" == "1" ]] && return 0
100
+
101
+ gh issue edit "$issue_id" --remove-label "$label" 2>/dev/null || return 1
102
+ }
103
+
104
+ # Add comment to issue
105
+ # Input: issue_id, body
106
+ # Output: none
107
+ provider_comment() {
108
+ local issue_id="$1"
109
+ local body="$2"
110
+
111
+ [[ -z "$issue_id" || -z "$body" ]] && return 1
112
+ [[ "${NO_GITHUB:-}" == "1" ]] && return 0
113
+
114
+ gh issue comment "$issue_id" --body "$body" 2>/dev/null || return 1
115
+ }
116
+
117
+ # Close/resolve issue
118
+ # Input: issue_id
119
+ # Output: none
120
+ provider_close_issue() {
121
+ local issue_id="$1"
122
+
123
+ [[ -z "$issue_id" ]] && return 1
124
+ [[ "${NO_GITHUB:-}" == "1" ]] && return 0
125
+
126
+ gh issue close "$issue_id" 2>/dev/null || return 1
127
+ }
128
+
129
+ # Create new issue
130
+ # Input: title, body, labels (comma-separated or space-separated)
131
+ # Output: JSON {id, title}
132
+ provider_create_issue() {
133
+ local title="$1"
134
+ local body="$2"
135
+ local labels="${3:-}"
136
+
137
+ [[ -z "$title" ]] && return 1
138
+ [[ "${NO_GITHUB:-}" == "1" ]] && return 0
139
+
140
+ local gh_args=(issue create)
141
+ gh_args+=(--title "$title")
142
+
143
+ if [[ -n "$body" ]]; then
144
+ gh_args+=(--body "$body")
145
+ fi
146
+
147
+ if [[ -n "$labels" ]]; then
148
+ # Convert space-separated to gh format (--label multiple times)
149
+ # Handle both "label1,label2" and "label1 label2"
150
+ local label_list
151
+ label_list=$(echo "$labels" | tr ',' '\n' | tr ' ' '\n' | grep -v '^$' || true)
152
+ while IFS= read -r label; do
153
+ [[ -n "$label" ]] && gh_args+=(--label "$label")
154
+ done <<< "$label_list"
155
+ fi
156
+
157
+ local response
158
+ response=$(gh "${gh_args[@]}" 2>/dev/null) || {
159
+ return 1
160
+ }
161
+
162
+ # Extract issue number from response or return error
163
+ # GitHub response is typically: "Created issue <repo>#<number>"
164
+ local issue_num
165
+ issue_num=$(echo "$response" | grep -oE '#[0-9]+' | head -1 | tr -d '#' || true)
166
+
167
+ if [[ -z "$issue_num" ]]; then
168
+ return 1
169
+ fi
170
+
171
+ echo "{\"id\": $issue_num, \"title\": \"$title\"}"
172
+ }
173
+
174
+ # ─── Main Provider Entry Point (Notification) ──────────────────────────────
175
+ # Called by tracker_notify() in sw-tracker.sh
176
+
177
+ provider_notify() {
178
+ local event="$1"
179
+ local gh_issue="${2:-}"
180
+ local detail="${3:-}"
181
+
182
+ # GitHub is the native provider — no external sync needed
183
+ # This function exists for consistency with Linear/Jira but is minimal
184
+ # Real integration happens through pipeline stages calling provider_* functions
185
+
186
+ # For now, just log the event
187
+ emit_event "tracker.notify" "provider=github" "event=$event" "issue=$gh_issue"
188
+ }
@@ -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() {