chief-clancy 0.1.1 → 0.1.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chief-clancy",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Autonomous, board-driven development for Claude Code — scaffolds docs, integrates Kanban boards, runs tickets in a loop.",
5
5
  "keywords": [
6
6
  "claude",
@@ -33,7 +33,7 @@
33
33
  "registry/"
34
34
  ],
35
35
  "scripts": {
36
- "test": "bash test/unit/jira.test.sh && bash test/unit/github.test.sh && bash test/unit/linear.test.sh",
36
+ "test": "bash test/unit/jira.test.sh && bash test/unit/github.test.sh && bash test/unit/linear.test.sh && bash test/unit/scaffold.test.sh",
37
37
  "smoke": "bash test/smoke/smoke.sh"
38
38
  },
39
39
  "engines": {
@@ -176,13 +176,13 @@ Store the detected (or confirmed) value as `CLANCY_BASE_BRANCH` in `.clancy/.env
176
176
 
177
177
  Create `.clancy/` directory and the following:
178
178
 
179
- 1. Copy the correct `clancy-once.sh` variant for the chosen board to `.clancy/clancy-once.sh`
180
- 2. Copy `clancy-afk.sh` to `.clancy/clancy-afk.sh`
179
+ 1. Write the correct `clancy-once.sh` for the chosen board to `.clancy/clancy-once.sh` — use the exact script content from scaffold.md, do not generate or modify it
180
+ 2. Write `clancy-afk.sh` to `.clancy/clancy-afk.sh` — use the exact script content from scaffold.md, do not generate or modify it
181
181
  3. Make both scripts executable: `chmod +x .clancy/*.sh`
182
182
  4. Create `.clancy/docs/` with 10 empty template files (UPPERCASE.md with section headings only):
183
183
  - STACK.md, INTEGRATIONS.md, ARCHITECTURE.md, CONVENTIONS.md, TESTING.md
184
184
  - GIT.md, DESIGN-SYSTEM.md, ACCESSIBILITY.md, DEFINITION-OF-DONE.md, CONCERNS.md
185
- 5. Copy the correct `.env.example` variant to `.clancy/.env.example`
185
+ 5. Write the correct `.env.example` for the chosen board to `.clancy/.env.example` — use the exact content from scaffold.md
186
186
  6. Write collected credentials to `.clancy/.env` (if the user provided them)
187
187
  7. Handle `CLAUDE.md` — follow the merge logic in scaffold.md exactly:
188
188
  - If no CLAUDE.md: write the full template as `CLAUDE.md`
@@ -287,3 +287,1050 @@ node_modules/
287
287
  # OS
288
288
  .DS_Store
289
289
  ```
290
+
291
+ ---
292
+
293
+ ## Shell scripts
294
+
295
+ Write these scripts exactly as shown — do not generate, summarise, or modify the content. Write the file contents byte-for-byte.
296
+
297
+ ### `.clancy/clancy-once.sh` — Jira
298
+
299
+ Write this file when the chosen board is **Jira**:
300
+
301
+ ```bash
302
+ #!/usr/bin/env bash
303
+ # Strict mode: exit on error (-e), undefined variables (-u), pipe failures (-o pipefail).
304
+ # This means any command that fails will stop the script immediately rather than silently continuing.
305
+ set -euo pipefail
306
+
307
+ # ─── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────
308
+ #
309
+ # Board: Jira
310
+ #
311
+ # 1. Preflight — checks all required tools, credentials, and board reachability
312
+ # 2. Fetch — pulls the next assigned "To Do" ticket from Jira (maxResults: 1)
313
+ # 3. Branch — creates a feature branch from the ticket's epic branch (or base branch)
314
+ # 4. Implement — passes the ticket to Claude Code, which reads .clancy/docs/ and implements it
315
+ # 5. Merge — squash-merges the feature branch back into the target branch
316
+ # 6. Log — appends a completion entry to .clancy/progress.txt
317
+ #
318
+ # This script is run once per ticket. The loop is handled by clancy-afk.sh.
319
+ #
320
+ # NOTE: This file has no -jira suffix by design. /clancy:init copies the correct
321
+ # board variant into the user's .clancy/ directory as clancy-once.sh regardless
322
+ # of board. The board is determined by which template was copied, not the filename.
323
+ #
324
+ # NOTE: Failures use exit 0, not exit 1. This is intentional — clancy-afk.sh
325
+ # detects stop conditions by reading script output rather than exit codes, so a
326
+ # non-zero exit would be treated as an unexpected crash rather than a clean stop.
327
+ #
328
+ # ───────────────────────────────────────────────────────────────────────────────
329
+
330
+ # ─── PREFLIGHT ─────────────────────────────────────────────────────────────────
331
+
332
+ command -v claude >/dev/null 2>&1 || {
333
+ echo "✗ claude CLI not found."
334
+ echo " Install it: https://claude.ai/code"
335
+ exit 0
336
+ }
337
+ command -v jq >/dev/null 2>&1 || {
338
+ echo "✗ jq not found."
339
+ echo " Install: brew install jq (mac) | apt install jq (linux)"
340
+ exit 0
341
+ }
342
+ command -v curl >/dev/null 2>&1 || {
343
+ echo "✗ curl not found. Install curl for your OS."
344
+ exit 0
345
+ }
346
+
347
+ [ -f .clancy/.env ] || {
348
+ echo "✗ .clancy/.env not found."
349
+ echo " Copy .clancy/.env.example to .clancy/.env and fill in your credentials."
350
+ echo " Then run: /clancy:init"
351
+ exit 0
352
+ }
353
+ # shellcheck source=/dev/null
354
+ source .clancy/.env
355
+
356
+ git rev-parse --git-dir >/dev/null 2>&1 || {
357
+ echo "✗ Not a git repository."
358
+ echo " Clancy must be run from the root of a git project."
359
+ exit 0
360
+ }
361
+
362
+ if ! git diff --quiet || ! git diff --cached --quiet; then
363
+ echo "⚠ Working directory has uncommitted changes."
364
+ echo " Consider stashing or committing first to avoid confusion."
365
+ fi
366
+
367
+ [ -n "${JIRA_BASE_URL:-}" ] || { echo "✗ JIRA_BASE_URL is not set in .clancy/.env"; exit 0; }
368
+ [ -n "${JIRA_USER:-}" ] || { echo "✗ JIRA_USER is not set in .clancy/.env"; exit 0; }
369
+ [ -n "${JIRA_API_TOKEN:-}" ] || { echo "✗ JIRA_API_TOKEN is not set in .clancy/.env"; exit 0; }
370
+ [ -n "${JIRA_PROJECT_KEY:-}" ] || { echo "✗ JIRA_PROJECT_KEY is not set in .clancy/.env"; exit 0; }
371
+ if ! echo "$JIRA_PROJECT_KEY" | grep -qE '^[A-Z][A-Z0-9]+$'; then
372
+ echo "✗ JIRA_PROJECT_KEY format is invalid. Expected uppercase letters and numbers only (e.g. PROJ, ENG2). Check JIRA_PROJECT_KEY in .clancy/.env."
373
+ exit 0
374
+ fi
375
+
376
+ PING=$(curl -s -o /dev/null -w "%{http_code}" \
377
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
378
+ "$JIRA_BASE_URL/rest/api/3/project/$JIRA_PROJECT_KEY")
379
+
380
+ case "$PING" in
381
+ 200) ;;
382
+ 401) echo "✗ Jira authentication failed. Check JIRA_USER and JIRA_API_TOKEN in .clancy/.env."; exit 0 ;;
383
+ 403) echo "✗ Jira access denied. Your token may lack Browse Projects permission."; exit 0 ;;
384
+ 404) echo "✗ Jira project '$JIRA_PROJECT_KEY' not found. Check JIRA_PROJECT_KEY in .clancy/.env."; exit 0 ;;
385
+ 000) echo "✗ Could not reach Jira at $JIRA_BASE_URL. Check JIRA_BASE_URL and your network."; exit 0 ;;
386
+ *) echo "✗ Jira returned unexpected status $PING. Check your config."; exit 0 ;;
387
+ esac
388
+
389
+ if [ "${PLAYWRIGHT_ENABLED:-}" = "true" ]; then
390
+ if lsof -ti:"${PLAYWRIGHT_DEV_PORT:-5173}" >/dev/null 2>&1; then
391
+ echo "⚠ Port ${PLAYWRIGHT_DEV_PORT:-5173} is already in use."
392
+ echo " Clancy will attempt to start the dev server on this port."
393
+ echo " If visual checks fail, stop whatever is using the port first."
394
+ fi
395
+ fi
396
+
397
+ echo "✓ Preflight passed. Starting Clancy..."
398
+
399
+ # ─── END PREFLIGHT ─────────────────────────────────────────────────────────────
400
+
401
+ # ─── FETCH TICKET ──────────────────────────────────────────────────────────────
402
+
403
+ # Validate user-controlled values to prevent JQL injection.
404
+ # JQL does not support parameterised queries, so we restrict to safe characters.
405
+ if [ -n "${CLANCY_LABEL:-}" ] && ! echo "$CLANCY_LABEL" | grep -qE '^[a-zA-Z0-9 _-]+$'; then
406
+ echo "✗ CLANCY_LABEL contains invalid characters. Use only letters, numbers, spaces, hyphens, and underscores."
407
+ exit 0
408
+ fi
409
+ if ! echo "${CLANCY_JQL_STATUS:-To Do}" | grep -qE '^[a-zA-Z0-9 _-]+$'; then
410
+ echo "✗ CLANCY_JQL_STATUS contains invalid characters. Use only letters, numbers, spaces, hyphens, and underscores."
411
+ exit 0
412
+ fi
413
+
414
+ # Build JQL — sprint filter is optional (requires Jira Software license).
415
+ # Uses the /rest/api/3/search/jql POST endpoint — the old GET /search was removed Aug 2025.
416
+ # maxResults:1 is intentional — pick one ticket per run, never paginate.
417
+ if [ -n "${CLANCY_JQL_SPRINT:-}" ]; then
418
+ SPRINT_CLAUSE="AND sprint in openSprints()"
419
+ else
420
+ SPRINT_CLAUSE=""
421
+ fi
422
+
423
+ # Optional label filter — set CLANCY_LABEL in .env to only pick up tickets with that label.
424
+ # Useful for mixed backlogs where not every ticket is suitable for autonomous implementation.
425
+ if [ -n "${CLANCY_LABEL:-}" ]; then
426
+ LABEL_CLAUSE="AND labels = \"$CLANCY_LABEL\""
427
+ else
428
+ LABEL_CLAUSE=""
429
+ fi
430
+
431
+ RESPONSE=$(curl -s \
432
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
433
+ -X POST \
434
+ -H "Content-Type: application/json" \
435
+ -H "Accept: application/json" \
436
+ "$JIRA_BASE_URL/rest/api/3/search/jql" \
437
+ -d "{
438
+ \"jql\": \"project=$JIRA_PROJECT_KEY $SPRINT_CLAUSE $LABEL_CLAUSE AND assignee=currentUser() AND status=\\\"${CLANCY_JQL_STATUS:-To Do}\\\" ORDER BY priority ASC\",
439
+ \"maxResults\": 1,
440
+ \"fields\": [\"summary\", \"description\", \"issuelinks\", \"parent\", \"customfield_10014\"]
441
+ }")
442
+
443
+ # New endpoint returns { "issues": [...], "isLast": bool } — no .total field
444
+ ISSUE_COUNT=$(echo "$RESPONSE" | jq '.issues | length')
445
+ if [ "$ISSUE_COUNT" -eq 0 ]; then
446
+ echo "No tickets found. All done!"
447
+ exit 0
448
+ fi
449
+
450
+ TICKET_KEY=$(echo "$RESPONSE" | jq -r '.issues[0].key')
451
+ SUMMARY=$(echo "$RESPONSE" | jq -r '.issues[0].fields.summary')
452
+
453
+ # Extract description via recursive ADF walk
454
+ DESCRIPTION=$(echo "$RESPONSE" | jq -r '
455
+ .issues[0].fields.description
456
+ | .. | strings
457
+ | select(length > 0)
458
+ | . + "\n"
459
+ ' 2>/dev/null || echo "No description")
460
+
461
+ # Extract epic — try parent first (next-gen), fall back to customfield_10014 (classic)
462
+ EPIC_INFO=$(echo "$RESPONSE" | jq -r '
463
+ .issues[0].fields.parent.key // .issues[0].fields.customfield_10014 // "none"
464
+ ')
465
+
466
+ # Extract blocking issue links
467
+ BLOCKERS=$(echo "$RESPONSE" | jq -r '
468
+ [.issues[0].fields.issuelinks[]?
469
+ | select(.type.name == "Blocks" and .inwardIssue?)
470
+ | .inwardIssue.key]
471
+ | if length > 0 then "Blocked by: " + join(", ") else "None" end
472
+ ' 2>/dev/null || echo "None")
473
+
474
+ BASE_BRANCH="${CLANCY_BASE_BRANCH:-main}"
475
+ TICKET_BRANCH="feature/$(echo "$TICKET_KEY" | tr '[:upper:]' '[:lower:]')"
476
+
477
+ # Auto-detect target branch from ticket's parent epic.
478
+ # If the ticket has a parent epic, branch from epic/{epic-key} (creating it from
479
+ # BASE_BRANCH if it doesn't exist yet). Otherwise branch from BASE_BRANCH directly.
480
+ if [ "$EPIC_INFO" != "none" ]; then
481
+ 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
+ else
485
+ TARGET_BRANCH="$BASE_BRANCH"
486
+ fi
487
+
488
+ # ─── IMPLEMENT ─────────────────────────────────────────────────────────────────
489
+
490
+ echo "Picking up: [$TICKET_KEY] $SUMMARY"
491
+ echo "Epic: $EPIC_INFO | Target branch: $TARGET_BRANCH | Blockers: $BLOCKERS"
492
+
493
+ git checkout "$TARGET_BRANCH"
494
+ # -B creates the branch if it doesn't exist, or resets it to HEAD if it does.
495
+ # This handles retries cleanly without failing on an already-existing branch.
496
+ git checkout -B "$TICKET_BRANCH"
497
+
498
+ PROMPT="You are implementing Jira ticket $TICKET_KEY.
499
+
500
+ Summary: $SUMMARY
501
+ Epic: $EPIC_INFO
502
+ Blockers: $BLOCKERS
503
+
504
+ Description:
505
+ $DESCRIPTION
506
+
507
+ Step 0 — Executability check (do this before any git or file operation):
508
+ Read the ticket summary and description above. Can this ticket be implemented entirely
509
+ as a code change committed to this repo? Consult the 'Executability check' section of
510
+ CLAUDE.md for the full list of skip conditions.
511
+
512
+ If you must SKIP this ticket:
513
+ 1. Output: ⚠ Skipping [$TICKET_KEY]: {one-line reason}
514
+ 2. Output: Ticket skipped — update it to be codebase-only work, then re-run.
515
+ 3. Append to .clancy/progress.txt: YYYY-MM-DD HH:MM | $TICKET_KEY | SKIPPED | {reason}
516
+ 4. Stop — no branches, no file changes, no git operations.
517
+
518
+ If the ticket IS implementable, continue:
519
+ 1. Read ALL docs in .clancy/docs/ — especially GIT.md for branching and commit conventions
520
+ 2. Follow the conventions in GIT.md exactly
521
+ 3. Implement the ticket fully
522
+ 4. Commit your work following the conventions in GIT.md
523
+ 5. When done, confirm you are finished."
524
+
525
+ CLAUDE_ARGS=(--dangerously-skip-permissions)
526
+ [ -n "${CLANCY_MODEL:-}" ] && CLAUDE_ARGS+=(--model "$CLANCY_MODEL")
527
+ echo "$PROMPT" | claude "${CLAUDE_ARGS[@]}"
528
+
529
+ # ─── MERGE & LOG ───────────────────────────────────────────────────────────────
530
+
531
+ # Squash all commits from the feature branch into a single commit on the target branch.
532
+ git checkout "$TARGET_BRANCH"
533
+ git merge --squash "$TICKET_BRANCH"
534
+ if git diff --cached --quiet; then
535
+ echo "⚠ No changes staged after squash merge. Claude may not have committed any work."
536
+ else
537
+ git commit -m "feat($TICKET_KEY): $SUMMARY"
538
+ fi
539
+
540
+ # Delete ticket branch locally (never push deletes)
541
+ git branch -d "$TICKET_BRANCH"
542
+
543
+ # Log progress
544
+ echo "$(date '+%Y-%m-%d %H:%M') | $TICKET_KEY | $SUMMARY | DONE" >> .clancy/progress.txt
545
+
546
+ echo "✓ $TICKET_KEY complete."
547
+
548
+ # Send completion notification if webhook is configured
549
+ if [ -n "${CLANCY_NOTIFY_WEBHOOK:-}" ]; then
550
+ NOTIFY_MSG="✓ Clancy completed [$TICKET_KEY] $SUMMARY"
551
+ if echo "$CLANCY_NOTIFY_WEBHOOK" | grep -q "hooks.slack.com"; then
552
+ curl -s -X POST "$CLANCY_NOTIFY_WEBHOOK" \
553
+ -H "Content-Type: application/json" \
554
+ -d "$(jq -n --arg text "$NOTIFY_MSG" '{"text": $text}')" >/dev/null 2>&1 || true
555
+ else
556
+ curl -s -X POST "$CLANCY_NOTIFY_WEBHOOK" \
557
+ -H "Content-Type: application/json" \
558
+ -d "$(jq -n --arg text "$NOTIFY_MSG" '{"type":"message","attachments":[{"contentType":"application/vnd.microsoft.card.adaptive","content":{"type":"AdaptiveCard","body":[{"type":"TextBlock","text":$text}]}}]}')" >/dev/null 2>&1 || true
559
+ fi
560
+ fi
561
+ ```
562
+
563
+ ---
564
+
565
+ ### `.clancy/clancy-once.sh` — GitHub Issues
566
+
567
+ Write this file when the chosen board is **GitHub Issues**:
568
+
569
+ ```bash
570
+ #!/usr/bin/env bash
571
+ # Strict mode: exit on error (-e), undefined variables (-u), pipe failures (-o pipefail).
572
+ # This means any command that fails will stop the script immediately rather than silently continuing.
573
+ set -euo pipefail
574
+
575
+ # ─── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────
576
+ #
577
+ # Board: GitHub Issues
578
+ #
579
+ # 1. Preflight — checks all required tools, credentials, and repo reachability
580
+ # 2. Fetch — pulls the next open issue with the 'clancy' label assigned to you
581
+ # 3. Branch — creates a feature branch from the issue's milestone branch (or base branch)
582
+ # 4. Implement — passes the issue to Claude Code, which reads .clancy/docs/ and implements it
583
+ # 5. Merge — squash-merges the feature branch back into the target branch
584
+ # 6. Close — marks the GitHub issue as closed via the API
585
+ # 7. Log — appends a completion entry to .clancy/progress.txt
586
+ #
587
+ # This script is run once per issue. The loop is handled by clancy-afk.sh.
588
+ #
589
+ # NOTE: GitHub's /issues endpoint returns pull requests too. This script filters
590
+ # them out by checking for the presence of the 'pull_request' key in each result.
591
+ #
592
+ # NOTE: Failures use exit 0, not exit 1. This is intentional — clancy-afk.sh
593
+ # detects stop conditions by reading script output rather than exit codes, so a
594
+ # non-zero exit would be treated as an unexpected crash rather than a clean stop.
595
+ #
596
+ # ───────────────────────────────────────────────────────────────────────────────
597
+
598
+ # ─── PREFLIGHT ─────────────────────────────────────────────────────────────────
599
+
600
+ command -v claude >/dev/null 2>&1 || {
601
+ echo "✗ claude CLI not found."
602
+ echo " Install it: https://claude.ai/code"
603
+ exit 0
604
+ }
605
+ command -v jq >/dev/null 2>&1 || {
606
+ echo "✗ jq not found."
607
+ echo " Install: brew install jq (mac) | apt install jq (linux)"
608
+ exit 0
609
+ }
610
+ command -v curl >/dev/null 2>&1 || {
611
+ echo "✗ curl not found. Install curl for your OS."
612
+ exit 0
613
+ }
614
+
615
+ [ -f .clancy/.env ] || {
616
+ echo "✗ .clancy/.env not found."
617
+ echo " Copy .clancy/.env.example to .clancy/.env and fill in your credentials."
618
+ echo " Then run: /clancy:init"
619
+ exit 0
620
+ }
621
+ # shellcheck source=/dev/null
622
+ source .clancy/.env
623
+
624
+ git rev-parse --git-dir >/dev/null 2>&1 || {
625
+ echo "✗ Not a git repository."
626
+ echo " Clancy must be run from the root of a git project."
627
+ exit 0
628
+ }
629
+
630
+ if ! git diff --quiet || ! git diff --cached --quiet; then
631
+ echo "⚠ Working directory has uncommitted changes."
632
+ echo " Consider stashing or committing first to avoid confusion."
633
+ fi
634
+
635
+ [ -n "${GITHUB_TOKEN:-}" ] || { echo "✗ GITHUB_TOKEN is not set in .clancy/.env"; exit 0; }
636
+ [ -n "${GITHUB_REPO:-}" ] || { echo "✗ GITHUB_REPO is not set in .clancy/.env"; exit 0; }
637
+
638
+ # Validate GITHUB_REPO format — must be owner/repo with safe characters only
639
+ if ! echo "$GITHUB_REPO" | grep -qE '^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+$'; then
640
+ echo "✗ GITHUB_REPO format is invalid. Expected owner/repo (e.g. acme/my-app). Check GITHUB_REPO in .clancy/.env."
641
+ exit 0
642
+ fi
643
+
644
+ PING=$(curl -s -o /dev/null -w "%{http_code}" \
645
+ -H "Authorization: Bearer $GITHUB_TOKEN" \
646
+ -H "X-GitHub-Api-Version: 2022-11-28" \
647
+ "https://api.github.com/repos/$GITHUB_REPO")
648
+
649
+ case "$PING" in
650
+ 200) ;;
651
+ 401) echo "✗ GitHub authentication failed. Check GITHUB_TOKEN in .clancy/.env."; exit 0 ;;
652
+ 403) echo "✗ GitHub access denied. Your token may lack the repo scope."; exit 0 ;;
653
+ 404) echo "✗ GitHub repo '$GITHUB_REPO' not found. Check GITHUB_REPO in .clancy/.env."; exit 0 ;;
654
+ 000) echo "✗ Could not reach GitHub. Check your network."; exit 0 ;;
655
+ *) echo "✗ GitHub returned unexpected status $PING. Check your config."; exit 0 ;;
656
+ esac
657
+
658
+ if [ "${PLAYWRIGHT_ENABLED:-}" = "true" ]; then
659
+ if lsof -ti:"${PLAYWRIGHT_DEV_PORT:-5173}" >/dev/null 2>&1; then
660
+ echo "⚠ Port ${PLAYWRIGHT_DEV_PORT:-5173} is already in use."
661
+ echo " If visual checks fail, stop whatever is using the port first."
662
+ fi
663
+ fi
664
+
665
+ echo "✓ Preflight passed. Starting Clancy..."
666
+
667
+ # ─── END PREFLIGHT ─────────────────────────────────────────────────────────────
668
+
669
+ # ─── FETCH ISSUE ───────────────────────────────────────────────────────────────
670
+
671
+ # Fetch open issues assigned to the authenticated user with the 'clancy' label.
672
+ # GitHub's issues endpoint returns PRs too — filter them out by checking for pull_request key.
673
+ # per_page=3 so we can find one real issue even if the first result(s) are PRs.
674
+ RESPONSE=$(curl -s \
675
+ -H "Authorization: Bearer $GITHUB_TOKEN" \
676
+ -H "X-GitHub-Api-Version: 2022-11-28" \
677
+ "https://api.github.com/repos/$GITHUB_REPO/issues?state=open&assignee=@me&labels=clancy&per_page=3")
678
+
679
+ # Verify response is an array before parsing (guards against error objects on rate limit / transient failure)
680
+ if ! echo "$RESPONSE" | jq -e 'type == "array"' >/dev/null 2>&1; then
681
+ ERR_MSG=$(echo "$RESPONSE" | jq -r '.message // "Unexpected response"' 2>/dev/null || echo "Unexpected response")
682
+ echo "✗ GitHub API error: $ERR_MSG. Check GITHUB_TOKEN in .clancy/.env."
683
+ exit 0
684
+ fi
685
+
686
+ # Filter out PRs and take first real issue
687
+ ISSUE=$(echo "$RESPONSE" | jq 'map(select(has("pull_request") | not)) | .[0]')
688
+
689
+ if [ "$(echo "$ISSUE" | jq 'type')" = '"null"' ] || [ -z "$(echo "$ISSUE" | jq -r '.number // empty')" ]; then
690
+ echo "No issues found. All done!"
691
+ exit 0
692
+ fi
693
+
694
+ ISSUE_NUMBER=$(echo "$ISSUE" | jq -r '.number')
695
+ TITLE=$(echo "$ISSUE" | jq -r '.title')
696
+ BODY=$(echo "$ISSUE" | jq -r '.body // "No description"')
697
+ MILESTONE=$(echo "$ISSUE" | jq -r '.milestone.title // "none"')
698
+
699
+ BASE_BRANCH="${CLANCY_BASE_BRANCH:-main}"
700
+ TICKET_BRANCH="feature/issue-${ISSUE_NUMBER}"
701
+
702
+ # GitHub has no native epic concept — use milestone as the grouping signal.
703
+ # If the issue has a milestone, branch from milestone/{slug} (creating it from
704
+ # BASE_BRANCH if it doesn't exist yet). Otherwise branch from BASE_BRANCH directly.
705
+ if [ "$MILESTONE" != "none" ]; then
706
+ MILESTONE_SLUG=$(echo "$MILESTONE" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd '[:alnum:]-')
707
+ TARGET_BRANCH="milestone/${MILESTONE_SLUG}"
708
+ git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
709
+ || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
710
+ else
711
+ TARGET_BRANCH="$BASE_BRANCH"
712
+ fi
713
+
714
+ # ─── IMPLEMENT ─────────────────────────────────────────────────────────────────
715
+
716
+ echo "Picking up: [#${ISSUE_NUMBER}] $TITLE"
717
+ echo "Milestone: $MILESTONE | Target branch: $TARGET_BRANCH"
718
+
719
+ git checkout "$TARGET_BRANCH"
720
+ # -B creates the branch if it doesn't exist, or resets it to HEAD if it does.
721
+ # This handles retries cleanly without failing on an already-existing branch.
722
+ git checkout -B "$TICKET_BRANCH"
723
+
724
+ PROMPT="You are implementing GitHub Issue #${ISSUE_NUMBER}.
725
+
726
+ Title: $TITLE
727
+ Milestone: $MILESTONE
728
+
729
+ Description:
730
+ $BODY
731
+
732
+ Step 0 — Executability check (do this before any git or file operation):
733
+ Read the issue title and description above. Can this issue be implemented entirely
734
+ as a code change committed to this repo? Consult the 'Executability check' section of
735
+ CLAUDE.md for the full list of skip conditions.
736
+
737
+ If you must SKIP this issue:
738
+ 1. Output: ⚠ Skipping [#${ISSUE_NUMBER}]: {one-line reason}
739
+ 2. Output: Ticket skipped — update it to be codebase-only work, then re-run.
740
+ 3. Append to .clancy/progress.txt: YYYY-MM-DD HH:MM | #${ISSUE_NUMBER} | SKIPPED | {reason}
741
+ 4. Stop — no branches, no file changes, no git operations.
742
+
743
+ If the issue IS implementable, continue:
744
+ 1. Read ALL docs in .clancy/docs/ — especially GIT.md for branching and commit conventions
745
+ 2. Follow the conventions in GIT.md exactly
746
+ 3. Implement the issue fully
747
+ 4. Commit your work following the conventions in GIT.md
748
+ 5. When done, confirm you are finished."
749
+
750
+ CLAUDE_ARGS=(--dangerously-skip-permissions)
751
+ [ -n "${CLANCY_MODEL:-}" ] && CLAUDE_ARGS+=(--model "$CLANCY_MODEL")
752
+ echo "$PROMPT" | claude "${CLAUDE_ARGS[@]}"
753
+
754
+ # ─── MERGE, CLOSE & LOG ────────────────────────────────────────────────────────
755
+
756
+ # Squash all commits from the feature branch into a single commit on the target branch.
757
+ git checkout "$TARGET_BRANCH"
758
+ git merge --squash "$TICKET_BRANCH"
759
+ if git diff --cached --quiet; then
760
+ echo "⚠ No changes staged after squash merge. Claude may not have committed any work."
761
+ else
762
+ git commit -m "feat(#${ISSUE_NUMBER}): $TITLE"
763
+ fi
764
+
765
+ # Delete ticket branch locally
766
+ git branch -d "$TICKET_BRANCH"
767
+
768
+ # Close the issue — warn but don't fail if this doesn't go through
769
+ CLOSE_HTTP=$(curl -s -o /dev/null -w "%{http_code}" -X PATCH \
770
+ -H "Authorization: Bearer $GITHUB_TOKEN" \
771
+ -H "X-GitHub-Api-Version: 2022-11-28" \
772
+ -H "Content-Type: application/json" \
773
+ "https://api.github.com/repos/$GITHUB_REPO/issues/${ISSUE_NUMBER}" \
774
+ -d '{"state": "closed"}')
775
+ [ "$CLOSE_HTTP" = "200" ] || echo "⚠ Could not close issue #${ISSUE_NUMBER} (HTTP $CLOSE_HTTP). Close it manually on GitHub."
776
+
777
+ # Log progress
778
+ echo "$(date '+%Y-%m-%d %H:%M') | #${ISSUE_NUMBER} | $TITLE | DONE" >> .clancy/progress.txt
779
+
780
+ echo "✓ #${ISSUE_NUMBER} complete."
781
+
782
+ # Send completion notification if webhook is configured
783
+ if [ -n "${CLANCY_NOTIFY_WEBHOOK:-}" ]; then
784
+ NOTIFY_MSG="✓ Clancy completed [#${ISSUE_NUMBER}] $TITLE"
785
+ if echo "$CLANCY_NOTIFY_WEBHOOK" | grep -q "hooks.slack.com"; then
786
+ curl -s -X POST "$CLANCY_NOTIFY_WEBHOOK" \
787
+ -H "Content-Type: application/json" \
788
+ -d "$(jq -n --arg text "$NOTIFY_MSG" '{"text": $text}')" >/dev/null 2>&1 || true
789
+ else
790
+ curl -s -X POST "$CLANCY_NOTIFY_WEBHOOK" \
791
+ -H "Content-Type: application/json" \
792
+ -d "$(jq -n --arg text "$NOTIFY_MSG" '{"type":"message","attachments":[{"contentType":"application/vnd.microsoft.card.adaptive","content":{"type":"AdaptiveCard","body":[{"type":"TextBlock","text":$text}]}}]}')" >/dev/null 2>&1 || true
793
+ fi
794
+ fi
795
+ ```
796
+
797
+ ---
798
+
799
+ ### `.clancy/clancy-once.sh` — Linear
800
+
801
+ Write this file when the chosen board is **Linear**:
802
+
803
+ ```bash
804
+ #!/usr/bin/env bash
805
+ # Strict mode: exit on error (-e), undefined variables (-u), pipe failures (-o pipefail).
806
+ # This means any command that fails will stop the script immediately rather than silently continuing.
807
+ set -euo pipefail
808
+
809
+ # ─── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────
810
+ #
811
+ # Board: Linear
812
+ #
813
+ # 1. Preflight — checks all required tools, credentials, and API reachability
814
+ # 2. Fetch — pulls the next unstarted issue assigned to you via GraphQL
815
+ # 3. Branch — creates a feature branch from the issue's parent branch (or base branch)
816
+ # 4. Implement — passes the issue to Claude Code, which reads .clancy/docs/ and implements it
817
+ # 5. Merge — squash-merges the feature branch back into the target branch
818
+ # 6. Log — appends a completion entry to .clancy/progress.txt
819
+ #
820
+ # This script is run once per issue. The loop is handled by clancy-afk.sh.
821
+ #
822
+ # NOTE: Linear personal API keys do NOT use a "Bearer" prefix in the Authorization
823
+ # header. OAuth access tokens do. This is correct per Linear's documentation.
824
+ #
825
+ # NOTE: state.type "unstarted" is a fixed enum value — it filters by state category,
826
+ # not state name. This works regardless of what your team named their backlog column.
827
+ #
828
+ # NOTE: Failures use exit 0, not exit 1. This is intentional — clancy-afk.sh
829
+ # detects stop conditions by reading script output rather than exit codes, so a
830
+ # non-zero exit would be treated as an unexpected crash rather than a clean stop.
831
+ #
832
+ # ───────────────────────────────────────────────────────────────────────────────
833
+
834
+ # ─── PREFLIGHT ─────────────────────────────────────────────────────────────────
835
+
836
+ command -v claude >/dev/null 2>&1 || {
837
+ echo "✗ claude CLI not found."
838
+ echo " Install it: https://claude.ai/code"
839
+ exit 0
840
+ }
841
+ command -v jq >/dev/null 2>&1 || {
842
+ echo "✗ jq not found."
843
+ echo " Install: brew install jq (mac) | apt install jq (linux)"
844
+ exit 0
845
+ }
846
+ command -v curl >/dev/null 2>&1 || {
847
+ echo "✗ curl not found. Install curl for your OS."
848
+ exit 0
849
+ }
850
+
851
+ [ -f .clancy/.env ] || {
852
+ echo "✗ .clancy/.env not found."
853
+ echo " Copy .clancy/.env.example to .clancy/.env and fill in your credentials."
854
+ echo " Then run: /clancy:init"
855
+ exit 0
856
+ }
857
+ # shellcheck source=/dev/null
858
+ source .clancy/.env
859
+
860
+ git rev-parse --git-dir >/dev/null 2>&1 || {
861
+ echo "✗ Not a git repository."
862
+ echo " Clancy must be run from the root of a git project."
863
+ exit 0
864
+ }
865
+
866
+ if ! git diff --quiet || ! git diff --cached --quiet; then
867
+ echo "⚠ Working directory has uncommitted changes."
868
+ echo " Consider stashing or committing first to avoid confusion."
869
+ fi
870
+
871
+ [ -n "${LINEAR_API_KEY:-}" ] || { echo "✗ LINEAR_API_KEY is not set in .clancy/.env"; exit 0; }
872
+ [ -n "${LINEAR_TEAM_ID:-}" ] || { echo "✗ LINEAR_TEAM_ID is not set in .clancy/.env"; exit 0; }
873
+
874
+ # Linear ping — verify API key with a minimal query
875
+ # Note: personal API keys do NOT use a "Bearer" prefix — this is correct per Linear docs.
876
+ # OAuth access tokens use "Bearer". Do not change this.
877
+ PING_BODY=$(curl -s -X POST https://api.linear.app/graphql \
878
+ -H "Content-Type: application/json" \
879
+ -H "Authorization: $LINEAR_API_KEY" \
880
+ -d '{"query": "{ viewer { id } }"}')
881
+
882
+ echo "$PING_BODY" | jq -e '.data.viewer.id' >/dev/null 2>&1 || {
883
+ echo "✗ Linear authentication failed. Check LINEAR_API_KEY in .clancy/.env."; exit 0
884
+ }
885
+
886
+ if [ "${PLAYWRIGHT_ENABLED:-}" = "true" ]; then
887
+ if lsof -ti:"${PLAYWRIGHT_DEV_PORT:-5173}" >/dev/null 2>&1; then
888
+ echo "⚠ Port ${PLAYWRIGHT_DEV_PORT:-5173} is already in use."
889
+ echo " If visual checks fail, stop whatever is using the port first."
890
+ fi
891
+ fi
892
+
893
+ echo "✓ Preflight passed. Starting Clancy..."
894
+
895
+ # ─── END PREFLIGHT ─────────────────────────────────────────────────────────────
896
+
897
+ # ─── FETCH ISSUE ───────────────────────────────────────────────────────────────
898
+
899
+ # Fetch one unstarted issue assigned to the current user on the configured team.
900
+ # Note: personal API keys do NOT use "Bearer" prefix — this is intentional.
901
+ #
902
+ # GraphQL query (expanded for readability):
903
+ # viewer {
904
+ # assignedIssues(
905
+ # filter: {
906
+ # state: { type: { eq: "unstarted" } } ← fixed enum, works regardless of column name
907
+ # team: { id: { eq: "$LINEAR_TEAM_ID" } }
908
+ # labels: { name: { eq: "$CLANCY_LABEL" } } ← only if CLANCY_LABEL is set
909
+ # }
910
+ # first: 1
911
+ # orderBy: priority
912
+ # ) {
913
+ # nodes { id identifier title description parent { identifier title } }
914
+ # }
915
+ # }
916
+
917
+ # Validate user-controlled values to prevent GraphQL injection.
918
+ # Values are passed via GraphQL variables (JSON-encoded by jq) rather than string-interpolated.
919
+ if ! echo "$LINEAR_TEAM_ID" | grep -qE '^[a-zA-Z0-9_-]+$'; then
920
+ echo "✗ LINEAR_TEAM_ID contains invalid characters. Check .clancy/.env."
921
+ exit 0
922
+ fi
923
+ if [ -n "${CLANCY_LABEL:-}" ] && ! echo "$CLANCY_LABEL" | grep -qE '^[a-zA-Z0-9 _-]+$'; then
924
+ echo "✗ CLANCY_LABEL contains invalid characters. Use only letters, numbers, spaces, hyphens, and underscores."
925
+ exit 0
926
+ fi
927
+
928
+ # Build request using GraphQL variables — values are JSON-encoded by jq, never interpolated into the query string.
929
+ # The label filter clause is only added to the query when CLANCY_LABEL is set, since passing null would match nothing.
930
+ if [ -n "${CLANCY_LABEL:-}" ]; then
931
+ REQUEST_BODY=$(jq -n \
932
+ --arg teamId "$LINEAR_TEAM_ID" \
933
+ --arg label "$CLANCY_LABEL" \
934
+ '{"query": "query($teamId: String!, $label: String) { viewer { assignedIssues(filter: { state: { type: { eq: \"unstarted\" } } team: { id: { eq: $teamId } } labels: { name: { eq: $label } } } first: 1 orderBy: priority) { nodes { id identifier title description parent { identifier title } } } } }", "variables": {"teamId": $teamId, "label": $label}}')
935
+ else
936
+ REQUEST_BODY=$(jq -n \
937
+ --arg teamId "$LINEAR_TEAM_ID" \
938
+ '{"query": "query($teamId: String!) { viewer { assignedIssues(filter: { state: { type: { eq: \"unstarted\" } } team: { id: { eq: $teamId } } } first: 1 orderBy: priority) { nodes { id identifier title description parent { identifier title } } } } }", "variables": {"teamId": $teamId}}')
939
+ fi
940
+
941
+ RESPONSE=$(curl -s -X POST https://api.linear.app/graphql \
942
+ -H "Content-Type: application/json" \
943
+ -H "Authorization: $LINEAR_API_KEY" \
944
+ -d "$REQUEST_BODY")
945
+
946
+ # Check for API errors before parsing (rate limit, permission error, etc.)
947
+ if ! echo "$RESPONSE" | jq -e '.data.viewer.assignedIssues' >/dev/null 2>&1; then
948
+ ERR_MSG=$(echo "$RESPONSE" | jq -r '.errors[0].message // "Unexpected response"' 2>/dev/null || echo "Unexpected response")
949
+ echo "✗ Linear API error: $ERR_MSG. Check LINEAR_API_KEY in .clancy/.env."
950
+ exit 0
951
+ fi
952
+
953
+ NODE_COUNT=$(echo "$RESPONSE" | jq '.data.viewer.assignedIssues.nodes | length')
954
+ if [ "$NODE_COUNT" -eq 0 ]; then
955
+ echo "No issues found. All done!"
956
+ exit 0
957
+ fi
958
+
959
+ IDENTIFIER=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].identifier')
960
+ TITLE=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].title')
961
+ DESCRIPTION=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].description // "No description"')
962
+ PARENT_ID=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].parent.identifier // "none"')
963
+ PARENT_TITLE=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].parent.title // ""')
964
+
965
+ EPIC_INFO="${PARENT_ID}"
966
+ if [ -n "$PARENT_TITLE" ] && [ "$PARENT_TITLE" != "null" ]; then
967
+ EPIC_INFO="${PARENT_ID} — ${PARENT_TITLE}"
968
+ fi
969
+
970
+ BASE_BRANCH="${CLANCY_BASE_BRANCH:-main}"
971
+ TICKET_BRANCH="feature/$(echo "$IDENTIFIER" | tr '[:upper:]' '[:lower:]')"
972
+
973
+ # Auto-detect target branch from ticket's parent.
974
+ # If the issue has a parent, branch from epic/{parent-id} (creating it from
975
+ # BASE_BRANCH if it doesn't exist yet). Otherwise branch from BASE_BRANCH directly.
976
+ if [ "$PARENT_ID" != "none" ]; then
977
+ 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
+ else
981
+ TARGET_BRANCH="$BASE_BRANCH"
982
+ fi
983
+
984
+ # ─── IMPLEMENT ─────────────────────────────────────────────────────────────────
985
+
986
+ echo "Picking up: [$IDENTIFIER] $TITLE"
987
+ echo "Epic: $EPIC_INFO | Target branch: $TARGET_BRANCH"
988
+
989
+ git checkout "$TARGET_BRANCH"
990
+ # -B creates the branch if it doesn't exist, or resets it to HEAD if it does.
991
+ # This handles retries cleanly without failing on an already-existing branch.
992
+ git checkout -B "$TICKET_BRANCH"
993
+
994
+ PROMPT="You are implementing Linear issue $IDENTIFIER.
995
+
996
+ Title: $TITLE
997
+ Epic: $EPIC_INFO
998
+
999
+ Description:
1000
+ $DESCRIPTION
1001
+
1002
+ Step 0 — Executability check (do this before any git or file operation):
1003
+ Read the issue title and description above. Can this issue be implemented entirely
1004
+ as a code change committed to this repo? Consult the 'Executability check' section of
1005
+ CLAUDE.md for the full list of skip conditions.
1006
+
1007
+ If you must SKIP this issue:
1008
+ 1. Output: ⚠ Skipping [$IDENTIFIER]: {one-line reason}
1009
+ 2. Output: Ticket skipped — update it to be codebase-only work, then re-run.
1010
+ 3. Append to .clancy/progress.txt: YYYY-MM-DD HH:MM | $IDENTIFIER | SKIPPED | {reason}
1011
+ 4. Stop — no branches, no file changes, no git operations.
1012
+
1013
+ If the issue IS implementable, continue:
1014
+ 1. Read ALL docs in .clancy/docs/ — especially GIT.md for branching and commit conventions
1015
+ 2. Follow the conventions in GIT.md exactly
1016
+ 3. Implement the issue fully
1017
+ 4. Commit your work following the conventions in GIT.md
1018
+ 5. When done, confirm you are finished."
1019
+
1020
+ CLAUDE_ARGS=(--dangerously-skip-permissions)
1021
+ [ -n "${CLANCY_MODEL:-}" ] && CLAUDE_ARGS+=(--model "$CLANCY_MODEL")
1022
+ echo "$PROMPT" | claude "${CLAUDE_ARGS[@]}"
1023
+
1024
+ # ─── MERGE & LOG ───────────────────────────────────────────────────────────────
1025
+
1026
+ # Squash all commits from the feature branch into a single commit on the target branch.
1027
+ git checkout "$TARGET_BRANCH"
1028
+ git merge --squash "$TICKET_BRANCH"
1029
+ if git diff --cached --quiet; then
1030
+ echo "⚠ No changes staged after squash merge. Claude may not have committed any work."
1031
+ else
1032
+ git commit -m "feat($IDENTIFIER): $TITLE"
1033
+ fi
1034
+
1035
+ # Delete ticket branch locally
1036
+ git branch -d "$TICKET_BRANCH"
1037
+
1038
+ # Log progress
1039
+ echo "$(date '+%Y-%m-%d %H:%M') | $IDENTIFIER | $TITLE | DONE" >> .clancy/progress.txt
1040
+
1041
+ echo "✓ $IDENTIFIER complete."
1042
+
1043
+ # Send completion notification if webhook is configured
1044
+ if [ -n "${CLANCY_NOTIFY_WEBHOOK:-}" ]; then
1045
+ NOTIFY_MSG="✓ Clancy completed [$IDENTIFIER] $TITLE"
1046
+ if echo "$CLANCY_NOTIFY_WEBHOOK" | grep -q "hooks.slack.com"; then
1047
+ curl -s -X POST "$CLANCY_NOTIFY_WEBHOOK" \
1048
+ -H "Content-Type: application/json" \
1049
+ -d "$(jq -n --arg text "$NOTIFY_MSG" '{"text": $text}')" >/dev/null 2>&1 || true
1050
+ else
1051
+ curl -s -X POST "$CLANCY_NOTIFY_WEBHOOK" \
1052
+ -H "Content-Type: application/json" \
1053
+ -d "$(jq -n --arg text "$NOTIFY_MSG" '{"type":"message","attachments":[{"contentType":"application/vnd.microsoft.card.adaptive","content":{"type":"AdaptiveCard","body":[{"type":"TextBlock","text":$text}]}}]}')" >/dev/null 2>&1 || true
1054
+ fi
1055
+ fi
1056
+ ```
1057
+
1058
+ ---
1059
+
1060
+ ### `.clancy/clancy-afk.sh` — all boards
1061
+
1062
+ Write this file regardless of board choice:
1063
+
1064
+ ```bash
1065
+ #!/usr/bin/env bash
1066
+ # Strict mode: exit on error (-e), undefined variables (-u), pipe failures (-o pipefail).
1067
+ # This means any command that fails will stop the script immediately rather than silently continuing.
1068
+ set -euo pipefail
1069
+
1070
+ # ─── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────
1071
+ #
1072
+ # Loop runner for Clancy. Calls clancy-once.sh repeatedly until:
1073
+ # - No more tickets are found ("No tickets found", "All done", etc.)
1074
+ # - A preflight check fails (output line starting with ✗)
1075
+ # - MAX_ITERATIONS is reached
1076
+ # - The user presses Ctrl+C
1077
+ #
1078
+ # This script does not know about boards. All board logic lives in clancy-once.sh,
1079
+ # which is always the runtime filename regardless of which board is configured.
1080
+ # /clancy:init copies the correct board variant as clancy-once.sh during setup.
1081
+ #
1082
+ # ───────────────────────────────────────────────────────────────────────────────
1083
+
1084
+ # ─── PREFLIGHT ─────────────────────────────────────────────────────────────────
1085
+
1086
+ command -v claude >/dev/null 2>&1 || {
1087
+ echo "✗ claude CLI not found."
1088
+ echo " Install it: https://claude.ai/code"
1089
+ exit 0
1090
+ }
1091
+ command -v jq >/dev/null 2>&1 || {
1092
+ echo "✗ jq not found."
1093
+ echo " Install: brew install jq (mac) | apt install jq (linux)"
1094
+ exit 0
1095
+ }
1096
+ command -v curl >/dev/null 2>&1 || {
1097
+ echo "✗ curl not found. Install curl for your OS."
1098
+ exit 0
1099
+ }
1100
+
1101
+ [ -f .clancy/.env ] || {
1102
+ echo "✗ .clancy/.env not found."
1103
+ echo " Copy .clancy/.env.example to .clancy/.env and fill in your credentials."
1104
+ exit 0
1105
+ }
1106
+ # shellcheck source=/dev/null
1107
+ source .clancy/.env
1108
+
1109
+ git rev-parse --git-dir >/dev/null 2>&1 || {
1110
+ echo "✗ Not a git repository."
1111
+ echo " Clancy must be run from the root of a git project."
1112
+ exit 0
1113
+ }
1114
+
1115
+ # ─── END PREFLIGHT ─────────────────────────────────────────────────────────────
1116
+
1117
+ MAX_ITERATIONS=${MAX_ITERATIONS:-5}
1118
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
1119
+
1120
+ # clancy-once.sh is always the runtime filename regardless of board.
1121
+ # /clancy:init copies the correct board variant as clancy-once.sh.
1122
+ ONCE_SCRIPT="$SCRIPT_DIR/clancy-once.sh"
1123
+
1124
+ if [ ! -f "$ONCE_SCRIPT" ]; then
1125
+ echo "✗ Script not found: $ONCE_SCRIPT"
1126
+ echo " Run /clancy:init to scaffold scripts."
1127
+ exit 0
1128
+ fi
1129
+
1130
+ echo "Starting Clancy — will process up to $MAX_ITERATIONS ticket(s). Ctrl+C to stop early."
1131
+ echo ""
1132
+
1133
+ i=0
1134
+ while [ "$i" -lt "$MAX_ITERATIONS" ]; do
1135
+ i=$((i + 1))
1136
+ echo ""
1137
+ echo "=== Iteration $i of $MAX_ITERATIONS ==="
1138
+
1139
+ # Run clancy-once.sh and stream its output live via tee.
1140
+ # tee writes to both stdout (visible to user) and a temp file (for stop-condition checks).
1141
+ # Without tee, output would be buffered in a variable and hidden during implementation.
1142
+ TMPFILE=$(mktemp)
1143
+ bash "$ONCE_SCRIPT" 2>&1 | tee "$TMPFILE"
1144
+ OUTPUT=$(cat "$TMPFILE")
1145
+ rm -f "$TMPFILE"
1146
+
1147
+ # Stop if no tickets remain
1148
+ if echo "$OUTPUT" | grep -qE "No tickets found|No issues found|All done"; then
1149
+ echo ""
1150
+ echo "✓ Clancy finished — no more tickets."
1151
+ exit 0
1152
+ fi
1153
+
1154
+ # Stop if Claude skipped the ticket (not implementable from the codebase).
1155
+ # Re-running would just fetch and skip the same ticket again — stop and let
1156
+ # the user update the ticket or remove it from the queue before continuing.
1157
+ if echo "$OUTPUT" | grep -q "Ticket skipped"; then
1158
+ echo ""
1159
+ echo "⚠ Clancy stopped — ticket was skipped (not implementable from the codebase)."
1160
+ echo " Update the ticket to focus on codebase work, then re-run."
1161
+ exit 0
1162
+ fi
1163
+
1164
+ # Stop if a preflight check failed (lines starting with ✗)
1165
+ if echo "$OUTPUT" | grep -qE "^✗ "; then
1166
+ echo ""
1167
+ echo "✗ Clancy stopped — preflight check failed. See output above."
1168
+ exit 0
1169
+ fi
1170
+
1171
+ sleep 2
1172
+ done
1173
+
1174
+ echo ""
1175
+ echo "Reached max iterations ($MAX_ITERATIONS). Run clancy-afk.sh again to continue."
1176
+ ```
1177
+
1178
+ ---
1179
+
1180
+ ## .env.example files
1181
+
1182
+ Write the correct `.env.example` for the chosen board to `.clancy/.env.example`.
1183
+
1184
+ ### Jira
1185
+
1186
+ ```
1187
+ # Clancy — Jira configuration
1188
+ # Copy this file to .env and fill in your values.
1189
+ # Never commit .env to version control.
1190
+
1191
+ # ─── Jira ─────────────────────────────────────────────────────────────────────
1192
+ JIRA_BASE_URL=https://your-org.atlassian.net
1193
+ JIRA_USER=your-email@example.com
1194
+ JIRA_API_TOKEN=your-api-token-from-id.atlassian.com
1195
+ JIRA_PROJECT_KEY=PROJ
1196
+
1197
+ # Status name for "ready to be picked up" (default: To Do)
1198
+ # Must be quoted if the status name contains spaces (e.g. "Selected for Development")
1199
+ CLANCY_JQL_STATUS="To Do"
1200
+
1201
+ # Set to any non-empty value to filter by open sprints (requires Jira Software)
1202
+ # Remove or leave empty if your project doesn't use sprints
1203
+ # CLANCY_JQL_SPRINT=true
1204
+
1205
+ # Optional: only pick up tickets with this label. Recommended for mixed backlogs
1206
+ # where not every ticket is suitable for autonomous implementation (e.g. non-code tasks).
1207
+ # Create the label in Jira first, then add it to any ticket you want Clancy to pick up.
1208
+ # CLANCY_LABEL="clancy"
1209
+
1210
+ # ─── Git ──────────────────────────────────────────────────────────────────────
1211
+ # Base integration branch. Clancy branches from here when a ticket has no parent epic.
1212
+ # When a ticket has a parent epic, Clancy auto-creates epic/{key} from this branch.
1213
+ CLANCY_BASE_BRANCH=main
1214
+
1215
+ # ─── Loop ─────────────────────────────────────────────────────────────────────
1216
+ # Max tickets to process per /clancy:run session (default: 5)
1217
+ MAX_ITERATIONS=5
1218
+
1219
+ # ─── Model ────────────────────────────────────────────────────────────────────
1220
+ # Claude model used for each ticket session. Leave unset to use the default.
1221
+ # Options: claude-opus-4-6 | claude-sonnet-4-6 | claude-haiku-4-5
1222
+ # CLANCY_MODEL=claude-sonnet-4-6
1223
+
1224
+ # ─── Optional: Figma MCP ──────────────────────────────────────────────────────
1225
+ # Fetch design specs from Figma when a ticket has a Figma URL in its description
1226
+ # FIGMA_API_KEY=your-figma-api-key
1227
+
1228
+ # ─── Optional: Playwright visual checks ───────────────────────────────────────
1229
+ # Run a visual check after implementing UI tickets
1230
+ # PLAYWRIGHT_ENABLED=true
1231
+ # PLAYWRIGHT_DEV_COMMAND="yarn dev"
1232
+ # PLAYWRIGHT_DEV_PORT=5173
1233
+ # PLAYWRIGHT_STORYBOOK_COMMAND="yarn storybook"
1234
+ # PLAYWRIGHT_STORYBOOK_PORT=6006
1235
+ # PLAYWRIGHT_STARTUP_WAIT=15
1236
+
1237
+ # ─── Optional: Notifications ──────────────────────────────────────────────────
1238
+ # Webhook URL for Slack or Teams notifications on ticket completion
1239
+ # CLANCY_NOTIFY_WEBHOOK=https://hooks.slack.com/services/your/webhook/url
1240
+ ```
1241
+
1242
+ ### GitHub Issues
1243
+
1244
+ ```
1245
+ # Clancy — GitHub Issues configuration
1246
+ # Copy this file to .env and fill in your values.
1247
+ # Never commit .env to version control.
1248
+
1249
+ # ─── GitHub Issues ────────────────────────────────────────────────────────────
1250
+ GITHUB_TOKEN=ghp_your-personal-access-token
1251
+ GITHUB_REPO=owner/repo-name
1252
+
1253
+ # Optional: only pick up issues with this label (in addition to 'clancy').
1254
+ # Useful for mixed backlogs where not every issue is suitable for autonomous implementation.
1255
+ # Create the label in GitHub first, then add it to any issue you want Clancy to pick up.
1256
+ # CLANCY_LABEL=clancy
1257
+
1258
+ # ─── Git ──────────────────────────────────────────────────────────────────────
1259
+ # Base integration branch. Clancy branches from here when an issue has no milestone.
1260
+ # When an issue has a milestone, Clancy auto-creates milestone/{slug} from this branch.
1261
+ CLANCY_BASE_BRANCH=main
1262
+
1263
+ # ─── Loop ─────────────────────────────────────────────────────────────────────
1264
+ # Max tickets to process per /clancy:run session (default: 20)
1265
+ MAX_ITERATIONS=20
1266
+
1267
+ # ─── Model ────────────────────────────────────────────────────────────────────
1268
+ # Claude model used for each ticket session. Leave unset to use the default.
1269
+ # Options: claude-opus-4-6 | claude-sonnet-4-6 | claude-haiku-4-5
1270
+ # CLANCY_MODEL=claude-sonnet-4-6
1271
+
1272
+ # ─── Optional: Figma MCP ──────────────────────────────────────────────────────
1273
+ # Fetch design specs from Figma when a ticket has a Figma URL in its description
1274
+ # FIGMA_API_KEY=your-figma-api-key
1275
+
1276
+ # ─── Optional: Playwright visual checks ───────────────────────────────────────
1277
+ # Run a visual check after implementing UI tickets
1278
+ # PLAYWRIGHT_ENABLED=true
1279
+ # PLAYWRIGHT_DEV_COMMAND="yarn dev"
1280
+ # PLAYWRIGHT_DEV_PORT=5173
1281
+ # PLAYWRIGHT_STORYBOOK_COMMAND="yarn storybook"
1282
+ # PLAYWRIGHT_STORYBOOK_PORT=6006
1283
+ # PLAYWRIGHT_STARTUP_WAIT=15
1284
+
1285
+ # ─── Optional: Notifications ──────────────────────────────────────────────────
1286
+ # Webhook URL for Slack or Teams notifications on ticket completion
1287
+ # CLANCY_NOTIFY_WEBHOOK=https://hooks.slack.com/services/your/webhook/url
1288
+ ```
1289
+
1290
+ ### Linear
1291
+
1292
+ ```
1293
+ # Clancy — Linear configuration
1294
+ # Copy this file to .env and fill in your values.
1295
+ # Never commit .env to version control.
1296
+
1297
+ # ─── Linear ───────────────────────────────────────────────────────────────────
1298
+ LINEAR_API_KEY=lin_api_your-personal-api-key
1299
+ LINEAR_TEAM_ID=your-team-uuid
1300
+
1301
+ # Optional: only pick up issues with this label. Recommended for mixed backlogs
1302
+ # where not every issue is suitable for autonomous implementation (e.g. non-code tasks).
1303
+ # Create the label in Linear first, then add it to any issue you want Clancy to pick up.
1304
+ # CLANCY_LABEL=clancy
1305
+
1306
+ # ─── Git ──────────────────────────────────────────────────────────────────────
1307
+ # Base integration branch. Clancy branches from here when an issue has no parent.
1308
+ # When an issue has a parent, Clancy auto-creates epic/{key} from this branch.
1309
+ CLANCY_BASE_BRANCH=main
1310
+
1311
+ # ─── Loop ─────────────────────────────────────────────────────────────────────
1312
+ # Max tickets to process per /clancy:run session (default: 20)
1313
+ MAX_ITERATIONS=20
1314
+
1315
+ # ─── Model ────────────────────────────────────────────────────────────────────
1316
+ # Claude model used for each ticket session. Leave unset to use the default.
1317
+ # Options: claude-opus-4-6 | claude-sonnet-4-6 | claude-haiku-4-5
1318
+ # CLANCY_MODEL=claude-sonnet-4-6
1319
+
1320
+ # ─── Optional: Figma MCP ──────────────────────────────────────────────────────
1321
+ # Fetch design specs from Figma when a ticket has a Figma URL in its description
1322
+ # FIGMA_API_KEY=your-figma-api-key
1323
+
1324
+ # ─── Optional: Playwright visual checks ───────────────────────────────────────
1325
+ # Run a visual check after implementing UI tickets
1326
+ # PLAYWRIGHT_ENABLED=true
1327
+ # PLAYWRIGHT_DEV_COMMAND="yarn dev"
1328
+ # PLAYWRIGHT_DEV_PORT=5173
1329
+ # PLAYWRIGHT_STORYBOOK_COMMAND="yarn storybook"
1330
+ # PLAYWRIGHT_STORYBOOK_PORT=6006
1331
+ # PLAYWRIGHT_STARTUP_WAIT=15
1332
+
1333
+ # ─── Optional: Notifications ──────────────────────────────────────────────────
1334
+ # Webhook URL for Slack or Teams notifications on ticket completion
1335
+ # CLANCY_NOTIFY_WEBHOOK=https://hooks.slack.com/services/your/webhook/url
1336
+ ```