chief-clancy 0.1.2 → 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.2",
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": {
@@ -317,6 +317,10 @@ set -euo pipefail
317
317
  #
318
318
  # This script is run once per ticket. The loop is handled by clancy-afk.sh.
319
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
+ #
320
324
  # NOTE: Failures use exit 0, not exit 1. This is intentional — clancy-afk.sh
321
325
  # detects stop conditions by reading script output rather than exit codes, so a
322
326
  # non-zero exit would be treated as an unexpected crash rather than a clean stop.
@@ -417,6 +421,7 @@ else
417
421
  fi
418
422
 
419
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.
420
425
  if [ -n "${CLANCY_LABEL:-}" ]; then
421
426
  LABEL_CLAUSE="AND labels = \"$CLANCY_LABEL\""
422
427
  else
@@ -469,6 +474,9 @@ BLOCKERS=$(echo "$RESPONSE" | jq -r '
469
474
  BASE_BRANCH="${CLANCY_BASE_BRANCH:-main}"
470
475
  TICKET_BRANCH="feature/$(echo "$TICKET_KEY" | tr '[:upper:]' '[:lower:]')"
471
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.
472
480
  if [ "$EPIC_INFO" != "none" ]; then
473
481
  TARGET_BRANCH="epic/$(echo "$EPIC_INFO" | tr '[:upper:]' '[:lower:]')"
474
482
  git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
@@ -483,6 +491,8 @@ echo "Picking up: [$TICKET_KEY] $SUMMARY"
483
491
  echo "Epic: $EPIC_INFO | Target branch: $TARGET_BRANCH | Blockers: $BLOCKERS"
484
492
 
485
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.
486
496
  git checkout -B "$TICKET_BRANCH"
487
497
 
488
498
  PROMPT="You are implementing Jira ticket $TICKET_KEY.
@@ -518,6 +528,7 @@ echo "$PROMPT" | claude "${CLAUDE_ARGS[@]}"
518
528
 
519
529
  # ─── MERGE & LOG ───────────────────────────────────────────────────────────────
520
530
 
531
+ # Squash all commits from the feature branch into a single commit on the target branch.
521
532
  git checkout "$TARGET_BRANCH"
522
533
  git merge --squash "$TICKET_BRANCH"
523
534
  if git diff --cached --quiet; then
@@ -526,12 +537,15 @@ else
526
537
  git commit -m "feat($TICKET_KEY): $SUMMARY"
527
538
  fi
528
539
 
540
+ # Delete ticket branch locally (never push deletes)
529
541
  git branch -d "$TICKET_BRANCH"
530
542
 
543
+ # Log progress
531
544
  echo "$(date '+%Y-%m-%d %H:%M') | $TICKET_KEY | $SUMMARY | DONE" >> .clancy/progress.txt
532
545
 
533
546
  echo "✓ $TICKET_KEY complete."
534
547
 
548
+ # Send completion notification if webhook is configured
535
549
  if [ -n "${CLANCY_NOTIFY_WEBHOOK:-}" ]; then
536
550
  NOTIFY_MSG="✓ Clancy completed [$TICKET_KEY] $SUMMARY"
537
551
  if echo "$CLANCY_NOTIFY_WEBHOOK" | grep -q "hooks.slack.com"; then
@@ -554,10 +568,14 @@ Write this file when the chosen board is **GitHub Issues**:
554
568
 
555
569
  ```bash
556
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.
557
573
  set -euo pipefail
558
574
 
559
575
  # ─── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────
576
+ #
560
577
  # Board: GitHub Issues
578
+ #
561
579
  # 1. Preflight — checks all required tools, credentials, and repo reachability
562
580
  # 2. Fetch — pulls the next open issue with the 'clancy' label assigned to you
563
581
  # 3. Branch — creates a feature branch from the issue's milestone branch (or base branch)
@@ -566,20 +584,48 @@ set -euo pipefail
566
584
  # 6. Close — marks the GitHub issue as closed via the API
567
585
  # 7. Log — appends a completion entry to .clancy/progress.txt
568
586
  #
587
+ # This script is run once per issue. The loop is handled by clancy-afk.sh.
588
+ #
569
589
  # NOTE: GitHub's /issues endpoint returns pull requests too. This script filters
570
590
  # them out by checking for the presence of the 'pull_request' key in each result.
571
- # NOTE: Failures use exit 0, not exit 1.
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
+ #
572
596
  # ───────────────────────────────────────────────────────────────────────────────
573
597
 
574
- command -v claude >/dev/null 2>&1 || { echo "✗ claude CLI not found. Install it: https://claude.ai/code"; exit 0; }
575
- command -v jq >/dev/null 2>&1 || { echo "✗ jq not found. Install: brew install jq (mac) | apt install jq (linux)"; exit 0; }
576
- command -v curl >/dev/null 2>&1 || { echo "✗ curl not found. Install curl for your OS."; exit 0; }
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
+ }
577
614
 
578
- [ -f .clancy/.env ] || { echo "✗ .clancy/.env not found. Run /clancy:init"; exit 0; }
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
+ }
579
621
  # shellcheck source=/dev/null
580
622
  source .clancy/.env
581
623
 
582
- git rev-parse --git-dir >/dev/null 2>&1 || { echo "✗ Not a git repository."; exit 0; }
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
+ }
583
629
 
584
630
  if ! git diff --quiet || ! git diff --cached --quiet; then
585
631
  echo "⚠ Working directory has uncommitted changes."
@@ -589,8 +635,9 @@ fi
589
635
  [ -n "${GITHUB_TOKEN:-}" ] || { echo "✗ GITHUB_TOKEN is not set in .clancy/.env"; exit 0; }
590
636
  [ -n "${GITHUB_REPO:-}" ] || { echo "✗ GITHUB_REPO is not set in .clancy/.env"; exit 0; }
591
637
 
638
+ # Validate GITHUB_REPO format — must be owner/repo with safe characters only
592
639
  if ! echo "$GITHUB_REPO" | grep -qE '^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+$'; then
593
- echo "✗ GITHUB_REPO format is invalid. Expected owner/repo (e.g. acme/my-app)."
640
+ echo "✗ GITHUB_REPO format is invalid. Expected owner/repo (e.g. acme/my-app). Check GITHUB_REPO in .clancy/.env."
594
641
  exit 0
595
642
  fi
596
643
 
@@ -611,22 +658,32 @@ esac
611
658
  if [ "${PLAYWRIGHT_ENABLED:-}" = "true" ]; then
612
659
  if lsof -ti:"${PLAYWRIGHT_DEV_PORT:-5173}" >/dev/null 2>&1; then
613
660
  echo "⚠ Port ${PLAYWRIGHT_DEV_PORT:-5173} is already in use."
661
+ echo " If visual checks fail, stop whatever is using the port first."
614
662
  fi
615
663
  fi
616
664
 
617
665
  echo "✓ Preflight passed. Starting Clancy..."
618
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.
619
674
  RESPONSE=$(curl -s \
620
675
  -H "Authorization: Bearer $GITHUB_TOKEN" \
621
676
  -H "X-GitHub-Api-Version: 2022-11-28" \
622
677
  "https://api.github.com/repos/$GITHUB_REPO/issues?state=open&assignee=@me&labels=clancy&per_page=3")
623
678
 
679
+ # Verify response is an array before parsing (guards against error objects on rate limit / transient failure)
624
680
  if ! echo "$RESPONSE" | jq -e 'type == "array"' >/dev/null 2>&1; then
625
681
  ERR_MSG=$(echo "$RESPONSE" | jq -r '.message // "Unexpected response"' 2>/dev/null || echo "Unexpected response")
626
682
  echo "✗ GitHub API error: $ERR_MSG. Check GITHUB_TOKEN in .clancy/.env."
627
683
  exit 0
628
684
  fi
629
685
 
686
+ # Filter out PRs and take first real issue
630
687
  ISSUE=$(echo "$RESPONSE" | jq 'map(select(has("pull_request") | not)) | .[0]')
631
688
 
632
689
  if [ "$(echo "$ISSUE" | jq 'type')" = '"null"' ] || [ -z "$(echo "$ISSUE" | jq -r '.number // empty')" ]; then
@@ -642,6 +699,9 @@ MILESTONE=$(echo "$ISSUE" | jq -r '.milestone.title // "none"')
642
699
  BASE_BRANCH="${CLANCY_BASE_BRANCH:-main}"
643
700
  TICKET_BRANCH="feature/issue-${ISSUE_NUMBER}"
644
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.
645
705
  if [ "$MILESTONE" != "none" ]; then
646
706
  MILESTONE_SLUG=$(echo "$MILESTONE" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd '[:alnum:]-')
647
707
  TARGET_BRANCH="milestone/${MILESTONE_SLUG}"
@@ -651,10 +711,14 @@ else
651
711
  TARGET_BRANCH="$BASE_BRANCH"
652
712
  fi
653
713
 
714
+ # ─── IMPLEMENT ─────────────────────────────────────────────────────────────────
715
+
654
716
  echo "Picking up: [#${ISSUE_NUMBER}] $TITLE"
655
717
  echo "Milestone: $MILESTONE | Target branch: $TARGET_BRANCH"
656
718
 
657
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.
658
722
  git checkout -B "$TICKET_BRANCH"
659
723
 
660
724
  PROMPT="You are implementing GitHub Issue #${ISSUE_NUMBER}.
@@ -687,6 +751,9 @@ CLAUDE_ARGS=(--dangerously-skip-permissions)
687
751
  [ -n "${CLANCY_MODEL:-}" ] && CLAUDE_ARGS+=(--model "$CLANCY_MODEL")
688
752
  echo "$PROMPT" | claude "${CLAUDE_ARGS[@]}"
689
753
 
754
+ # ─── MERGE, CLOSE & LOG ────────────────────────────────────────────────────────
755
+
756
+ # Squash all commits from the feature branch into a single commit on the target branch.
690
757
  git checkout "$TARGET_BRANCH"
691
758
  git merge --squash "$TICKET_BRANCH"
692
759
  if git diff --cached --quiet; then
@@ -695,8 +762,10 @@ else
695
762
  git commit -m "feat(#${ISSUE_NUMBER}): $TITLE"
696
763
  fi
697
764
 
765
+ # Delete ticket branch locally
698
766
  git branch -d "$TICKET_BRANCH"
699
767
 
768
+ # Close the issue — warn but don't fail if this doesn't go through
700
769
  CLOSE_HTTP=$(curl -s -o /dev/null -w "%{http_code}" -X PATCH \
701
770
  -H "Authorization: Bearer $GITHUB_TOKEN" \
702
771
  -H "X-GitHub-Api-Version: 2022-11-28" \
@@ -705,10 +774,12 @@ CLOSE_HTTP=$(curl -s -o /dev/null -w "%{http_code}" -X PATCH \
705
774
  -d '{"state": "closed"}')
706
775
  [ "$CLOSE_HTTP" = "200" ] || echo "⚠ Could not close issue #${ISSUE_NUMBER} (HTTP $CLOSE_HTTP). Close it manually on GitHub."
707
776
 
777
+ # Log progress
708
778
  echo "$(date '+%Y-%m-%d %H:%M') | #${ISSUE_NUMBER} | $TITLE | DONE" >> .clancy/progress.txt
709
779
 
710
780
  echo "✓ #${ISSUE_NUMBER} complete."
711
781
 
782
+ # Send completion notification if webhook is configured
712
783
  if [ -n "${CLANCY_NOTIFY_WEBHOOK:-}" ]; then
713
784
  NOTIFY_MSG="✓ Clancy completed [#${ISSUE_NUMBER}] $TITLE"
714
785
  if echo "$CLANCY_NOTIFY_WEBHOOK" | grep -q "hooks.slack.com"; then
@@ -731,10 +802,14 @@ Write this file when the chosen board is **Linear**:
731
802
 
732
803
  ```bash
733
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.
734
807
  set -euo pipefail
735
808
 
736
809
  # ─── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────
810
+ #
737
811
  # Board: Linear
812
+ #
738
813
  # 1. Preflight — checks all required tools, credentials, and API reachability
739
814
  # 2. Fetch — pulls the next unstarted issue assigned to you via GraphQL
740
815
  # 3. Branch — creates a feature branch from the issue's parent branch (or base branch)
@@ -742,21 +817,51 @@ set -euo pipefail
742
817
  # 5. Merge — squash-merges the feature branch back into the target branch
743
818
  # 6. Log — appends a completion entry to .clancy/progress.txt
744
819
  #
820
+ # This script is run once per issue. The loop is handled by clancy-afk.sh.
821
+ #
745
822
  # NOTE: Linear personal API keys do NOT use a "Bearer" prefix in the Authorization
746
823
  # header. OAuth access tokens do. This is correct per Linear's documentation.
747
- # NOTE: state.type "unstarted" is a fixed enum value.
748
- # NOTE: Failures use exit 0, not exit 1.
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
+ #
749
832
  # ───────────────────────────────────────────────────────────────────────────────
750
833
 
751
- command -v claude >/dev/null 2>&1 || { echo "✗ claude CLI not found. Install it: https://claude.ai/code"; exit 0; }
752
- command -v jq >/dev/null 2>&1 || { echo "✗ jq not found. Install: brew install jq (mac) | apt install jq (linux)"; exit 0; }
753
- command -v curl >/dev/null 2>&1 || { echo "✗ curl not found. Install curl for your OS."; exit 0; }
834
+ # ─── PREFLIGHT ─────────────────────────────────────────────────────────────────
754
835
 
755
- [ -f .clancy/.env ] || { echo "✗ .clancy/.env not found. Run /clancy:init"; exit 0; }
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
+ }
756
857
  # shellcheck source=/dev/null
757
858
  source .clancy/.env
758
859
 
759
- git rev-parse --git-dir >/dev/null 2>&1 || { echo "✗ Not a git repository."; exit 0; }
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
+ }
760
865
 
761
866
  if ! git diff --quiet || ! git diff --cached --quiet; then
762
867
  echo "⚠ Working directory has uncommitted changes."
@@ -768,6 +873,7 @@ fi
768
873
 
769
874
  # Linear ping — verify API key with a minimal query
770
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.
771
877
  PING_BODY=$(curl -s -X POST https://api.linear.app/graphql \
772
878
  -H "Content-Type: application/json" \
773
879
  -H "Authorization: $LINEAR_API_KEY" \
@@ -780,20 +886,47 @@ echo "$PING_BODY" | jq -e '.data.viewer.id' >/dev/null 2>&1 || {
780
886
  if [ "${PLAYWRIGHT_ENABLED:-}" = "true" ]; then
781
887
  if lsof -ti:"${PLAYWRIGHT_DEV_PORT:-5173}" >/dev/null 2>&1; then
782
888
  echo "⚠ Port ${PLAYWRIGHT_DEV_PORT:-5173} is already in use."
889
+ echo " If visual checks fail, stop whatever is using the port first."
783
890
  fi
784
891
  fi
785
892
 
786
893
  echo "✓ Preflight passed. Starting Clancy..."
787
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.
788
919
  if ! echo "$LINEAR_TEAM_ID" | grep -qE '^[a-zA-Z0-9_-]+$'; then
789
920
  echo "✗ LINEAR_TEAM_ID contains invalid characters. Check .clancy/.env."
790
921
  exit 0
791
922
  fi
792
923
  if [ -n "${CLANCY_LABEL:-}" ] && ! echo "$CLANCY_LABEL" | grep -qE '^[a-zA-Z0-9 _-]+$'; then
793
- echo "✗ CLANCY_LABEL contains invalid characters."
924
+ echo "✗ CLANCY_LABEL contains invalid characters. Use only letters, numbers, spaces, hyphens, and underscores."
794
925
  exit 0
795
926
  fi
796
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.
797
930
  if [ -n "${CLANCY_LABEL:-}" ]; then
798
931
  REQUEST_BODY=$(jq -n \
799
932
  --arg teamId "$LINEAR_TEAM_ID" \
@@ -810,6 +943,7 @@ RESPONSE=$(curl -s -X POST https://api.linear.app/graphql \
810
943
  -H "Authorization: $LINEAR_API_KEY" \
811
944
  -d "$REQUEST_BODY")
812
945
 
946
+ # Check for API errors before parsing (rate limit, permission error, etc.)
813
947
  if ! echo "$RESPONSE" | jq -e '.data.viewer.assignedIssues' >/dev/null 2>&1; then
814
948
  ERR_MSG=$(echo "$RESPONSE" | jq -r '.errors[0].message // "Unexpected response"' 2>/dev/null || echo "Unexpected response")
815
949
  echo "✗ Linear API error: $ERR_MSG. Check LINEAR_API_KEY in .clancy/.env."
@@ -836,6 +970,9 @@ fi
836
970
  BASE_BRANCH="${CLANCY_BASE_BRANCH:-main}"
837
971
  TICKET_BRANCH="feature/$(echo "$IDENTIFIER" | tr '[:upper:]' '[:lower:]')"
838
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.
839
976
  if [ "$PARENT_ID" != "none" ]; then
840
977
  TARGET_BRANCH="epic/$(echo "$PARENT_ID" | tr '[:upper:]' '[:lower:]')"
841
978
  git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
@@ -844,10 +981,14 @@ else
844
981
  TARGET_BRANCH="$BASE_BRANCH"
845
982
  fi
846
983
 
984
+ # ─── IMPLEMENT ─────────────────────────────────────────────────────────────────
985
+
847
986
  echo "Picking up: [$IDENTIFIER] $TITLE"
848
987
  echo "Epic: $EPIC_INFO | Target branch: $TARGET_BRANCH"
849
988
 
850
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.
851
992
  git checkout -B "$TICKET_BRANCH"
852
993
 
853
994
  PROMPT="You are implementing Linear issue $IDENTIFIER.
@@ -880,6 +1021,9 @@ CLAUDE_ARGS=(--dangerously-skip-permissions)
880
1021
  [ -n "${CLANCY_MODEL:-}" ] && CLAUDE_ARGS+=(--model "$CLANCY_MODEL")
881
1022
  echo "$PROMPT" | claude "${CLAUDE_ARGS[@]}"
882
1023
 
1024
+ # ─── MERGE & LOG ───────────────────────────────────────────────────────────────
1025
+
1026
+ # Squash all commits from the feature branch into a single commit on the target branch.
883
1027
  git checkout "$TARGET_BRANCH"
884
1028
  git merge --squash "$TICKET_BRANCH"
885
1029
  if git diff --cached --quiet; then
@@ -888,12 +1032,15 @@ else
888
1032
  git commit -m "feat($IDENTIFIER): $TITLE"
889
1033
  fi
890
1034
 
1035
+ # Delete ticket branch locally
891
1036
  git branch -d "$TICKET_BRANCH"
892
1037
 
1038
+ # Log progress
893
1039
  echo "$(date '+%Y-%m-%d %H:%M') | $IDENTIFIER | $TITLE | DONE" >> .clancy/progress.txt
894
1040
 
895
1041
  echo "✓ $IDENTIFIER complete."
896
1042
 
1043
+ # Send completion notification if webhook is configured
897
1044
  if [ -n "${CLANCY_NOTIFY_WEBHOOK:-}" ]; then
898
1045
  NOTIFY_MSG="✓ Clancy completed [$IDENTIFIER] $TITLE"
899
1046
  if echo "$CLANCY_NOTIFY_WEBHOOK" | grep -q "hooks.slack.com"; then
@@ -916,28 +1063,62 @@ Write this file regardless of board choice:
916
1063
 
917
1064
  ```bash
918
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.
919
1068
  set -euo pipefail
920
1069
 
921
1070
  # ─── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────
1071
+ #
922
1072
  # Loop runner for Clancy. Calls clancy-once.sh repeatedly until:
923
1073
  # - No more tickets are found ("No tickets found", "All done", etc.)
924
1074
  # - A preflight check fails (output line starting with ✗)
925
1075
  # - MAX_ITERATIONS is reached
926
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
+ #
927
1082
  # ───────────────────────────────────────────────────────────────────────────────
928
1083
 
929
- command -v claude >/dev/null 2>&1 || { echo "✗ claude CLI not found. Install it: https://claude.ai/code"; exit 0; }
930
- command -v jq >/dev/null 2>&1 || { echo "✗ jq not found. Install: brew install jq (mac) | apt install jq (linux)"; exit 0; }
931
- command -v curl >/dev/null 2>&1 || { echo "✗ curl not found. Install curl for your OS."; exit 0; }
1084
+ # ─── PREFLIGHT ─────────────────────────────────────────────────────────────────
932
1085
 
933
- [ -f .clancy/.env ] || { echo "✗ .clancy/.env not found. Run /clancy:init"; exit 0; }
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
+ }
934
1106
  # shellcheck source=/dev/null
935
1107
  source .clancy/.env
936
1108
 
937
- git rev-parse --git-dir >/dev/null 2>&1 || { echo "✗ Not a git repository."; exit 0; }
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 ─────────────────────────────────────────────────────────────
938
1116
 
939
1117
  MAX_ITERATIONS=${MAX_ITERATIONS:-5}
940
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.
941
1122
  ONCE_SCRIPT="$SCRIPT_DIR/clancy-once.sh"
942
1123
 
943
1124
  if [ ! -f "$ONCE_SCRIPT" ]; then
@@ -955,17 +1136,24 @@ while [ "$i" -lt "$MAX_ITERATIONS" ]; do
955
1136
  echo ""
956
1137
  echo "=== Iteration $i of $MAX_ITERATIONS ==="
957
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.
958
1142
  TMPFILE=$(mktemp)
959
1143
  bash "$ONCE_SCRIPT" 2>&1 | tee "$TMPFILE"
960
1144
  OUTPUT=$(cat "$TMPFILE")
961
1145
  rm -f "$TMPFILE"
962
1146
 
1147
+ # Stop if no tickets remain
963
1148
  if echo "$OUTPUT" | grep -qE "No tickets found|No issues found|All done"; then
964
1149
  echo ""
965
1150
  echo "✓ Clancy finished — no more tickets."
966
1151
  exit 0
967
1152
  fi
968
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.
969
1157
  if echo "$OUTPUT" | grep -q "Ticket skipped"; then
970
1158
  echo ""
971
1159
  echo "⚠ Clancy stopped — ticket was skipped (not implementable from the codebase)."
@@ -973,6 +1161,7 @@ while [ "$i" -lt "$MAX_ITERATIONS" ]; do
973
1161
  exit 0
974
1162
  fi
975
1163
 
1164
+ # Stop if a preflight check failed (lines starting with ✗)
976
1165
  if echo "$OUTPUT" | grep -qE "^✗ "; then
977
1166
  echo ""
978
1167
  echo "✗ Clancy stopped — preflight check failed. See output above."
@@ -1006,27 +1195,38 @@ JIRA_API_TOKEN=your-api-token-from-id.atlassian.com
1006
1195
  JIRA_PROJECT_KEY=PROJ
1007
1196
 
1008
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")
1009
1199
  CLANCY_JQL_STATUS="To Do"
1010
1200
 
1011
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
1012
1203
  # CLANCY_JQL_SPRINT=true
1013
1204
 
1014
- # Optional: only pick up tickets with this label.
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.
1015
1208
  # CLANCY_LABEL="clancy"
1016
1209
 
1017
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.
1018
1213
  CLANCY_BASE_BRANCH=main
1019
1214
 
1020
1215
  # ─── Loop ─────────────────────────────────────────────────────────────────────
1216
+ # Max tickets to process per /clancy:run session (default: 5)
1021
1217
  MAX_ITERATIONS=5
1022
1218
 
1023
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
1024
1222
  # CLANCY_MODEL=claude-sonnet-4-6
1025
1223
 
1026
1224
  # ─── Optional: Figma MCP ──────────────────────────────────────────────────────
1225
+ # Fetch design specs from Figma when a ticket has a Figma URL in its description
1027
1226
  # FIGMA_API_KEY=your-figma-api-key
1028
1227
 
1029
1228
  # ─── Optional: Playwright visual checks ───────────────────────────────────────
1229
+ # Run a visual check after implementing UI tickets
1030
1230
  # PLAYWRIGHT_ENABLED=true
1031
1231
  # PLAYWRIGHT_DEV_COMMAND="yarn dev"
1032
1232
  # PLAYWRIGHT_DEV_PORT=5173
@@ -1035,6 +1235,7 @@ MAX_ITERATIONS=5
1035
1235
  # PLAYWRIGHT_STARTUP_WAIT=15
1036
1236
 
1037
1237
  # ─── Optional: Notifications ──────────────────────────────────────────────────
1238
+ # Webhook URL for Slack or Teams notifications on ticket completion
1038
1239
  # CLANCY_NOTIFY_WEBHOOK=https://hooks.slack.com/services/your/webhook/url
1039
1240
  ```
1040
1241
 
@@ -1050,21 +1251,30 @@ GITHUB_TOKEN=ghp_your-personal-access-token
1050
1251
  GITHUB_REPO=owner/repo-name
1051
1252
 
1052
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.
1053
1256
  # CLANCY_LABEL=clancy
1054
1257
 
1055
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.
1056
1261
  CLANCY_BASE_BRANCH=main
1057
1262
 
1058
1263
  # ─── Loop ─────────────────────────────────────────────────────────────────────
1264
+ # Max tickets to process per /clancy:run session (default: 20)
1059
1265
  MAX_ITERATIONS=20
1060
1266
 
1061
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
1062
1270
  # CLANCY_MODEL=claude-sonnet-4-6
1063
1271
 
1064
1272
  # ─── Optional: Figma MCP ──────────────────────────────────────────────────────
1273
+ # Fetch design specs from Figma when a ticket has a Figma URL in its description
1065
1274
  # FIGMA_API_KEY=your-figma-api-key
1066
1275
 
1067
1276
  # ─── Optional: Playwright visual checks ───────────────────────────────────────
1277
+ # Run a visual check after implementing UI tickets
1068
1278
  # PLAYWRIGHT_ENABLED=true
1069
1279
  # PLAYWRIGHT_DEV_COMMAND="yarn dev"
1070
1280
  # PLAYWRIGHT_DEV_PORT=5173
@@ -1073,6 +1283,7 @@ MAX_ITERATIONS=20
1073
1283
  # PLAYWRIGHT_STARTUP_WAIT=15
1074
1284
 
1075
1285
  # ─── Optional: Notifications ──────────────────────────────────────────────────
1286
+ # Webhook URL for Slack or Teams notifications on ticket completion
1076
1287
  # CLANCY_NOTIFY_WEBHOOK=https://hooks.slack.com/services/your/webhook/url
1077
1288
  ```
1078
1289
 
@@ -1087,22 +1298,31 @@ MAX_ITERATIONS=20
1087
1298
  LINEAR_API_KEY=lin_api_your-personal-api-key
1088
1299
  LINEAR_TEAM_ID=your-team-uuid
1089
1300
 
1090
- # Optional: only pick up issues with this label.
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.
1091
1304
  # CLANCY_LABEL=clancy
1092
1305
 
1093
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.
1094
1309
  CLANCY_BASE_BRANCH=main
1095
1310
 
1096
1311
  # ─── Loop ─────────────────────────────────────────────────────────────────────
1312
+ # Max tickets to process per /clancy:run session (default: 20)
1097
1313
  MAX_ITERATIONS=20
1098
1314
 
1099
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
1100
1318
  # CLANCY_MODEL=claude-sonnet-4-6
1101
1319
 
1102
1320
  # ─── Optional: Figma MCP ──────────────────────────────────────────────────────
1321
+ # Fetch design specs from Figma when a ticket has a Figma URL in its description
1103
1322
  # FIGMA_API_KEY=your-figma-api-key
1104
1323
 
1105
1324
  # ─── Optional: Playwright visual checks ───────────────────────────────────────
1325
+ # Run a visual check after implementing UI tickets
1106
1326
  # PLAYWRIGHT_ENABLED=true
1107
1327
  # PLAYWRIGHT_DEV_COMMAND="yarn dev"
1108
1328
  # PLAYWRIGHT_DEV_PORT=5173
@@ -1111,5 +1331,6 @@ MAX_ITERATIONS=20
1111
1331
  # PLAYWRIGHT_STARTUP_WAIT=15
1112
1332
 
1113
1333
  # ─── Optional: Notifications ──────────────────────────────────────────────────
1334
+ # Webhook URL for Slack or Teams notifications on ticket completion
1114
1335
  # CLANCY_NOTIFY_WEBHOOK=https://hooks.slack.com/services/your/webhook/url
1115
1336
  ```