chief-clancy 0.2.0-beta.3 → 0.3.0

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