chief-clancy 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/install.js CHANGED
@@ -128,6 +128,24 @@ async function main() {
128
128
  copyDir(COMMANDS_SRC, dest);
129
129
  copyDir(WORKFLOWS_SRC, workflowsDest);
130
130
 
131
+ // For global installs, @-file references in command files resolve relative to the
132
+ // project root — not ~/.claude/ — so the workflow files won't be found at runtime.
133
+ // Fix: inline the workflow content directly into the installed command files.
134
+ if (dest === GLOBAL_DEST) {
135
+ const WORKFLOW_REF = /^@\.claude\/clancy\/workflows\/(.+\.md)$/m;
136
+ for (const file of fs.readdirSync(dest)) {
137
+ if (!file.endsWith('.md')) continue;
138
+ const cmdPath = path.join(dest, file);
139
+ const content = fs.readFileSync(cmdPath, 'utf8');
140
+ const match = content.match(WORKFLOW_REF);
141
+ if (!match) continue;
142
+ const workflowFile = path.join(workflowsDest, match[1]);
143
+ if (!fs.existsSync(workflowFile)) continue;
144
+ const workflowContent = fs.readFileSync(workflowFile, 'utf8');
145
+ fs.writeFileSync(cmdPath, content.replace(match[0], workflowContent));
146
+ }
147
+ }
148
+
131
149
  // Write VERSION file so /clancy:doctor and /clancy:update can read the installed version
132
150
  fs.writeFileSync(path.join(dest, 'VERSION'), PKG.version);
133
151
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chief-clancy",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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",
@@ -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,829 @@ 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: Failures use exit 0, not exit 1. This is intentional — clancy-afk.sh
321
+ # detects stop conditions by reading script output rather than exit codes, so a
322
+ # non-zero exit would be treated as an unexpected crash rather than a clean stop.
323
+ #
324
+ # ───────────────────────────────────────────────────────────────────────────────
325
+
326
+ # ─── PREFLIGHT ─────────────────────────────────────────────────────────────────
327
+
328
+ command -v claude >/dev/null 2>&1 || {
329
+ echo "✗ claude CLI not found."
330
+ echo " Install it: https://claude.ai/code"
331
+ exit 0
332
+ }
333
+ command -v jq >/dev/null 2>&1 || {
334
+ echo "✗ jq not found."
335
+ echo " Install: brew install jq (mac) | apt install jq (linux)"
336
+ exit 0
337
+ }
338
+ command -v curl >/dev/null 2>&1 || {
339
+ echo "✗ curl not found. Install curl for your OS."
340
+ exit 0
341
+ }
342
+
343
+ [ -f .clancy/.env ] || {
344
+ echo "✗ .clancy/.env not found."
345
+ echo " Copy .clancy/.env.example to .clancy/.env and fill in your credentials."
346
+ echo " Then run: /clancy:init"
347
+ exit 0
348
+ }
349
+ # shellcheck source=/dev/null
350
+ source .clancy/.env
351
+
352
+ git rev-parse --git-dir >/dev/null 2>&1 || {
353
+ echo "✗ Not a git repository."
354
+ echo " Clancy must be run from the root of a git project."
355
+ exit 0
356
+ }
357
+
358
+ if ! git diff --quiet || ! git diff --cached --quiet; then
359
+ echo "⚠ Working directory has uncommitted changes."
360
+ echo " Consider stashing or committing first to avoid confusion."
361
+ fi
362
+
363
+ [ -n "${JIRA_BASE_URL:-}" ] || { echo "✗ JIRA_BASE_URL is not set in .clancy/.env"; exit 0; }
364
+ [ -n "${JIRA_USER:-}" ] || { echo "✗ JIRA_USER is not set in .clancy/.env"; exit 0; }
365
+ [ -n "${JIRA_API_TOKEN:-}" ] || { echo "✗ JIRA_API_TOKEN is not set in .clancy/.env"; exit 0; }
366
+ [ -n "${JIRA_PROJECT_KEY:-}" ] || { echo "✗ JIRA_PROJECT_KEY is not set in .clancy/.env"; exit 0; }
367
+ if ! echo "$JIRA_PROJECT_KEY" | grep -qE '^[A-Z][A-Z0-9]+$'; then
368
+ echo "✗ JIRA_PROJECT_KEY format is invalid. Expected uppercase letters and numbers only (e.g. PROJ, ENG2). Check JIRA_PROJECT_KEY in .clancy/.env."
369
+ exit 0
370
+ fi
371
+
372
+ PING=$(curl -s -o /dev/null -w "%{http_code}" \
373
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
374
+ "$JIRA_BASE_URL/rest/api/3/project/$JIRA_PROJECT_KEY")
375
+
376
+ case "$PING" in
377
+ 200) ;;
378
+ 401) echo "✗ Jira authentication failed. Check JIRA_USER and JIRA_API_TOKEN in .clancy/.env."; exit 0 ;;
379
+ 403) echo "✗ Jira access denied. Your token may lack Browse Projects permission."; exit 0 ;;
380
+ 404) echo "✗ Jira project '$JIRA_PROJECT_KEY' not found. Check JIRA_PROJECT_KEY in .clancy/.env."; exit 0 ;;
381
+ 000) echo "✗ Could not reach Jira at $JIRA_BASE_URL. Check JIRA_BASE_URL and your network."; exit 0 ;;
382
+ *) echo "✗ Jira returned unexpected status $PING. Check your config."; exit 0 ;;
383
+ esac
384
+
385
+ if [ "${PLAYWRIGHT_ENABLED:-}" = "true" ]; then
386
+ if lsof -ti:"${PLAYWRIGHT_DEV_PORT:-5173}" >/dev/null 2>&1; then
387
+ echo "⚠ Port ${PLAYWRIGHT_DEV_PORT:-5173} is already in use."
388
+ echo " Clancy will attempt to start the dev server on this port."
389
+ echo " If visual checks fail, stop whatever is using the port first."
390
+ fi
391
+ fi
392
+
393
+ echo "✓ Preflight passed. Starting Clancy..."
394
+
395
+ # ─── END PREFLIGHT ─────────────────────────────────────────────────────────────
396
+
397
+ # ─── FETCH TICKET ──────────────────────────────────────────────────────────────
398
+
399
+ # Validate user-controlled values to prevent JQL injection.
400
+ # JQL does not support parameterised queries, so we restrict to safe characters.
401
+ if [ -n "${CLANCY_LABEL:-}" ] && ! echo "$CLANCY_LABEL" | grep -qE '^[a-zA-Z0-9 _-]+$'; then
402
+ echo "✗ CLANCY_LABEL contains invalid characters. Use only letters, numbers, spaces, hyphens, and underscores."
403
+ exit 0
404
+ fi
405
+ if ! echo "${CLANCY_JQL_STATUS:-To Do}" | grep -qE '^[a-zA-Z0-9 _-]+$'; then
406
+ echo "✗ CLANCY_JQL_STATUS contains invalid characters. Use only letters, numbers, spaces, hyphens, and underscores."
407
+ exit 0
408
+ fi
409
+
410
+ # Build JQL — sprint filter is optional (requires Jira Software license).
411
+ # Uses the /rest/api/3/search/jql POST endpoint — the old GET /search was removed Aug 2025.
412
+ # maxResults:1 is intentional — pick one ticket per run, never paginate.
413
+ if [ -n "${CLANCY_JQL_SPRINT:-}" ]; then
414
+ SPRINT_CLAUSE="AND sprint in openSprints()"
415
+ else
416
+ SPRINT_CLAUSE=""
417
+ fi
418
+
419
+ # Optional label filter — set CLANCY_LABEL in .env to only pick up tickets with that label.
420
+ if [ -n "${CLANCY_LABEL:-}" ]; then
421
+ LABEL_CLAUSE="AND labels = \"$CLANCY_LABEL\""
422
+ else
423
+ LABEL_CLAUSE=""
424
+ fi
425
+
426
+ RESPONSE=$(curl -s \
427
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
428
+ -X POST \
429
+ -H "Content-Type: application/json" \
430
+ -H "Accept: application/json" \
431
+ "$JIRA_BASE_URL/rest/api/3/search/jql" \
432
+ -d "{
433
+ \"jql\": \"project=$JIRA_PROJECT_KEY $SPRINT_CLAUSE $LABEL_CLAUSE AND assignee=currentUser() AND status=\\\"${CLANCY_JQL_STATUS:-To Do}\\\" ORDER BY priority ASC\",
434
+ \"maxResults\": 1,
435
+ \"fields\": [\"summary\", \"description\", \"issuelinks\", \"parent\", \"customfield_10014\"]
436
+ }")
437
+
438
+ # New endpoint returns { "issues": [...], "isLast": bool } — no .total field
439
+ ISSUE_COUNT=$(echo "$RESPONSE" | jq '.issues | length')
440
+ if [ "$ISSUE_COUNT" -eq 0 ]; then
441
+ echo "No tickets found. All done!"
442
+ exit 0
443
+ fi
444
+
445
+ TICKET_KEY=$(echo "$RESPONSE" | jq -r '.issues[0].key')
446
+ SUMMARY=$(echo "$RESPONSE" | jq -r '.issues[0].fields.summary')
447
+
448
+ # Extract description via recursive ADF walk
449
+ DESCRIPTION=$(echo "$RESPONSE" | jq -r '
450
+ .issues[0].fields.description
451
+ | .. | strings
452
+ | select(length > 0)
453
+ | . + "\n"
454
+ ' 2>/dev/null || echo "No description")
455
+
456
+ # Extract epic — try parent first (next-gen), fall back to customfield_10014 (classic)
457
+ EPIC_INFO=$(echo "$RESPONSE" | jq -r '
458
+ .issues[0].fields.parent.key // .issues[0].fields.customfield_10014 // "none"
459
+ ')
460
+
461
+ # Extract blocking issue links
462
+ BLOCKERS=$(echo "$RESPONSE" | jq -r '
463
+ [.issues[0].fields.issuelinks[]?
464
+ | select(.type.name == "Blocks" and .inwardIssue?)
465
+ | .inwardIssue.key]
466
+ | if length > 0 then "Blocked by: " + join(", ") else "None" end
467
+ ' 2>/dev/null || echo "None")
468
+
469
+ BASE_BRANCH="${CLANCY_BASE_BRANCH:-main}"
470
+ TICKET_BRANCH="feature/$(echo "$TICKET_KEY" | tr '[:upper:]' '[:lower:]')"
471
+
472
+ if [ "$EPIC_INFO" != "none" ]; then
473
+ TARGET_BRANCH="epic/$(echo "$EPIC_INFO" | tr '[:upper:]' '[:lower:]')"
474
+ git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
475
+ || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
476
+ else
477
+ TARGET_BRANCH="$BASE_BRANCH"
478
+ fi
479
+
480
+ # ─── IMPLEMENT ─────────────────────────────────────────────────────────────────
481
+
482
+ echo "Picking up: [$TICKET_KEY] $SUMMARY"
483
+ echo "Epic: $EPIC_INFO | Target branch: $TARGET_BRANCH | Blockers: $BLOCKERS"
484
+
485
+ git checkout "$TARGET_BRANCH"
486
+ git checkout -B "$TICKET_BRANCH"
487
+
488
+ PROMPT="You are implementing Jira ticket $TICKET_KEY.
489
+
490
+ Summary: $SUMMARY
491
+ Epic: $EPIC_INFO
492
+ Blockers: $BLOCKERS
493
+
494
+ Description:
495
+ $DESCRIPTION
496
+
497
+ Step 0 — Executability check (do this before any git or file operation):
498
+ Read the ticket summary and description above. Can this ticket be implemented entirely
499
+ as a code change committed to this repo? Consult the 'Executability check' section of
500
+ CLAUDE.md for the full list of skip conditions.
501
+
502
+ If you must SKIP this ticket:
503
+ 1. Output: ⚠ Skipping [$TICKET_KEY]: {one-line reason}
504
+ 2. Output: Ticket skipped — update it to be codebase-only work, then re-run.
505
+ 3. Append to .clancy/progress.txt: YYYY-MM-DD HH:MM | $TICKET_KEY | SKIPPED | {reason}
506
+ 4. Stop — no branches, no file changes, no git operations.
507
+
508
+ If the ticket IS implementable, continue:
509
+ 1. Read ALL docs in .clancy/docs/ — especially GIT.md for branching and commit conventions
510
+ 2. Follow the conventions in GIT.md exactly
511
+ 3. Implement the ticket fully
512
+ 4. Commit your work following the conventions in GIT.md
513
+ 5. When done, confirm you are finished."
514
+
515
+ CLAUDE_ARGS=(--dangerously-skip-permissions)
516
+ [ -n "${CLANCY_MODEL:-}" ] && CLAUDE_ARGS+=(--model "$CLANCY_MODEL")
517
+ echo "$PROMPT" | claude "${CLAUDE_ARGS[@]}"
518
+
519
+ # ─── MERGE & LOG ───────────────────────────────────────────────────────────────
520
+
521
+ git checkout "$TARGET_BRANCH"
522
+ git merge --squash "$TICKET_BRANCH"
523
+ if git diff --cached --quiet; then
524
+ echo "⚠ No changes staged after squash merge. Claude may not have committed any work."
525
+ else
526
+ git commit -m "feat($TICKET_KEY): $SUMMARY"
527
+ fi
528
+
529
+ git branch -d "$TICKET_BRANCH"
530
+
531
+ echo "$(date '+%Y-%m-%d %H:%M') | $TICKET_KEY | $SUMMARY | DONE" >> .clancy/progress.txt
532
+
533
+ echo "✓ $TICKET_KEY complete."
534
+
535
+ if [ -n "${CLANCY_NOTIFY_WEBHOOK:-}" ]; then
536
+ NOTIFY_MSG="✓ Clancy completed [$TICKET_KEY] $SUMMARY"
537
+ if echo "$CLANCY_NOTIFY_WEBHOOK" | grep -q "hooks.slack.com"; then
538
+ curl -s -X POST "$CLANCY_NOTIFY_WEBHOOK" \
539
+ -H "Content-Type: application/json" \
540
+ -d "$(jq -n --arg text "$NOTIFY_MSG" '{"text": $text}')" >/dev/null 2>&1 || true
541
+ else
542
+ curl -s -X POST "$CLANCY_NOTIFY_WEBHOOK" \
543
+ -H "Content-Type: application/json" \
544
+ -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
545
+ fi
546
+ fi
547
+ ```
548
+
549
+ ---
550
+
551
+ ### `.clancy/clancy-once.sh` — GitHub Issues
552
+
553
+ Write this file when the chosen board is **GitHub Issues**:
554
+
555
+ ```bash
556
+ #!/usr/bin/env bash
557
+ set -euo pipefail
558
+
559
+ # ─── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────
560
+ # Board: GitHub Issues
561
+ # 1. Preflight — checks all required tools, credentials, and repo reachability
562
+ # 2. Fetch — pulls the next open issue with the 'clancy' label assigned to you
563
+ # 3. Branch — creates a feature branch from the issue's milestone branch (or base branch)
564
+ # 4. Implement — passes the issue to Claude Code, which reads .clancy/docs/ and implements it
565
+ # 5. Merge — squash-merges the feature branch back into the target branch
566
+ # 6. Close — marks the GitHub issue as closed via the API
567
+ # 7. Log — appends a completion entry to .clancy/progress.txt
568
+ #
569
+ # NOTE: GitHub's /issues endpoint returns pull requests too. This script filters
570
+ # them out by checking for the presence of the 'pull_request' key in each result.
571
+ # NOTE: Failures use exit 0, not exit 1.
572
+ # ───────────────────────────────────────────────────────────────────────────────
573
+
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; }
577
+
578
+ [ -f .clancy/.env ] || { echo "✗ .clancy/.env not found. Run /clancy:init"; exit 0; }
579
+ # shellcheck source=/dev/null
580
+ source .clancy/.env
581
+
582
+ git rev-parse --git-dir >/dev/null 2>&1 || { echo "✗ Not a git repository."; exit 0; }
583
+
584
+ if ! git diff --quiet || ! git diff --cached --quiet; then
585
+ echo "⚠ Working directory has uncommitted changes."
586
+ echo " Consider stashing or committing first to avoid confusion."
587
+ fi
588
+
589
+ [ -n "${GITHUB_TOKEN:-}" ] || { echo "✗ GITHUB_TOKEN is not set in .clancy/.env"; exit 0; }
590
+ [ -n "${GITHUB_REPO:-}" ] || { echo "✗ GITHUB_REPO is not set in .clancy/.env"; exit 0; }
591
+
592
+ 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)."
594
+ exit 0
595
+ fi
596
+
597
+ PING=$(curl -s -o /dev/null -w "%{http_code}" \
598
+ -H "Authorization: Bearer $GITHUB_TOKEN" \
599
+ -H "X-GitHub-Api-Version: 2022-11-28" \
600
+ "https://api.github.com/repos/$GITHUB_REPO")
601
+
602
+ case "$PING" in
603
+ 200) ;;
604
+ 401) echo "✗ GitHub authentication failed. Check GITHUB_TOKEN in .clancy/.env."; exit 0 ;;
605
+ 403) echo "✗ GitHub access denied. Your token may lack the repo scope."; exit 0 ;;
606
+ 404) echo "✗ GitHub repo '$GITHUB_REPO' not found. Check GITHUB_REPO in .clancy/.env."; exit 0 ;;
607
+ 000) echo "✗ Could not reach GitHub. Check your network."; exit 0 ;;
608
+ *) echo "✗ GitHub returned unexpected status $PING. Check your config."; exit 0 ;;
609
+ esac
610
+
611
+ if [ "${PLAYWRIGHT_ENABLED:-}" = "true" ]; then
612
+ if lsof -ti:"${PLAYWRIGHT_DEV_PORT:-5173}" >/dev/null 2>&1; then
613
+ echo "⚠ Port ${PLAYWRIGHT_DEV_PORT:-5173} is already in use."
614
+ fi
615
+ fi
616
+
617
+ echo "✓ Preflight passed. Starting Clancy..."
618
+
619
+ RESPONSE=$(curl -s \
620
+ -H "Authorization: Bearer $GITHUB_TOKEN" \
621
+ -H "X-GitHub-Api-Version: 2022-11-28" \
622
+ "https://api.github.com/repos/$GITHUB_REPO/issues?state=open&assignee=@me&labels=clancy&per_page=3")
623
+
624
+ if ! echo "$RESPONSE" | jq -e 'type == "array"' >/dev/null 2>&1; then
625
+ ERR_MSG=$(echo "$RESPONSE" | jq -r '.message // "Unexpected response"' 2>/dev/null || echo "Unexpected response")
626
+ echo "✗ GitHub API error: $ERR_MSG. Check GITHUB_TOKEN in .clancy/.env."
627
+ exit 0
628
+ fi
629
+
630
+ ISSUE=$(echo "$RESPONSE" | jq 'map(select(has("pull_request") | not)) | .[0]')
631
+
632
+ if [ "$(echo "$ISSUE" | jq 'type')" = '"null"' ] || [ -z "$(echo "$ISSUE" | jq -r '.number // empty')" ]; then
633
+ echo "No issues found. All done!"
634
+ exit 0
635
+ fi
636
+
637
+ ISSUE_NUMBER=$(echo "$ISSUE" | jq -r '.number')
638
+ TITLE=$(echo "$ISSUE" | jq -r '.title')
639
+ BODY=$(echo "$ISSUE" | jq -r '.body // "No description"')
640
+ MILESTONE=$(echo "$ISSUE" | jq -r '.milestone.title // "none"')
641
+
642
+ BASE_BRANCH="${CLANCY_BASE_BRANCH:-main}"
643
+ TICKET_BRANCH="feature/issue-${ISSUE_NUMBER}"
644
+
645
+ if [ "$MILESTONE" != "none" ]; then
646
+ MILESTONE_SLUG=$(echo "$MILESTONE" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd '[:alnum:]-')
647
+ TARGET_BRANCH="milestone/${MILESTONE_SLUG}"
648
+ git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
649
+ || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
650
+ else
651
+ TARGET_BRANCH="$BASE_BRANCH"
652
+ fi
653
+
654
+ echo "Picking up: [#${ISSUE_NUMBER}] $TITLE"
655
+ echo "Milestone: $MILESTONE | Target branch: $TARGET_BRANCH"
656
+
657
+ git checkout "$TARGET_BRANCH"
658
+ git checkout -B "$TICKET_BRANCH"
659
+
660
+ PROMPT="You are implementing GitHub Issue #${ISSUE_NUMBER}.
661
+
662
+ Title: $TITLE
663
+ Milestone: $MILESTONE
664
+
665
+ Description:
666
+ $BODY
667
+
668
+ Step 0 — Executability check (do this before any git or file operation):
669
+ Read the issue title and description above. Can this issue be implemented entirely
670
+ as a code change committed to this repo? Consult the 'Executability check' section of
671
+ CLAUDE.md for the full list of skip conditions.
672
+
673
+ If you must SKIP this issue:
674
+ 1. Output: ⚠ Skipping [#${ISSUE_NUMBER}]: {one-line reason}
675
+ 2. Output: Ticket skipped — update it to be codebase-only work, then re-run.
676
+ 3. Append to .clancy/progress.txt: YYYY-MM-DD HH:MM | #${ISSUE_NUMBER} | SKIPPED | {reason}
677
+ 4. Stop — no branches, no file changes, no git operations.
678
+
679
+ If the issue IS implementable, continue:
680
+ 1. Read ALL docs in .clancy/docs/ — especially GIT.md for branching and commit conventions
681
+ 2. Follow the conventions in GIT.md exactly
682
+ 3. Implement the issue fully
683
+ 4. Commit your work following the conventions in GIT.md
684
+ 5. When done, confirm you are finished."
685
+
686
+ CLAUDE_ARGS=(--dangerously-skip-permissions)
687
+ [ -n "${CLANCY_MODEL:-}" ] && CLAUDE_ARGS+=(--model "$CLANCY_MODEL")
688
+ echo "$PROMPT" | claude "${CLAUDE_ARGS[@]}"
689
+
690
+ git checkout "$TARGET_BRANCH"
691
+ git merge --squash "$TICKET_BRANCH"
692
+ if git diff --cached --quiet; then
693
+ echo "⚠ No changes staged after squash merge. Claude may not have committed any work."
694
+ else
695
+ git commit -m "feat(#${ISSUE_NUMBER}): $TITLE"
696
+ fi
697
+
698
+ git branch -d "$TICKET_BRANCH"
699
+
700
+ CLOSE_HTTP=$(curl -s -o /dev/null -w "%{http_code}" -X PATCH \
701
+ -H "Authorization: Bearer $GITHUB_TOKEN" \
702
+ -H "X-GitHub-Api-Version: 2022-11-28" \
703
+ -H "Content-Type: application/json" \
704
+ "https://api.github.com/repos/$GITHUB_REPO/issues/${ISSUE_NUMBER}" \
705
+ -d '{"state": "closed"}')
706
+ [ "$CLOSE_HTTP" = "200" ] || echo "⚠ Could not close issue #${ISSUE_NUMBER} (HTTP $CLOSE_HTTP). Close it manually on GitHub."
707
+
708
+ echo "$(date '+%Y-%m-%d %H:%M') | #${ISSUE_NUMBER} | $TITLE | DONE" >> .clancy/progress.txt
709
+
710
+ echo "✓ #${ISSUE_NUMBER} complete."
711
+
712
+ if [ -n "${CLANCY_NOTIFY_WEBHOOK:-}" ]; then
713
+ NOTIFY_MSG="✓ Clancy completed [#${ISSUE_NUMBER}] $TITLE"
714
+ if echo "$CLANCY_NOTIFY_WEBHOOK" | grep -q "hooks.slack.com"; then
715
+ curl -s -X POST "$CLANCY_NOTIFY_WEBHOOK" \
716
+ -H "Content-Type: application/json" \
717
+ -d "$(jq -n --arg text "$NOTIFY_MSG" '{"text": $text}')" >/dev/null 2>&1 || true
718
+ else
719
+ curl -s -X POST "$CLANCY_NOTIFY_WEBHOOK" \
720
+ -H "Content-Type: application/json" \
721
+ -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
722
+ fi
723
+ fi
724
+ ```
725
+
726
+ ---
727
+
728
+ ### `.clancy/clancy-once.sh` — Linear
729
+
730
+ Write this file when the chosen board is **Linear**:
731
+
732
+ ```bash
733
+ #!/usr/bin/env bash
734
+ set -euo pipefail
735
+
736
+ # ─── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────
737
+ # Board: Linear
738
+ # 1. Preflight — checks all required tools, credentials, and API reachability
739
+ # 2. Fetch — pulls the next unstarted issue assigned to you via GraphQL
740
+ # 3. Branch — creates a feature branch from the issue's parent branch (or base branch)
741
+ # 4. Implement — passes the issue to Claude Code, which reads .clancy/docs/ and implements it
742
+ # 5. Merge — squash-merges the feature branch back into the target branch
743
+ # 6. Log — appends a completion entry to .clancy/progress.txt
744
+ #
745
+ # NOTE: Linear personal API keys do NOT use a "Bearer" prefix in the Authorization
746
+ # 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.
749
+ # ───────────────────────────────────────────────────────────────────────────────
750
+
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; }
754
+
755
+ [ -f .clancy/.env ] || { echo "✗ .clancy/.env not found. Run /clancy:init"; exit 0; }
756
+ # shellcheck source=/dev/null
757
+ source .clancy/.env
758
+
759
+ git rev-parse --git-dir >/dev/null 2>&1 || { echo "✗ Not a git repository."; exit 0; }
760
+
761
+ if ! git diff --quiet || ! git diff --cached --quiet; then
762
+ echo "⚠ Working directory has uncommitted changes."
763
+ echo " Consider stashing or committing first to avoid confusion."
764
+ fi
765
+
766
+ [ -n "${LINEAR_API_KEY:-}" ] || { echo "✗ LINEAR_API_KEY is not set in .clancy/.env"; exit 0; }
767
+ [ -n "${LINEAR_TEAM_ID:-}" ] || { echo "✗ LINEAR_TEAM_ID is not set in .clancy/.env"; exit 0; }
768
+
769
+ # Linear ping — verify API key with a minimal query
770
+ # Note: personal API keys do NOT use a "Bearer" prefix — this is correct per Linear docs.
771
+ PING_BODY=$(curl -s -X POST https://api.linear.app/graphql \
772
+ -H "Content-Type: application/json" \
773
+ -H "Authorization: $LINEAR_API_KEY" \
774
+ -d '{"query": "{ viewer { id } }"}')
775
+
776
+ echo "$PING_BODY" | jq -e '.data.viewer.id' >/dev/null 2>&1 || {
777
+ echo "✗ Linear authentication failed. Check LINEAR_API_KEY in .clancy/.env."; exit 0
778
+ }
779
+
780
+ if [ "${PLAYWRIGHT_ENABLED:-}" = "true" ]; then
781
+ if lsof -ti:"${PLAYWRIGHT_DEV_PORT:-5173}" >/dev/null 2>&1; then
782
+ echo "⚠ Port ${PLAYWRIGHT_DEV_PORT:-5173} is already in use."
783
+ fi
784
+ fi
785
+
786
+ echo "✓ Preflight passed. Starting Clancy..."
787
+
788
+ if ! echo "$LINEAR_TEAM_ID" | grep -qE '^[a-zA-Z0-9_-]+$'; then
789
+ echo "✗ LINEAR_TEAM_ID contains invalid characters. Check .clancy/.env."
790
+ exit 0
791
+ fi
792
+ if [ -n "${CLANCY_LABEL:-}" ] && ! echo "$CLANCY_LABEL" | grep -qE '^[a-zA-Z0-9 _-]+$'; then
793
+ echo "✗ CLANCY_LABEL contains invalid characters."
794
+ exit 0
795
+ fi
796
+
797
+ if [ -n "${CLANCY_LABEL:-}" ]; then
798
+ REQUEST_BODY=$(jq -n \
799
+ --arg teamId "$LINEAR_TEAM_ID" \
800
+ --arg label "$CLANCY_LABEL" \
801
+ '{"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}}')
802
+ else
803
+ REQUEST_BODY=$(jq -n \
804
+ --arg teamId "$LINEAR_TEAM_ID" \
805
+ '{"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}}')
806
+ fi
807
+
808
+ RESPONSE=$(curl -s -X POST https://api.linear.app/graphql \
809
+ -H "Content-Type: application/json" \
810
+ -H "Authorization: $LINEAR_API_KEY" \
811
+ -d "$REQUEST_BODY")
812
+
813
+ if ! echo "$RESPONSE" | jq -e '.data.viewer.assignedIssues' >/dev/null 2>&1; then
814
+ ERR_MSG=$(echo "$RESPONSE" | jq -r '.errors[0].message // "Unexpected response"' 2>/dev/null || echo "Unexpected response")
815
+ echo "✗ Linear API error: $ERR_MSG. Check LINEAR_API_KEY in .clancy/.env."
816
+ exit 0
817
+ fi
818
+
819
+ NODE_COUNT=$(echo "$RESPONSE" | jq '.data.viewer.assignedIssues.nodes | length')
820
+ if [ "$NODE_COUNT" -eq 0 ]; then
821
+ echo "No issues found. All done!"
822
+ exit 0
823
+ fi
824
+
825
+ IDENTIFIER=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].identifier')
826
+ TITLE=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].title')
827
+ DESCRIPTION=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].description // "No description"')
828
+ PARENT_ID=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].parent.identifier // "none"')
829
+ PARENT_TITLE=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].parent.title // ""')
830
+
831
+ EPIC_INFO="${PARENT_ID}"
832
+ if [ -n "$PARENT_TITLE" ] && [ "$PARENT_TITLE" != "null" ]; then
833
+ EPIC_INFO="${PARENT_ID} — ${PARENT_TITLE}"
834
+ fi
835
+
836
+ BASE_BRANCH="${CLANCY_BASE_BRANCH:-main}"
837
+ TICKET_BRANCH="feature/$(echo "$IDENTIFIER" | tr '[:upper:]' '[:lower:]')"
838
+
839
+ if [ "$PARENT_ID" != "none" ]; then
840
+ TARGET_BRANCH="epic/$(echo "$PARENT_ID" | tr '[:upper:]' '[:lower:]')"
841
+ git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
842
+ || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
843
+ else
844
+ TARGET_BRANCH="$BASE_BRANCH"
845
+ fi
846
+
847
+ echo "Picking up: [$IDENTIFIER] $TITLE"
848
+ echo "Epic: $EPIC_INFO | Target branch: $TARGET_BRANCH"
849
+
850
+ git checkout "$TARGET_BRANCH"
851
+ git checkout -B "$TICKET_BRANCH"
852
+
853
+ PROMPT="You are implementing Linear issue $IDENTIFIER.
854
+
855
+ Title: $TITLE
856
+ Epic: $EPIC_INFO
857
+
858
+ Description:
859
+ $DESCRIPTION
860
+
861
+ Step 0 — Executability check (do this before any git or file operation):
862
+ Read the issue title and description above. Can this issue be implemented entirely
863
+ as a code change committed to this repo? Consult the 'Executability check' section of
864
+ CLAUDE.md for the full list of skip conditions.
865
+
866
+ If you must SKIP this issue:
867
+ 1. Output: ⚠ Skipping [$IDENTIFIER]: {one-line reason}
868
+ 2. Output: Ticket skipped — update it to be codebase-only work, then re-run.
869
+ 3. Append to .clancy/progress.txt: YYYY-MM-DD HH:MM | $IDENTIFIER | SKIPPED | {reason}
870
+ 4. Stop — no branches, no file changes, no git operations.
871
+
872
+ If the issue IS implementable, continue:
873
+ 1. Read ALL docs in .clancy/docs/ — especially GIT.md for branching and commit conventions
874
+ 2. Follow the conventions in GIT.md exactly
875
+ 3. Implement the issue fully
876
+ 4. Commit your work following the conventions in GIT.md
877
+ 5. When done, confirm you are finished."
878
+
879
+ CLAUDE_ARGS=(--dangerously-skip-permissions)
880
+ [ -n "${CLANCY_MODEL:-}" ] && CLAUDE_ARGS+=(--model "$CLANCY_MODEL")
881
+ echo "$PROMPT" | claude "${CLAUDE_ARGS[@]}"
882
+
883
+ git checkout "$TARGET_BRANCH"
884
+ git merge --squash "$TICKET_BRANCH"
885
+ if git diff --cached --quiet; then
886
+ echo "⚠ No changes staged after squash merge. Claude may not have committed any work."
887
+ else
888
+ git commit -m "feat($IDENTIFIER): $TITLE"
889
+ fi
890
+
891
+ git branch -d "$TICKET_BRANCH"
892
+
893
+ echo "$(date '+%Y-%m-%d %H:%M') | $IDENTIFIER | $TITLE | DONE" >> .clancy/progress.txt
894
+
895
+ echo "✓ $IDENTIFIER complete."
896
+
897
+ if [ -n "${CLANCY_NOTIFY_WEBHOOK:-}" ]; then
898
+ NOTIFY_MSG="✓ Clancy completed [$IDENTIFIER] $TITLE"
899
+ if echo "$CLANCY_NOTIFY_WEBHOOK" | grep -q "hooks.slack.com"; then
900
+ curl -s -X POST "$CLANCY_NOTIFY_WEBHOOK" \
901
+ -H "Content-Type: application/json" \
902
+ -d "$(jq -n --arg text "$NOTIFY_MSG" '{"text": $text}')" >/dev/null 2>&1 || true
903
+ else
904
+ curl -s -X POST "$CLANCY_NOTIFY_WEBHOOK" \
905
+ -H "Content-Type: application/json" \
906
+ -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
907
+ fi
908
+ fi
909
+ ```
910
+
911
+ ---
912
+
913
+ ### `.clancy/clancy-afk.sh` — all boards
914
+
915
+ Write this file regardless of board choice:
916
+
917
+ ```bash
918
+ #!/usr/bin/env bash
919
+ set -euo pipefail
920
+
921
+ # ─── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────
922
+ # Loop runner for Clancy. Calls clancy-once.sh repeatedly until:
923
+ # - No more tickets are found ("No tickets found", "All done", etc.)
924
+ # - A preflight check fails (output line starting with ✗)
925
+ # - MAX_ITERATIONS is reached
926
+ # - The user presses Ctrl+C
927
+ # ───────────────────────────────────────────────────────────────────────────────
928
+
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; }
932
+
933
+ [ -f .clancy/.env ] || { echo "✗ .clancy/.env not found. Run /clancy:init"; exit 0; }
934
+ # shellcheck source=/dev/null
935
+ source .clancy/.env
936
+
937
+ git rev-parse --git-dir >/dev/null 2>&1 || { echo "✗ Not a git repository."; exit 0; }
938
+
939
+ MAX_ITERATIONS=${MAX_ITERATIONS:-5}
940
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
941
+ ONCE_SCRIPT="$SCRIPT_DIR/clancy-once.sh"
942
+
943
+ if [ ! -f "$ONCE_SCRIPT" ]; then
944
+ echo "✗ Script not found: $ONCE_SCRIPT"
945
+ echo " Run /clancy:init to scaffold scripts."
946
+ exit 0
947
+ fi
948
+
949
+ echo "Starting Clancy — will process up to $MAX_ITERATIONS ticket(s). Ctrl+C to stop early."
950
+ echo ""
951
+
952
+ i=0
953
+ while [ "$i" -lt "$MAX_ITERATIONS" ]; do
954
+ i=$((i + 1))
955
+ echo ""
956
+ echo "=== Iteration $i of $MAX_ITERATIONS ==="
957
+
958
+ TMPFILE=$(mktemp)
959
+ bash "$ONCE_SCRIPT" 2>&1 | tee "$TMPFILE"
960
+ OUTPUT=$(cat "$TMPFILE")
961
+ rm -f "$TMPFILE"
962
+
963
+ if echo "$OUTPUT" | grep -qE "No tickets found|No issues found|All done"; then
964
+ echo ""
965
+ echo "✓ Clancy finished — no more tickets."
966
+ exit 0
967
+ fi
968
+
969
+ if echo "$OUTPUT" | grep -q "Ticket skipped"; then
970
+ echo ""
971
+ echo "⚠ Clancy stopped — ticket was skipped (not implementable from the codebase)."
972
+ echo " Update the ticket to focus on codebase work, then re-run."
973
+ exit 0
974
+ fi
975
+
976
+ if echo "$OUTPUT" | grep -qE "^✗ "; then
977
+ echo ""
978
+ echo "✗ Clancy stopped — preflight check failed. See output above."
979
+ exit 0
980
+ fi
981
+
982
+ sleep 2
983
+ done
984
+
985
+ echo ""
986
+ echo "Reached max iterations ($MAX_ITERATIONS). Run clancy-afk.sh again to continue."
987
+ ```
988
+
989
+ ---
990
+
991
+ ## .env.example files
992
+
993
+ Write the correct `.env.example` for the chosen board to `.clancy/.env.example`.
994
+
995
+ ### Jira
996
+
997
+ ```
998
+ # Clancy — Jira configuration
999
+ # Copy this file to .env and fill in your values.
1000
+ # Never commit .env to version control.
1001
+
1002
+ # ─── Jira ─────────────────────────────────────────────────────────────────────
1003
+ JIRA_BASE_URL=https://your-org.atlassian.net
1004
+ JIRA_USER=your-email@example.com
1005
+ JIRA_API_TOKEN=your-api-token-from-id.atlassian.com
1006
+ JIRA_PROJECT_KEY=PROJ
1007
+
1008
+ # Status name for "ready to be picked up" (default: To Do)
1009
+ CLANCY_JQL_STATUS="To Do"
1010
+
1011
+ # Set to any non-empty value to filter by open sprints (requires Jira Software)
1012
+ # CLANCY_JQL_SPRINT=true
1013
+
1014
+ # Optional: only pick up tickets with this label.
1015
+ # CLANCY_LABEL="clancy"
1016
+
1017
+ # ─── Git ──────────────────────────────────────────────────────────────────────
1018
+ CLANCY_BASE_BRANCH=main
1019
+
1020
+ # ─── Loop ─────────────────────────────────────────────────────────────────────
1021
+ MAX_ITERATIONS=5
1022
+
1023
+ # ─── Model ────────────────────────────────────────────────────────────────────
1024
+ # CLANCY_MODEL=claude-sonnet-4-6
1025
+
1026
+ # ─── Optional: Figma MCP ──────────────────────────────────────────────────────
1027
+ # FIGMA_API_KEY=your-figma-api-key
1028
+
1029
+ # ─── Optional: Playwright visual checks ───────────────────────────────────────
1030
+ # PLAYWRIGHT_ENABLED=true
1031
+ # PLAYWRIGHT_DEV_COMMAND="yarn dev"
1032
+ # PLAYWRIGHT_DEV_PORT=5173
1033
+ # PLAYWRIGHT_STORYBOOK_COMMAND="yarn storybook"
1034
+ # PLAYWRIGHT_STORYBOOK_PORT=6006
1035
+ # PLAYWRIGHT_STARTUP_WAIT=15
1036
+
1037
+ # ─── Optional: Notifications ──────────────────────────────────────────────────
1038
+ # CLANCY_NOTIFY_WEBHOOK=https://hooks.slack.com/services/your/webhook/url
1039
+ ```
1040
+
1041
+ ### GitHub Issues
1042
+
1043
+ ```
1044
+ # Clancy — GitHub Issues configuration
1045
+ # Copy this file to .env and fill in your values.
1046
+ # Never commit .env to version control.
1047
+
1048
+ # ─── GitHub Issues ────────────────────────────────────────────────────────────
1049
+ GITHUB_TOKEN=ghp_your-personal-access-token
1050
+ GITHUB_REPO=owner/repo-name
1051
+
1052
+ # Optional: only pick up issues with this label (in addition to 'clancy').
1053
+ # CLANCY_LABEL=clancy
1054
+
1055
+ # ─── Git ──────────────────────────────────────────────────────────────────────
1056
+ CLANCY_BASE_BRANCH=main
1057
+
1058
+ # ─── Loop ─────────────────────────────────────────────────────────────────────
1059
+ MAX_ITERATIONS=20
1060
+
1061
+ # ─── Model ────────────────────────────────────────────────────────────────────
1062
+ # CLANCY_MODEL=claude-sonnet-4-6
1063
+
1064
+ # ─── Optional: Figma MCP ──────────────────────────────────────────────────────
1065
+ # FIGMA_API_KEY=your-figma-api-key
1066
+
1067
+ # ─── Optional: Playwright visual checks ───────────────────────────────────────
1068
+ # PLAYWRIGHT_ENABLED=true
1069
+ # PLAYWRIGHT_DEV_COMMAND="yarn dev"
1070
+ # PLAYWRIGHT_DEV_PORT=5173
1071
+ # PLAYWRIGHT_STORYBOOK_COMMAND="yarn storybook"
1072
+ # PLAYWRIGHT_STORYBOOK_PORT=6006
1073
+ # PLAYWRIGHT_STARTUP_WAIT=15
1074
+
1075
+ # ─── Optional: Notifications ──────────────────────────────────────────────────
1076
+ # CLANCY_NOTIFY_WEBHOOK=https://hooks.slack.com/services/your/webhook/url
1077
+ ```
1078
+
1079
+ ### Linear
1080
+
1081
+ ```
1082
+ # Clancy — Linear configuration
1083
+ # Copy this file to .env and fill in your values.
1084
+ # Never commit .env to version control.
1085
+
1086
+ # ─── Linear ───────────────────────────────────────────────────────────────────
1087
+ LINEAR_API_KEY=lin_api_your-personal-api-key
1088
+ LINEAR_TEAM_ID=your-team-uuid
1089
+
1090
+ # Optional: only pick up issues with this label.
1091
+ # CLANCY_LABEL=clancy
1092
+
1093
+ # ─── Git ──────────────────────────────────────────────────────────────────────
1094
+ CLANCY_BASE_BRANCH=main
1095
+
1096
+ # ─── Loop ─────────────────────────────────────────────────────────────────────
1097
+ MAX_ITERATIONS=20
1098
+
1099
+ # ─── Model ────────────────────────────────────────────────────────────────────
1100
+ # CLANCY_MODEL=claude-sonnet-4-6
1101
+
1102
+ # ─── Optional: Figma MCP ──────────────────────────────────────────────────────
1103
+ # FIGMA_API_KEY=your-figma-api-key
1104
+
1105
+ # ─── Optional: Playwright visual checks ───────────────────────────────────────
1106
+ # PLAYWRIGHT_ENABLED=true
1107
+ # PLAYWRIGHT_DEV_COMMAND="yarn dev"
1108
+ # PLAYWRIGHT_DEV_PORT=5173
1109
+ # PLAYWRIGHT_STORYBOOK_COMMAND="yarn storybook"
1110
+ # PLAYWRIGHT_STORYBOOK_PORT=6006
1111
+ # PLAYWRIGHT_STARTUP_WAIT=15
1112
+
1113
+ # ─── Optional: Notifications ──────────────────────────────────────────────────
1114
+ # CLANCY_NOTIFY_WEBHOOK=https://hooks.slack.com/services/your/webhook/url
1115
+ ```