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 +2 -2
- package/src/workflows/scaffold.md +243 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chief-clancy",
|
|
3
|
-
"version": "0.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": {
|
|
@@ -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
|
-
#
|
|
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
|
-
|
|
575
|
-
|
|
576
|
-
command -v
|
|
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 ] || {
|
|
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 || {
|
|
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
|
-
#
|
|
748
|
-
# NOTE:
|
|
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
|
-
|
|
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
|
-
|
|
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 || {
|
|
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
|
-
|
|
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
|
-
|
|
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 || {
|
|
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
|
```
|