chief-clancy 0.1.7 → 0.2.0-beta.2

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.
@@ -3,6 +3,15 @@
3
3
  # This means any command that fails will stop the script immediately rather than silently continuing.
4
4
  set -euo pipefail
5
5
 
6
+ # Parse flags — must happen before preflight so --dry-run works without side effects.
7
+ DRY_RUN=false
8
+ for arg in "$@"; do
9
+ case "$arg" in
10
+ --dry-run) DRY_RUN=true ;;
11
+ esac
12
+ done
13
+ readonly DRY_RUN
14
+
6
15
  # ─── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────
7
16
  #
8
17
  # Board: GitHub Issues
@@ -136,17 +145,31 @@ TICKET_BRANCH="feature/issue-${ISSUE_NUMBER}"
136
145
  if [ "$MILESTONE" != "none" ]; then
137
146
  MILESTONE_SLUG=$(echo "$MILESTONE" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd '[:alnum:]-')
138
147
  TARGET_BRANCH="milestone/${MILESTONE_SLUG}"
139
- git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
140
- || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
141
148
  else
142
149
  TARGET_BRANCH="$BASE_BRANCH"
143
150
  fi
144
151
 
152
+ # ─── DRY RUN ───────────────────────────────────────────────────────────────────
153
+
154
+ if [ "$DRY_RUN" = "true" ]; then
155
+ echo ""
156
+ echo "── Dry run ──────────────────────────────────────"
157
+ echo " Issue: [#${ISSUE_NUMBER}] $TITLE"
158
+ echo " Milestone: $MILESTONE"
159
+ echo " Target branch: $TARGET_BRANCH"
160
+ echo " Feature branch: $TICKET_BRANCH"
161
+ echo "─────────────────────────────────────────────────"
162
+ echo " No changes made. Remove --dry-run to run for real."
163
+ exit 0
164
+ fi
165
+
145
166
  # ─── IMPLEMENT ─────────────────────────────────────────────────────────────────
146
167
 
147
168
  echo "Picking up: [#${ISSUE_NUMBER}] $TITLE"
148
169
  echo "Milestone: $MILESTONE | Target branch: $TARGET_BRANCH"
149
170
 
171
+ git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
172
+ || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
150
173
  git checkout "$TARGET_BRANCH"
151
174
  # -B creates the branch if it doesn't exist, or resets it to HEAD if it does.
152
175
  # This handles retries cleanly without failing on an already-existing branch.
@@ -172,7 +195,8 @@ If you must SKIP this issue:
172
195
  4. Stop — no branches, no file changes, no git operations.
173
196
 
174
197
  If the issue IS implementable, continue:
175
- 1. Read ALL docs in .clancy/docs/ especially GIT.md for branching and commit conventions
198
+ 1. Read core docs in .clancy/docs/: STACK.md, ARCHITECTURE.md, CONVENTIONS.md, GIT.md, DEFINITION-OF-DONE.md, CONCERNS.md
199
+ Also read if relevant to this ticket: INTEGRATIONS.md (external APIs/services/auth), TESTING.md (tests/specs/coverage), DESIGN-SYSTEM.md (UI/components/styles), ACCESSIBILITY.md (accessibility/ARIA/WCAG)
176
200
  2. Follow the conventions in GIT.md exactly
177
201
  3. Implement the issue fully
178
202
  4. Commit your work following the conventions in GIT.md
@@ -3,6 +3,15 @@
3
3
  # This means any command that fails will stop the script immediately rather than silently continuing.
4
4
  set -euo pipefail
5
5
 
6
+ # Parse flags — must happen before preflight so --dry-run works without side effects.
7
+ DRY_RUN=false
8
+ for arg in "$@"; do
9
+ case "$arg" in
10
+ --dry-run) DRY_RUN=true ;;
11
+ esac
12
+ done
13
+ readonly DRY_RUN
14
+
6
15
  # ─── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────
7
16
  #
8
17
  # Board: Linear
@@ -153,6 +162,7 @@ if [ "$NODE_COUNT" -eq 0 ]; then
153
162
  exit 0
154
163
  fi
155
164
 
165
+ ISSUE_ID=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].id')
156
166
  IDENTIFIER=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].identifier')
157
167
  TITLE=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].title')
158
168
  DESCRIPTION=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].description // "No description"')
@@ -172,22 +182,58 @@ TICKET_BRANCH="feature/$(echo "$IDENTIFIER" | tr '[:upper:]' '[:lower:]')"
172
182
  # BASE_BRANCH if it doesn't exist yet). Otherwise branch from BASE_BRANCH directly.
173
183
  if [ "$PARENT_ID" != "none" ]; then
174
184
  TARGET_BRANCH="epic/$(echo "$PARENT_ID" | tr '[:upper:]' '[:lower:]')"
175
- git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
176
- || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
177
185
  else
178
186
  TARGET_BRANCH="$BASE_BRANCH"
179
187
  fi
180
188
 
189
+ # ─── DRY RUN ───────────────────────────────────────────────────────────────────
190
+
191
+ if [ "$DRY_RUN" = "true" ]; then
192
+ echo ""
193
+ echo "── Dry run ──────────────────────────────────────"
194
+ echo " Issue: [$IDENTIFIER] $TITLE"
195
+ echo " Epic: $EPIC_INFO"
196
+ echo " Target branch: $TARGET_BRANCH"
197
+ echo " Feature branch: $TICKET_BRANCH"
198
+ echo "─────────────────────────────────────────────────"
199
+ echo " No changes made. Remove --dry-run to run for real."
200
+ exit 0
201
+ fi
202
+
181
203
  # ─── IMPLEMENT ─────────────────────────────────────────────────────────────────
182
204
 
183
205
  echo "Picking up: [$IDENTIFIER] $TITLE"
184
206
  echo "Epic: $EPIC_INFO | Target branch: $TARGET_BRANCH"
185
207
 
208
+ git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
209
+ || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
186
210
  git checkout "$TARGET_BRANCH"
187
211
  # -B creates the branch if it doesn't exist, or resets it to HEAD if it does.
188
212
  # This handles retries cleanly without failing on an already-existing branch.
189
213
  git checkout -B "$TICKET_BRANCH"
190
214
 
215
+ # Transition issue to In Progress (best-effort — never fails the run).
216
+ # Queries team workflow states by type "started", picks the first match.
217
+ if [ -n "${CLANCY_STATUS_IN_PROGRESS:-}" ]; then
218
+ STATE_RESP=$(curl -s -X POST https://api.linear.app/graphql \
219
+ -H "Content-Type: application/json" \
220
+ -H "Authorization: $LINEAR_API_KEY" \
221
+ -d "$(jq -n --arg teamId "$LINEAR_TEAM_ID" --arg name "$CLANCY_STATUS_IN_PROGRESS" \
222
+ '{"query": "query($teamId: String!, $name: String!) { workflowStates(filter: { team: { id: { eq: $teamId } } name: { eq: $name } }) { nodes { id } } }", "variables": {"teamId": $teamId, "name": $name}}')")
223
+ IN_PROGRESS_STATE_ID=$(echo "$STATE_RESP" | jq -r '.data.workflowStates.nodes[0].id // empty')
224
+ if [ -n "$IN_PROGRESS_STATE_ID" ]; then
225
+ curl -s -X POST https://api.linear.app/graphql \
226
+ -H "Content-Type: application/json" \
227
+ -H "Authorization: $LINEAR_API_KEY" \
228
+ -d "$(jq -n --arg issueId "$ISSUE_ID" --arg stateId "$IN_PROGRESS_STATE_ID" \
229
+ '{"query": "mutation($issueId: String!, $stateId: String!) { issueUpdate(id: $issueId, input: { stateId: $stateId }) { success } }", "variables": {"issueId": $issueId, "stateId": $stateId}}')" \
230
+ >/dev/null 2>&1 || true
231
+ echo " → Transitioned to $CLANCY_STATUS_IN_PROGRESS"
232
+ else
233
+ echo " ⚠ Workflow state '$CLANCY_STATUS_IN_PROGRESS' not found — check CLANCY_STATUS_IN_PROGRESS in .clancy/.env."
234
+ fi
235
+ fi
236
+
191
237
  PROMPT="You are implementing Linear issue $IDENTIFIER.
192
238
 
193
239
  Title: $TITLE
@@ -208,7 +254,8 @@ If you must SKIP this issue:
208
254
  4. Stop — no branches, no file changes, no git operations.
209
255
 
210
256
  If the issue IS implementable, continue:
211
- 1. Read ALL docs in .clancy/docs/ especially GIT.md for branching and commit conventions
257
+ 1. Read core docs in .clancy/docs/: STACK.md, ARCHITECTURE.md, CONVENTIONS.md, GIT.md, DEFINITION-OF-DONE.md, CONCERNS.md
258
+ Also read if relevant to this ticket: INTEGRATIONS.md (external APIs/services/auth), TESTING.md (tests/specs/coverage), DESIGN-SYSTEM.md (UI/components/styles), ACCESSIBILITY.md (accessibility/ARIA/WCAG)
212
259
  2. Follow the conventions in GIT.md exactly
213
260
  3. Implement the issue fully
214
261
  4. Commit your work following the conventions in GIT.md
@@ -232,6 +279,27 @@ fi
232
279
  # Delete ticket branch locally
233
280
  git branch -d "$TICKET_BRANCH"
234
281
 
282
+ # Transition issue to Done (best-effort — never fails the run).
283
+ if [ -n "${CLANCY_STATUS_DONE:-}" ]; then
284
+ STATE_RESP=$(curl -s -X POST https://api.linear.app/graphql \
285
+ -H "Content-Type: application/json" \
286
+ -H "Authorization: $LINEAR_API_KEY" \
287
+ -d "$(jq -n --arg teamId "$LINEAR_TEAM_ID" --arg name "$CLANCY_STATUS_DONE" \
288
+ '{"query": "query($teamId: String!, $name: String!) { workflowStates(filter: { team: { id: { eq: $teamId } } name: { eq: $name } }) { nodes { id } } }", "variables": {"teamId": $teamId, "name": $name}}')")
289
+ DONE_STATE_ID=$(echo "$STATE_RESP" | jq -r '.data.workflowStates.nodes[0].id // empty')
290
+ if [ -n "$DONE_STATE_ID" ]; then
291
+ curl -s -X POST https://api.linear.app/graphql \
292
+ -H "Content-Type: application/json" \
293
+ -H "Authorization: $LINEAR_API_KEY" \
294
+ -d "$(jq -n --arg issueId "$ISSUE_ID" --arg stateId "$DONE_STATE_ID" \
295
+ '{"query": "mutation($issueId: String!, $stateId: String!) { issueUpdate(id: $issueId, input: { stateId: $stateId }) { success } }", "variables": {"issueId": $issueId, "stateId": $stateId}}')" \
296
+ >/dev/null 2>&1 || true
297
+ echo " → Transitioned to $CLANCY_STATUS_DONE"
298
+ else
299
+ echo " ⚠ Workflow state '$CLANCY_STATUS_DONE' not found — check CLANCY_STATUS_DONE in .clancy/.env."
300
+ fi
301
+ fi
302
+
235
303
  # Log progress
236
304
  echo "$(date '+%Y-%m-%d %H:%M') | $IDENTIFIER | $TITLE | DONE" >> .clancy/progress.txt
237
305
 
@@ -3,6 +3,15 @@
3
3
  # This means any command that fails will stop the script immediately rather than silently continuing.
4
4
  set -euo pipefail
5
5
 
6
+ # Parse flags — must happen before preflight so --dry-run works without side effects.
7
+ DRY_RUN=false
8
+ for arg in "$@"; do
9
+ case "$arg" in
10
+ --dry-run) DRY_RUN=true ;;
11
+ esac
12
+ done
13
+ readonly DRY_RUN
14
+
6
15
  # ─── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────
7
16
  #
8
17
  # Board: Jira
@@ -178,22 +187,56 @@ TICKET_BRANCH="feature/$(echo "$TICKET_KEY" | tr '[:upper:]' '[:lower:]')"
178
187
  # BASE_BRANCH if it doesn't exist yet). Otherwise branch from BASE_BRANCH directly.
179
188
  if [ "$EPIC_INFO" != "none" ]; then
180
189
  TARGET_BRANCH="epic/$(echo "$EPIC_INFO" | tr '[:upper:]' '[:lower:]')"
181
- git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
182
- || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
183
190
  else
184
191
  TARGET_BRANCH="$BASE_BRANCH"
185
192
  fi
186
193
 
194
+ # ─── DRY RUN ───────────────────────────────────────────────────────────────────
195
+
196
+ if [ "$DRY_RUN" = "true" ]; then
197
+ echo ""
198
+ echo "── Dry run ──────────────────────────────────────"
199
+ echo " Ticket: [$TICKET_KEY] $SUMMARY"
200
+ echo " Epic: $EPIC_INFO"
201
+ echo " Blockers: $BLOCKERS"
202
+ echo " Target branch: $TARGET_BRANCH"
203
+ echo " Feature branch: $TICKET_BRANCH"
204
+ echo "─────────────────────────────────────────────────"
205
+ echo " No changes made. Remove --dry-run to run for real."
206
+ exit 0
207
+ fi
208
+
187
209
  # ─── IMPLEMENT ─────────────────────────────────────────────────────────────────
188
210
 
189
211
  echo "Picking up: [$TICKET_KEY] $SUMMARY"
190
212
  echo "Epic: $EPIC_INFO | Target branch: $TARGET_BRANCH | Blockers: $BLOCKERS"
191
213
 
214
+ git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
215
+ || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
192
216
  git checkout "$TARGET_BRANCH"
193
217
  # -B creates the branch if it doesn't exist, or resets it to HEAD if it does.
194
218
  # This handles retries cleanly without failing on an already-existing branch.
195
219
  git checkout -B "$TICKET_BRANCH"
196
220
 
221
+ # Transition ticket to In Progress (best-effort — never fails the run)
222
+ if [ -n "${CLANCY_STATUS_IN_PROGRESS:-}" ]; then
223
+ TRANSITIONS=$(curl -s \
224
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
225
+ -H "Accept: application/json" \
226
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/transitions")
227
+ IN_PROGRESS_ID=$(echo "$TRANSITIONS" | jq -r \
228
+ --arg name "$CLANCY_STATUS_IN_PROGRESS" \
229
+ '.transitions[] | select(.name == $name) | .id' | head -1)
230
+ if [ -n "$IN_PROGRESS_ID" ]; then
231
+ curl -s -X POST \
232
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
233
+ -H "Content-Type: application/json" \
234
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/transitions" \
235
+ -d "$(jq -n --arg id "$IN_PROGRESS_ID" '{"transition":{"id":$id}}')" >/dev/null 2>&1 || true
236
+ echo " → Transitioned to $CLANCY_STATUS_IN_PROGRESS"
237
+ fi
238
+ fi
239
+
197
240
  PROMPT="You are implementing Jira ticket $TICKET_KEY.
198
241
 
199
242
  Summary: $SUMMARY
@@ -215,7 +258,8 @@ If you must SKIP this ticket:
215
258
  4. Stop — no branches, no file changes, no git operations.
216
259
 
217
260
  If the ticket IS implementable, continue:
218
- 1. Read ALL docs in .clancy/docs/ especially GIT.md for branching and commit conventions
261
+ 1. Read core docs in .clancy/docs/: STACK.md, ARCHITECTURE.md, CONVENTIONS.md, GIT.md, DEFINITION-OF-DONE.md, CONCERNS.md
262
+ Also read if relevant to this ticket: INTEGRATIONS.md (external APIs/services/auth), TESTING.md (tests/specs/coverage), DESIGN-SYSTEM.md (UI/components/styles), ACCESSIBILITY.md (accessibility/ARIA/WCAG)
219
263
  2. Follow the conventions in GIT.md exactly
220
264
  3. Implement the ticket fully
221
265
  4. Commit your work following the conventions in GIT.md
@@ -239,6 +283,25 @@ fi
239
283
  # Delete ticket branch locally (never push deletes)
240
284
  git branch -d "$TICKET_BRANCH"
241
285
 
286
+ # Transition ticket to Done (best-effort — never fails the run)
287
+ if [ -n "${CLANCY_STATUS_DONE:-}" ]; then
288
+ TRANSITIONS=$(curl -s \
289
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
290
+ -H "Accept: application/json" \
291
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/transitions")
292
+ DONE_ID=$(echo "$TRANSITIONS" | jq -r \
293
+ --arg name "$CLANCY_STATUS_DONE" \
294
+ '.transitions[] | select(.name == $name) | .id' | head -1)
295
+ if [ -n "$DONE_ID" ]; then
296
+ curl -s -X POST \
297
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
298
+ -H "Content-Type: application/json" \
299
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/transitions" \
300
+ -d "$(jq -n --arg id "$DONE_ID" '{"transition":{"id":$id}}')" >/dev/null 2>&1 || true
301
+ echo " → Transitioned to $CLANCY_STATUS_DONE"
302
+ fi
303
+ fi
304
+
242
305
  # Log progress
243
306
  echo "$(date '+%Y-%m-%d %H:%M') | $TICKET_KEY | $SUMMARY | DONE" >> .clancy/progress.txt
244
307
 
@@ -420,4 +420,4 @@ Clancy is ready.
420
420
  - Config: `.clancy/.env`
421
421
  - CLAUDE.md: updated
422
422
 
423
- Run `/clancy:once` to pick up your first ticket, or `/clancy:run` to process the full queue.
423
+ Run `/clancy:dry-run` to preview the first ticket without making changes, `/clancy:once` to pick it up, or `/clancy:run` to process the full queue.
@@ -45,6 +45,10 @@ Pick up exactly one ticket from the Kanban board, implement it, commit, squash-m
45
45
 
46
46
  ## Step 2 — Run
47
47
 
48
+ Check if the user passed `--dry-run` as an argument to the slash command.
49
+
50
+ **Without `--dry-run`:**
51
+
48
52
  Display:
49
53
  ```
50
54
  Running Clancy for one ticket.
@@ -55,6 +59,18 @@ Execute:
55
59
  bash .clancy/clancy-once.sh
56
60
  ```
57
61
 
62
+ **With `--dry-run`:**
63
+
64
+ Display:
65
+ ```
66
+ Running Clancy in dry-run mode — no changes will be made.
67
+ ```
68
+
69
+ Execute:
70
+ ```bash
71
+ bash .clancy/clancy-once.sh --dry-run
72
+ ```
73
+
58
74
  Stream output directly — do not buffer or summarise.
59
75
 
60
76
  ---
@@ -304,6 +304,15 @@ Write this file when the chosen board is **Jira**:
304
304
  # This means any command that fails will stop the script immediately rather than silently continuing.
305
305
  set -euo pipefail
306
306
 
307
+ # Parse flags — must happen before preflight so --dry-run works without side effects.
308
+ DRY_RUN=false
309
+ for arg in "$@"; do
310
+ case "$arg" in
311
+ --dry-run) DRY_RUN=true ;;
312
+ esac
313
+ done
314
+ readonly DRY_RUN
315
+
307
316
  # ─── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────
308
317
  #
309
318
  # Board: Jira
@@ -479,22 +488,56 @@ TICKET_BRANCH="feature/$(echo "$TICKET_KEY" | tr '[:upper:]' '[:lower:]')"
479
488
  # BASE_BRANCH if it doesn't exist yet). Otherwise branch from BASE_BRANCH directly.
480
489
  if [ "$EPIC_INFO" != "none" ]; then
481
490
  TARGET_BRANCH="epic/$(echo "$EPIC_INFO" | tr '[:upper:]' '[:lower:]')"
482
- git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
483
- || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
484
491
  else
485
492
  TARGET_BRANCH="$BASE_BRANCH"
486
493
  fi
487
494
 
495
+ # ─── DRY RUN ───────────────────────────────────────────────────────────────────
496
+
497
+ if [ "$DRY_RUN" = "true" ]; then
498
+ echo ""
499
+ echo "── Dry run ──────────────────────────────────────"
500
+ echo " Ticket: [$TICKET_KEY] $SUMMARY"
501
+ echo " Epic: $EPIC_INFO"
502
+ echo " Blockers: $BLOCKERS"
503
+ echo " Target branch: $TARGET_BRANCH"
504
+ echo " Feature branch: $TICKET_BRANCH"
505
+ echo "─────────────────────────────────────────────────"
506
+ echo " No changes made. Remove --dry-run to run for real."
507
+ exit 0
508
+ fi
509
+
488
510
  # ─── IMPLEMENT ─────────────────────────────────────────────────────────────────
489
511
 
490
512
  echo "Picking up: [$TICKET_KEY] $SUMMARY"
491
513
  echo "Epic: $EPIC_INFO | Target branch: $TARGET_BRANCH | Blockers: $BLOCKERS"
492
514
 
515
+ git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
516
+ || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
493
517
  git checkout "$TARGET_BRANCH"
494
518
  # -B creates the branch if it doesn't exist, or resets it to HEAD if it does.
495
519
  # This handles retries cleanly without failing on an already-existing branch.
496
520
  git checkout -B "$TICKET_BRANCH"
497
521
 
522
+ # Transition ticket to In Progress (best-effort — never fails the run)
523
+ if [ -n "${CLANCY_STATUS_IN_PROGRESS:-}" ]; then
524
+ TRANSITIONS=$(curl -s \
525
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
526
+ -H "Accept: application/json" \
527
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/transitions")
528
+ IN_PROGRESS_ID=$(echo "$TRANSITIONS" | jq -r \
529
+ --arg name "$CLANCY_STATUS_IN_PROGRESS" \
530
+ '.transitions[] | select(.name == $name) | .id' | head -1)
531
+ if [ -n "$IN_PROGRESS_ID" ]; then
532
+ curl -s -X POST \
533
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
534
+ -H "Content-Type: application/json" \
535
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/transitions" \
536
+ -d "$(jq -n --arg id "$IN_PROGRESS_ID" '{"transition":{"id":$id}}')" >/dev/null 2>&1 || true
537
+ echo " → Transitioned to $CLANCY_STATUS_IN_PROGRESS"
538
+ fi
539
+ fi
540
+
498
541
  PROMPT="You are implementing Jira ticket $TICKET_KEY.
499
542
 
500
543
  Summary: $SUMMARY
@@ -516,7 +559,8 @@ If you must SKIP this ticket:
516
559
  4. Stop — no branches, no file changes, no git operations.
517
560
 
518
561
  If the ticket IS implementable, continue:
519
- 1. Read ALL docs in .clancy/docs/ especially GIT.md for branching and commit conventions
562
+ 1. Read core docs in .clancy/docs/: STACK.md, ARCHITECTURE.md, CONVENTIONS.md, GIT.md, DEFINITION-OF-DONE.md, CONCERNS.md
563
+ Also read if relevant to this ticket: INTEGRATIONS.md (external APIs/services/auth), TESTING.md (tests/specs/coverage), DESIGN-SYSTEM.md (UI/components/styles), ACCESSIBILITY.md (accessibility/ARIA/WCAG)
520
564
  2. Follow the conventions in GIT.md exactly
521
565
  3. Implement the ticket fully
522
566
  4. Commit your work following the conventions in GIT.md
@@ -540,6 +584,25 @@ fi
540
584
  # Delete ticket branch locally (never push deletes)
541
585
  git branch -d "$TICKET_BRANCH"
542
586
 
587
+ # Transition ticket to Done (best-effort — never fails the run)
588
+ if [ -n "${CLANCY_STATUS_DONE:-}" ]; then
589
+ TRANSITIONS=$(curl -s \
590
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
591
+ -H "Accept: application/json" \
592
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/transitions")
593
+ DONE_ID=$(echo "$TRANSITIONS" | jq -r \
594
+ --arg name "$CLANCY_STATUS_DONE" \
595
+ '.transitions[] | select(.name == $name) | .id' | head -1)
596
+ if [ -n "$DONE_ID" ]; then
597
+ curl -s -X POST \
598
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
599
+ -H "Content-Type: application/json" \
600
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/transitions" \
601
+ -d "$(jq -n --arg id "$DONE_ID" '{"transition":{"id":$id}}')" >/dev/null 2>&1 || true
602
+ echo " → Transitioned to $CLANCY_STATUS_DONE"
603
+ fi
604
+ fi
605
+
543
606
  # Log progress
544
607
  echo "$(date '+%Y-%m-%d %H:%M') | $TICKET_KEY | $SUMMARY | DONE" >> .clancy/progress.txt
545
608
 
@@ -572,6 +635,15 @@ Write this file when the chosen board is **GitHub Issues**:
572
635
  # This means any command that fails will stop the script immediately rather than silently continuing.
573
636
  set -euo pipefail
574
637
 
638
+ # Parse flags — must happen before preflight so --dry-run works without side effects.
639
+ DRY_RUN=false
640
+ for arg in "$@"; do
641
+ case "$arg" in
642
+ --dry-run) DRY_RUN=true ;;
643
+ esac
644
+ done
645
+ readonly DRY_RUN
646
+
575
647
  # ─── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────
576
648
  #
577
649
  # Board: GitHub Issues
@@ -705,17 +777,31 @@ TICKET_BRANCH="feature/issue-${ISSUE_NUMBER}"
705
777
  if [ "$MILESTONE" != "none" ]; then
706
778
  MILESTONE_SLUG=$(echo "$MILESTONE" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd '[:alnum:]-')
707
779
  TARGET_BRANCH="milestone/${MILESTONE_SLUG}"
708
- git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
709
- || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
710
780
  else
711
781
  TARGET_BRANCH="$BASE_BRANCH"
712
782
  fi
713
783
 
784
+ # ─── DRY RUN ───────────────────────────────────────────────────────────────────
785
+
786
+ if [ "$DRY_RUN" = "true" ]; then
787
+ echo ""
788
+ echo "── Dry run ──────────────────────────────────────"
789
+ echo " Issue: [#${ISSUE_NUMBER}] $TITLE"
790
+ echo " Milestone: $MILESTONE"
791
+ echo " Target branch: $TARGET_BRANCH"
792
+ echo " Feature branch: $TICKET_BRANCH"
793
+ echo "─────────────────────────────────────────────────"
794
+ echo " No changes made. Remove --dry-run to run for real."
795
+ exit 0
796
+ fi
797
+
714
798
  # ─── IMPLEMENT ─────────────────────────────────────────────────────────────────
715
799
 
716
800
  echo "Picking up: [#${ISSUE_NUMBER}] $TITLE"
717
801
  echo "Milestone: $MILESTONE | Target branch: $TARGET_BRANCH"
718
802
 
803
+ git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
804
+ || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
719
805
  git checkout "$TARGET_BRANCH"
720
806
  # -B creates the branch if it doesn't exist, or resets it to HEAD if it does.
721
807
  # This handles retries cleanly without failing on an already-existing branch.
@@ -741,7 +827,8 @@ If you must SKIP this issue:
741
827
  4. Stop — no branches, no file changes, no git operations.
742
828
 
743
829
  If the issue IS implementable, continue:
744
- 1. Read ALL docs in .clancy/docs/ especially GIT.md for branching and commit conventions
830
+ 1. Read core docs in .clancy/docs/: STACK.md, ARCHITECTURE.md, CONVENTIONS.md, GIT.md, DEFINITION-OF-DONE.md, CONCERNS.md
831
+ Also read if relevant to this ticket: INTEGRATIONS.md (external APIs/services/auth), TESTING.md (tests/specs/coverage), DESIGN-SYSTEM.md (UI/components/styles), ACCESSIBILITY.md (accessibility/ARIA/WCAG)
745
832
  2. Follow the conventions in GIT.md exactly
746
833
  3. Implement the issue fully
747
834
  4. Commit your work following the conventions in GIT.md
@@ -806,6 +893,15 @@ Write this file when the chosen board is **Linear**:
806
893
  # This means any command that fails will stop the script immediately rather than silently continuing.
807
894
  set -euo pipefail
808
895
 
896
+ # Parse flags — must happen before preflight so --dry-run works without side effects.
897
+ DRY_RUN=false
898
+ for arg in "$@"; do
899
+ case "$arg" in
900
+ --dry-run) DRY_RUN=true ;;
901
+ esac
902
+ done
903
+ readonly DRY_RUN
904
+
809
905
  # ─── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────
810
906
  #
811
907
  # Board: Linear
@@ -956,6 +1052,7 @@ if [ "$NODE_COUNT" -eq 0 ]; then
956
1052
  exit 0
957
1053
  fi
958
1054
 
1055
+ ISSUE_ID=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].id')
959
1056
  IDENTIFIER=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].identifier')
960
1057
  TITLE=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].title')
961
1058
  DESCRIPTION=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].description // "No description"')
@@ -975,22 +1072,58 @@ TICKET_BRANCH="feature/$(echo "$IDENTIFIER" | tr '[:upper:]' '[:lower:]')"
975
1072
  # BASE_BRANCH if it doesn't exist yet). Otherwise branch from BASE_BRANCH directly.
976
1073
  if [ "$PARENT_ID" != "none" ]; then
977
1074
  TARGET_BRANCH="epic/$(echo "$PARENT_ID" | tr '[:upper:]' '[:lower:]')"
978
- git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
979
- || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
980
1075
  else
981
1076
  TARGET_BRANCH="$BASE_BRANCH"
982
1077
  fi
983
1078
 
1079
+ # ─── DRY RUN ───────────────────────────────────────────────────────────────────
1080
+
1081
+ if [ "$DRY_RUN" = "true" ]; then
1082
+ echo ""
1083
+ echo "── Dry run ──────────────────────────────────────"
1084
+ echo " Issue: [$IDENTIFIER] $TITLE"
1085
+ echo " Epic: $EPIC_INFO"
1086
+ echo " Target branch: $TARGET_BRANCH"
1087
+ echo " Feature branch: $TICKET_BRANCH"
1088
+ echo "─────────────────────────────────────────────────"
1089
+ echo " No changes made. Remove --dry-run to run for real."
1090
+ exit 0
1091
+ fi
1092
+
984
1093
  # ─── IMPLEMENT ─────────────────────────────────────────────────────────────────
985
1094
 
986
1095
  echo "Picking up: [$IDENTIFIER] $TITLE"
987
1096
  echo "Epic: $EPIC_INFO | Target branch: $TARGET_BRANCH"
988
1097
 
1098
+ git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
1099
+ || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
989
1100
  git checkout "$TARGET_BRANCH"
990
1101
  # -B creates the branch if it doesn't exist, or resets it to HEAD if it does.
991
1102
  # This handles retries cleanly without failing on an already-existing branch.
992
1103
  git checkout -B "$TICKET_BRANCH"
993
1104
 
1105
+ # Transition issue to In Progress (best-effort — never fails the run).
1106
+ # Queries team workflow states by type "started", picks the first match.
1107
+ if [ -n "${CLANCY_STATUS_IN_PROGRESS:-}" ]; then
1108
+ STATE_RESP=$(curl -s -X POST https://api.linear.app/graphql \
1109
+ -H "Content-Type: application/json" \
1110
+ -H "Authorization: $LINEAR_API_KEY" \
1111
+ -d "$(jq -n --arg teamId "$LINEAR_TEAM_ID" --arg name "$CLANCY_STATUS_IN_PROGRESS" \
1112
+ '{"query": "query($teamId: String!, $name: String!) { workflowStates(filter: { team: { id: { eq: $teamId } } name: { eq: $name } }) { nodes { id } } }", "variables": {"teamId": $teamId, "name": $name}}')")
1113
+ IN_PROGRESS_STATE_ID=$(echo "$STATE_RESP" | jq -r '.data.workflowStates.nodes[0].id // empty')
1114
+ if [ -n "$IN_PROGRESS_STATE_ID" ]; then
1115
+ curl -s -X POST https://api.linear.app/graphql \
1116
+ -H "Content-Type: application/json" \
1117
+ -H "Authorization: $LINEAR_API_KEY" \
1118
+ -d "$(jq -n --arg issueId "$ISSUE_ID" --arg stateId "$IN_PROGRESS_STATE_ID" \
1119
+ '{"query": "mutation($issueId: String!, $stateId: String!) { issueUpdate(id: $issueId, input: { stateId: $stateId }) { success } }", "variables": {"issueId": $issueId, "stateId": $stateId}}')" \
1120
+ >/dev/null 2>&1 || true
1121
+ echo " → Transitioned to $CLANCY_STATUS_IN_PROGRESS"
1122
+ else
1123
+ echo " ⚠ Workflow state '$CLANCY_STATUS_IN_PROGRESS' not found — check CLANCY_STATUS_IN_PROGRESS in .clancy/.env."
1124
+ fi
1125
+ fi
1126
+
994
1127
  PROMPT="You are implementing Linear issue $IDENTIFIER.
995
1128
 
996
1129
  Title: $TITLE
@@ -1011,7 +1144,8 @@ If you must SKIP this issue:
1011
1144
  4. Stop — no branches, no file changes, no git operations.
1012
1145
 
1013
1146
  If the issue IS implementable, continue:
1014
- 1. Read ALL docs in .clancy/docs/ especially GIT.md for branching and commit conventions
1147
+ 1. Read core docs in .clancy/docs/: STACK.md, ARCHITECTURE.md, CONVENTIONS.md, GIT.md, DEFINITION-OF-DONE.md, CONCERNS.md
1148
+ Also read if relevant to this ticket: INTEGRATIONS.md (external APIs/services/auth), TESTING.md (tests/specs/coverage), DESIGN-SYSTEM.md (UI/components/styles), ACCESSIBILITY.md (accessibility/ARIA/WCAG)
1015
1149
  2. Follow the conventions in GIT.md exactly
1016
1150
  3. Implement the issue fully
1017
1151
  4. Commit your work following the conventions in GIT.md
@@ -1035,6 +1169,27 @@ fi
1035
1169
  # Delete ticket branch locally
1036
1170
  git branch -d "$TICKET_BRANCH"
1037
1171
 
1172
+ # Transition issue to Done (best-effort — never fails the run).
1173
+ if [ -n "${CLANCY_STATUS_DONE:-}" ]; then
1174
+ STATE_RESP=$(curl -s -X POST https://api.linear.app/graphql \
1175
+ -H "Content-Type: application/json" \
1176
+ -H "Authorization: $LINEAR_API_KEY" \
1177
+ -d "$(jq -n --arg teamId "$LINEAR_TEAM_ID" --arg name "$CLANCY_STATUS_DONE" \
1178
+ '{"query": "query($teamId: String!, $name: String!) { workflowStates(filter: { team: { id: { eq: $teamId } } name: { eq: $name } }) { nodes { id } } }", "variables": {"teamId": $teamId, "name": $name}}')")
1179
+ DONE_STATE_ID=$(echo "$STATE_RESP" | jq -r '.data.workflowStates.nodes[0].id // empty')
1180
+ if [ -n "$DONE_STATE_ID" ]; then
1181
+ curl -s -X POST https://api.linear.app/graphql \
1182
+ -H "Content-Type: application/json" \
1183
+ -H "Authorization: $LINEAR_API_KEY" \
1184
+ -d "$(jq -n --arg issueId "$ISSUE_ID" --arg stateId "$DONE_STATE_ID" \
1185
+ '{"query": "mutation($issueId: String!, $stateId: String!) { issueUpdate(id: $issueId, input: { stateId: $stateId }) { success } }", "variables": {"issueId": $issueId, "stateId": $stateId}}')" \
1186
+ >/dev/null 2>&1 || true
1187
+ echo " → Transitioned to $CLANCY_STATUS_DONE"
1188
+ else
1189
+ echo " ⚠ Workflow state '$CLANCY_STATUS_DONE' not found — check CLANCY_STATUS_DONE in .clancy/.env."
1190
+ fi
1191
+ fi
1192
+
1038
1193
  # Log progress
1039
1194
  echo "$(date '+%Y-%m-%d %H:%M') | $IDENTIFIER | $TITLE | DONE" >> .clancy/progress.txt
1040
1195
 
@@ -1234,6 +1389,12 @@ MAX_ITERATIONS=5
1234
1389
  # PLAYWRIGHT_STORYBOOK_PORT=6006
1235
1390
  # PLAYWRIGHT_STARTUP_WAIT=15
1236
1391
 
1392
+ # ─── Optional: Status transitions ────────────────────────────────────────────
1393
+ # Move tickets automatically when Clancy picks up or completes them.
1394
+ # Set to the exact status name shown in your Jira board column header.
1395
+ # CLANCY_STATUS_IN_PROGRESS="In Progress"
1396
+ # CLANCY_STATUS_DONE="Done"
1397
+
1237
1398
  # ─── Optional: Notifications ──────────────────────────────────────────────────
1238
1399
  # Webhook URL for Slack or Teams notifications on ticket completion
1239
1400
  # CLANCY_NOTIFY_WEBHOOK=https://hooks.slack.com/services/your/webhook/url
@@ -1330,6 +1491,12 @@ MAX_ITERATIONS=20
1330
1491
  # PLAYWRIGHT_STORYBOOK_PORT=6006
1331
1492
  # PLAYWRIGHT_STARTUP_WAIT=15
1332
1493
 
1494
+ # ─── Optional: Status transitions ────────────────────────────────────────────
1495
+ # Move issues automatically when Clancy picks up or completes them.
1496
+ # Set to the exact workflow state name shown in your Linear board column header.
1497
+ # CLANCY_STATUS_IN_PROGRESS="In Progress"
1498
+ # CLANCY_STATUS_DONE="Done"
1499
+
1333
1500
  # ─── Optional: Notifications ──────────────────────────────────────────────────
1334
1501
  # Webhook URL for Slack or Teams notifications on ticket completion
1335
1502
  # CLANCY_NOTIFY_WEBHOOK=https://hooks.slack.com/services/your/webhook/url