chief-clancy 0.5.12 → 0.7.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 (102) hide show
  1. package/README.md +15 -8
  2. package/dist/bundle/clancy-afk.js +6 -2
  3. package/dist/bundle/clancy-once.js +71 -48
  4. package/dist/installer/hook-installer/hook-installer.d.ts +2 -0
  5. package/dist/installer/hook-installer/hook-installer.d.ts.map +1 -1
  6. package/dist/installer/hook-installer/hook-installer.js +36 -1
  7. package/dist/installer/hook-installer/hook-installer.js.map +1 -1
  8. package/dist/installer/install.js +16 -1
  9. package/dist/installer/install.js.map +1 -1
  10. package/dist/schemas/env.d.ts +36 -0
  11. package/dist/schemas/env.d.ts.map +1 -1
  12. package/dist/schemas/env.js +11 -0
  13. package/dist/schemas/env.js.map +1 -1
  14. package/dist/schemas/github-issues.d.ts +6 -0
  15. package/dist/schemas/github-issues.d.ts.map +1 -1
  16. package/dist/schemas/github-issues.js +3 -0
  17. package/dist/schemas/github-issues.js.map +1 -1
  18. package/dist/schemas/jira.d.ts +21 -0
  19. package/dist/schemas/jira.d.ts.map +1 -1
  20. package/dist/schemas/jira.js +18 -0
  21. package/dist/schemas/jira.js.map +1 -1
  22. package/dist/schemas/linear.d.ts +41 -0
  23. package/dist/schemas/linear.d.ts.map +1 -1
  24. package/dist/schemas/linear.js +34 -0
  25. package/dist/schemas/linear.js.map +1 -1
  26. package/dist/scripts/afk/afk.d.ts.map +1 -1
  27. package/dist/scripts/afk/afk.js +28 -0
  28. package/dist/scripts/afk/afk.js.map +1 -1
  29. package/dist/scripts/afk/report/report.d.ts +47 -0
  30. package/dist/scripts/afk/report/report.d.ts.map +1 -0
  31. package/dist/scripts/afk/report/report.js +194 -0
  32. package/dist/scripts/afk/report/report.js.map +1 -0
  33. package/dist/scripts/board/github/github.d.ts +33 -3
  34. package/dist/scripts/board/github/github.d.ts.map +1 -1
  35. package/dist/scripts/board/github/github.js +112 -27
  36. package/dist/scripts/board/github/github.js.map +1 -1
  37. package/dist/scripts/board/jira/jira.d.ts +36 -4
  38. package/dist/scripts/board/jira/jira.d.ts.map +1 -1
  39. package/dist/scripts/board/jira/jira.js +138 -65
  40. package/dist/scripts/board/jira/jira.js.map +1 -1
  41. package/dist/scripts/board/linear/linear.d.ts +32 -5
  42. package/dist/scripts/board/linear/linear.d.ts.map +1 -1
  43. package/dist/scripts/board/linear/linear.js +135 -17
  44. package/dist/scripts/board/linear/linear.js.map +1 -1
  45. package/dist/scripts/once/board-ops/board-ops.d.ts.map +1 -1
  46. package/dist/scripts/once/board-ops/board-ops.js +1 -1
  47. package/dist/scripts/once/board-ops/board-ops.js.map +1 -1
  48. package/dist/scripts/once/cost/cost.d.ts +10 -0
  49. package/dist/scripts/once/cost/cost.d.ts.map +1 -0
  50. package/dist/scripts/once/cost/cost.js +23 -0
  51. package/dist/scripts/once/cost/cost.js.map +1 -0
  52. package/dist/scripts/once/deliver/deliver.d.ts.map +1 -1
  53. package/dist/scripts/once/deliver/deliver.js +18 -1
  54. package/dist/scripts/once/deliver/deliver.js.map +1 -1
  55. package/dist/scripts/once/fetch-ticket/fetch-ticket.d.ts +19 -1
  56. package/dist/scripts/once/fetch-ticket/fetch-ticket.d.ts.map +1 -1
  57. package/dist/scripts/once/fetch-ticket/fetch-ticket.js +93 -29
  58. package/dist/scripts/once/fetch-ticket/fetch-ticket.js.map +1 -1
  59. package/dist/scripts/once/lock/lock.d.ts +17 -0
  60. package/dist/scripts/once/lock/lock.d.ts.map +1 -0
  61. package/dist/scripts/once/lock/lock.js +70 -0
  62. package/dist/scripts/once/lock/lock.js.map +1 -0
  63. package/dist/scripts/once/once.d.ts.map +1 -1
  64. package/dist/scripts/once/once.js +100 -4
  65. package/dist/scripts/once/once.js.map +1 -1
  66. package/dist/scripts/once/resume/resume.d.ts +24 -0
  67. package/dist/scripts/once/resume/resume.d.ts.map +1 -0
  68. package/dist/scripts/once/resume/resume.js +159 -0
  69. package/dist/scripts/once/resume/resume.js.map +1 -0
  70. package/dist/scripts/shared/format/format.d.ts.map +1 -1
  71. package/dist/scripts/shared/format/format.js +6 -1
  72. package/dist/scripts/shared/format/format.js.map +1 -1
  73. package/dist/scripts/shared/progress/progress.d.ts +18 -2
  74. package/dist/scripts/shared/progress/progress.d.ts.map +1 -1
  75. package/dist/scripts/shared/progress/progress.js +31 -4
  76. package/dist/scripts/shared/progress/progress.js.map +1 -1
  77. package/dist/scripts/shared/pull-request/pr-body/pr-body.d.ts +2 -1
  78. package/dist/scripts/shared/pull-request/pr-body/pr-body.d.ts.map +1 -1
  79. package/dist/scripts/shared/pull-request/pr-body/pr-body.js +10 -1
  80. package/dist/scripts/shared/pull-request/pr-body/pr-body.js.map +1 -1
  81. package/dist/types/remote.d.ts +1 -1
  82. package/dist/types/remote.d.ts.map +1 -1
  83. package/hooks/clancy-branch-guard.js +129 -0
  84. package/hooks/clancy-check-update.js +43 -0
  85. package/hooks/clancy-context-monitor.js +134 -46
  86. package/hooks/clancy-post-compact.js +53 -0
  87. package/hooks/package.json +3 -0
  88. package/package.json +3 -2
  89. package/src/agents/devils-advocate.md +53 -0
  90. package/src/agents/verification-gate.md +128 -0
  91. package/src/roles/planner/workflows/approve-plan.md +2 -2
  92. package/src/roles/reviewer/workflows/logs.md +9 -6
  93. package/src/roles/setup/commands/help.md +7 -0
  94. package/src/roles/setup/workflows/init.md +111 -6
  95. package/src/roles/setup/workflows/scaffold.md +57 -0
  96. package/src/roles/setup/workflows/settings.md +145 -0
  97. package/src/roles/setup/workflows/update.md +18 -0
  98. package/src/roles/strategist/commands/approve-brief.md +20 -0
  99. package/src/roles/strategist/commands/brief.md +27 -0
  100. package/src/roles/strategist/workflows/approve-brief.md +763 -0
  101. package/src/roles/strategist/workflows/brief.md +732 -0
  102. package/src/templates/CLAUDE.md +8 -1
@@ -0,0 +1,763 @@
1
+ # Clancy Approve Brief Workflow
2
+
3
+ ## Overview
4
+
5
+ Approve a reviewed strategic brief by creating child tickets on the board, linking dependencies, and posting a tracking summary on the parent ticket. Tickets are created sequentially in topological (dependency) order. Partial failures stop immediately — re-run to resume.
6
+
7
+ ---
8
+
9
+ ## Step 1 — Preflight checks
10
+
11
+ 1. Check `.clancy/` exists and `.clancy/.env` is present. If not:
12
+ ```
13
+ .clancy/ not found. Run /clancy:init to set up Clancy first.
14
+ ```
15
+ Stop.
16
+
17
+ 2. Source `.clancy/.env` and check board credentials are present.
18
+
19
+ 3. Check `CLANCY_ROLES` includes `strategist` (or env var is unset, which indicates a global install where all roles are available). If `CLANCY_ROLES` is set but does not include `strategist`:
20
+ ```
21
+ The Strategist role is not enabled. Add "strategist" to CLANCY_ROLES in .clancy/.env or run /clancy:settings.
22
+ ```
23
+ Stop.
24
+
25
+ 4. Branch freshness check:
26
+ ```bash
27
+ git fetch origin
28
+ ```
29
+ Compare HEAD with `origin/$CLANCY_BASE_BRANCH`. If behind:
30
+ ```
31
+ Behind by N commits. [1] Pull latest [2] Continue [3] Abort
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Step 2 — Load brief
37
+
38
+ Scan `.clancy/briefs/` for unapproved files — `.md` files WITHOUT a corresponding `.md.approved` marker file.
39
+
40
+ ### Selection logic
41
+
42
+ Parse the argument (if any) against the unapproved list:
43
+
44
+ **No argument + 0 unapproved briefs:**
45
+ ```
46
+ No unapproved briefs found. Run /clancy:brief to generate one.
47
+ ```
48
+ Stop.
49
+
50
+ **No argument + 1 unapproved brief:**
51
+ Auto-select the single brief. Continue.
52
+
53
+ **No argument + 2+ unapproved briefs:**
54
+ Display numbered list:
55
+ ```
56
+ Multiple unapproved briefs found:
57
+
58
+ [1] {slug-a} ({date}) — {N} tickets — Source: {source}
59
+ [2] {slug-b} ({date}) — {N} tickets — Source: {source}
60
+
61
+ Which brief to approve? [1-N]
62
+ ```
63
+
64
+ **Argument is a positive integer (e.g. `2`):**
65
+ Select the Nth unapproved brief by index. If out of range: `Index out of range.` Stop.
66
+
67
+ **Argument matches a ticket identifier (`#\d+`, `[A-Z]+-\d+`):**
68
+ Scan unapproved brief files for a `**Source:**` line containing the identifier. If 0 matches: show available briefs and stop. If 1 match: load it. If 2+ matches: show numbered list and ask.
69
+
70
+ **Argument is other text:**
71
+ Match by slug (filename contains the argument). If 0 matches: `No brief matching "{arg}" found.` Stop. If 1 match: load it. If 2+ matches: list and ask.
72
+
73
+ ### Already-approved guard
74
+
75
+ After loading, verify the `.md.approved` marker does not exist. If it does:
76
+ ```
77
+ Brief "{slug}" is already approved. No action needed.
78
+ ```
79
+ Stop.
80
+
81
+ ---
82
+
83
+ ## Step 3 — Parse decomposition table
84
+
85
+ Read the `## Ticket Decomposition` table from the brief file. Extract each row:
86
+
87
+ | Column | Field |
88
+ |--------|-------|
89
+ | `#` | Sequence number |
90
+ | `Title` | Ticket title |
91
+ | `Description` | 1-2 sentence description |
92
+ | `Size` | S / M / L |
93
+ | `Dependencies` | References like `#1`, `#3` |
94
+ | `Mode` | AFK or HITL |
95
+ | `Ticket` | Board key (if partially created) |
96
+
97
+ ### Validation
98
+
99
+ - **0 tickets:** `No tickets found in the decomposition table. Add at least one ticket to the brief before approving.` Stop.
100
+ - **>10 tickets:** Warn: `Brief has {N} tickets (max recommended: 10). Large decompositions may indicate the idea should be split further.` Continue — advisory only.
101
+ - **Tickets already in Ticket column:** These were created in a prior partial run. Mark them for skipping during creation.
102
+ - **Circular dependencies:** Run cycle detection on the dependency graph. If a cycle exists: `Circular dependency detected between #{N} and #{M}. Edit the brief to resolve, then re-run.` Stop.
103
+
104
+ ### Topological sort
105
+
106
+ Order tickets by their dependency graph so blockers are created before dependents. This ensures blocking relationships can be linked immediately after creation.
107
+
108
+ ---
109
+
110
+ ## Step 4 — Confirm with user
111
+
112
+ Display the ticket list in dependency order:
113
+
114
+ ```
115
+ Clancy — Approve Brief
116
+
117
+ Brief: {slug}
118
+ Parent: {ticket key or "none (standalone)"}
119
+
120
+ Tickets to create (dependency order):
121
+ #1 [S] [AFK] Title — No deps
122
+ #2 [M] [HITL] Title — No deps
123
+ #3 [M] [AFK] Title — After #2
124
+ #4 [S] [AFK] Title — After #1, #3
125
+
126
+ Labels: {list of labels to apply}
127
+ Issue type: {type} (Jira only)
128
+ AFK-ready: {X} | Needs human: {Y}
129
+
130
+ Create {N} tickets? [Y/n]
131
+ ```
132
+
133
+ If user declines: `Cancelled. No changes made.` Stop.
134
+
135
+ ---
136
+
137
+ ## Step 4a — Dry-run check
138
+
139
+ If `--dry-run` flag is set:
140
+ ```
141
+ Dry run complete. No tickets created.
142
+ ```
143
+ Stop. No API calls beyond preflight.
144
+
145
+ ---
146
+
147
+ ## Step 5 — Resolve parent
148
+
149
+ The parent ticket is where child tickets are linked. Determine the parent from one of these sources (in priority order):
150
+
151
+ ### Source 1 — Board-sourced brief
152
+
153
+ If the brief's `**Source:**` line contains a board ticket identifier (e.g. `[#50]`, `[PROJ-200]`, `[ENG-42]`), that ticket is the parent. Fetch it to validate:
154
+
155
+ #### GitHub
156
+ ```bash
157
+ curl -s \
158
+ -H "Authorization: Bearer $GITHUB_TOKEN" \
159
+ -H "Accept: application/vnd.github+json" \
160
+ -H "X-GitHub-Api-Version: 2022-11-28" \
161
+ "https://api.github.com/repos/$GITHUB_REPO/issues/$PARENT_NUMBER"
162
+ ```
163
+
164
+ Check: `pull_request` present? -> `Parent #{N} is a PR, not an issue.` Stop. `state == "closed"`? -> Warn, ask `[y/N]`.
165
+
166
+ #### Jira
167
+ ```bash
168
+ curl -s \
169
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
170
+ -H "Accept: application/json" \
171
+ "$JIRA_BASE_URL/rest/api/3/issue/$PARENT_KEY?fields=summary,status,issuetype,priority"
172
+ ```
173
+
174
+ Check: `status.statusCategory.key == "done"`? -> Warn, ask `[y/N]`. Note if `issuetype.name == "Epic"` (ideal case for parent linking).
175
+
176
+ #### Linear
177
+ ```graphql
178
+ query {
179
+ issues(filter: { identifier: { eq: "$PARENT_KEY" } }) {
180
+ nodes {
181
+ id identifier title
182
+ state { type }
183
+ team { id key }
184
+ children { nodes { id identifier title } }
185
+ }
186
+ }
187
+ }
188
+ ```
189
+
190
+ Check: `state.type == "completed"` or `"canceled"`? -> Warn, ask `[y/N]`. `team.id != LINEAR_TEAM_ID`? -> Warn about cross-team children, ask `[Y/n]`.
191
+
192
+ ### Source 2 — `--epic` flag
193
+
194
+ If `--epic` is provided (e.g. `--epic PROJ-200`, `--epic #100`, `--epic ENG-42`), validate the target using the same checks as Source 1. If not found or is Done: stop.
195
+
196
+ Note: `--epic` is ignored for board-sourced briefs (the source ticket IS the parent).
197
+
198
+ ### Source 3 — `CLANCY_BRIEF_EPIC` default
199
+
200
+ If no board source and no `--epic` flag, check `.clancy/.env` for `CLANCY_BRIEF_EPIC`. If set, validate and use it as the parent.
201
+
202
+ ### Source 4 — No parent (standalone)
203
+
204
+ If none of the above apply:
205
+ ```
206
+ No parent specified. Tickets will be created as standalone.
207
+ Use --epic {KEY} to attach to a parent.
208
+ ```
209
+ Continue — omit parent fields from creation payloads. No tracking comment will be posted.
210
+
211
+ ---
212
+
213
+ ## Step 6 — Look up queue state / labels
214
+
215
+ Platform-specific pre-creation lookups.
216
+
217
+ ### GitHub
218
+
219
+ **Labels to apply per ticket:**
220
+ - `CLANCY_PLAN_LABEL` (default: `needs-refinement`) — planning queue
221
+ - `CLANCY_LABEL` (if set) — Clancy identifier
222
+ - `component:{CLANCY_COMPONENT}` (if `CLANCY_COMPONENT` set)
223
+ - `size:{S|M|L}` — from decomposition table
224
+ - `clancy:afk` or `clancy:hitl` — from Mode column
225
+
226
+ **Label pre-creation:** For each unique label, attempt to create it on the repo. If it already exists, GitHub returns 422 — ignore that error. If 403 (no admin access), note the label as unavailable.
227
+
228
+ ```bash
229
+ curl -s \
230
+ -H "Authorization: Bearer $GITHUB_TOKEN" \
231
+ -H "Accept: application/vnd.github+json" \
232
+ -H "X-GitHub-Api-Version: 2022-11-28" \
233
+ -H "Content-Type: application/json" \
234
+ -X POST \
235
+ "https://api.github.com/repos/$GITHUB_REPO/labels" \
236
+ -d '{"name": "$LABEL_NAME", "color": "d4c5f9"}'
237
+ ```
238
+
239
+ If label creation fails with 403: warn, continue without that label.
240
+
241
+ **Resolve username** for assignee:
242
+ ```bash
243
+ curl -s \
244
+ -H "Authorization: Bearer $GITHUB_TOKEN" \
245
+ -H "Accept: application/vnd.github+json" \
246
+ "https://api.github.com/user"
247
+ ```
248
+ Cache the `login` field for all ticket creations.
249
+
250
+ ### Jira
251
+
252
+ **Validate issue type exists in project:**
253
+ ```bash
254
+ curl -s \
255
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
256
+ -H "Accept: application/json" \
257
+ "$JIRA_BASE_URL/rest/api/3/issue/createmeta/$JIRA_PROJECT_KEY/issuetypes"
258
+ ```
259
+
260
+ Look for `CLANCY_BRIEF_ISSUE_TYPE` (default: `Task`). If not found:
261
+ ```
262
+ Issue type "{type}" not available in project {PROJ}. Available types: {list}.
263
+ Set CLANCY_BRIEF_ISSUE_TYPE in .clancy/.env.
264
+ ```
265
+ Stop.
266
+
267
+ **Labels:** Jira auto-creates labels — no pre-creation needed. Apply: `CLANCY_LABEL` (if set), `clancy:afk` or `clancy:hitl`.
268
+
269
+ **Components:** If `CLANCY_COMPONENT` is set, it maps to the Jira `components` field.
270
+
271
+ **Priority:** Inherit from parent ticket if available. Otherwise omit (Jira uses project default).
272
+
273
+ ### Linear
274
+
275
+ **Look up backlog state UUID:**
276
+ ```graphql
277
+ query {
278
+ workflowStates(filter: {
279
+ team: { id: { eq: "$LINEAR_TEAM_ID" } }
280
+ type: { eq: "backlog" }
281
+ }) {
282
+ nodes { id name }
283
+ }
284
+ }
285
+ ```
286
+
287
+ Use `nodes[0].id`. If empty, fall back to `triage` type, then `unstarted`, then any first state. If truly nothing found, warn and use the team default.
288
+
289
+ **Look up label UUIDs:**
290
+ ```graphql
291
+ query {
292
+ team(id: "$LINEAR_TEAM_ID") {
293
+ labels { nodes { id name } }
294
+ }
295
+ }
296
+ ```
297
+
298
+ For each required label (`CLANCY_LABEL`, `component:{CLANCY_COMPONENT}`, `clancy:afk`, `clancy:hitl`): search by exact name. If not found in team labels, check workspace labels:
299
+
300
+ ```graphql
301
+ query {
302
+ issueLabels(filter: { name: { eq: "$LABEL_NAME" } }) {
303
+ nodes { id name }
304
+ }
305
+ }
306
+ ```
307
+
308
+ If still not found, auto-create at team level:
309
+ ```graphql
310
+ mutation {
311
+ issueLabelCreate(input: { teamId: "$LINEAR_TEAM_ID", name: "$LABEL_NAME" }) {
312
+ success
313
+ issueLabel { id name }
314
+ }
315
+ }
316
+ ```
317
+
318
+ On failure to create label: warn, continue without it.
319
+
320
+ ---
321
+
322
+ ## Step 6a — Pre-creation race check
323
+
324
+ Create the `.approved` marker file with exclusive create (O_EXCL) to prevent concurrent approval:
325
+
326
+ ```
327
+ Marker path: .clancy/briefs/{filename}.approved
328
+ ```
329
+
330
+ If the file already exists (`EEXIST`):
331
+ ```
332
+ Brief is being approved by another process.
333
+ ```
334
+ Stop.
335
+
336
+ If creation succeeds: the marker acts as a lock. **If ticket creation fails later, delete this marker** (partial failure = not approved).
337
+
338
+ ---
339
+
340
+ ## Step 7 — Create child tickets
341
+
342
+ Create tickets **sequentially** in topological (dependency) order. Wait **500ms** between each creation to respect rate limits.
343
+
344
+ For each ticket in dependency order:
345
+
346
+ 1. **Check if already created:** If the Ticket column has a value from a prior partial run, skip this ticket. Display: `[{i}/{N}] skip {KEY} — already exists`
347
+
348
+ 2. **Build the creation payload** (platform-specific — see below).
349
+
350
+ 3. **Send the API request.**
351
+
352
+ 4. **On success:** Record the created key/number. Map the decomposition `#N` to the board key for dependency linking. Display: `[{i}/{N}] + {KEY} — {Title} [{Mode}]`
353
+
354
+ 5. **On failure:** STOP immediately. Do NOT attempt remaining tickets. See partial failure handling in Step 10.
355
+
356
+ 6. **Wait 500ms** before the next creation.
357
+
358
+ ### GitHub — POST /repos/{owner}/{repo}/issues
359
+
360
+ ```bash
361
+ curl -s \
362
+ -H "Authorization: Bearer $GITHUB_TOKEN" \
363
+ -H "Accept: application/vnd.github+json" \
364
+ -H "X-GitHub-Api-Version: 2022-11-28" \
365
+ -H "Content-Type: application/json" \
366
+ -X POST \
367
+ "https://api.github.com/repos/$GITHUB_REPO/issues" \
368
+ -d '{
369
+ "title": "{ticket title}",
370
+ "body": "Epic: #{parent_number}\n\n## {Title}\n\n{Description}\n\n---\n\n**Parent:** #{parent_number}\n**Brief:** {slug}\n**Size:** {S|M|L}\n\n### Dependencies\n\n{Depends on #NN lines or None}\n\n---\n\n*Created by Clancy from strategic brief.*",
371
+ "labels": ["{CLANCY_PLAN_LABEL}", "size:{size}", "clancy:{mode}", ...],
372
+ "assignees": ["{resolved_username}"]
373
+ }'
374
+ ```
375
+
376
+ The `Epic: #{parent_number}` line is always the FIRST line of the body — this enables cross-platform epic completion detection.
377
+
378
+ Dependencies in the body use resolved issue numbers: `Depends on #51` (not decomposition indices).
379
+
380
+ If no parent: omit `Epic:` and `Parent:` lines, omit `assignees` if not resolvable.
381
+
382
+ **On 422 (label validation error):** Retry without the invalid label(s). Warn: `Label "{name}" does not exist. Created issue without it.`
383
+
384
+ **On 429 (rate limited):** Check `X-RateLimit-Reset` header. Wait until reset, retry once. If still limited: stop, enter partial failure flow.
385
+
386
+ ### Jira — POST /rest/api/3/issue
387
+
388
+ ```bash
389
+ curl -s \
390
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
391
+ -H "Content-Type: application/json" \
392
+ -H "Accept: application/json" \
393
+ -X POST \
394
+ "$JIRA_BASE_URL/rest/api/3/issue" \
395
+ -d '{
396
+ "fields": {
397
+ "project": { "key": "$JIRA_PROJECT_KEY" },
398
+ "summary": "{ticket title}",
399
+ "description": {
400
+ "version": 1,
401
+ "type": "doc",
402
+ "content": [
403
+ {
404
+ "type": "paragraph",
405
+ "content": [
406
+ { "type": "text", "text": "Epic: {PARENT_KEY}\n\n{Description}" }
407
+ ]
408
+ }
409
+ ]
410
+ },
411
+ "issuetype": { "name": "{CLANCY_BRIEF_ISSUE_TYPE or Task}" },
412
+ "parent": { "key": "{PARENT_KEY}" },
413
+ "labels": ["{CLANCY_LABEL}", "clancy:{mode}"]
414
+ }
415
+ }'
416
+ ```
417
+
418
+ **Conditional fields (include only when set):**
419
+ - `CLANCY_COMPONENT` -> `"components": [{ "name": "{value}" }]`
420
+ - Parent has priority -> `"priority": { "name": "{priority}" }`
421
+
422
+ **Parent field fallback (classic projects):**
423
+ If the API returns 400 with an error mentioning the `parent` field, retry with `customfield_10014` instead:
424
+ ```json
425
+ "customfield_10014": "{PARENT_KEY}"
426
+ ```
427
+ Cache which field works for the remaining tickets in this batch.
428
+
429
+ If both `parent` and `customfield_10014` fail:
430
+ ```
431
+ Could not set parent. Continue creating tickets without parent? [y/N]
432
+ ```
433
+ Default: N (stop). If Y: create remaining tickets without parent field.
434
+
435
+ **On 400 (component not found):** Retry without `components` field. Warn.
436
+
437
+ **On 429 (rate limited):** Honour `Retry-After` header. Wait, retry once. If still 429: stop, enter partial failure flow.
438
+
439
+ ### Linear — issueCreate mutation
440
+
441
+ ```graphql
442
+ mutation {
443
+ issueCreate(input: {
444
+ teamId: "$LINEAR_TEAM_ID"
445
+ title: "{ticket title}"
446
+ description: "Epic: {PARENT_IDENTIFIER}\n\n{Description}"
447
+ parentId: "{PARENT_UUID}"
448
+ stateId: "{BACKLOG_STATE_UUID}"
449
+ labelIds: ["{label UUIDs}"]
450
+ priority: 0
451
+ }) {
452
+ success
453
+ issue {
454
+ id
455
+ identifier
456
+ title
457
+ url
458
+ }
459
+ }
460
+ }
461
+ ```
462
+
463
+ If no parent: omit `parentId`.
464
+
465
+ **On rate limit (RATELIMITED error code):** Wait 60s, retry once. If still limited: stop, enter partial failure flow.
466
+
467
+ ---
468
+
469
+ ## Step 8 — Link dependencies
470
+
471
+ For each ticket with dependencies, create blocking relationships on the board. This is **best-effort** — warn on failure, do not stop.
472
+
473
+ Skip links involving uncreated tickets (from partial failures).
474
+
475
+ ### GitHub
476
+
477
+ No separate API call needed. Dependencies are embedded in the issue body as `Depends on #{number}` text. GitHub auto-creates cross-references from the `#N` mentions.
478
+
479
+ ### Jira — POST /rest/api/3/issueLink
480
+
481
+ For each dependency (e.g. ticket #3 depends on #2):
482
+
483
+ ```bash
484
+ curl -s \
485
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
486
+ -H "Content-Type: application/json" \
487
+ -H "Accept: application/json" \
488
+ -X POST \
489
+ "$JIRA_BASE_URL/rest/api/3/issueLink" \
490
+ -d '{
491
+ "type": { "name": "Blocks" },
492
+ "inwardIssue": { "key": "{BLOCKER_KEY}" },
493
+ "outwardIssue": { "key": "{DEPENDENT_KEY}" }
494
+ }'
495
+ ```
496
+
497
+ `inwardIssue` = the blocker, `outwardIssue` = the dependent ticket.
498
+
499
+ Wait **200ms** between each link creation.
500
+
501
+ On failure: `Could not link {DEPENDENT} -> {BLOCKER} (dependency). Link manually if needed.`
502
+
503
+ ### Linear — issueRelationCreate mutation
504
+
505
+ ```graphql
506
+ mutation {
507
+ issueRelationCreate(input: {
508
+ issueId: "{DEPENDENT_UUID}"
509
+ relatedIssueId: "{BLOCKER_UUID}"
510
+ type: blockedBy
511
+ }) {
512
+ success
513
+ }
514
+ }
515
+ ```
516
+
517
+ `issueId` = the dependent ticket, `relatedIssueId` = the blocker.
518
+
519
+ Wait **200ms** between each relation creation.
520
+
521
+ On failure: `Could not link {DEPENDENT} -> {BLOCKER}. Add manually in Linear.`
522
+
523
+ ---
524
+
525
+ ## Step 9 — Update brief file
526
+
527
+ After creating tickets, update the local brief file:
528
+
529
+ 1. **Add ticket keys** to the `Ticket` column in the decomposition table:
530
+ ```markdown
531
+ | # | Title | Description | Size | Dependencies | Mode | Ticket |
532
+ |---|-------|-------------|------|--------------|------|--------|
533
+ | 1 | Set up route structure | ... | S | None | AFK | PROJ-201 |
534
+ | 2 | SSO login flow | ... | M | None | HITL | PROJ-202 |
535
+ ```
536
+
537
+ 2. **Update status** from `Draft` to `Approved` (in the `**Status:**` line if present).
538
+
539
+ ---
540
+
541
+ ## Step 10 — Mark approved
542
+
543
+ Check whether all tickets were created successfully:
544
+
545
+ **All created:**
546
+ - The `.approved` marker file (created in Step 6a) stays in place.
547
+
548
+ **Partial failure (some tickets failed):**
549
+ - **DELETE** the `.approved` marker file. Partial failure = not approved.
550
+ - Update the brief file with the tickets that WERE created (Step 9 still applies).
551
+ - Display partial failure summary:
552
+ ```
553
+ Partial: {M} of {N} tickets created
554
+
555
+ PROJ-201 [S] Set up route structure
556
+ PROJ-202 [M] SSO login flow
557
+ X #3 Role-based access control — FAILED ({error})
558
+ - #4-#{N} not attempted
559
+
560
+ Created tickets are live on the board. To complete:
561
+ 1. Fix the issue (check board status/permissions)
562
+ 2. Re-run /clancy:approve-brief to resume creating remaining tickets
563
+ ```
564
+ - Log: `YYYY-MM-DD HH:MM | APPROVE_BRIEF | {slug} | PARTIAL — {M} of {N} created`
565
+ - Stop (do not post tracking comment for partial failures).
566
+
567
+ ---
568
+
569
+ ## Step 11 — Post tracking summary on parent
570
+
571
+ Only if a parent ticket exists AND all tickets were created successfully.
572
+
573
+ Post a comment on the parent ticket listing all created children.
574
+
575
+ ### GitHub
576
+
577
+ ```bash
578
+ curl -s \
579
+ -H "Authorization: Bearer $GITHUB_TOKEN" \
580
+ -H "Accept: application/vnd.github+json" \
581
+ -H "X-GitHub-Api-Version: 2022-11-28" \
582
+ -H "Content-Type: application/json" \
583
+ -X POST \
584
+ "https://api.github.com/repos/$GITHUB_REPO/issues/$PARENT_NUMBER/comments" \
585
+ -d '{"body": "<tracking comment markdown>"}'
586
+ ```
587
+
588
+ ### Jira
589
+
590
+ ```bash
591
+ curl -s \
592
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
593
+ -H "Content-Type: application/json" \
594
+ -H "Accept: application/json" \
595
+ -X POST \
596
+ "$JIRA_BASE_URL/rest/api/3/issue/$PARENT_KEY/comment" \
597
+ -d '{"body": { "version": 1, "type": "doc", "content": [/* ADF table */] }}'
598
+ ```
599
+
600
+ ### Linear
601
+
602
+ ```graphql
603
+ mutation {
604
+ commentCreate(input: {
605
+ issueId: "$PARENT_UUID"
606
+ body: "<tracking comment markdown>"
607
+ }) {
608
+ success
609
+ }
610
+ }
611
+ ```
612
+
613
+ ### Tracking comment format
614
+
615
+ ```markdown
616
+ ## Clancy — Approved Tickets
617
+
618
+ | # | Ticket | Title | Size | Mode |
619
+ |---|--------|-------|------|------|
620
+ | 1 | {KEY} | {Title} | S | AFK |
621
+ | 2 | {KEY} | {Title} | M | HITL |
622
+ | 3 | {KEY} | {Title} | M | AFK |
623
+
624
+ Dependencies linked: {N}
625
+ Created by Clancy on {YYYY-MM-DD}.
626
+ ```
627
+
628
+ **On failure:** Warn: `Could not post tracking summary on {PARENT}. Tickets are created regardless.` Continue — best-effort.
629
+
630
+ ---
631
+
632
+ ## Step 12 — Display summary
633
+
634
+ Show the final result:
635
+
636
+ ```
637
+ {N} tickets created under {PARENT_KEY}
638
+
639
+ {KEY} [{Size}] [{Mode}] {Title}
640
+ {KEY} [{Size}] [{Mode}] {Title}
641
+ {KEY} [{Size}] [{Mode}] {Title}
642
+
643
+ Dependencies linked: {N}
644
+ AFK-ready: {X} | Needs human: {Y}
645
+ Epic branch: epic/{parent-key-lowercase}
646
+
647
+ Next: run /clancy:plan to generate implementation plans.
648
+
649
+ "We've got ourselves a case."
650
+ ```
651
+
652
+ If no parent:
653
+ ```
654
+ {N} standalone tickets created.
655
+ ...
656
+ Next: run /clancy:plan to generate implementation plans.
657
+
658
+ "We've got ourselves a case."
659
+ ```
660
+
661
+ ---
662
+
663
+ ## Step 13 — Log
664
+
665
+ Append to `.clancy/progress.txt`:
666
+
667
+ ```
668
+ YYYY-MM-DD HH:MM | APPROVE_BRIEF | {slug} | {N} tickets created
669
+ ```
670
+
671
+ ---
672
+
673
+ ## Error handling reference
674
+
675
+ ### Rate limiting
676
+
677
+ | Platform | Detection | Response |
678
+ |----------|-----------|----------|
679
+ | GitHub | 403 + `X-RateLimit-Remaining: 0` | Wait until `X-RateLimit-Reset`, retry once |
680
+ | Jira | 429 + `Retry-After` header | Wait the specified seconds, retry once |
681
+ | Linear | `RATELIMITED` error code | Wait 60s, retry once |
682
+
683
+ If retry also fails: stop, enter partial failure flow.
684
+
685
+ ### Auth failure mid-batch
686
+
687
+ If a 401/403 occurs during ticket creation (token expired or revoked after preflight):
688
+ ```
689
+ Auth failed during ticket creation. {M} of {N} created before failure.
690
+ Check credentials and re-run to resume.
691
+ ```
692
+ Enter partial failure flow (Step 10).
693
+
694
+ ### Timeout
695
+
696
+ | Platform | Timeout |
697
+ |----------|---------|
698
+ | GitHub | 15s per API call |
699
+ | Jira | 30s per API call |
700
+ | Linear | 30s per GraphQL call |
701
+
702
+ On timeout: `Request timed out. Ticket may have been created server-side. Check board before re-run.` Enter partial failure flow.
703
+
704
+ ### Duplicate guard (on resume)
705
+
706
+ When resuming from a partial failure, before creating each ticket check the brief's decomposition table for an existing Ticket column entry. Additionally, scan the board for existing children of the parent with matching titles (case-insensitive exact match):
707
+
708
+ #### Jira
709
+ ```bash
710
+ curl -s \
711
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
712
+ -H "Content-Type: application/json" \
713
+ -H "Accept: application/json" \
714
+ -X POST \
715
+ "$JIRA_BASE_URL/rest/api/3/search/jql" \
716
+ -d '{"jql": "parent = $PARENT_KEY", "maxResults": 50, "fields": ["summary"]}'
717
+ ```
718
+
719
+ #### GitHub
720
+ ```bash
721
+ curl -s \
722
+ -H "Authorization: Bearer $GITHUB_TOKEN" \
723
+ -H "Accept: application/vnd.github+json" \
724
+ -H "X-GitHub-Api-Version: 2022-11-28" \
725
+ "https://api.github.com/repos/$GITHUB_REPO/issues?state=open&per_page=30"
726
+ ```
727
+ Filter for issues containing `Epic: #{parent_number}` in the body.
728
+
729
+ #### Linear
730
+ ```graphql
731
+ query {
732
+ issues(filter: { identifier: { eq: "$PARENT_KEY" } }) {
733
+ nodes {
734
+ children { nodes { id identifier title } }
735
+ }
736
+ }
737
+ }
738
+ ```
739
+
740
+ If matching children found:
741
+ ```
742
+ {PARENT} already has children with similar titles:
743
+ - {KEY}: "{Title}" (matches proposed ticket #{N})
744
+
745
+ This brief may have already been approved. Continue anyway? [y/N]
746
+ ```
747
+ Default: N (don't create duplicates).
748
+
749
+ ---
750
+
751
+ ## Notes
752
+
753
+ - The `Epic: {key}` convention is always the first line of every child ticket description across all platforms — this enables cross-platform epic completion detection by `fetchChildrenStatus`
754
+ - Mode labels (`clancy:afk` / `clancy:hitl`) are used by `/clancy:run` to decide whether to pick up a ticket autonomously or skip it for human attention
755
+ - Jira ADF construction: if complex content fails, wrap in a `codeBlock` node as fallback (matches the pattern used by `/clancy:approve-plan`)
756
+ - The `.approved` marker filename is the full brief filename with `.approved` appended (e.g. `.clancy/briefs/2026-03-14-auth-rework.md.approved`)
757
+ - Tickets are created sequentially (not in parallel) to maintain dependency ordering and respect rate limits
758
+ - The 500ms delay between ticket creations is sufficient for all platforms under normal rate limit conditions
759
+ - Dependency links use "Blocks" for Jira, `blockedBy` for Linear, and body text cross-references for GitHub
760
+ - Labels on Jira are auto-created by the platform; on GitHub they must be pre-created or the 422 fallback handles it; on Linear they are looked up and auto-created if missing
761
+ - Sprint/milestone assignment is deliberately not set — this is a team planning decision
762
+ - Linear `priority: 0` means "No priority" — the team triages after creation
763
+ - Jira priority is inherited from the parent if available; Linear and GitHub do not inherit priority