chief-clancy 0.1.1 → 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/package.json +1 -1
- package/src/workflows/init.md +3 -3
- package/src/workflows/scaffold.md +826 -0
package/package.json
CHANGED
package/src/workflows/init.md
CHANGED
|
@@ -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.
|
|
180
|
-
2.
|
|
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.
|
|
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
|
+
```
|