chief-clancy 0.2.0 → 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
@@ -1,320 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Strict mode: exit on error (-e), undefined variables (-u), pipe failures (-o pipefail).
3
- # This means any command that fails will stop the script immediately rather than silently continuing.
4
- set -euo pipefail
5
-
6
- # Parse flags — must happen before preflight so --dry-run works without side effects.
7
- DRY_RUN=false
8
- for arg in "$@"; do
9
- case "$arg" in
10
- --dry-run) DRY_RUN=true ;;
11
- esac
12
- done
13
- readonly DRY_RUN
14
-
15
- # ─── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────
16
- #
17
- # Board: Linear
18
- #
19
- # 1. Preflight — checks all required tools, credentials, and API reachability
20
- # 2. Fetch — pulls the next unstarted issue assigned to you via GraphQL
21
- # 3. Branch — creates a feature branch from the issue's parent branch (or base branch)
22
- # 4. Implement — passes the issue to Claude Code, which reads .clancy/docs/ and implements it
23
- # 5. Merge — squash-merges the feature branch back into the target branch
24
- # 6. Log — appends a completion entry to .clancy/progress.txt
25
- #
26
- # This script is run once per issue. The loop is handled by clancy-afk.sh.
27
- #
28
- # NOTE: Linear personal API keys do NOT use a "Bearer" prefix in the Authorization
29
- # header. OAuth access tokens do. This is correct per Linear's documentation.
30
- #
31
- # NOTE: state.type "unstarted" is a fixed enum value — it filters by state category,
32
- # not state name. This works regardless of what your team named their backlog column.
33
- #
34
- # NOTE: Failures use exit 0, not exit 1. This is intentional — clancy-afk.sh
35
- # detects stop conditions by reading script output rather than exit codes, so a
36
- # non-zero exit would be treated as an unexpected crash rather than a clean stop.
37
- #
38
- # ───────────────────────────────────────────────────────────────────────────────
39
-
40
- # ─── PREFLIGHT ─────────────────────────────────────────────────────────────────
41
-
42
- command -v claude >/dev/null 2>&1 || {
43
- echo "✗ claude CLI not found."
44
- echo " Install it: https://claude.ai/code"
45
- exit 0
46
- }
47
- command -v jq >/dev/null 2>&1 || {
48
- echo "✗ jq not found."
49
- echo " Install: brew install jq (mac) | apt install jq (linux)"
50
- exit 0
51
- }
52
- command -v curl >/dev/null 2>&1 || {
53
- echo "✗ curl not found. Install curl for your OS."
54
- exit 0
55
- }
56
-
57
- [ -f .clancy/.env ] || {
58
- echo "✗ .clancy/.env not found."
59
- echo " Copy .clancy/.env.example to .clancy/.env and fill in your credentials."
60
- echo " Then run: /clancy:init"
61
- exit 0
62
- }
63
- # shellcheck source=/dev/null
64
- source .clancy/.env
65
-
66
- git rev-parse --git-dir >/dev/null 2>&1 || {
67
- echo "✗ Not a git repository."
68
- echo " Clancy must be run from the root of a git project."
69
- exit 0
70
- }
71
-
72
- if ! git diff --quiet || ! git diff --cached --quiet; then
73
- echo "⚠ Working directory has uncommitted changes."
74
- echo " Consider stashing or committing first to avoid confusion."
75
- fi
76
-
77
- [ -n "${LINEAR_API_KEY:-}" ] || { echo "✗ LINEAR_API_KEY is not set in .clancy/.env"; exit 0; }
78
- [ -n "${LINEAR_TEAM_ID:-}" ] || { echo "✗ LINEAR_TEAM_ID is not set in .clancy/.env"; exit 0; }
79
-
80
- # Linear ping — verify API key with a minimal query
81
- # Note: personal API keys do NOT use a "Bearer" prefix — this is correct per Linear docs.
82
- # OAuth access tokens use "Bearer". Do not change this.
83
- PING_BODY=$(curl -s -X POST https://api.linear.app/graphql \
84
- -H "Content-Type: application/json" \
85
- -H "Authorization: $LINEAR_API_KEY" \
86
- -d '{"query": "{ viewer { id } }"}')
87
-
88
- echo "$PING_BODY" | jq -e '.data.viewer.id' >/dev/null 2>&1 || {
89
- echo "✗ Linear authentication failed. Check LINEAR_API_KEY in .clancy/.env."; exit 0
90
- }
91
-
92
- if [ "${PLAYWRIGHT_ENABLED:-}" = "true" ]; then
93
- if lsof -ti:"${PLAYWRIGHT_DEV_PORT:-5173}" >/dev/null 2>&1; then
94
- echo "⚠ Port ${PLAYWRIGHT_DEV_PORT:-5173} is already in use."
95
- echo " If visual checks fail, stop whatever is using the port first."
96
- fi
97
- fi
98
-
99
- echo "✓ Preflight passed. Starting Clancy..."
100
-
101
- # ─── END PREFLIGHT ─────────────────────────────────────────────────────────────
102
-
103
- # ─── FETCH ISSUE ───────────────────────────────────────────────────────────────
104
-
105
- # Fetch one unstarted issue assigned to the current user on the configured team.
106
- # Note: personal API keys do NOT use "Bearer" prefix — this is intentional.
107
- #
108
- # GraphQL query (expanded for readability):
109
- # viewer {
110
- # assignedIssues(
111
- # filter: {
112
- # state: { type: { eq: "unstarted" } } ← fixed enum, works regardless of column name
113
- # team: { id: { eq: "$LINEAR_TEAM_ID" } }
114
- # labels: { name: { eq: "$CLANCY_LABEL" } } ← only if CLANCY_LABEL is set
115
- # }
116
- # first: 1
117
- # orderBy: priority
118
- # ) {
119
- # nodes { id identifier title description parent { identifier title } }
120
- # }
121
- # }
122
-
123
- # Validate user-controlled values to prevent GraphQL injection.
124
- # Values are passed via GraphQL variables (JSON-encoded by jq) rather than string-interpolated.
125
- if ! echo "$LINEAR_TEAM_ID" | grep -qE '^[a-zA-Z0-9_-]+$'; then
126
- echo "✗ LINEAR_TEAM_ID contains invalid characters. Check .clancy/.env."
127
- exit 0
128
- fi
129
- if [ -n "${CLANCY_LABEL:-}" ] && ! echo "$CLANCY_LABEL" | grep -qE '^[a-zA-Z0-9 _-]+$'; then
130
- echo "✗ CLANCY_LABEL contains invalid characters. Use only letters, numbers, spaces, hyphens, and underscores."
131
- exit 0
132
- fi
133
-
134
- # Build request using GraphQL variables — values are JSON-encoded by jq, never interpolated into the query string.
135
- # The label filter clause is only added to the query when CLANCY_LABEL is set, since passing null would match nothing.
136
- if [ -n "${CLANCY_LABEL:-}" ]; then
137
- REQUEST_BODY=$(jq -n \
138
- --arg teamId "$LINEAR_TEAM_ID" \
139
- --arg label "$CLANCY_LABEL" \
140
- '{"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}}')
141
- else
142
- REQUEST_BODY=$(jq -n \
143
- --arg teamId "$LINEAR_TEAM_ID" \
144
- '{"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}}')
145
- fi
146
-
147
- RESPONSE=$(curl -s -X POST https://api.linear.app/graphql \
148
- -H "Content-Type: application/json" \
149
- -H "Authorization: $LINEAR_API_KEY" \
150
- -d "$REQUEST_BODY")
151
-
152
- # Check for API errors before parsing (rate limit, permission error, etc.)
153
- if ! echo "$RESPONSE" | jq -e '.data.viewer.assignedIssues' >/dev/null 2>&1; then
154
- ERR_MSG=$(echo "$RESPONSE" | jq -r '.errors[0].message // "Unexpected response"' 2>/dev/null || echo "Unexpected response")
155
- echo "✗ Linear API error: $ERR_MSG. Check LINEAR_API_KEY in .clancy/.env."
156
- exit 0
157
- fi
158
-
159
- NODE_COUNT=$(echo "$RESPONSE" | jq '.data.viewer.assignedIssues.nodes | length')
160
- if [ "$NODE_COUNT" -eq 0 ]; then
161
- echo "No issues found. All done!"
162
- exit 0
163
- fi
164
-
165
- ISSUE_ID=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].id')
166
- IDENTIFIER=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].identifier')
167
- TITLE=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].title')
168
- DESCRIPTION=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].description // "No description"')
169
- PARENT_ID=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].parent.identifier // "none"')
170
- PARENT_TITLE=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].parent.title // ""')
171
-
172
- EPIC_INFO="${PARENT_ID}"
173
- if [ -n "$PARENT_TITLE" ] && [ "$PARENT_TITLE" != "null" ]; then
174
- EPIC_INFO="${PARENT_ID} — ${PARENT_TITLE}"
175
- fi
176
-
177
- BASE_BRANCH="${CLANCY_BASE_BRANCH:-main}"
178
- TICKET_BRANCH="feature/$(echo "$IDENTIFIER" | tr '[:upper:]' '[:lower:]')"
179
-
180
- # Auto-detect target branch from ticket's parent.
181
- # If the issue has a parent, branch from epic/{parent-id} (creating it from
182
- # BASE_BRANCH if it doesn't exist yet). Otherwise branch from BASE_BRANCH directly.
183
- if [ "$PARENT_ID" != "none" ]; then
184
- TARGET_BRANCH="epic/$(echo "$PARENT_ID" | tr '[:upper:]' '[:lower:]')"
185
- else
186
- TARGET_BRANCH="$BASE_BRANCH"
187
- fi
188
-
189
- # ─── DRY RUN ───────────────────────────────────────────────────────────────────
190
-
191
- if [ "$DRY_RUN" = "true" ]; then
192
- echo ""
193
- echo "── Dry run ──────────────────────────────────────"
194
- echo " Issue: [$IDENTIFIER] $TITLE"
195
- echo " Epic: $EPIC_INFO"
196
- echo " Target branch: $TARGET_BRANCH"
197
- echo " Feature branch: $TICKET_BRANCH"
198
- echo "─────────────────────────────────────────────────"
199
- echo " No changes made. Remove --dry-run to run for real."
200
- exit 0
201
- fi
202
-
203
- # ─── IMPLEMENT ─────────────────────────────────────────────────────────────────
204
-
205
- echo "Picking up: [$IDENTIFIER] $TITLE"
206
- echo "Epic: $EPIC_INFO | Target branch: $TARGET_BRANCH"
207
-
208
- git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
209
- || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
210
- git checkout "$TARGET_BRANCH"
211
- # -B creates the branch if it doesn't exist, or resets it to HEAD if it does.
212
- # This handles retries cleanly without failing on an already-existing branch.
213
- git checkout -B "$TICKET_BRANCH"
214
-
215
- # Transition issue to In Progress (best-effort — never fails the run).
216
- # Queries team workflow states by type "started", picks the first match.
217
- if [ -n "${CLANCY_STATUS_IN_PROGRESS:-}" ]; then
218
- STATE_RESP=$(curl -s -X POST https://api.linear.app/graphql \
219
- -H "Content-Type: application/json" \
220
- -H "Authorization: $LINEAR_API_KEY" \
221
- -d "$(jq -n --arg teamId "$LINEAR_TEAM_ID" --arg name "$CLANCY_STATUS_IN_PROGRESS" \
222
- '{"query": "query($teamId: String!, $name: String!) { workflowStates(filter: { team: { id: { eq: $teamId } } name: { eq: $name } }) { nodes { id } } }", "variables": {"teamId": $teamId, "name": $name}}')")
223
- IN_PROGRESS_STATE_ID=$(echo "$STATE_RESP" | jq -r '.data.workflowStates.nodes[0].id // empty')
224
- if [ -n "$IN_PROGRESS_STATE_ID" ]; then
225
- curl -s -X POST https://api.linear.app/graphql \
226
- -H "Content-Type: application/json" \
227
- -H "Authorization: $LINEAR_API_KEY" \
228
- -d "$(jq -n --arg issueId "$ISSUE_ID" --arg stateId "$IN_PROGRESS_STATE_ID" \
229
- '{"query": "mutation($issueId: String!, $stateId: String!) { issueUpdate(id: $issueId, input: { stateId: $stateId }) { success } }", "variables": {"issueId": $issueId, "stateId": $stateId}}')" \
230
- >/dev/null 2>&1 || true
231
- echo " → Transitioned to $CLANCY_STATUS_IN_PROGRESS"
232
- else
233
- echo " ⚠ Workflow state '$CLANCY_STATUS_IN_PROGRESS' not found — check CLANCY_STATUS_IN_PROGRESS in .clancy/.env."
234
- fi
235
- fi
236
-
237
- PROMPT="You are implementing Linear issue $IDENTIFIER.
238
-
239
- Title: $TITLE
240
- Epic: $EPIC_INFO
241
-
242
- Description:
243
- $DESCRIPTION
244
-
245
- Step 0 — Executability check (do this before any git or file operation):
246
- Read the issue title and description above. Can this issue be implemented entirely
247
- as a code change committed to this repo? Consult the 'Executability check' section of
248
- CLAUDE.md for the full list of skip conditions.
249
-
250
- If you must SKIP this issue:
251
- 1. Output: ⚠ Skipping [$IDENTIFIER]: {one-line reason}
252
- 2. Output: Ticket skipped — update it to be codebase-only work, then re-run.
253
- 3. Append to .clancy/progress.txt: YYYY-MM-DD HH:MM | $IDENTIFIER | SKIPPED | {reason}
254
- 4. Stop — no branches, no file changes, no git operations.
255
-
256
- If the issue IS implementable, continue:
257
- 1. Read core docs in .clancy/docs/: STACK.md, ARCHITECTURE.md, CONVENTIONS.md, GIT.md, DEFINITION-OF-DONE.md, CONCERNS.md
258
- 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)
259
- 2. Follow the conventions in GIT.md exactly
260
- 3. Implement the issue fully
261
- 4. Commit your work following the conventions in GIT.md
262
- 5. When done, confirm you are finished."
263
-
264
- CLAUDE_ARGS=(--dangerously-skip-permissions)
265
- [ -n "${CLANCY_MODEL:-}" ] && CLAUDE_ARGS+=(--model "$CLANCY_MODEL")
266
- echo "$PROMPT" | claude "${CLAUDE_ARGS[@]}"
267
-
268
- # ─── MERGE & LOG ───────────────────────────────────────────────────────────────
269
-
270
- # Squash all commits from the feature branch into a single commit on the target branch.
271
- git checkout "$TARGET_BRANCH"
272
- git merge --squash "$TICKET_BRANCH"
273
- if git diff --cached --quiet; then
274
- echo "⚠ No changes staged after squash merge. Claude may not have committed any work."
275
- else
276
- git commit -m "feat($IDENTIFIER): $TITLE"
277
- fi
278
-
279
- # Delete ticket branch locally
280
- git branch -d "$TICKET_BRANCH"
281
-
282
- # Transition issue to Done (best-effort — never fails the run).
283
- if [ -n "${CLANCY_STATUS_DONE:-}" ]; then
284
- STATE_RESP=$(curl -s -X POST https://api.linear.app/graphql \
285
- -H "Content-Type: application/json" \
286
- -H "Authorization: $LINEAR_API_KEY" \
287
- -d "$(jq -n --arg teamId "$LINEAR_TEAM_ID" --arg name "$CLANCY_STATUS_DONE" \
288
- '{"query": "query($teamId: String!, $name: String!) { workflowStates(filter: { team: { id: { eq: $teamId } } name: { eq: $name } }) { nodes { id } } }", "variables": {"teamId": $teamId, "name": $name}}')")
289
- DONE_STATE_ID=$(echo "$STATE_RESP" | jq -r '.data.workflowStates.nodes[0].id // empty')
290
- if [ -n "$DONE_STATE_ID" ]; then
291
- curl -s -X POST https://api.linear.app/graphql \
292
- -H "Content-Type: application/json" \
293
- -H "Authorization: $LINEAR_API_KEY" \
294
- -d "$(jq -n --arg issueId "$ISSUE_ID" --arg stateId "$DONE_STATE_ID" \
295
- '{"query": "mutation($issueId: String!, $stateId: String!) { issueUpdate(id: $issueId, input: { stateId: $stateId }) { success } }", "variables": {"issueId": $issueId, "stateId": $stateId}}')" \
296
- >/dev/null 2>&1 || true
297
- echo " → Transitioned to $CLANCY_STATUS_DONE"
298
- else
299
- echo " ⚠ Workflow state '$CLANCY_STATUS_DONE' not found — check CLANCY_STATUS_DONE in .clancy/.env."
300
- fi
301
- fi
302
-
303
- # Log progress
304
- echo "$(date '+%Y-%m-%d %H:%M') | $IDENTIFIER | $TITLE | DONE" >> .clancy/progress.txt
305
-
306
- echo "✓ $IDENTIFIER complete."
307
-
308
- # Send completion notification if webhook is configured
309
- if [ -n "${CLANCY_NOTIFY_WEBHOOK:-}" ]; then
310
- NOTIFY_MSG="✓ Clancy completed [$IDENTIFIER] $TITLE"
311
- if echo "$CLANCY_NOTIFY_WEBHOOK" | grep -q "hooks.slack.com"; then
312
- curl -s -X POST "$CLANCY_NOTIFY_WEBHOOK" \
313
- -H "Content-Type: application/json" \
314
- -d "$(jq -n --arg text "$NOTIFY_MSG" '{"text": $text}')" >/dev/null 2>&1 || true
315
- else
316
- curl -s -X POST "$CLANCY_NOTIFY_WEBHOOK" \
317
- -H "Content-Type: application/json" \
318
- -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
319
- fi
320
- fi
@@ -1,322 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Strict mode: exit on error (-e), undefined variables (-u), pipe failures (-o pipefail).
3
- # This means any command that fails will stop the script immediately rather than silently continuing.
4
- set -euo pipefail
5
-
6
- # Parse flags — must happen before preflight so --dry-run works without side effects.
7
- DRY_RUN=false
8
- for arg in "$@"; do
9
- case "$arg" in
10
- --dry-run) DRY_RUN=true ;;
11
- esac
12
- done
13
- readonly DRY_RUN
14
-
15
- # ─── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────
16
- #
17
- # Board: Jira
18
- #
19
- # 1. Preflight — checks all required tools, credentials, and board reachability
20
- # 2. Fetch — pulls the next assigned "To Do" ticket from Jira (maxResults: 1)
21
- # 3. Branch — creates a feature branch from the ticket's epic branch (or base branch)
22
- # 4. Implement — passes the ticket to Claude Code, which reads .clancy/docs/ and implements it
23
- # 5. Merge — squash-merges the feature branch back into the target branch
24
- # 6. Log — appends a completion entry to .clancy/progress.txt
25
- #
26
- # This script is run once per ticket. The loop is handled by clancy-afk.sh.
27
- #
28
- # NOTE: This file has no -jira suffix by design. /clancy:init copies the correct
29
- # board variant into the user's .clancy/ directory as clancy-once.sh regardless
30
- # of board. The board is determined by which template was copied, not the filename.
31
- #
32
- # NOTE: Failures use exit 0, not exit 1. This is intentional — clancy-afk.sh
33
- # detects stop conditions by reading script output rather than exit codes, so a
34
- # non-zero exit would be treated as an unexpected crash rather than a clean stop.
35
- #
36
- # ───────────────────────────────────────────────────────────────────────────────
37
-
38
- # ─── PREFLIGHT ─────────────────────────────────────────────────────────────────
39
-
40
- command -v claude >/dev/null 2>&1 || {
41
- echo "✗ claude CLI not found."
42
- echo " Install it: https://claude.ai/code"
43
- exit 0
44
- }
45
- command -v jq >/dev/null 2>&1 || {
46
- echo "✗ jq not found."
47
- echo " Install: brew install jq (mac) | apt install jq (linux)"
48
- exit 0
49
- }
50
- command -v curl >/dev/null 2>&1 || {
51
- echo "✗ curl not found. Install curl for your OS."
52
- exit 0
53
- }
54
-
55
- [ -f .clancy/.env ] || {
56
- echo "✗ .clancy/.env not found."
57
- echo " Copy .clancy/.env.example to .clancy/.env and fill in your credentials."
58
- echo " Then run: /clancy:init"
59
- exit 0
60
- }
61
- # shellcheck source=/dev/null
62
- source .clancy/.env
63
-
64
- git rev-parse --git-dir >/dev/null 2>&1 || {
65
- echo "✗ Not a git repository."
66
- echo " Clancy must be run from the root of a git project."
67
- exit 0
68
- }
69
-
70
- if ! git diff --quiet || ! git diff --cached --quiet; then
71
- echo "⚠ Working directory has uncommitted changes."
72
- echo " Consider stashing or committing first to avoid confusion."
73
- fi
74
-
75
- [ -n "${JIRA_BASE_URL:-}" ] || { echo "✗ JIRA_BASE_URL is not set in .clancy/.env"; exit 0; }
76
- [ -n "${JIRA_USER:-}" ] || { echo "✗ JIRA_USER is not set in .clancy/.env"; exit 0; }
77
- [ -n "${JIRA_API_TOKEN:-}" ] || { echo "✗ JIRA_API_TOKEN is not set in .clancy/.env"; exit 0; }
78
- [ -n "${JIRA_PROJECT_KEY:-}" ] || { echo "✗ JIRA_PROJECT_KEY is not set in .clancy/.env"; exit 0; }
79
- if ! echo "$JIRA_PROJECT_KEY" | grep -qE '^[A-Z][A-Z0-9]+$'; then
80
- echo "✗ JIRA_PROJECT_KEY format is invalid. Expected uppercase letters and numbers only (e.g. PROJ, ENG2). Check JIRA_PROJECT_KEY in .clancy/.env."
81
- exit 0
82
- fi
83
-
84
- PING=$(curl -s -o /dev/null -w "%{http_code}" \
85
- -u "$JIRA_USER:$JIRA_API_TOKEN" \
86
- "$JIRA_BASE_URL/rest/api/3/project/$JIRA_PROJECT_KEY")
87
-
88
- case "$PING" in
89
- 200) ;;
90
- 401) echo "✗ Jira authentication failed. Check JIRA_USER and JIRA_API_TOKEN in .clancy/.env."; exit 0 ;;
91
- 403) echo "✗ Jira access denied. Your token may lack Browse Projects permission."; exit 0 ;;
92
- 404) echo "✗ Jira project '$JIRA_PROJECT_KEY' not found. Check JIRA_PROJECT_KEY in .clancy/.env."; exit 0 ;;
93
- 000) echo "✗ Could not reach Jira at $JIRA_BASE_URL. Check JIRA_BASE_URL and your network."; exit 0 ;;
94
- *) echo "✗ Jira returned unexpected status $PING. Check your config."; exit 0 ;;
95
- esac
96
-
97
- if [ "${PLAYWRIGHT_ENABLED:-}" = "true" ]; then
98
- if lsof -ti:"${PLAYWRIGHT_DEV_PORT:-5173}" >/dev/null 2>&1; then
99
- echo "⚠ Port ${PLAYWRIGHT_DEV_PORT:-5173} is already in use."
100
- echo " Clancy will attempt to start the dev server on this port."
101
- echo " If visual checks fail, stop whatever is using the port first."
102
- fi
103
- fi
104
-
105
- echo "✓ Preflight passed. Starting Clancy..."
106
-
107
- # ─── END PREFLIGHT ─────────────────────────────────────────────────────────────
108
-
109
- # ─── FETCH TICKET ──────────────────────────────────────────────────────────────
110
-
111
- # Validate user-controlled values to prevent JQL injection.
112
- # JQL does not support parameterised queries, so we restrict to safe characters.
113
- if [ -n "${CLANCY_LABEL:-}" ] && ! echo "$CLANCY_LABEL" | grep -qE '^[a-zA-Z0-9 _-]+$'; then
114
- echo "✗ CLANCY_LABEL contains invalid characters. Use only letters, numbers, spaces, hyphens, and underscores."
115
- exit 0
116
- fi
117
- if ! echo "${CLANCY_JQL_STATUS:-To Do}" | grep -qE '^[a-zA-Z0-9 _-]+$'; then
118
- echo "✗ CLANCY_JQL_STATUS contains invalid characters. Use only letters, numbers, spaces, hyphens, and underscores."
119
- exit 0
120
- fi
121
-
122
- # Build JQL — sprint filter is optional (requires Jira Software license).
123
- # Uses the /rest/api/3/search/jql POST endpoint — the old GET /search was removed Aug 2025.
124
- # maxResults:1 is intentional — pick one ticket per run, never paginate.
125
- if [ -n "${CLANCY_JQL_SPRINT:-}" ]; then
126
- SPRINT_CLAUSE="AND sprint in openSprints()"
127
- else
128
- SPRINT_CLAUSE=""
129
- fi
130
-
131
- # Optional label filter — set CLANCY_LABEL in .env to only pick up tickets with that label.
132
- # Useful for mixed backlogs where not every ticket is suitable for autonomous implementation.
133
- if [ -n "${CLANCY_LABEL:-}" ]; then
134
- LABEL_CLAUSE="AND labels = \"$CLANCY_LABEL\""
135
- else
136
- LABEL_CLAUSE=""
137
- fi
138
-
139
- RESPONSE=$(curl -s \
140
- -u "$JIRA_USER:$JIRA_API_TOKEN" \
141
- -X POST \
142
- -H "Content-Type: application/json" \
143
- -H "Accept: application/json" \
144
- "$JIRA_BASE_URL/rest/api/3/search/jql" \
145
- -d "{
146
- \"jql\": \"project=$JIRA_PROJECT_KEY $SPRINT_CLAUSE $LABEL_CLAUSE AND assignee=currentUser() AND status=\\\"${CLANCY_JQL_STATUS:-To Do}\\\" ORDER BY priority ASC\",
147
- \"maxResults\": 1,
148
- \"fields\": [\"summary\", \"description\", \"issuelinks\", \"parent\", \"customfield_10014\"]
149
- }")
150
-
151
- # New endpoint returns { "issues": [...], "isLast": bool } — no .total field
152
- ISSUE_COUNT=$(echo "$RESPONSE" | jq '.issues | length')
153
- if [ "$ISSUE_COUNT" -eq 0 ]; then
154
- echo "No tickets found. All done!"
155
- exit 0
156
- fi
157
-
158
- TICKET_KEY=$(echo "$RESPONSE" | jq -r '.issues[0].key')
159
- SUMMARY=$(echo "$RESPONSE" | jq -r '.issues[0].fields.summary')
160
-
161
- # Extract description via recursive ADF walk
162
- DESCRIPTION=$(echo "$RESPONSE" | jq -r '
163
- .issues[0].fields.description
164
- | .. | strings
165
- | select(length > 0)
166
- | . + "\n"
167
- ' 2>/dev/null || echo "No description")
168
-
169
- # Extract epic — try parent first (next-gen), fall back to customfield_10014 (classic)
170
- EPIC_INFO=$(echo "$RESPONSE" | jq -r '
171
- .issues[0].fields.parent.key // .issues[0].fields.customfield_10014 // "none"
172
- ')
173
-
174
- # Extract blocking issue links
175
- BLOCKERS=$(echo "$RESPONSE" | jq -r '
176
- [.issues[0].fields.issuelinks[]?
177
- | select(.type.name == "Blocks" and .inwardIssue?)
178
- | .inwardIssue.key]
179
- | if length > 0 then "Blocked by: " + join(", ") else "None" end
180
- ' 2>/dev/null || echo "None")
181
-
182
- BASE_BRANCH="${CLANCY_BASE_BRANCH:-main}"
183
- TICKET_BRANCH="feature/$(echo "$TICKET_KEY" | tr '[:upper:]' '[:lower:]')"
184
-
185
- # Auto-detect target branch from ticket's parent epic.
186
- # If the ticket has a parent epic, branch from epic/{epic-key} (creating it from
187
- # BASE_BRANCH if it doesn't exist yet). Otherwise branch from BASE_BRANCH directly.
188
- if [ "$EPIC_INFO" != "none" ]; then
189
- TARGET_BRANCH="epic/$(echo "$EPIC_INFO" | tr '[:upper:]' '[:lower:]')"
190
- else
191
- TARGET_BRANCH="$BASE_BRANCH"
192
- fi
193
-
194
- # ─── DRY RUN ───────────────────────────────────────────────────────────────────
195
-
196
- if [ "$DRY_RUN" = "true" ]; then
197
- echo ""
198
- echo "── Dry run ──────────────────────────────────────"
199
- echo " Ticket: [$TICKET_KEY] $SUMMARY"
200
- echo " Epic: $EPIC_INFO"
201
- echo " Blockers: $BLOCKERS"
202
- echo " Target branch: $TARGET_BRANCH"
203
- echo " Feature branch: $TICKET_BRANCH"
204
- echo "─────────────────────────────────────────────────"
205
- echo " No changes made. Remove --dry-run to run for real."
206
- exit 0
207
- fi
208
-
209
- # ─── IMPLEMENT ─────────────────────────────────────────────────────────────────
210
-
211
- echo "Picking up: [$TICKET_KEY] $SUMMARY"
212
- echo "Epic: $EPIC_INFO | Target branch: $TARGET_BRANCH | Blockers: $BLOCKERS"
213
-
214
- git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
215
- || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
216
- git checkout "$TARGET_BRANCH"
217
- # -B creates the branch if it doesn't exist, or resets it to HEAD if it does.
218
- # This handles retries cleanly without failing on an already-existing branch.
219
- git checkout -B "$TICKET_BRANCH"
220
-
221
- # Transition ticket to In Progress (best-effort — never fails the run)
222
- if [ -n "${CLANCY_STATUS_IN_PROGRESS:-}" ]; then
223
- TRANSITIONS=$(curl -s \
224
- -u "$JIRA_USER:$JIRA_API_TOKEN" \
225
- -H "Accept: application/json" \
226
- "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/transitions")
227
- IN_PROGRESS_ID=$(echo "$TRANSITIONS" | jq -r \
228
- --arg name "$CLANCY_STATUS_IN_PROGRESS" \
229
- '.transitions[] | select(.name == $name) | .id' | head -1)
230
- if [ -n "$IN_PROGRESS_ID" ]; then
231
- curl -s -X POST \
232
- -u "$JIRA_USER:$JIRA_API_TOKEN" \
233
- -H "Content-Type: application/json" \
234
- "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/transitions" \
235
- -d "$(jq -n --arg id "$IN_PROGRESS_ID" '{"transition":{"id":$id}}')" >/dev/null 2>&1 || true
236
- echo " → Transitioned to $CLANCY_STATUS_IN_PROGRESS"
237
- fi
238
- fi
239
-
240
- PROMPT="You are implementing Jira ticket $TICKET_KEY.
241
-
242
- Summary: $SUMMARY
243
- Epic: $EPIC_INFO
244
- Blockers: $BLOCKERS
245
-
246
- Description:
247
- $DESCRIPTION
248
-
249
- Step 0 — Executability check (do this before any git or file operation):
250
- Read the ticket summary and description above. Can this ticket be implemented entirely
251
- as a code change committed to this repo? Consult the 'Executability check' section of
252
- CLAUDE.md for the full list of skip conditions.
253
-
254
- If you must SKIP this ticket:
255
- 1. Output: ⚠ Skipping [$TICKET_KEY]: {one-line reason}
256
- 2. Output: Ticket skipped — update it to be codebase-only work, then re-run.
257
- 3. Append to .clancy/progress.txt: YYYY-MM-DD HH:MM | $TICKET_KEY | SKIPPED | {reason}
258
- 4. Stop — no branches, no file changes, no git operations.
259
-
260
- If the ticket IS implementable, continue:
261
- 1. Read core docs in .clancy/docs/: STACK.md, ARCHITECTURE.md, CONVENTIONS.md, GIT.md, DEFINITION-OF-DONE.md, CONCERNS.md
262
- 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)
263
- 2. Follow the conventions in GIT.md exactly
264
- 3. Implement the ticket fully
265
- 4. Commit your work following the conventions in GIT.md
266
- 5. When done, confirm you are finished."
267
-
268
- CLAUDE_ARGS=(--dangerously-skip-permissions)
269
- [ -n "${CLANCY_MODEL:-}" ] && CLAUDE_ARGS+=(--model "$CLANCY_MODEL")
270
- echo "$PROMPT" | claude "${CLAUDE_ARGS[@]}"
271
-
272
- # ─── MERGE & LOG ───────────────────────────────────────────────────────────────
273
-
274
- # Squash all commits from the feature branch into a single commit on the target branch.
275
- git checkout "$TARGET_BRANCH"
276
- git merge --squash "$TICKET_BRANCH"
277
- if git diff --cached --quiet; then
278
- echo "⚠ No changes staged after squash merge. Claude may not have committed any work."
279
- else
280
- git commit -m "feat($TICKET_KEY): $SUMMARY"
281
- fi
282
-
283
- # Delete ticket branch locally (never push deletes)
284
- git branch -d "$TICKET_BRANCH"
285
-
286
- # Transition ticket to Done (best-effort — never fails the run)
287
- if [ -n "${CLANCY_STATUS_DONE:-}" ]; then
288
- TRANSITIONS=$(curl -s \
289
- -u "$JIRA_USER:$JIRA_API_TOKEN" \
290
- -H "Accept: application/json" \
291
- "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/transitions")
292
- DONE_ID=$(echo "$TRANSITIONS" | jq -r \
293
- --arg name "$CLANCY_STATUS_DONE" \
294
- '.transitions[] | select(.name == $name) | .id' | head -1)
295
- if [ -n "$DONE_ID" ]; then
296
- curl -s -X POST \
297
- -u "$JIRA_USER:$JIRA_API_TOKEN" \
298
- -H "Content-Type: application/json" \
299
- "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/transitions" \
300
- -d "$(jq -n --arg id "$DONE_ID" '{"transition":{"id":$id}}')" >/dev/null 2>&1 || true
301
- echo " → Transitioned to $CLANCY_STATUS_DONE"
302
- fi
303
- fi
304
-
305
- # Log progress
306
- echo "$(date '+%Y-%m-%d %H:%M') | $TICKET_KEY | $SUMMARY | DONE" >> .clancy/progress.txt
307
-
308
- echo "✓ $TICKET_KEY complete."
309
-
310
- # Send completion notification if webhook is configured
311
- if [ -n "${CLANCY_NOTIFY_WEBHOOK:-}" ]; then
312
- NOTIFY_MSG="✓ Clancy completed [$TICKET_KEY] $SUMMARY"
313
- if echo "$CLANCY_NOTIFY_WEBHOOK" | grep -q "hooks.slack.com"; then
314
- curl -s -X POST "$CLANCY_NOTIFY_WEBHOOK" \
315
- -H "Content-Type: application/json" \
316
- -d "$(jq -n --arg text "$NOTIFY_MSG" '{"text": $text}')" >/dev/null 2>&1 || true
317
- else
318
- curl -s -X POST "$CLANCY_NOTIFY_WEBHOOK" \
319
- -H "Content-Type: application/json" \
320
- -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
321
- fi
322
- fi