@chief-clancy/terminal 0.1.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.
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/hooks/clancy-branch-guard.js +1 -0
- package/dist/hooks/clancy-check-update.js +2 -0
- package/dist/hooks/clancy-context-monitor.js +9 -0
- package/dist/hooks/clancy-credential-guard.js +2 -0
- package/dist/hooks/clancy-drift-detector.js +1 -0
- package/dist/hooks/clancy-notification.js +1 -0
- package/dist/hooks/clancy-post-compact.js +2 -0
- package/dist/hooks/clancy-statusline.js +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/installer/file-ops/file-ops.d.ts +35 -0
- package/dist/installer/file-ops/file-ops.d.ts.map +1 -0
- package/dist/installer/file-ops/file-ops.js +95 -0
- package/dist/installer/file-ops/file-ops.js.map +1 -0
- package/dist/installer/file-ops/index.d.ts +2 -0
- package/dist/installer/file-ops/index.d.ts.map +1 -0
- package/dist/installer/file-ops/index.js +2 -0
- package/dist/installer/file-ops/index.js.map +1 -0
- package/dist/installer/hook-installer/hook-installer.d.ts +22 -0
- package/dist/installer/hook-installer/hook-installer.d.ts.map +1 -0
- package/dist/installer/hook-installer/hook-installer.js +213 -0
- package/dist/installer/hook-installer/hook-installer.js.map +1 -0
- package/dist/installer/hook-installer/index.d.ts +2 -0
- package/dist/installer/hook-installer/index.d.ts.map +1 -0
- package/dist/installer/hook-installer/index.js +2 -0
- package/dist/installer/hook-installer/index.js.map +1 -0
- package/dist/installer/install/index.d.ts +3 -0
- package/dist/installer/install/index.d.ts.map +1 -0
- package/dist/installer/install/index.js +2 -0
- package/dist/installer/install/index.js.map +1 -0
- package/dist/installer/install/install.d.ts +124 -0
- package/dist/installer/install/install.d.ts.map +1 -0
- package/dist/installer/install/install.js +255 -0
- package/dist/installer/install/install.js.map +1 -0
- package/dist/installer/manifest/index.d.ts +2 -0
- package/dist/installer/manifest/index.d.ts.map +1 -0
- package/dist/installer/manifest/index.js +2 -0
- package/dist/installer/manifest/index.js.map +1 -0
- package/dist/installer/manifest/manifest.d.ts +46 -0
- package/dist/installer/manifest/manifest.d.ts.map +1 -0
- package/dist/installer/manifest/manifest.js +180 -0
- package/dist/installer/manifest/manifest.js.map +1 -0
- package/dist/installer/prompts/index.d.ts +2 -0
- package/dist/installer/prompts/index.d.ts.map +1 -0
- package/dist/installer/prompts/index.js +2 -0
- package/dist/installer/prompts/index.js.map +1 -0
- package/dist/installer/prompts/prompts.d.ts +34 -0
- package/dist/installer/prompts/prompts.d.ts.map +1 -0
- package/dist/installer/prompts/prompts.js +28 -0
- package/dist/installer/prompts/prompts.js.map +1 -0
- package/dist/installer/role-filter/index.d.ts +2 -0
- package/dist/installer/role-filter/index.d.ts.map +1 -0
- package/dist/installer/role-filter/index.js +2 -0
- package/dist/installer/role-filter/index.js.map +1 -0
- package/dist/installer/role-filter/role-filter.d.ts +33 -0
- package/dist/installer/role-filter/role-filter.d.ts.map +1 -0
- package/dist/installer/role-filter/role-filter.js +91 -0
- package/dist/installer/role-filter/role-filter.js.map +1 -0
- package/dist/installer/shared/fs-errors/fs-errors.d.ts +3 -0
- package/dist/installer/shared/fs-errors/fs-errors.d.ts.map +1 -0
- package/dist/installer/shared/fs-errors/fs-errors.js +7 -0
- package/dist/installer/shared/fs-errors/fs-errors.js.map +1 -0
- package/dist/installer/shared/fs-errors/index.d.ts +2 -0
- package/dist/installer/shared/fs-errors/index.d.ts.map +1 -0
- package/dist/installer/shared/fs-errors/index.js +2 -0
- package/dist/installer/shared/fs-errors/index.js.map +1 -0
- package/dist/installer/shared/fs-guards/fs-guards.d.ts +3 -0
- package/dist/installer/shared/fs-guards/fs-guards.d.ts.map +1 -0
- package/dist/installer/shared/fs-guards/fs-guards.js +18 -0
- package/dist/installer/shared/fs-guards/fs-guards.js.map +1 -0
- package/dist/installer/shared/fs-guards/index.d.ts +2 -0
- package/dist/installer/shared/fs-guards/index.d.ts.map +1 -0
- package/dist/installer/shared/fs-guards/index.js +2 -0
- package/dist/installer/shared/fs-guards/index.js.map +1 -0
- package/dist/installer/shared/type-guards/index.d.ts +2 -0
- package/dist/installer/shared/type-guards/index.d.ts.map +1 -0
- package/dist/installer/shared/type-guards/index.js +2 -0
- package/dist/installer/shared/type-guards/index.js.map +1 -0
- package/dist/installer/shared/type-guards/type-guards.d.ts +8 -0
- package/dist/installer/shared/type-guards/type-guards.d.ts.map +1 -0
- package/dist/installer/shared/type-guards/type-guards.js +10 -0
- package/dist/installer/shared/type-guards/type-guards.js.map +1 -0
- package/dist/installer/ui/index.d.ts +2 -0
- package/dist/installer/ui/index.d.ts.map +1 -0
- package/dist/installer/ui/index.js +2 -0
- package/dist/installer/ui/index.js.map +1 -0
- package/dist/installer/ui/ui.d.ts +23 -0
- package/dist/installer/ui/ui.d.ts.map +1 -0
- package/dist/installer/ui/ui.js +121 -0
- package/dist/installer/ui/ui.js.map +1 -0
- package/dist/runner/autopilot/autopilot.d.ts +71 -0
- package/dist/runner/autopilot/autopilot.d.ts.map +1 -0
- package/dist/runner/autopilot/autopilot.js +206 -0
- package/dist/runner/autopilot/autopilot.js.map +1 -0
- package/dist/runner/autopilot/index.d.ts +2 -0
- package/dist/runner/autopilot/index.d.ts.map +1 -0
- package/dist/runner/autopilot/index.js +2 -0
- package/dist/runner/autopilot/index.js.map +1 -0
- package/dist/runner/cli-bridge/cli-bridge.d.ts +34 -0
- package/dist/runner/cli-bridge/cli-bridge.d.ts.map +1 -0
- package/dist/runner/cli-bridge/cli-bridge.js +53 -0
- package/dist/runner/cli-bridge/cli-bridge.js.map +1 -0
- package/dist/runner/cli-bridge/index.d.ts +2 -0
- package/dist/runner/cli-bridge/index.d.ts.map +1 -0
- package/dist/runner/cli-bridge/index.js +2 -0
- package/dist/runner/cli-bridge/index.js.map +1 -0
- package/dist/runner/dep-factory/deliver-phase.d.ts +24 -0
- package/dist/runner/dep-factory/deliver-phase.d.ts.map +1 -0
- package/dist/runner/dep-factory/deliver-phase.js +57 -0
- package/dist/runner/dep-factory/deliver-phase.js.map +1 -0
- package/dist/runner/dep-factory/dep-factory.d.ts +38 -0
- package/dist/runner/dep-factory/dep-factory.d.ts.map +1 -0
- package/dist/runner/dep-factory/dep-factory.js +193 -0
- package/dist/runner/dep-factory/dep-factory.js.map +1 -0
- package/dist/runner/dep-factory/index.d.ts +2 -0
- package/dist/runner/dep-factory/index.d.ts.map +1 -0
- package/dist/runner/dep-factory/index.js +2 -0
- package/dist/runner/dep-factory/index.js.map +1 -0
- package/dist/runner/dep-factory/invoke-phase.d.ts +20 -0
- package/dist/runner/dep-factory/invoke-phase.d.ts.map +1 -0
- package/dist/runner/dep-factory/invoke-phase.js +45 -0
- package/dist/runner/dep-factory/invoke-phase.js.map +1 -0
- package/dist/runner/implement/implement.d.ts +38 -0
- package/dist/runner/implement/implement.d.ts.map +1 -0
- package/dist/runner/implement/implement.js +61 -0
- package/dist/runner/implement/implement.js.map +1 -0
- package/dist/runner/implement/index.d.ts +2 -0
- package/dist/runner/implement/index.d.ts.map +1 -0
- package/dist/runner/implement/index.js +2 -0
- package/dist/runner/implement/index.js.map +1 -0
- package/dist/runner/notify/index.d.ts +2 -0
- package/dist/runner/notify/index.d.ts.map +1 -0
- package/dist/runner/notify/index.js +2 -0
- package/dist/runner/notify/index.js.map +1 -0
- package/dist/runner/notify/notify.d.ts +49 -0
- package/dist/runner/notify/notify.d.ts.map +1 -0
- package/dist/runner/notify/notify.js +90 -0
- package/dist/runner/notify/notify.js.map +1 -0
- package/dist/runner/prompt-builder/index.d.ts +2 -0
- package/dist/runner/prompt-builder/index.d.ts.map +1 -0
- package/dist/runner/prompt-builder/index.js +2 -0
- package/dist/runner/prompt-builder/index.js.map +1 -0
- package/dist/runner/prompt-builder/prompt-builder.d.ts +53 -0
- package/dist/runner/prompt-builder/prompt-builder.d.ts.map +1 -0
- package/dist/runner/prompt-builder/prompt-builder.js +122 -0
- package/dist/runner/prompt-builder/prompt-builder.js.map +1 -0
- package/dist/runner/session-report/index.d.ts +2 -0
- package/dist/runner/session-report/index.d.ts.map +1 -0
- package/dist/runner/session-report/index.js +2 -0
- package/dist/runner/session-report/index.js.map +1 -0
- package/dist/runner/session-report/session-report.d.ts +81 -0
- package/dist/runner/session-report/session-report.d.ts.map +1 -0
- package/dist/runner/session-report/session-report.js +227 -0
- package/dist/runner/session-report/session-report.js.map +1 -0
- package/dist/runner/shared/types.d.ts +30 -0
- package/dist/runner/shared/types.d.ts.map +1 -0
- package/dist/runner/shared/types.js +2 -0
- package/dist/runner/shared/types.js.map +1 -0
- package/dist/shared/ansi/ansi.d.ts +59 -0
- package/dist/shared/ansi/ansi.d.ts.map +1 -0
- package/dist/shared/ansi/ansi.js +59 -0
- package/dist/shared/ansi/ansi.js.map +1 -0
- package/dist/shared/ansi/index.d.ts +2 -0
- package/dist/shared/ansi/index.d.ts.map +1 -0
- package/dist/shared/ansi/index.js +2 -0
- package/dist/shared/ansi/index.js.map +1 -0
- package/package.json +52 -0
- package/src/agents/agents.test.ts +57 -0
- package/src/agents/arch-agent.md +80 -0
- package/src/agents/concerns-agent.md +96 -0
- package/src/agents/design-agent.md +146 -0
- package/src/agents/devils-advocate.md +54 -0
- package/src/agents/quality-agent.md +178 -0
- package/src/agents/tech-agent.md +101 -0
- package/src/agents/verification-gate.md +128 -0
- package/src/roles/implementer/commands/autopilot.md +11 -0
- package/src/roles/implementer/commands/dry-run.md +15 -0
- package/src/roles/implementer/commands/implement.md +19 -0
- package/src/roles/implementer/workflows/autopilot.md +136 -0
- package/src/roles/implementer/workflows/implement.md +161 -0
- package/src/roles/planner/commands/approve-plan.md +11 -0
- package/src/roles/planner/commands/plan.md +22 -0
- package/src/roles/planner/workflows/approve-plan.md +970 -0
- package/src/roles/planner/workflows/plan.md +868 -0
- package/src/roles/reviewer/commands/logs.md +7 -0
- package/src/roles/reviewer/commands/review.md +9 -0
- package/src/roles/reviewer/commands/status.md +9 -0
- package/src/roles/reviewer/workflows/logs.md +109 -0
- package/src/roles/reviewer/workflows/review.md +197 -0
- package/src/roles/reviewer/workflows/status.md +142 -0
- package/src/roles/roles.test.ts +87 -0
- package/src/roles/setup/commands/doctor.md +7 -0
- package/src/roles/setup/commands/help.md +80 -0
- package/src/roles/setup/commands/init.md +7 -0
- package/src/roles/setup/commands/map-codebase.md +17 -0
- package/src/roles/setup/commands/settings.md +7 -0
- package/src/roles/setup/commands/uninstall.md +5 -0
- package/src/roles/setup/commands/update-docs.md +9 -0
- package/src/roles/setup/commands/update.md +13 -0
- package/src/roles/setup/workflows/doctor.md +131 -0
- package/src/roles/setup/workflows/init.md +1096 -0
- package/src/roles/setup/workflows/map-codebase.md +130 -0
- package/src/roles/setup/workflows/scaffold.md +872 -0
- package/src/roles/setup/workflows/settings.md +958 -0
- package/src/roles/setup/workflows/uninstall.md +170 -0
- package/src/roles/setup/workflows/update-docs.md +95 -0
- package/src/roles/setup/workflows/update.md +287 -0
- package/src/roles/strategist/commands/approve-brief.md +23 -0
- package/src/roles/strategist/commands/brief.md +29 -0
- package/src/roles/strategist/workflows/approve-brief.md +1540 -0
- package/src/roles/strategist/workflows/brief.md +1330 -0
- package/src/templates/CLAUDE.md +101 -0
- package/src/templates/templates.test.ts +53 -0
|
@@ -0,0 +1,1540 @@
|
|
|
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
|
+
```
|
|
14
|
+
.clancy/ not found. Run /clancy:init to set up Clancy first.
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Stop.
|
|
18
|
+
|
|
19
|
+
2. Source `.clancy/.env` and check board credentials are present.
|
|
20
|
+
|
|
21
|
+
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`:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
The Strategist role is not enabled. Add "strategist" to CLANCY_ROLES in .clancy/.env or run /clancy:settings.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Stop.
|
|
28
|
+
|
|
29
|
+
4. Branch freshness check:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
git fetch origin
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Compare HEAD with `origin/$CLANCY_BASE_BRANCH`. If behind:
|
|
36
|
+
|
|
37
|
+
**AFK mode** (`--afk` flag or `CLANCY_MODE=afk`): auto-pull without prompting.
|
|
38
|
+
|
|
39
|
+
**Interactive mode:**
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
Behind by N commits. [1] Pull latest [2] Continue [3] Abort
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Step 2 — Load brief
|
|
48
|
+
|
|
49
|
+
Scan `.clancy/briefs/` for unapproved files — `.md` files WITHOUT a corresponding `.md.approved` marker file.
|
|
50
|
+
|
|
51
|
+
### Selection logic
|
|
52
|
+
|
|
53
|
+
Parse the argument (if any) against the unapproved list:
|
|
54
|
+
|
|
55
|
+
**No argument + 0 unapproved briefs:**
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
No unapproved briefs found. Run /clancy:brief to generate one.
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Stop.
|
|
62
|
+
|
|
63
|
+
**No argument + 1 unapproved brief:**
|
|
64
|
+
Auto-select the single brief. Continue.
|
|
65
|
+
|
|
66
|
+
**No argument + 2+ unapproved briefs:**
|
|
67
|
+
Display numbered list:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
Multiple unapproved briefs found:
|
|
71
|
+
|
|
72
|
+
[1] {slug-a} ({date}) — {N} tickets — Source: {source}
|
|
73
|
+
[2] {slug-b} ({date}) — {N} tickets — Source: {source}
|
|
74
|
+
|
|
75
|
+
Which brief to approve? [1-N]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Argument is a positive integer (e.g. `2`):**
|
|
79
|
+
Select the Nth unapproved brief by index. If out of range: `Index out of range.` Stop.
|
|
80
|
+
|
|
81
|
+
**Argument matches a ticket identifier (`#\d+`, `[A-Z]+-\d+`, bare number for Azure DevOps, `[A-Za-z]{1,5}-\d+`/bare number for Shortcut, or UUID/`notion-XXXXXXXX` for Notion):**
|
|
82
|
+
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.
|
|
83
|
+
|
|
84
|
+
**Argument is other text:**
|
|
85
|
+
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.
|
|
86
|
+
|
|
87
|
+
### Already-approved guard
|
|
88
|
+
|
|
89
|
+
After loading, verify the `.md.approved` marker does not exist. If it does:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
Brief "{slug}" is already approved. No action needed.
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Stop.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Step 3 — Parse decomposition table
|
|
100
|
+
|
|
101
|
+
Read the `## Ticket Decomposition` table from the brief file. Extract each row:
|
|
102
|
+
|
|
103
|
+
| Column | Field |
|
|
104
|
+
| -------------- | -------------------------------- |
|
|
105
|
+
| `#` | Sequence number |
|
|
106
|
+
| `Title` | Ticket title |
|
|
107
|
+
| `Description` | 1-2 sentence description |
|
|
108
|
+
| `Size` | S / M / L |
|
|
109
|
+
| `Dependencies` | References like `#1`, `#3` |
|
|
110
|
+
| `Mode` | AFK or HITL |
|
|
111
|
+
| `Ticket` | Board key (if partially created) |
|
|
112
|
+
|
|
113
|
+
### Validation
|
|
114
|
+
|
|
115
|
+
- **0 tickets:** `No tickets found in the decomposition table. Add at least one ticket to the brief before approving.` Stop.
|
|
116
|
+
- **>10 tickets:** Warn: `Brief has {N} tickets (max recommended: 10). Large decompositions may indicate the idea should be split further.` Continue — advisory only.
|
|
117
|
+
- **Tickets already in Ticket column:** These were created in a prior partial run. Mark them for skipping during creation.
|
|
118
|
+
- **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.
|
|
119
|
+
|
|
120
|
+
### Topological sort
|
|
121
|
+
|
|
122
|
+
Order tickets by their dependency graph so blockers are created before dependents. This ensures blocking relationships can be linked immediately after creation.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Step 4 — Confirm with user
|
|
127
|
+
|
|
128
|
+
Display the ticket list in dependency order:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
Clancy — Approve Brief
|
|
132
|
+
|
|
133
|
+
Brief: {slug}
|
|
134
|
+
Parent: {ticket key or "none (standalone)"}
|
|
135
|
+
|
|
136
|
+
Tickets to create (dependency order):
|
|
137
|
+
#1 [S] [AFK] Title — No deps
|
|
138
|
+
#2 [M] [HITL] Title — No deps
|
|
139
|
+
#3 [M] [AFK] Title — After #2
|
|
140
|
+
#4 [S] [AFK] Title — After #1, #3
|
|
141
|
+
|
|
142
|
+
Labels: {list of labels to apply}
|
|
143
|
+
Issue type: {type} (Jira only)
|
|
144
|
+
AFK-ready: {X} | Needs human: {Y}
|
|
145
|
+
|
|
146
|
+
Create {N} tickets? [Y/n]
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**AFK mode:** If running in AFK mode (`--afk` flag OR `CLANCY_MODE=afk`), skip the confirmation prompt and auto-confirm. Display the ticket list for logging purposes but proceed without waiting for input.
|
|
150
|
+
|
|
151
|
+
If user declines (interactive only): `Cancelled. No changes made.` Stop.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Step 4a — Dry-run check
|
|
156
|
+
|
|
157
|
+
If `--dry-run` flag is set:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
Dry run complete. No tickets created.
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Stop. No API calls beyond preflight.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Step 5 — Resolve parent
|
|
168
|
+
|
|
169
|
+
The parent ticket is where child tickets are linked. Determine the parent from one of these sources (in priority order):
|
|
170
|
+
|
|
171
|
+
### Source 1 — Board-sourced brief
|
|
172
|
+
|
|
173
|
+
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:
|
|
174
|
+
|
|
175
|
+
#### GitHub
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
curl -s \
|
|
179
|
+
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
|
180
|
+
-H "Accept: application/vnd.github+json" \
|
|
181
|
+
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
182
|
+
"https://api.github.com/repos/$GITHUB_REPO/issues/$PARENT_NUMBER"
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Check: `pull_request` present? -> `Parent #{N} is a PR, not an issue.` Stop. `state == "closed"`? -> Warn, ask `[y/N]`.
|
|
186
|
+
|
|
187
|
+
#### Jira
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
curl -s \
|
|
191
|
+
-u "$JIRA_USER:$JIRA_API_TOKEN" \
|
|
192
|
+
-H "Accept: application/json" \
|
|
193
|
+
"$JIRA_BASE_URL/rest/api/3/issue/$PARENT_KEY?fields=summary,status,issuetype,priority"
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Check: `status.statusCategory.key == "done"`? -> Warn, ask `[y/N]`. Note if `issuetype.name == "Epic"` (ideal case for parent linking).
|
|
197
|
+
|
|
198
|
+
#### Linear
|
|
199
|
+
|
|
200
|
+
```graphql
|
|
201
|
+
query {
|
|
202
|
+
issues(filter: { identifier: { eq: "$PARENT_KEY" } }) {
|
|
203
|
+
nodes {
|
|
204
|
+
id
|
|
205
|
+
identifier
|
|
206
|
+
title
|
|
207
|
+
state {
|
|
208
|
+
type
|
|
209
|
+
}
|
|
210
|
+
team {
|
|
211
|
+
id
|
|
212
|
+
key
|
|
213
|
+
}
|
|
214
|
+
children {
|
|
215
|
+
nodes {
|
|
216
|
+
id
|
|
217
|
+
identifier
|
|
218
|
+
title
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Check: `state.type == "completed"` or `"canceled"`? -> Warn, ask `[y/N]`. `team.id != LINEAR_TEAM_ID`? -> Warn about cross-team children, ask `[Y/n]`.
|
|
227
|
+
|
|
228
|
+
#### Azure DevOps
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
curl -s \
|
|
232
|
+
-u ":$AZDO_PAT" \
|
|
233
|
+
-H "Accept: application/json" \
|
|
234
|
+
"https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$PARENT_ID?\$expand=relations&api-version=7.1"
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Check: `fields["System.State"]` is `Done`/`Closed`/`Resolved`? -> Warn, ask `[y/N]`. Check `relations` for existing children (type `System.LinkTypes.Hierarchy-Forward`).
|
|
238
|
+
|
|
239
|
+
#### Shortcut
|
|
240
|
+
|
|
241
|
+
If the parent is a Shortcut epic, fetch it:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
curl -s \
|
|
245
|
+
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
|
|
246
|
+
"https://api.app.shortcut.com/api/v3/epics/$EPIC_ID"
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Check: `completed` or `archived`? -> Warn, ask `[y/N]`. Fetch epic stories via `GET /api/v3/epics/$EPIC_ID/stories` to check for existing children.
|
|
250
|
+
|
|
251
|
+
If the parent is a story (not an epic), fetch it:
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
curl -s \
|
|
255
|
+
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
|
|
256
|
+
"https://api.app.shortcut.com/api/v3/stories/$STORY_ID"
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Check: `completed` or `archived`? -> Warn, ask `[y/N]`. Check `story_links` for existing children.
|
|
260
|
+
|
|
261
|
+
#### Notion
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
curl -s \
|
|
265
|
+
-H "Authorization: Bearer $NOTION_TOKEN" \
|
|
266
|
+
-H "Notion-Version: 2022-06-28" \
|
|
267
|
+
"https://api.notion.com/v1/pages/$PAGE_ID"
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Check: `archived` is `true`? -> Warn, ask `[y/N]`. Check `relation` properties for existing child pages.
|
|
271
|
+
|
|
272
|
+
**Notion limitation:** Notion does not have a native parent-child hierarchy like other boards. Parent relationships are modelled via `relation` properties in the database. If no relation property is configured, children are standalone pages in the same database.
|
|
273
|
+
|
|
274
|
+
### Source 2 — `--epic` flag
|
|
275
|
+
|
|
276
|
+
If `--epic` is provided (e.g. `--epic PROJ-200`, `--epic #100`, `--epic ENG-42`, `--epic SC-123`, `--epic notion-XXXXXXXX`), validate the target using the same checks as Source 1. If not found or is Done: stop.
|
|
277
|
+
|
|
278
|
+
Note: `--epic` is ignored for board-sourced briefs (the source ticket IS the parent).
|
|
279
|
+
|
|
280
|
+
### Source 3 — `CLANCY_BRIEF_EPIC` default
|
|
281
|
+
|
|
282
|
+
If no board source and no `--epic` flag, check `.clancy/.env` for `CLANCY_BRIEF_EPIC`. If set, validate and use it as the parent.
|
|
283
|
+
|
|
284
|
+
### Source 4 — No parent (standalone)
|
|
285
|
+
|
|
286
|
+
If none of the above apply:
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
No parent specified. Tickets will be created as standalone.
|
|
290
|
+
Use --epic {KEY} to attach to a parent.
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Continue — omit parent fields from creation payloads. No tracking comment will be posted.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Step 6 — Look up queue state / labels
|
|
298
|
+
|
|
299
|
+
Platform-specific pre-creation lookups.
|
|
300
|
+
|
|
301
|
+
### GitHub
|
|
302
|
+
|
|
303
|
+
**Pipeline label for children — this is mandatory, always apply a pipeline label to every child ticket.** Determine which label to use:
|
|
304
|
+
|
|
305
|
+
- `--skip-plan` flag → use `CLANCY_LABEL_BUILD` from `.clancy/.env` if set, otherwise `clancy:build`
|
|
306
|
+
- Planner role enabled (`CLANCY_ROLES` includes `planner`) → use `CLANCY_LABEL_PLAN` from `.clancy/.env` if set, otherwise `clancy:plan`
|
|
307
|
+
- Planner role NOT enabled → use `CLANCY_LABEL_BUILD` from `.clancy/.env` if set, otherwise `clancy:build`
|
|
308
|
+
Ensure the label exists on the board (create it if missing), then add it to each child ticket.
|
|
309
|
+
|
|
310
|
+
**Labels to apply per ticket:**
|
|
311
|
+
|
|
312
|
+
- The pipeline label determined above (`CLANCY_LABEL_PLAN` or `CLANCY_LABEL_BUILD`) — replaces `CLANCY_LABEL` on children
|
|
313
|
+
- `component:{CLANCY_COMPONENT}` (if `CLANCY_COMPONENT` set)
|
|
314
|
+
- `size:{S|M|L}` — from decomposition table
|
|
315
|
+
- `clancy:afk` or `clancy:hitl` — from Mode column
|
|
316
|
+
|
|
317
|
+
Note: `CLANCY_LABEL` is NOT applied to child tickets when pipeline labels are active. The pipeline label (`clancy:plan` or `clancy:build`) serves as the queue identifier.
|
|
318
|
+
|
|
319
|
+
**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.
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
curl -s \
|
|
323
|
+
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
|
324
|
+
-H "Accept: application/vnd.github+json" \
|
|
325
|
+
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
326
|
+
-H "Content-Type: application/json" \
|
|
327
|
+
-X POST \
|
|
328
|
+
"https://api.github.com/repos/$GITHUB_REPO/labels" \
|
|
329
|
+
-d '{"name": "$LABEL_NAME", "color": "d4c5f9"}'
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
If label creation fails with 403: warn, continue without that label.
|
|
333
|
+
|
|
334
|
+
**Resolve username** for assignee:
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
curl -s \
|
|
338
|
+
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
|
339
|
+
-H "Accept: application/vnd.github+json" \
|
|
340
|
+
"https://api.github.com/user"
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Cache the `login` field for all ticket creations.
|
|
344
|
+
|
|
345
|
+
### Jira
|
|
346
|
+
|
|
347
|
+
**Validate issue type exists in project:**
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
curl -s \
|
|
351
|
+
-u "$JIRA_USER:$JIRA_API_TOKEN" \
|
|
352
|
+
-H "Accept: application/json" \
|
|
353
|
+
"$JIRA_BASE_URL/rest/api/3/issue/createmeta/$JIRA_PROJECT_KEY/issuetypes"
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Look for `CLANCY_BRIEF_ISSUE_TYPE` (default: `Task`). If not found:
|
|
357
|
+
|
|
358
|
+
```
|
|
359
|
+
Issue type "{type}" not available in project {PROJ}. Available types: {list}.
|
|
360
|
+
Set CLANCY_BRIEF_ISSUE_TYPE in .clancy/.env.
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Stop.
|
|
364
|
+
|
|
365
|
+
**Pipeline label for children — mandatory, same logic as GitHub.** Use `CLANCY_LABEL_PLAN` or `CLANCY_LABEL_BUILD` from `.clancy/.env` (defaults: `clancy:plan` / `clancy:build`). Always apply to every child ticket.
|
|
366
|
+
|
|
367
|
+
**Labels:** Jira auto-creates labels — no pre-creation needed. Apply: the pipeline label determined above, `clancy:afk` or `clancy:hitl`. `CLANCY_LABEL` is NOT applied to children when pipeline labels are active.
|
|
368
|
+
|
|
369
|
+
**Components:** If `CLANCY_COMPONENT` is set, it maps to the Jira `components` field.
|
|
370
|
+
|
|
371
|
+
**Priority:** Inherit from parent ticket if available. Otherwise omit (Jira uses project default).
|
|
372
|
+
|
|
373
|
+
### Linear
|
|
374
|
+
|
|
375
|
+
**Look up backlog state UUID:**
|
|
376
|
+
|
|
377
|
+
```graphql
|
|
378
|
+
query {
|
|
379
|
+
workflowStates(
|
|
380
|
+
filter: { team: { id: { eq: "$LINEAR_TEAM_ID" } }, type: { eq: "backlog" } }
|
|
381
|
+
) {
|
|
382
|
+
nodes {
|
|
383
|
+
id
|
|
384
|
+
name
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
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.
|
|
391
|
+
|
|
392
|
+
**Look up label UUIDs:**
|
|
393
|
+
|
|
394
|
+
```graphql
|
|
395
|
+
query {
|
|
396
|
+
team(id: "$LINEAR_TEAM_ID") {
|
|
397
|
+
labels {
|
|
398
|
+
nodes {
|
|
399
|
+
id
|
|
400
|
+
name
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**Pipeline label for children — mandatory, same logic as GitHub/Jira.** Use `CLANCY_LABEL_PLAN` or `CLANCY_LABEL_BUILD` from `.clancy/.env` (defaults: `clancy:plan` / `clancy:build`). Always apply to every child ticket.
|
|
408
|
+
|
|
409
|
+
For each required label (the pipeline label, `component:{CLANCY_COMPONENT}`, `clancy:afk`, `clancy:hitl`): search by exact name. `CLANCY_LABEL` is NOT applied to children when pipeline labels are active. If not found in team labels, check workspace labels:
|
|
410
|
+
|
|
411
|
+
```graphql
|
|
412
|
+
query {
|
|
413
|
+
issueLabels(filter: { name: { eq: "$LABEL_NAME" } }) {
|
|
414
|
+
nodes {
|
|
415
|
+
id
|
|
416
|
+
name
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
If still not found, auto-create at team level:
|
|
423
|
+
|
|
424
|
+
```graphql
|
|
425
|
+
mutation {
|
|
426
|
+
issueLabelCreate(input: { teamId: "$LINEAR_TEAM_ID", name: "$LABEL_NAME" }) {
|
|
427
|
+
success
|
|
428
|
+
issueLabel {
|
|
429
|
+
id
|
|
430
|
+
name
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
On failure to create label: warn, continue without it.
|
|
437
|
+
|
|
438
|
+
### Azure DevOps
|
|
439
|
+
|
|
440
|
+
**Work item type:** Azure DevOps uses configurable work item types. Use `CLANCY_BRIEF_ISSUE_TYPE` (default: `Task`). Validate by fetching available types:
|
|
441
|
+
|
|
442
|
+
```bash
|
|
443
|
+
curl -s \
|
|
444
|
+
-u ":$AZDO_PAT" \
|
|
445
|
+
-H "Accept: application/json" \
|
|
446
|
+
"https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitemtypes?api-version=7.1"
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
If the specified type is not found, stop with the available types listed.
|
|
450
|
+
|
|
451
|
+
**Pipeline tag for children — mandatory, same logic as other boards.** Use `CLANCY_LABEL_PLAN` or `CLANCY_LABEL_BUILD` from `.clancy/.env` (defaults: `clancy:plan` / `clancy:build`). Tags are applied via the `System.Tags` field (semicolon-delimited).
|
|
452
|
+
|
|
453
|
+
**Tags:** Azure DevOps auto-creates tags — no pre-creation needed. Apply: the pipeline tag, `clancy:afk` or `clancy:hitl`. `CLANCY_LABEL` is NOT applied to children when pipeline tags are active.
|
|
454
|
+
|
|
455
|
+
**Resolve authenticated user** for assignment:
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
curl -s \
|
|
459
|
+
-u ":$AZDO_PAT" \
|
|
460
|
+
-H "Accept: application/json" \
|
|
461
|
+
"https://dev.azure.com/$AZDO_ORG/_apis/connectionData"
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
Use `authenticatedUser.providerDisplayName` for the `System.AssignedTo` field.
|
|
465
|
+
|
|
466
|
+
### Shortcut
|
|
467
|
+
|
|
468
|
+
**Story type:** Shortcut creates stories by default. Use `story_type` field if needed (`feature`, `bug`, `chore` — default: `feature`).
|
|
469
|
+
|
|
470
|
+
**Pipeline label for children — mandatory, same logic as other boards.** Use `CLANCY_LABEL_PLAN` or `CLANCY_LABEL_BUILD` from `.clancy/.env` (defaults: `clancy:plan` / `clancy:build`).
|
|
471
|
+
|
|
472
|
+
**Labels:** Resolve label IDs via `GET /api/v3/labels`. If the required label does not exist, create it:
|
|
473
|
+
|
|
474
|
+
```bash
|
|
475
|
+
curl -s \
|
|
476
|
+
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
|
|
477
|
+
-H "Content-Type: application/json" \
|
|
478
|
+
-X POST \
|
|
479
|
+
"https://api.app.shortcut.com/api/v3/labels" \
|
|
480
|
+
-d '{"name": "$LABEL_NAME", "color": "#d4c5f9"}'
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
On failure to create label: warn, continue without it. Apply: the pipeline label, `clancy:afk` or `clancy:hitl`. `CLANCY_LABEL` is NOT applied to children when pipeline labels are active.
|
|
484
|
+
|
|
485
|
+
**Resolve current member** for assignment:
|
|
486
|
+
|
|
487
|
+
```bash
|
|
488
|
+
curl -s \
|
|
489
|
+
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
|
|
490
|
+
"https://api.app.shortcut.com/api/v3/member-info"
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
Use `id` as the `owner_ids` value for story creation.
|
|
494
|
+
|
|
495
|
+
### Notion
|
|
496
|
+
|
|
497
|
+
**Pipeline label for children — mandatory, same logic as other boards.** Use `CLANCY_LABEL_PLAN` or `CLANCY_LABEL_BUILD` from `.clancy/.env` (defaults: `clancy:plan` / `clancy:build`). Labels are applied via multi-select properties.
|
|
498
|
+
|
|
499
|
+
**Labels:** Notion auto-creates multi-select options when first used — no pre-creation needed. Apply: the pipeline label, `clancy:afk` or `clancy:hitl`. `CLANCY_LABEL` is NOT applied to children when pipeline labels are active.
|
|
500
|
+
|
|
501
|
+
Use `CLANCY_NOTION_LABELS` to specify which multi-select property holds labels (configurable per database).
|
|
502
|
+
|
|
503
|
+
**Assignee:** Use `CLANCY_NOTION_ASSIGNEE` property name. Resolve the current user via the Notion API:
|
|
504
|
+
|
|
505
|
+
```bash
|
|
506
|
+
curl -s \
|
|
507
|
+
-H "Authorization: Bearer $NOTION_TOKEN" \
|
|
508
|
+
-H "Notion-Version: 2022-06-28" \
|
|
509
|
+
"https://api.notion.com/v1/users/me"
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
Use `id` for the people property value.
|
|
513
|
+
|
|
514
|
+
**Status:** Resolve the backlog status value from `CLANCY_PLAN_STATUS` (default: `Backlog`). Use `CLANCY_NOTION_STATUS` for the status property name.
|
|
515
|
+
|
|
516
|
+
---
|
|
517
|
+
|
|
518
|
+
## Step 6a — Pre-creation race check
|
|
519
|
+
|
|
520
|
+
Create the `.approved` marker file with exclusive create (O_EXCL) to prevent concurrent approval:
|
|
521
|
+
|
|
522
|
+
```
|
|
523
|
+
Marker path: .clancy/briefs/{filename}.approved
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
If the file already exists (`EEXIST`):
|
|
527
|
+
|
|
528
|
+
```
|
|
529
|
+
Brief is being approved by another process.
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
Stop.
|
|
533
|
+
|
|
534
|
+
If creation succeeds: the marker acts as a lock. **If ticket creation fails later, delete this marker** (partial failure = not approved).
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
## Step 7 — Create child tickets
|
|
539
|
+
|
|
540
|
+
Create tickets **sequentially** in topological (dependency) order. Wait **500ms** between each creation to respect rate limits.
|
|
541
|
+
|
|
542
|
+
For each ticket in dependency order:
|
|
543
|
+
|
|
544
|
+
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`
|
|
545
|
+
|
|
546
|
+
2. **Build the creation payload** (platform-specific — see below).
|
|
547
|
+
|
|
548
|
+
3. **Send the API request.**
|
|
549
|
+
|
|
550
|
+
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}]`
|
|
551
|
+
|
|
552
|
+
5. **On failure:** STOP immediately. Do NOT attempt remaining tickets. See partial failure handling in Step 10.
|
|
553
|
+
|
|
554
|
+
6. **Wait 500ms** before the next creation.
|
|
555
|
+
|
|
556
|
+
### GitHub — POST /repos/{owner}/{repo}/issues
|
|
557
|
+
|
|
558
|
+
```bash
|
|
559
|
+
curl -s \
|
|
560
|
+
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
|
561
|
+
-H "Accept: application/vnd.github+json" \
|
|
562
|
+
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
563
|
+
-H "Content-Type: application/json" \
|
|
564
|
+
-X POST \
|
|
565
|
+
"https://api.github.com/repos/$GITHUB_REPO/issues" \
|
|
566
|
+
-d '{
|
|
567
|
+
"title": "{ticket title}",
|
|
568
|
+
"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.*",
|
|
569
|
+
"labels": ["{PIPELINE_LABEL}", "size:{size}", "clancy:{mode}", ...],
|
|
570
|
+
"assignees": ["{resolved_username}"]
|
|
571
|
+
}'
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
The `Epic: #{parent_number}` line is always the FIRST line of the body — this enables cross-platform epic completion detection.
|
|
575
|
+
|
|
576
|
+
Dependencies in the body use resolved issue numbers: `Depends on #51` (not decomposition indices).
|
|
577
|
+
|
|
578
|
+
If no parent: omit `Epic:` and `Parent:` lines, omit `assignees` if not resolvable.
|
|
579
|
+
|
|
580
|
+
**On 422 (label validation error):** Retry without the invalid label(s). Warn: `Label "{name}" does not exist. Created issue without it.`
|
|
581
|
+
|
|
582
|
+
**On 429 (rate limited):** Check `X-RateLimit-Reset` header. Wait until reset, retry once. If still limited: stop, enter partial failure flow.
|
|
583
|
+
|
|
584
|
+
### Jira — POST /rest/api/3/issue
|
|
585
|
+
|
|
586
|
+
```bash
|
|
587
|
+
curl -s \
|
|
588
|
+
-u "$JIRA_USER:$JIRA_API_TOKEN" \
|
|
589
|
+
-H "Content-Type: application/json" \
|
|
590
|
+
-H "Accept: application/json" \
|
|
591
|
+
-X POST \
|
|
592
|
+
"$JIRA_BASE_URL/rest/api/3/issue" \
|
|
593
|
+
-d '{
|
|
594
|
+
"fields": {
|
|
595
|
+
"project": { "key": "$JIRA_PROJECT_KEY" },
|
|
596
|
+
"summary": "{ticket title}",
|
|
597
|
+
"description": {
|
|
598
|
+
"version": 1,
|
|
599
|
+
"type": "doc",
|
|
600
|
+
"content": [
|
|
601
|
+
{
|
|
602
|
+
"type": "paragraph",
|
|
603
|
+
"content": [
|
|
604
|
+
{ "type": "text", "text": "Epic: {PARENT_KEY}\n\n{Description}" }
|
|
605
|
+
]
|
|
606
|
+
}
|
|
607
|
+
]
|
|
608
|
+
},
|
|
609
|
+
"issuetype": { "name": "{CLANCY_BRIEF_ISSUE_TYPE or Task}" },
|
|
610
|
+
"parent": { "key": "{PARENT_KEY}" },
|
|
611
|
+
"labels": ["{PIPELINE_LABEL}", "clancy:{mode}"]
|
|
612
|
+
}
|
|
613
|
+
}'
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
**Conditional fields (include only when set):**
|
|
617
|
+
|
|
618
|
+
- `CLANCY_COMPONENT` -> `"components": [{ "name": "{value}" }]`
|
|
619
|
+
- Parent has priority -> `"priority": { "name": "{priority}" }`
|
|
620
|
+
|
|
621
|
+
**Parent field fallback (classic projects):**
|
|
622
|
+
If the API returns 400 with an error mentioning the `parent` field, retry with `customfield_10014` instead:
|
|
623
|
+
|
|
624
|
+
```json
|
|
625
|
+
"customfield_10014": "{PARENT_KEY}"
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
Cache which field works for the remaining tickets in this batch.
|
|
629
|
+
|
|
630
|
+
If both `parent` and `customfield_10014` fail:
|
|
631
|
+
|
|
632
|
+
```
|
|
633
|
+
Could not set parent. Continue creating tickets without parent? [y/N]
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
Default: N (stop). If Y: create remaining tickets without parent field.
|
|
637
|
+
|
|
638
|
+
**On 400 (component not found):** Retry without `components` field. Warn.
|
|
639
|
+
|
|
640
|
+
**On 429 (rate limited):** Honour `Retry-After` header. Wait, retry once. If still 429: stop, enter partial failure flow.
|
|
641
|
+
|
|
642
|
+
### Linear — issueCreate mutation
|
|
643
|
+
|
|
644
|
+
```graphql
|
|
645
|
+
mutation {
|
|
646
|
+
issueCreate(
|
|
647
|
+
input: {
|
|
648
|
+
teamId: "$LINEAR_TEAM_ID"
|
|
649
|
+
title: "{ticket title}"
|
|
650
|
+
description: "Epic: {PARENT_IDENTIFIER}\n\n{Description}"
|
|
651
|
+
parentId: "{PARENT_UUID}"
|
|
652
|
+
stateId: "{BACKLOG_STATE_UUID}"
|
|
653
|
+
labelIds: ["{label UUIDs}"]
|
|
654
|
+
priority: 0
|
|
655
|
+
}
|
|
656
|
+
) {
|
|
657
|
+
success
|
|
658
|
+
issue {
|
|
659
|
+
id
|
|
660
|
+
identifier
|
|
661
|
+
title
|
|
662
|
+
url
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
If no parent: omit `parentId`.
|
|
669
|
+
|
|
670
|
+
**On rate limit (RATELIMITED error code):** Wait 60s, retry once. If still limited: stop, enter partial failure flow.
|
|
671
|
+
|
|
672
|
+
### Azure DevOps — POST work item
|
|
673
|
+
|
|
674
|
+
```bash
|
|
675
|
+
curl -s \
|
|
676
|
+
-u ":$AZDO_PAT" \
|
|
677
|
+
-X POST \
|
|
678
|
+
-H "Content-Type: application/json-patch+json" \
|
|
679
|
+
"https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/\$${WORK_ITEM_TYPE}?api-version=7.1" \
|
|
680
|
+
-d '[
|
|
681
|
+
{"op": "add", "path": "/fields/System.Title", "value": "{ticket title}"},
|
|
682
|
+
{"op": "add", "path": "/fields/System.Description", "value": "<h3>{Title}</h3><p>{Description as HTML}</p><hr><p><strong>Brief:</strong> {slug}</p><p><strong>Size:</strong> {S|M|L}</p>"},
|
|
683
|
+
{"op": "add", "path": "/fields/System.Tags", "value": "{PIPELINE_TAG}; clancy:{mode}"},
|
|
684
|
+
{"op": "add", "path": "/fields/System.AssignedTo", "value": "{resolved_user}"},
|
|
685
|
+
{"op": "add", "path": "/relations/-", "value": {"rel": "System.LinkTypes.Hierarchy-Reverse", "url": "https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$PARENT_ID"}}
|
|
686
|
+
]'
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
**Work item type:** Use `CLANCY_BRIEF_ISSUE_TYPE` (default: `Task`). The type is part of the URL path: `$${type}`.
|
|
690
|
+
|
|
691
|
+
**Parent linking:** Azure DevOps uses the `relations` array with `System.LinkTypes.Hierarchy-Reverse` to link children to parents. This is included in the creation payload as a relation add operation.
|
|
692
|
+
|
|
693
|
+
**Description:** Azure DevOps uses HTML. Convert the ticket description markdown to HTML.
|
|
694
|
+
|
|
695
|
+
If no parent: omit the `relations/-` operation from the JSON Patch array.
|
|
696
|
+
|
|
697
|
+
**On 400 (validation error):** Check if the work item type or a field is invalid. Warn with the specific error and stop.
|
|
698
|
+
|
|
699
|
+
**On 429 (rate limited):** Check `Retry-After` header. Wait, retry once. If still 429: stop, enter partial failure flow.
|
|
700
|
+
|
|
701
|
+
### Shortcut — POST story
|
|
702
|
+
|
|
703
|
+
```bash
|
|
704
|
+
curl -s \
|
|
705
|
+
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
|
|
706
|
+
-H "Content-Type: application/json" \
|
|
707
|
+
-X POST \
|
|
708
|
+
"https://api.app.shortcut.com/api/v3/stories" \
|
|
709
|
+
-d '{
|
|
710
|
+
"name": "{ticket title}",
|
|
711
|
+
"description": "Epic: {PARENT_KEY}\n\n{Description}\n\n---\n\n**Brief:** {slug}\n**Size:** {S|M|L}",
|
|
712
|
+
"story_type": "feature",
|
|
713
|
+
"labels": [{"name": "{PIPELINE_LABEL}"}, {"name": "clancy:{mode}"}],
|
|
714
|
+
"owner_ids": ["{resolved_member_id}"],
|
|
715
|
+
"epic_id": {EPIC_ID_or_null},
|
|
716
|
+
"workflow_state_id": {BACKLOG_STATE_ID}
|
|
717
|
+
}'
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
**Parent linking:** If the parent is a Shortcut epic, use the `epic_id` field. If the parent is a story, omit `epic_id` and link via `story_links` after creation (see Step 8).
|
|
721
|
+
|
|
722
|
+
**Description:** Shortcut uses markdown. The `Epic: {PARENT_KEY}` line is always the first line of the description.
|
|
723
|
+
|
|
724
|
+
**Workflow state:** Resolve the backlog state ID from the default workflow:
|
|
725
|
+
|
|
726
|
+
```bash
|
|
727
|
+
WORKFLOWS=$(curl -s \
|
|
728
|
+
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
|
|
729
|
+
"https://api.app.shortcut.com/api/v3/workflows")
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
Find the state with `type: "backlog"` (or `"unstarted"` as fallback). Use its `id` for `workflow_state_id`.
|
|
733
|
+
|
|
734
|
+
If no parent: omit `epic_id` (or set to `null`).
|
|
735
|
+
|
|
736
|
+
**On 429 (rate limited):** Check `Retry-After` header. Wait, retry once. If still 429: stop, enter partial failure flow.
|
|
737
|
+
|
|
738
|
+
### Notion — POST page
|
|
739
|
+
|
|
740
|
+
```bash
|
|
741
|
+
curl -s \
|
|
742
|
+
-H "Authorization: Bearer $NOTION_TOKEN" \
|
|
743
|
+
-H "Notion-Version: 2022-06-28" \
|
|
744
|
+
-X POST \
|
|
745
|
+
"https://api.notion.com/v1/pages" \
|
|
746
|
+
-d '{
|
|
747
|
+
"parent": {"database_id": "$NOTION_DATABASE_ID"},
|
|
748
|
+
"properties": {
|
|
749
|
+
"Name": {"title": [{"text": {"content": "{ticket title}"}}]},
|
|
750
|
+
"$CLANCY_NOTION_STATUS": {"status": {"name": "$CLANCY_PLAN_STATUS"}},
|
|
751
|
+
"$CLANCY_NOTION_LABELS": {"multi_select": [{"name": "{PIPELINE_LABEL}"}, {"name": "clancy:{mode}"}]},
|
|
752
|
+
"$CLANCY_NOTION_ASSIGNEE": {"people": [{"id": "{resolved_user_id}"}]}
|
|
753
|
+
}
|
|
754
|
+
}'
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
After creation, append the description as page content blocks:
|
|
758
|
+
|
|
759
|
+
```bash
|
|
760
|
+
curl -s \
|
|
761
|
+
-H "Authorization: Bearer $NOTION_TOKEN" \
|
|
762
|
+
-H "Notion-Version: 2022-06-28" \
|
|
763
|
+
-X PATCH \
|
|
764
|
+
"https://api.notion.com/v1/blocks/$NEW_PAGE_ID/children" \
|
|
765
|
+
-d '{"children": [
|
|
766
|
+
{"type": "paragraph", "paragraph": {"rich_text": [{"type": "text", "text": {"content": "Epic: {PARENT_KEY}"}}]}},
|
|
767
|
+
{"type": "divider", "divider": {}},
|
|
768
|
+
{"type": "paragraph", "paragraph": {"rich_text": [{"type": "text", "text": {"content": "{Description}"}}]}},
|
|
769
|
+
{"type": "divider", "divider": {}},
|
|
770
|
+
{"type": "paragraph", "paragraph": {"rich_text": [{"type": "text", "text": {"content": "Brief: {slug}\nSize: {S|M|L}"}}]}}
|
|
771
|
+
]}'
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
**Parent linking:** If the parent is a Notion page with a `relation` property configured, update the relation on the child page to point to the parent. If no relation property: children are standalone pages in the same database.
|
|
775
|
+
|
|
776
|
+
**Notion limitation:** Page content is stored as blocks, not a description field. The `Epic: {PARENT_KEY}` line is the first block of every child page. Each `rich_text` block has a 2000-character limit — split longer descriptions across multiple blocks.
|
|
777
|
+
|
|
778
|
+
If no parent: omit the `Epic:` paragraph block and relation property.
|
|
779
|
+
|
|
780
|
+
**On 429 (rate limited):** Check `Retry-After` header. Wait, retry once. If still 429: stop, enter partial failure flow.
|
|
781
|
+
|
|
782
|
+
---
|
|
783
|
+
|
|
784
|
+
## Step 8 — Link dependencies
|
|
785
|
+
|
|
786
|
+
For each ticket with dependencies, create blocking relationships on the board. This is **best-effort** — warn on failure, do not stop.
|
|
787
|
+
|
|
788
|
+
Skip links involving uncreated tickets (from partial failures).
|
|
789
|
+
|
|
790
|
+
### GitHub
|
|
791
|
+
|
|
792
|
+
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.
|
|
793
|
+
|
|
794
|
+
### Jira — POST /rest/api/3/issueLink
|
|
795
|
+
|
|
796
|
+
For each dependency (e.g. ticket #3 depends on #2):
|
|
797
|
+
|
|
798
|
+
```bash
|
|
799
|
+
curl -s \
|
|
800
|
+
-u "$JIRA_USER:$JIRA_API_TOKEN" \
|
|
801
|
+
-H "Content-Type: application/json" \
|
|
802
|
+
-H "Accept: application/json" \
|
|
803
|
+
-X POST \
|
|
804
|
+
"$JIRA_BASE_URL/rest/api/3/issueLink" \
|
|
805
|
+
-d '{
|
|
806
|
+
"type": { "name": "Blocks" },
|
|
807
|
+
"inwardIssue": { "key": "{BLOCKER_KEY}" },
|
|
808
|
+
"outwardIssue": { "key": "{DEPENDENT_KEY}" }
|
|
809
|
+
}'
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
`inwardIssue` = the blocker, `outwardIssue` = the dependent ticket.
|
|
813
|
+
|
|
814
|
+
Wait **200ms** between each link creation.
|
|
815
|
+
|
|
816
|
+
On failure: `Could not link {DEPENDENT} -> {BLOCKER} (dependency). Link manually if needed.`
|
|
817
|
+
|
|
818
|
+
### Linear — issueRelationCreate mutation
|
|
819
|
+
|
|
820
|
+
```graphql
|
|
821
|
+
mutation {
|
|
822
|
+
issueRelationCreate(
|
|
823
|
+
input: {
|
|
824
|
+
issueId: "{DEPENDENT_UUID}"
|
|
825
|
+
relatedIssueId: "{BLOCKER_UUID}"
|
|
826
|
+
type: blockedBy
|
|
827
|
+
}
|
|
828
|
+
) {
|
|
829
|
+
success
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
`issueId` = the dependent ticket, `relatedIssueId` = the blocker.
|
|
835
|
+
|
|
836
|
+
Wait **200ms** between each relation creation.
|
|
837
|
+
|
|
838
|
+
On failure: `Could not link {DEPENDENT} -> {BLOCKER}. Add manually in Linear.`
|
|
839
|
+
|
|
840
|
+
### Azure DevOps — PATCH work item (add relation)
|
|
841
|
+
|
|
842
|
+
For each dependency, add a `System.LinkTypes.Dependency-Reverse` relation (predecessor link):
|
|
843
|
+
|
|
844
|
+
```bash
|
|
845
|
+
curl -s \
|
|
846
|
+
-u ":$AZDO_PAT" \
|
|
847
|
+
-X PATCH \
|
|
848
|
+
-H "Content-Type: application/json-patch+json" \
|
|
849
|
+
"https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$DEPENDENT_ID?api-version=7.1" \
|
|
850
|
+
-d '[{"op": "add", "path": "/relations/-", "value": {"rel": "System.LinkTypes.Dependency-Reverse", "url": "https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$BLOCKER_ID", "attributes": {"comment": "Depends on #$BLOCKER_ID (from Clancy brief)"}}}]'
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
`System.LinkTypes.Dependency-Reverse` = "Predecessor" (blocker must finish before dependent can start).
|
|
854
|
+
|
|
855
|
+
Wait **200ms** between each link creation.
|
|
856
|
+
|
|
857
|
+
On failure: `Could not link {DEPENDENT} -> {BLOCKER}. Link manually in Azure DevOps.`
|
|
858
|
+
|
|
859
|
+
### Shortcut — POST story link
|
|
860
|
+
|
|
861
|
+
For each dependency (e.g. ticket #3 depends on #2), create a story link:
|
|
862
|
+
|
|
863
|
+
```bash
|
|
864
|
+
curl -s \
|
|
865
|
+
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
|
|
866
|
+
-H "Content-Type: application/json" \
|
|
867
|
+
-X POST \
|
|
868
|
+
"https://api.app.shortcut.com/api/v3/story-links" \
|
|
869
|
+
-d '{
|
|
870
|
+
"subject_id": {BLOCKER_STORY_ID},
|
|
871
|
+
"object_id": {DEPENDENT_STORY_ID},
|
|
872
|
+
"verb": "blocks"
|
|
873
|
+
}'
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
`subject_id` = the blocker story, `object_id` = the dependent story. The `verb: "blocks"` means "subject blocks object".
|
|
877
|
+
|
|
878
|
+
Wait **200ms** between each link creation.
|
|
879
|
+
|
|
880
|
+
On failure: `Could not link {DEPENDENT} -> {BLOCKER}. Link manually in Shortcut.`
|
|
881
|
+
|
|
882
|
+
### Notion — relation properties
|
|
883
|
+
|
|
884
|
+
Notion uses **relation properties** for dependencies between pages. If the database has a relation property configured for dependencies:
|
|
885
|
+
|
|
886
|
+
```bash
|
|
887
|
+
curl -s \
|
|
888
|
+
-H "Authorization: Bearer $NOTION_TOKEN" \
|
|
889
|
+
-H "Notion-Version: 2022-06-28" \
|
|
890
|
+
-X PATCH \
|
|
891
|
+
"https://api.notion.com/v1/pages/$DEPENDENT_PAGE_ID" \
|
|
892
|
+
-d '{"properties": {"$DEPENDENCY_RELATION_PROPERTY": {"relation": [{"id": "$BLOCKER_PAGE_ID"}, ...existing_relations]}}}'
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
**Notion limitation:** If no relation property is configured for dependencies, dependency linking is not possible via the API. In this case, add a `Depends on: notion-XXXXXXXX` line to the page content blocks instead (best-effort text reference).
|
|
896
|
+
|
|
897
|
+
Wait **200ms** between each relation update.
|
|
898
|
+
|
|
899
|
+
On failure: `Could not link {DEPENDENT} -> {BLOCKER}. Link manually in Notion.`
|
|
900
|
+
|
|
901
|
+
---
|
|
902
|
+
|
|
903
|
+
## Step 9 — Update brief file
|
|
904
|
+
|
|
905
|
+
After creating tickets, update the local brief file:
|
|
906
|
+
|
|
907
|
+
1. **Add ticket keys** to the `Ticket` column in the decomposition table:
|
|
908
|
+
|
|
909
|
+
```markdown
|
|
910
|
+
| # | Title | Description | Size | Dependencies | Mode | Ticket |
|
|
911
|
+
| --- | ---------------------- | ----------- | ---- | ------------ | ---- | -------- |
|
|
912
|
+
| 1 | Set up route structure | ... | S | None | AFK | PROJ-201 |
|
|
913
|
+
| 2 | SSO login flow | ... | M | None | HITL | PROJ-202 |
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
2. **Update status** from `Draft` to `Approved` (in the `**Status:**` line if present).
|
|
917
|
+
|
|
918
|
+
---
|
|
919
|
+
|
|
920
|
+
## Step 10 — Mark approved
|
|
921
|
+
|
|
922
|
+
Check whether all tickets were created successfully:
|
|
923
|
+
|
|
924
|
+
**All created:**
|
|
925
|
+
|
|
926
|
+
- The `.approved` marker file (created in Step 6a) stays in place.
|
|
927
|
+
|
|
928
|
+
**Partial failure (some tickets failed):**
|
|
929
|
+
|
|
930
|
+
- **DELETE** the `.approved` marker file. Partial failure = not approved.
|
|
931
|
+
- Update the brief file with the tickets that WERE created (Step 9 still applies).
|
|
932
|
+
- Display partial failure summary:
|
|
933
|
+
|
|
934
|
+
```
|
|
935
|
+
Partial: {M} of {N} tickets created
|
|
936
|
+
|
|
937
|
+
PROJ-201 [S] Set up route structure
|
|
938
|
+
PROJ-202 [M] SSO login flow
|
|
939
|
+
X #3 Role-based access control — FAILED ({error})
|
|
940
|
+
- #4-#{N} not attempted
|
|
941
|
+
|
|
942
|
+
Created tickets are live on the board. To complete:
|
|
943
|
+
1. Fix the issue (check board status/permissions)
|
|
944
|
+
2. Re-run /clancy:approve-brief to resume creating remaining tickets
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
- Log: `YYYY-MM-DD HH:MM | APPROVE_BRIEF | {slug} | PARTIAL — {M} of {N} created`
|
|
948
|
+
- Stop (do not post tracking comment for partial failures).
|
|
949
|
+
|
|
950
|
+
---
|
|
951
|
+
|
|
952
|
+
## Step 11 — Post tracking summary on parent
|
|
953
|
+
|
|
954
|
+
Only if a parent ticket exists AND all tickets were created successfully.
|
|
955
|
+
|
|
956
|
+
Post a comment on the parent ticket listing all created children.
|
|
957
|
+
|
|
958
|
+
### GitHub
|
|
959
|
+
|
|
960
|
+
```bash
|
|
961
|
+
curl -s \
|
|
962
|
+
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
|
963
|
+
-H "Accept: application/vnd.github+json" \
|
|
964
|
+
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
965
|
+
-H "Content-Type: application/json" \
|
|
966
|
+
-X POST \
|
|
967
|
+
"https://api.github.com/repos/$GITHUB_REPO/issues/$PARENT_NUMBER/comments" \
|
|
968
|
+
-d '{"body": "<tracking comment markdown>"}'
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
### Jira
|
|
972
|
+
|
|
973
|
+
```bash
|
|
974
|
+
curl -s \
|
|
975
|
+
-u "$JIRA_USER:$JIRA_API_TOKEN" \
|
|
976
|
+
-H "Content-Type: application/json" \
|
|
977
|
+
-H "Accept: application/json" \
|
|
978
|
+
-X POST \
|
|
979
|
+
"$JIRA_BASE_URL/rest/api/3/issue/$PARENT_KEY/comment" \
|
|
980
|
+
-d '{"body": { "version": 1, "type": "doc", "content": [/* ADF table */] }}'
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
### Linear
|
|
984
|
+
|
|
985
|
+
```graphql
|
|
986
|
+
mutation {
|
|
987
|
+
commentCreate(
|
|
988
|
+
input: { issueId: "$PARENT_UUID", body: "<tracking comment markdown>" }
|
|
989
|
+
) {
|
|
990
|
+
success
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
### Azure DevOps
|
|
996
|
+
|
|
997
|
+
```bash
|
|
998
|
+
curl -s \
|
|
999
|
+
-u ":$AZDO_PAT" \
|
|
1000
|
+
-X POST \
|
|
1001
|
+
-H "Content-Type: application/json" \
|
|
1002
|
+
"https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$PARENT_ID/comments?api-version=7.1-preview.4" \
|
|
1003
|
+
-d '{"text": "<tracking comment as HTML>"}'
|
|
1004
|
+
```
|
|
1005
|
+
|
|
1006
|
+
Convert the tracking comment markdown to HTML (table → `<table>`, headings → `<h2>`, etc.).
|
|
1007
|
+
|
|
1008
|
+
### Shortcut — POST comment on parent
|
|
1009
|
+
|
|
1010
|
+
If the parent is an epic:
|
|
1011
|
+
|
|
1012
|
+
```bash
|
|
1013
|
+
curl -s \
|
|
1014
|
+
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
|
|
1015
|
+
-H "Content-Type: application/json" \
|
|
1016
|
+
-X POST \
|
|
1017
|
+
"https://api.app.shortcut.com/api/v3/epics/$EPIC_ID/comments" \
|
|
1018
|
+
-d '{"text": "<tracking comment markdown>"}'
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
If the parent is a story:
|
|
1022
|
+
|
|
1023
|
+
```bash
|
|
1024
|
+
curl -s \
|
|
1025
|
+
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
|
|
1026
|
+
-H "Content-Type: application/json" \
|
|
1027
|
+
-X POST \
|
|
1028
|
+
"https://api.app.shortcut.com/api/v3/stories/$STORY_ID/comments" \
|
|
1029
|
+
-d '{"text": "<tracking comment markdown>"}'
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
Shortcut accepts Markdown directly in comment text.
|
|
1033
|
+
|
|
1034
|
+
### Notion — POST comment on parent
|
|
1035
|
+
|
|
1036
|
+
```bash
|
|
1037
|
+
curl -s \
|
|
1038
|
+
-H "Authorization: Bearer $NOTION_TOKEN" \
|
|
1039
|
+
-H "Notion-Version: 2022-06-28" \
|
|
1040
|
+
-X POST \
|
|
1041
|
+
"https://api.notion.com/v1/comments" \
|
|
1042
|
+
-d '{"parent": {"page_id": "$PARENT_PAGE_ID"}, "rich_text": [{"type": "text", "text": {"content": "<tracking comment text>"}}]}'
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
**Notion limitation:** Comments use `rich_text` blocks, not markdown. The `rich_text` array has a 2000-character limit per text block. Split the tracking table across multiple blocks if needed.
|
|
1046
|
+
|
|
1047
|
+
### Tracking comment format
|
|
1048
|
+
|
|
1049
|
+
```markdown
|
|
1050
|
+
## Clancy — Approved Tickets
|
|
1051
|
+
|
|
1052
|
+
| # | Ticket | Title | Size | Mode |
|
|
1053
|
+
| --- | ------ | ------- | ---- | ---- |
|
|
1054
|
+
| 1 | {KEY} | {Title} | S | AFK |
|
|
1055
|
+
| 2 | {KEY} | {Title} | M | HITL |
|
|
1056
|
+
| 3 | {KEY} | {Title} | M | AFK |
|
|
1057
|
+
|
|
1058
|
+
Dependencies linked: {N}
|
|
1059
|
+
Created by Clancy on {YYYY-MM-DD}.
|
|
1060
|
+
```
|
|
1061
|
+
|
|
1062
|
+
**On failure:** Warn: `Could not post tracking summary on {PARENT}. Tickets are created regardless.` Continue — best-effort.
|
|
1063
|
+
|
|
1064
|
+
---
|
|
1065
|
+
|
|
1066
|
+
## Step 11a — Remove brief label from parent
|
|
1067
|
+
|
|
1068
|
+
Only if a parent ticket exists AND all tickets were created successfully. Remove the brief label from the parent ticket. Best-effort — warn on failure, never stop.
|
|
1069
|
+
|
|
1070
|
+
Use `CLANCY_LABEL_BRIEF` from `.clancy/.env` if set, otherwise `clancy:brief`.
|
|
1071
|
+
|
|
1072
|
+
### GitHub
|
|
1073
|
+
|
|
1074
|
+
```bash
|
|
1075
|
+
curl -s \
|
|
1076
|
+
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
|
1077
|
+
-H "Accept: application/vnd.github+json" \
|
|
1078
|
+
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
1079
|
+
-X DELETE \
|
|
1080
|
+
"https://api.github.com/repos/$GITHUB_REPO/issues/$PARENT_NUMBER/labels/$(echo $CLANCY_LABEL_BRIEF | jq -Rr @uri)"
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
Ignore 404 (label may not be present on the parent).
|
|
1084
|
+
|
|
1085
|
+
### Jira
|
|
1086
|
+
|
|
1087
|
+
```bash
|
|
1088
|
+
CURRENT_LABELS=$(curl -s \
|
|
1089
|
+
-u "$JIRA_USER:$JIRA_API_TOKEN" \
|
|
1090
|
+
-H "Accept: application/json" \
|
|
1091
|
+
"$JIRA_BASE_URL/rest/api/3/issue/$PARENT_KEY?fields=labels" | jq -r '.fields.labels')
|
|
1092
|
+
|
|
1093
|
+
UPDATED_LABELS=$(echo "$CURRENT_LABELS" | jq --arg brief "$CLANCY_LABEL_BRIEF" '[.[] | select(. != $brief)]')
|
|
1094
|
+
|
|
1095
|
+
curl -s \
|
|
1096
|
+
-u "$JIRA_USER:$JIRA_API_TOKEN" \
|
|
1097
|
+
-X PUT \
|
|
1098
|
+
-H "Content-Type: application/json" \
|
|
1099
|
+
"$JIRA_BASE_URL/rest/api/3/issue/$PARENT_KEY" \
|
|
1100
|
+
-d "{\"fields\": {\"labels\": $UPDATED_LABELS}}"
|
|
1101
|
+
```
|
|
1102
|
+
|
|
1103
|
+
### Linear
|
|
1104
|
+
|
|
1105
|
+
```graphql
|
|
1106
|
+
# Fetch current label IDs on the parent issue
|
|
1107
|
+
query {
|
|
1108
|
+
issues(filter: { identifier: { eq: "$PARENT_KEY" } }) {
|
|
1109
|
+
nodes {
|
|
1110
|
+
id
|
|
1111
|
+
labels {
|
|
1112
|
+
nodes {
|
|
1113
|
+
id
|
|
1114
|
+
name
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
# Filter out the brief label ID, then update with remaining labelIds
|
|
1122
|
+
mutation {
|
|
1123
|
+
issueUpdate(
|
|
1124
|
+
id: "$PARENT_ISSUE_UUID"
|
|
1125
|
+
input: { labelIds: [currentLabelIds, without, briefLabelId] }
|
|
1126
|
+
) {
|
|
1127
|
+
success
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
```
|
|
1131
|
+
|
|
1132
|
+
### Azure DevOps
|
|
1133
|
+
|
|
1134
|
+
```bash
|
|
1135
|
+
# Fetch current tags, remove brief tag from semicolon-delimited string
|
|
1136
|
+
CURRENT=$(curl -s \
|
|
1137
|
+
-u ":$AZDO_PAT" \
|
|
1138
|
+
-H "Accept: application/json" \
|
|
1139
|
+
"https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$PARENT_ID?fields=System.Tags&api-version=7.1")
|
|
1140
|
+
|
|
1141
|
+
curl -s \
|
|
1142
|
+
-u ":$AZDO_PAT" \
|
|
1143
|
+
-X PATCH \
|
|
1144
|
+
-H "Content-Type: application/json-patch+json" \
|
|
1145
|
+
"https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$PARENT_ID?api-version=7.1" \
|
|
1146
|
+
-d '[{"op": "replace", "path": "/fields/System.Tags", "value": "<tags without brief tag>"}]'
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
### Shortcut
|
|
1150
|
+
|
|
1151
|
+
```bash
|
|
1152
|
+
# Fetch current story/epic labels, filter out brief label, update
|
|
1153
|
+
# For stories:
|
|
1154
|
+
STORY=$(curl -s \
|
|
1155
|
+
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
|
|
1156
|
+
"https://api.app.shortcut.com/api/v3/stories/$PARENT_STORY_ID")
|
|
1157
|
+
|
|
1158
|
+
# Remove brief label from labels array, then update:
|
|
1159
|
+
curl -s \
|
|
1160
|
+
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
|
|
1161
|
+
-H "Content-Type: application/json" \
|
|
1162
|
+
-X PUT \
|
|
1163
|
+
"https://api.app.shortcut.com/api/v3/stories/$PARENT_STORY_ID" \
|
|
1164
|
+
-d '{"labels": [labels_without_brief_label]}'
|
|
1165
|
+
```
|
|
1166
|
+
|
|
1167
|
+
For epics, use `GET /api/v3/epics/$EPIC_ID` and `PUT /api/v3/epics/$EPIC_ID` with the same label-filtering pattern.
|
|
1168
|
+
|
|
1169
|
+
### Notion
|
|
1170
|
+
|
|
1171
|
+
```bash
|
|
1172
|
+
# Fetch current page properties, remove brief label from multi-select
|
|
1173
|
+
PAGE=$(curl -s \
|
|
1174
|
+
-H "Authorization: Bearer $NOTION_TOKEN" \
|
|
1175
|
+
-H "Notion-Version: 2022-06-28" \
|
|
1176
|
+
"https://api.notion.com/v1/pages/$PARENT_PAGE_ID")
|
|
1177
|
+
|
|
1178
|
+
# Update multi-select property without brief label
|
|
1179
|
+
curl -s \
|
|
1180
|
+
-H "Authorization: Bearer $NOTION_TOKEN" \
|
|
1181
|
+
-H "Notion-Version: 2022-06-28" \
|
|
1182
|
+
-X PATCH \
|
|
1183
|
+
"https://api.notion.com/v1/pages/$PARENT_PAGE_ID" \
|
|
1184
|
+
-d '{"properties": {"$CLANCY_NOTION_LABELS": {"multi_select": [options_without_brief_label]}}}'
|
|
1185
|
+
```
|
|
1186
|
+
|
|
1187
|
+
### On failure
|
|
1188
|
+
|
|
1189
|
+
```
|
|
1190
|
+
⚠️ Could not remove brief label from {PARENT_KEY}. Remove it manually if needed.
|
|
1191
|
+
```
|
|
1192
|
+
|
|
1193
|
+
Continue — do not stop.
|
|
1194
|
+
|
|
1195
|
+
---
|
|
1196
|
+
|
|
1197
|
+
## Step 11b — Add plan label to parent
|
|
1198
|
+
|
|
1199
|
+
Only if a parent ticket exists AND all tickets were created successfully. Add the plan label to the parent ticket so it enters the planning queue. Best-effort — warn on failure, never stop.
|
|
1200
|
+
|
|
1201
|
+
**Crash safety:** Step 11a removes the brief label and Step 11b adds the plan label. If Step 11b fails, the parent loses its pipeline label — warn the user to add it manually.
|
|
1202
|
+
|
|
1203
|
+
Use `CLANCY_LABEL_PLAN` from `.clancy/.env` if set, otherwise `clancy:plan`.
|
|
1204
|
+
|
|
1205
|
+
### GitHub
|
|
1206
|
+
|
|
1207
|
+
```bash
|
|
1208
|
+
# Ensure plan label exists
|
|
1209
|
+
curl -s \
|
|
1210
|
+
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
|
1211
|
+
-H "Accept: application/vnd.github+json" \
|
|
1212
|
+
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
1213
|
+
-X POST \
|
|
1214
|
+
"https://api.github.com/repos/$GITHUB_REPO/labels" \
|
|
1215
|
+
-d "{\"name\": \"$CLANCY_LABEL_PLAN\", \"color\": \"0075ca\"}"
|
|
1216
|
+
# Ignore 422 (label already exists)
|
|
1217
|
+
|
|
1218
|
+
# Add label to parent
|
|
1219
|
+
curl -s \
|
|
1220
|
+
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
|
1221
|
+
-H "Accept: application/vnd.github+json" \
|
|
1222
|
+
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
1223
|
+
-X POST \
|
|
1224
|
+
"https://api.github.com/repos/$GITHUB_REPO/issues/$PARENT_NUMBER/labels" \
|
|
1225
|
+
-d "{\"labels\": [\"$CLANCY_LABEL_PLAN\"]}"
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
### Jira
|
|
1229
|
+
|
|
1230
|
+
```bash
|
|
1231
|
+
CURRENT_LABELS=$(curl -s \
|
|
1232
|
+
-u "$JIRA_USER:$JIRA_API_TOKEN" \
|
|
1233
|
+
-H "Accept: application/json" \
|
|
1234
|
+
"$JIRA_BASE_URL/rest/api/3/issue/$PARENT_KEY?fields=labels" | jq -r '.fields.labels')
|
|
1235
|
+
|
|
1236
|
+
UPDATED_LABELS=$(echo "$CURRENT_LABELS" | jq --arg plan "$CLANCY_LABEL_PLAN" '. + [$plan] | unique')
|
|
1237
|
+
|
|
1238
|
+
curl -s \
|
|
1239
|
+
-u "$JIRA_USER:$JIRA_API_TOKEN" \
|
|
1240
|
+
-X PUT \
|
|
1241
|
+
-H "Content-Type: application/json" \
|
|
1242
|
+
"$JIRA_BASE_URL/rest/api/3/issue/$PARENT_KEY" \
|
|
1243
|
+
-d "{\"fields\": {\"labels\": $UPDATED_LABELS}}"
|
|
1244
|
+
```
|
|
1245
|
+
|
|
1246
|
+
### Linear
|
|
1247
|
+
|
|
1248
|
+
```graphql
|
|
1249
|
+
# Ensure plan label exists — check team labels, workspace labels, create if missing
|
|
1250
|
+
mutation {
|
|
1251
|
+
issueLabelCreate(input: {
|
|
1252
|
+
teamId: "$LINEAR_TEAM_ID"
|
|
1253
|
+
name: "$CLANCY_LABEL_PLAN"
|
|
1254
|
+
color: "#0075ca"
|
|
1255
|
+
}) { success issueLabel { id } }
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
# Fetch current label IDs on the parent, add plan label ID
|
|
1259
|
+
mutation {
|
|
1260
|
+
issueUpdate(
|
|
1261
|
+
id: "$PARENT_ISSUE_UUID"
|
|
1262
|
+
input: { labelIds: [...currentLabelIds, planLabelId] }
|
|
1263
|
+
) { success }
|
|
1264
|
+
}
|
|
1265
|
+
```
|
|
1266
|
+
|
|
1267
|
+
### Azure DevOps
|
|
1268
|
+
|
|
1269
|
+
```bash
|
|
1270
|
+
# Fetch current tags, append plan tag
|
|
1271
|
+
CURRENT=$(curl -s \
|
|
1272
|
+
-u ":$AZDO_PAT" \
|
|
1273
|
+
-H "Accept: application/json" \
|
|
1274
|
+
"https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$PARENT_ID?fields=System.Tags&api-version=7.1")
|
|
1275
|
+
|
|
1276
|
+
# Append plan tag to semicolon-delimited string
|
|
1277
|
+
curl -s \
|
|
1278
|
+
-u ":$AZDO_PAT" \
|
|
1279
|
+
-X PATCH \
|
|
1280
|
+
-H "Content-Type: application/json-patch+json" \
|
|
1281
|
+
"https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$PARENT_ID?api-version=7.1" \
|
|
1282
|
+
-d '[{"op": "replace", "path": "/fields/System.Tags", "value": "<existing tags>; $CLANCY_LABEL_PLAN"}]'
|
|
1283
|
+
```
|
|
1284
|
+
|
|
1285
|
+
### Shortcut
|
|
1286
|
+
|
|
1287
|
+
```bash
|
|
1288
|
+
# Resolve plan label ID (create if missing)
|
|
1289
|
+
LABELS=$(curl -s \
|
|
1290
|
+
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
|
|
1291
|
+
"https://api.app.shortcut.com/api/v3/labels")
|
|
1292
|
+
|
|
1293
|
+
# Find or create the plan label, get its ID
|
|
1294
|
+
# Then add to story (merge with existing labels):
|
|
1295
|
+
curl -s \
|
|
1296
|
+
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
|
|
1297
|
+
-H "Content-Type: application/json" \
|
|
1298
|
+
-X PUT \
|
|
1299
|
+
"https://api.app.shortcut.com/api/v3/stories/$PARENT_STORY_ID" \
|
|
1300
|
+
-d '{"labels": [existing_labels_plus_plan_label]}'
|
|
1301
|
+
```
|
|
1302
|
+
|
|
1303
|
+
For epics, use `GET /api/v3/epics/$EPIC_ID` and `PUT /api/v3/epics/$EPIC_ID` with the same label pattern.
|
|
1304
|
+
|
|
1305
|
+
### Notion
|
|
1306
|
+
|
|
1307
|
+
```bash
|
|
1308
|
+
# Fetch current page properties, add plan label to multi-select
|
|
1309
|
+
PAGE=$(curl -s \
|
|
1310
|
+
-H "Authorization: Bearer $NOTION_TOKEN" \
|
|
1311
|
+
-H "Notion-Version: 2022-06-28" \
|
|
1312
|
+
"https://api.notion.com/v1/pages/$PARENT_PAGE_ID")
|
|
1313
|
+
|
|
1314
|
+
# Update multi-select property with plan label added
|
|
1315
|
+
curl -s \
|
|
1316
|
+
-H "Authorization: Bearer $NOTION_TOKEN" \
|
|
1317
|
+
-H "Notion-Version: 2022-06-28" \
|
|
1318
|
+
-X PATCH \
|
|
1319
|
+
"https://api.notion.com/v1/pages/$PARENT_PAGE_ID" \
|
|
1320
|
+
-d '{"properties": {"$CLANCY_NOTION_LABELS": {"multi_select": [existing_options_plus_plan_label]}}}'
|
|
1321
|
+
```
|
|
1322
|
+
|
|
1323
|
+
### On failure
|
|
1324
|
+
|
|
1325
|
+
If adding the plan label fails, **re-add the brief label** so the ticket isn't orphaned without any pipeline label. Use the same platform-specific API calls from Step 11a (but adding instead of removing). Then warn:
|
|
1326
|
+
|
|
1327
|
+
```
|
|
1328
|
+
⚠️ Could not add plan label to {PARENT_KEY}. Re-added brief label as fallback.
|
|
1329
|
+
Add the plan label manually so the ticket enters the planning queue.
|
|
1330
|
+
```
|
|
1331
|
+
|
|
1332
|
+
Continue — do not stop.
|
|
1333
|
+
|
|
1334
|
+
---
|
|
1335
|
+
|
|
1336
|
+
## Step 12 — Display summary
|
|
1337
|
+
|
|
1338
|
+
Show the final result:
|
|
1339
|
+
|
|
1340
|
+
```
|
|
1341
|
+
{N} tickets created under {PARENT_KEY}
|
|
1342
|
+
|
|
1343
|
+
{KEY} [{Size}] [{Mode}] {Title}
|
|
1344
|
+
{KEY} [{Size}] [{Mode}] {Title}
|
|
1345
|
+
{KEY} [{Size}] [{Mode}] {Title}
|
|
1346
|
+
|
|
1347
|
+
Dependencies linked: {N}
|
|
1348
|
+
AFK-ready: {X} | Needs human: {Y}
|
|
1349
|
+
Epic branch: epic/{parent-key-lowercase}
|
|
1350
|
+
|
|
1351
|
+
Next: run /clancy:plan to generate implementation plans.
|
|
1352
|
+
|
|
1353
|
+
"We've got ourselves a case."
|
|
1354
|
+
```
|
|
1355
|
+
|
|
1356
|
+
If no parent:
|
|
1357
|
+
|
|
1358
|
+
```
|
|
1359
|
+
{N} standalone tickets created.
|
|
1360
|
+
...
|
|
1361
|
+
Next: run /clancy:plan to generate implementation plans.
|
|
1362
|
+
|
|
1363
|
+
"We've got ourselves a case."
|
|
1364
|
+
```
|
|
1365
|
+
|
|
1366
|
+
---
|
|
1367
|
+
|
|
1368
|
+
## Step 13 — Log
|
|
1369
|
+
|
|
1370
|
+
Append to `.clancy/progress.txt`:
|
|
1371
|
+
|
|
1372
|
+
```
|
|
1373
|
+
YYYY-MM-DD HH:MM | APPROVE_BRIEF | {slug} | {N} tickets created
|
|
1374
|
+
```
|
|
1375
|
+
|
|
1376
|
+
---
|
|
1377
|
+
|
|
1378
|
+
## Error handling reference
|
|
1379
|
+
|
|
1380
|
+
### Rate limiting
|
|
1381
|
+
|
|
1382
|
+
| Platform | Detection | Response |
|
|
1383
|
+
| ------------ | -------------------------------- | ------------------------------------------ |
|
|
1384
|
+
| GitHub | 403 + `X-RateLimit-Remaining: 0` | Wait until `X-RateLimit-Reset`, retry once |
|
|
1385
|
+
| Jira | 429 + `Retry-After` header | Wait the specified seconds, retry once |
|
|
1386
|
+
| Linear | `RATELIMITED` error code | Wait 60s, retry once |
|
|
1387
|
+
| Azure DevOps | 429 + `Retry-After` header | Wait the specified seconds, retry once |
|
|
1388
|
+
| Shortcut | 429 + `Retry-After` header | Wait the specified seconds, retry once |
|
|
1389
|
+
| Notion | 429 + `Retry-After` header | Wait the specified seconds, retry once |
|
|
1390
|
+
|
|
1391
|
+
If retry also fails: stop, enter partial failure flow.
|
|
1392
|
+
|
|
1393
|
+
### Auth failure mid-batch
|
|
1394
|
+
|
|
1395
|
+
If a 401/403 occurs during ticket creation (token expired or revoked after preflight):
|
|
1396
|
+
|
|
1397
|
+
```
|
|
1398
|
+
Auth failed during ticket creation. {M} of {N} created before failure.
|
|
1399
|
+
Check credentials and re-run to resume.
|
|
1400
|
+
```
|
|
1401
|
+
|
|
1402
|
+
Enter partial failure flow (Step 10).
|
|
1403
|
+
|
|
1404
|
+
### Timeout
|
|
1405
|
+
|
|
1406
|
+
| Platform | Timeout |
|
|
1407
|
+
| ------------ | -------------------- |
|
|
1408
|
+
| GitHub | 15s per API call |
|
|
1409
|
+
| Jira | 30s per API call |
|
|
1410
|
+
| Linear | 30s per GraphQL call |
|
|
1411
|
+
| Azure DevOps | 30s per API call |
|
|
1412
|
+
| Shortcut | 30s per API call |
|
|
1413
|
+
| Notion | 30s per API call |
|
|
1414
|
+
|
|
1415
|
+
On timeout: `Request timed out. Ticket may have been created server-side. Check board before re-run.` Enter partial failure flow.
|
|
1416
|
+
|
|
1417
|
+
### Duplicate guard (on resume)
|
|
1418
|
+
|
|
1419
|
+
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):
|
|
1420
|
+
|
|
1421
|
+
#### Jira
|
|
1422
|
+
|
|
1423
|
+
```bash
|
|
1424
|
+
curl -s \
|
|
1425
|
+
-u "$JIRA_USER:$JIRA_API_TOKEN" \
|
|
1426
|
+
-H "Content-Type: application/json" \
|
|
1427
|
+
-H "Accept: application/json" \
|
|
1428
|
+
-X POST \
|
|
1429
|
+
"$JIRA_BASE_URL/rest/api/3/search/jql" \
|
|
1430
|
+
-d '{"jql": "parent = $PARENT_KEY", "maxResults": 50, "fields": ["summary"]}'
|
|
1431
|
+
```
|
|
1432
|
+
|
|
1433
|
+
#### GitHub
|
|
1434
|
+
|
|
1435
|
+
```bash
|
|
1436
|
+
curl -s \
|
|
1437
|
+
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
|
1438
|
+
-H "Accept: application/vnd.github+json" \
|
|
1439
|
+
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
1440
|
+
"https://api.github.com/repos/$GITHUB_REPO/issues?state=open&per_page=30"
|
|
1441
|
+
```
|
|
1442
|
+
|
|
1443
|
+
Filter for issues containing `Epic: #{parent_number}` in the body.
|
|
1444
|
+
|
|
1445
|
+
#### Linear
|
|
1446
|
+
|
|
1447
|
+
```graphql
|
|
1448
|
+
query {
|
|
1449
|
+
issues(filter: { identifier: { eq: "$PARENT_KEY" } }) {
|
|
1450
|
+
nodes {
|
|
1451
|
+
children {
|
|
1452
|
+
nodes {
|
|
1453
|
+
id
|
|
1454
|
+
identifier
|
|
1455
|
+
title
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
```
|
|
1462
|
+
|
|
1463
|
+
#### Azure DevOps
|
|
1464
|
+
|
|
1465
|
+
```bash
|
|
1466
|
+
# Fetch parent work item relations to find existing children
|
|
1467
|
+
curl -s \
|
|
1468
|
+
-u ":$AZDO_PAT" \
|
|
1469
|
+
-H "Accept: application/json" \
|
|
1470
|
+
"https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$PARENT_ID?\$expand=relations&api-version=7.1"
|
|
1471
|
+
```
|
|
1472
|
+
|
|
1473
|
+
Filter `relations` array for entries with `rel: "System.LinkTypes.Hierarchy-Forward"`. Extract child work item IDs from relation URLs, batch fetch titles, and compare against proposed ticket titles (case-insensitive exact match).
|
|
1474
|
+
|
|
1475
|
+
#### Shortcut
|
|
1476
|
+
|
|
1477
|
+
If the parent is an epic, fetch its stories:
|
|
1478
|
+
|
|
1479
|
+
```bash
|
|
1480
|
+
curl -s \
|
|
1481
|
+
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
|
|
1482
|
+
"https://api.app.shortcut.com/api/v3/epics/$EPIC_ID/stories"
|
|
1483
|
+
```
|
|
1484
|
+
|
|
1485
|
+
Compare story `name` fields against proposed ticket titles (case-insensitive exact match).
|
|
1486
|
+
|
|
1487
|
+
If the parent is a story, check `story_links` for related stories and compare titles.
|
|
1488
|
+
|
|
1489
|
+
#### Notion
|
|
1490
|
+
|
|
1491
|
+
If the parent page has a `relation` property for children, query related pages:
|
|
1492
|
+
|
|
1493
|
+
```bash
|
|
1494
|
+
PAGE=$(curl -s \
|
|
1495
|
+
-H "Authorization: Bearer $NOTION_TOKEN" \
|
|
1496
|
+
-H "Notion-Version: 2022-06-28" \
|
|
1497
|
+
"https://api.notion.com/v1/pages/$PARENT_PAGE_ID")
|
|
1498
|
+
```
|
|
1499
|
+
|
|
1500
|
+
Extract page IDs from the relation property, fetch each page's title, and compare against proposed ticket titles (case-insensitive exact match).
|
|
1501
|
+
|
|
1502
|
+
If no relation property is configured, query the database for pages with matching titles:
|
|
1503
|
+
|
|
1504
|
+
```bash
|
|
1505
|
+
curl -s \
|
|
1506
|
+
-H "Authorization: Bearer $NOTION_TOKEN" \
|
|
1507
|
+
-H "Notion-Version: 2022-06-28" \
|
|
1508
|
+
-X POST \
|
|
1509
|
+
"https://api.notion.com/v1/databases/$NOTION_DATABASE_ID/query" \
|
|
1510
|
+
-d '{"filter": {"property": "Name", "title": {"equals": "{ticket title}"}}}'
|
|
1511
|
+
```
|
|
1512
|
+
|
|
1513
|
+
If matching children found:
|
|
1514
|
+
|
|
1515
|
+
```
|
|
1516
|
+
{PARENT} already has children with similar titles:
|
|
1517
|
+
- {KEY}: "{Title}" (matches proposed ticket #{N})
|
|
1518
|
+
|
|
1519
|
+
This brief may have already been approved. Continue anyway? [y/N]
|
|
1520
|
+
```
|
|
1521
|
+
|
|
1522
|
+
Default: N (don't create duplicates).
|
|
1523
|
+
|
|
1524
|
+
---
|
|
1525
|
+
|
|
1526
|
+
## Notes
|
|
1527
|
+
|
|
1528
|
+
- 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`
|
|
1529
|
+
- Mode labels (`clancy:afk` / `clancy:hitl`) are used by `/clancy:autopilot` to decide whether to pick up a ticket autonomously or skip it for human attention
|
|
1530
|
+
- Jira ADF construction: if complex content fails, wrap in a `codeBlock` node as fallback (matches the pattern used by `/clancy:approve-plan`)
|
|
1531
|
+
- The `.approved` marker filename is the full brief filename with `.approved` appended (e.g. `.clancy/briefs/2026-03-14-auth-rework.md.approved`)
|
|
1532
|
+
- Tickets are created sequentially (not in parallel) to maintain dependency ordering and respect rate limits
|
|
1533
|
+
- The 500ms delay between ticket creations is sufficient for all platforms under normal rate limit conditions
|
|
1534
|
+
- Dependency links use "Blocks" for Jira, `blockedBy` for Linear, `System.LinkTypes.Dependency-Reverse` for Azure DevOps, `story_links` with `blocks` verb for Shortcut, `relation` properties for Notion, and body text cross-references for GitHub
|
|
1535
|
+
- Labels on Jira, Azure DevOps, and Notion are auto-created by the platform; on GitHub they must be pre-created or the 422 fallback handles it; on Linear and Shortcut they are looked up and auto-created if missing
|
|
1536
|
+
- Shortcut uses `Shortcut-Token` header (not `Authorization: Bearer`). API base: `https://api.app.shortcut.com/api/v3`. Stories use `name` (not `title`), descriptions use markdown, and parent linking uses `epic_id` for epic parents or `story_links` for story parents
|
|
1537
|
+
- Notion uses `Authorization: Bearer $NOTION_TOKEN` and `Notion-Version: 2022-06-28` headers. API base: `https://api.notion.com/v1`. Comments use `rich_text` blocks (not markdown) with a 2000-character limit per text block. Page content is blocks (not a description field). Labels use multi-select properties. The API does not support editing comments (post new instead). Parent-child relationships use `relation` properties (not native hierarchy)
|
|
1538
|
+
- Sprint/milestone assignment is deliberately not set — this is a team planning decision
|
|
1539
|
+
- Linear `priority: 0` means "No priority" — the team triages after creation
|
|
1540
|
+
- Jira priority is inherited from the parent if available; Linear and GitHub do not inherit priority
|