@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.
Files changed (217) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/dist/hooks/clancy-branch-guard.js +1 -0
  4. package/dist/hooks/clancy-check-update.js +2 -0
  5. package/dist/hooks/clancy-context-monitor.js +9 -0
  6. package/dist/hooks/clancy-credential-guard.js +2 -0
  7. package/dist/hooks/clancy-drift-detector.js +1 -0
  8. package/dist/hooks/clancy-notification.js +1 -0
  9. package/dist/hooks/clancy-post-compact.js +2 -0
  10. package/dist/hooks/clancy-statusline.js +1 -0
  11. package/dist/index.d.ts +24 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +23 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/installer/file-ops/file-ops.d.ts +35 -0
  16. package/dist/installer/file-ops/file-ops.d.ts.map +1 -0
  17. package/dist/installer/file-ops/file-ops.js +95 -0
  18. package/dist/installer/file-ops/file-ops.js.map +1 -0
  19. package/dist/installer/file-ops/index.d.ts +2 -0
  20. package/dist/installer/file-ops/index.d.ts.map +1 -0
  21. package/dist/installer/file-ops/index.js +2 -0
  22. package/dist/installer/file-ops/index.js.map +1 -0
  23. package/dist/installer/hook-installer/hook-installer.d.ts +22 -0
  24. package/dist/installer/hook-installer/hook-installer.d.ts.map +1 -0
  25. package/dist/installer/hook-installer/hook-installer.js +213 -0
  26. package/dist/installer/hook-installer/hook-installer.js.map +1 -0
  27. package/dist/installer/hook-installer/index.d.ts +2 -0
  28. package/dist/installer/hook-installer/index.d.ts.map +1 -0
  29. package/dist/installer/hook-installer/index.js +2 -0
  30. package/dist/installer/hook-installer/index.js.map +1 -0
  31. package/dist/installer/install/index.d.ts +3 -0
  32. package/dist/installer/install/index.d.ts.map +1 -0
  33. package/dist/installer/install/index.js +2 -0
  34. package/dist/installer/install/index.js.map +1 -0
  35. package/dist/installer/install/install.d.ts +124 -0
  36. package/dist/installer/install/install.d.ts.map +1 -0
  37. package/dist/installer/install/install.js +255 -0
  38. package/dist/installer/install/install.js.map +1 -0
  39. package/dist/installer/manifest/index.d.ts +2 -0
  40. package/dist/installer/manifest/index.d.ts.map +1 -0
  41. package/dist/installer/manifest/index.js +2 -0
  42. package/dist/installer/manifest/index.js.map +1 -0
  43. package/dist/installer/manifest/manifest.d.ts +46 -0
  44. package/dist/installer/manifest/manifest.d.ts.map +1 -0
  45. package/dist/installer/manifest/manifest.js +180 -0
  46. package/dist/installer/manifest/manifest.js.map +1 -0
  47. package/dist/installer/prompts/index.d.ts +2 -0
  48. package/dist/installer/prompts/index.d.ts.map +1 -0
  49. package/dist/installer/prompts/index.js +2 -0
  50. package/dist/installer/prompts/index.js.map +1 -0
  51. package/dist/installer/prompts/prompts.d.ts +34 -0
  52. package/dist/installer/prompts/prompts.d.ts.map +1 -0
  53. package/dist/installer/prompts/prompts.js +28 -0
  54. package/dist/installer/prompts/prompts.js.map +1 -0
  55. package/dist/installer/role-filter/index.d.ts +2 -0
  56. package/dist/installer/role-filter/index.d.ts.map +1 -0
  57. package/dist/installer/role-filter/index.js +2 -0
  58. package/dist/installer/role-filter/index.js.map +1 -0
  59. package/dist/installer/role-filter/role-filter.d.ts +33 -0
  60. package/dist/installer/role-filter/role-filter.d.ts.map +1 -0
  61. package/dist/installer/role-filter/role-filter.js +91 -0
  62. package/dist/installer/role-filter/role-filter.js.map +1 -0
  63. package/dist/installer/shared/fs-errors/fs-errors.d.ts +3 -0
  64. package/dist/installer/shared/fs-errors/fs-errors.d.ts.map +1 -0
  65. package/dist/installer/shared/fs-errors/fs-errors.js +7 -0
  66. package/dist/installer/shared/fs-errors/fs-errors.js.map +1 -0
  67. package/dist/installer/shared/fs-errors/index.d.ts +2 -0
  68. package/dist/installer/shared/fs-errors/index.d.ts.map +1 -0
  69. package/dist/installer/shared/fs-errors/index.js +2 -0
  70. package/dist/installer/shared/fs-errors/index.js.map +1 -0
  71. package/dist/installer/shared/fs-guards/fs-guards.d.ts +3 -0
  72. package/dist/installer/shared/fs-guards/fs-guards.d.ts.map +1 -0
  73. package/dist/installer/shared/fs-guards/fs-guards.js +18 -0
  74. package/dist/installer/shared/fs-guards/fs-guards.js.map +1 -0
  75. package/dist/installer/shared/fs-guards/index.d.ts +2 -0
  76. package/dist/installer/shared/fs-guards/index.d.ts.map +1 -0
  77. package/dist/installer/shared/fs-guards/index.js +2 -0
  78. package/dist/installer/shared/fs-guards/index.js.map +1 -0
  79. package/dist/installer/shared/type-guards/index.d.ts +2 -0
  80. package/dist/installer/shared/type-guards/index.d.ts.map +1 -0
  81. package/dist/installer/shared/type-guards/index.js +2 -0
  82. package/dist/installer/shared/type-guards/index.js.map +1 -0
  83. package/dist/installer/shared/type-guards/type-guards.d.ts +8 -0
  84. package/dist/installer/shared/type-guards/type-guards.d.ts.map +1 -0
  85. package/dist/installer/shared/type-guards/type-guards.js +10 -0
  86. package/dist/installer/shared/type-guards/type-guards.js.map +1 -0
  87. package/dist/installer/ui/index.d.ts +2 -0
  88. package/dist/installer/ui/index.d.ts.map +1 -0
  89. package/dist/installer/ui/index.js +2 -0
  90. package/dist/installer/ui/index.js.map +1 -0
  91. package/dist/installer/ui/ui.d.ts +23 -0
  92. package/dist/installer/ui/ui.d.ts.map +1 -0
  93. package/dist/installer/ui/ui.js +121 -0
  94. package/dist/installer/ui/ui.js.map +1 -0
  95. package/dist/runner/autopilot/autopilot.d.ts +71 -0
  96. package/dist/runner/autopilot/autopilot.d.ts.map +1 -0
  97. package/dist/runner/autopilot/autopilot.js +206 -0
  98. package/dist/runner/autopilot/autopilot.js.map +1 -0
  99. package/dist/runner/autopilot/index.d.ts +2 -0
  100. package/dist/runner/autopilot/index.d.ts.map +1 -0
  101. package/dist/runner/autopilot/index.js +2 -0
  102. package/dist/runner/autopilot/index.js.map +1 -0
  103. package/dist/runner/cli-bridge/cli-bridge.d.ts +34 -0
  104. package/dist/runner/cli-bridge/cli-bridge.d.ts.map +1 -0
  105. package/dist/runner/cli-bridge/cli-bridge.js +53 -0
  106. package/dist/runner/cli-bridge/cli-bridge.js.map +1 -0
  107. package/dist/runner/cli-bridge/index.d.ts +2 -0
  108. package/dist/runner/cli-bridge/index.d.ts.map +1 -0
  109. package/dist/runner/cli-bridge/index.js +2 -0
  110. package/dist/runner/cli-bridge/index.js.map +1 -0
  111. package/dist/runner/dep-factory/deliver-phase.d.ts +24 -0
  112. package/dist/runner/dep-factory/deliver-phase.d.ts.map +1 -0
  113. package/dist/runner/dep-factory/deliver-phase.js +57 -0
  114. package/dist/runner/dep-factory/deliver-phase.js.map +1 -0
  115. package/dist/runner/dep-factory/dep-factory.d.ts +38 -0
  116. package/dist/runner/dep-factory/dep-factory.d.ts.map +1 -0
  117. package/dist/runner/dep-factory/dep-factory.js +193 -0
  118. package/dist/runner/dep-factory/dep-factory.js.map +1 -0
  119. package/dist/runner/dep-factory/index.d.ts +2 -0
  120. package/dist/runner/dep-factory/index.d.ts.map +1 -0
  121. package/dist/runner/dep-factory/index.js +2 -0
  122. package/dist/runner/dep-factory/index.js.map +1 -0
  123. package/dist/runner/dep-factory/invoke-phase.d.ts +20 -0
  124. package/dist/runner/dep-factory/invoke-phase.d.ts.map +1 -0
  125. package/dist/runner/dep-factory/invoke-phase.js +45 -0
  126. package/dist/runner/dep-factory/invoke-phase.js.map +1 -0
  127. package/dist/runner/implement/implement.d.ts +38 -0
  128. package/dist/runner/implement/implement.d.ts.map +1 -0
  129. package/dist/runner/implement/implement.js +61 -0
  130. package/dist/runner/implement/implement.js.map +1 -0
  131. package/dist/runner/implement/index.d.ts +2 -0
  132. package/dist/runner/implement/index.d.ts.map +1 -0
  133. package/dist/runner/implement/index.js +2 -0
  134. package/dist/runner/implement/index.js.map +1 -0
  135. package/dist/runner/notify/index.d.ts +2 -0
  136. package/dist/runner/notify/index.d.ts.map +1 -0
  137. package/dist/runner/notify/index.js +2 -0
  138. package/dist/runner/notify/index.js.map +1 -0
  139. package/dist/runner/notify/notify.d.ts +49 -0
  140. package/dist/runner/notify/notify.d.ts.map +1 -0
  141. package/dist/runner/notify/notify.js +90 -0
  142. package/dist/runner/notify/notify.js.map +1 -0
  143. package/dist/runner/prompt-builder/index.d.ts +2 -0
  144. package/dist/runner/prompt-builder/index.d.ts.map +1 -0
  145. package/dist/runner/prompt-builder/index.js +2 -0
  146. package/dist/runner/prompt-builder/index.js.map +1 -0
  147. package/dist/runner/prompt-builder/prompt-builder.d.ts +53 -0
  148. package/dist/runner/prompt-builder/prompt-builder.d.ts.map +1 -0
  149. package/dist/runner/prompt-builder/prompt-builder.js +122 -0
  150. package/dist/runner/prompt-builder/prompt-builder.js.map +1 -0
  151. package/dist/runner/session-report/index.d.ts +2 -0
  152. package/dist/runner/session-report/index.d.ts.map +1 -0
  153. package/dist/runner/session-report/index.js +2 -0
  154. package/dist/runner/session-report/index.js.map +1 -0
  155. package/dist/runner/session-report/session-report.d.ts +81 -0
  156. package/dist/runner/session-report/session-report.d.ts.map +1 -0
  157. package/dist/runner/session-report/session-report.js +227 -0
  158. package/dist/runner/session-report/session-report.js.map +1 -0
  159. package/dist/runner/shared/types.d.ts +30 -0
  160. package/dist/runner/shared/types.d.ts.map +1 -0
  161. package/dist/runner/shared/types.js +2 -0
  162. package/dist/runner/shared/types.js.map +1 -0
  163. package/dist/shared/ansi/ansi.d.ts +59 -0
  164. package/dist/shared/ansi/ansi.d.ts.map +1 -0
  165. package/dist/shared/ansi/ansi.js +59 -0
  166. package/dist/shared/ansi/ansi.js.map +1 -0
  167. package/dist/shared/ansi/index.d.ts +2 -0
  168. package/dist/shared/ansi/index.d.ts.map +1 -0
  169. package/dist/shared/ansi/index.js +2 -0
  170. package/dist/shared/ansi/index.js.map +1 -0
  171. package/package.json +52 -0
  172. package/src/agents/agents.test.ts +57 -0
  173. package/src/agents/arch-agent.md +80 -0
  174. package/src/agents/concerns-agent.md +96 -0
  175. package/src/agents/design-agent.md +146 -0
  176. package/src/agents/devils-advocate.md +54 -0
  177. package/src/agents/quality-agent.md +178 -0
  178. package/src/agents/tech-agent.md +101 -0
  179. package/src/agents/verification-gate.md +128 -0
  180. package/src/roles/implementer/commands/autopilot.md +11 -0
  181. package/src/roles/implementer/commands/dry-run.md +15 -0
  182. package/src/roles/implementer/commands/implement.md +19 -0
  183. package/src/roles/implementer/workflows/autopilot.md +136 -0
  184. package/src/roles/implementer/workflows/implement.md +161 -0
  185. package/src/roles/planner/commands/approve-plan.md +11 -0
  186. package/src/roles/planner/commands/plan.md +22 -0
  187. package/src/roles/planner/workflows/approve-plan.md +970 -0
  188. package/src/roles/planner/workflows/plan.md +868 -0
  189. package/src/roles/reviewer/commands/logs.md +7 -0
  190. package/src/roles/reviewer/commands/review.md +9 -0
  191. package/src/roles/reviewer/commands/status.md +9 -0
  192. package/src/roles/reviewer/workflows/logs.md +109 -0
  193. package/src/roles/reviewer/workflows/review.md +197 -0
  194. package/src/roles/reviewer/workflows/status.md +142 -0
  195. package/src/roles/roles.test.ts +87 -0
  196. package/src/roles/setup/commands/doctor.md +7 -0
  197. package/src/roles/setup/commands/help.md +80 -0
  198. package/src/roles/setup/commands/init.md +7 -0
  199. package/src/roles/setup/commands/map-codebase.md +17 -0
  200. package/src/roles/setup/commands/settings.md +7 -0
  201. package/src/roles/setup/commands/uninstall.md +5 -0
  202. package/src/roles/setup/commands/update-docs.md +9 -0
  203. package/src/roles/setup/commands/update.md +13 -0
  204. package/src/roles/setup/workflows/doctor.md +131 -0
  205. package/src/roles/setup/workflows/init.md +1096 -0
  206. package/src/roles/setup/workflows/map-codebase.md +130 -0
  207. package/src/roles/setup/workflows/scaffold.md +872 -0
  208. package/src/roles/setup/workflows/settings.md +958 -0
  209. package/src/roles/setup/workflows/uninstall.md +170 -0
  210. package/src/roles/setup/workflows/update-docs.md +95 -0
  211. package/src/roles/setup/workflows/update.md +287 -0
  212. package/src/roles/strategist/commands/approve-brief.md +23 -0
  213. package/src/roles/strategist/commands/brief.md +29 -0
  214. package/src/roles/strategist/workflows/approve-brief.md +1540 -0
  215. package/src/roles/strategist/workflows/brief.md +1330 -0
  216. package/src/templates/CLAUDE.md +101 -0
  217. package/src/templates/templates.test.ts +53 -0
@@ -0,0 +1,970 @@
1
+ # Clancy Approve Plan Workflow
2
+
3
+ ## Overview
4
+
5
+ Promote an approved Clancy plan from a ticket comment to the ticket description. The plan is appended below the existing description, never replacing it. After promotion, the ticket is transitioned to the implementation queue.
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
+ ---
22
+
23
+ ## Step 2 — Parse argument / Resolve ticket
24
+
25
+ ### If no argument provided:
26
+
27
+ 1. Scan `.clancy/progress.txt` for entries matching `| PLAN |` or `| REVISED |` that have no subsequent `| APPROVE_PLAN |` for the same key.
28
+ 2. Sort by timestamp ascending (oldest first).
29
+ 3. If 0 found:
30
+ ```
31
+ No planned tickets awaiting approval. Run /clancy:plan first.
32
+ ```
33
+ Stop.
34
+ 4. If 1+ found, auto-select the oldest. Show:
35
+ ```
36
+ Auto-selected [{KEY}] {Title} (planned {date}). Promote this plan? [Y/n]
37
+ ```
38
+ To resolve the title, fetch the ticket from the board:
39
+ - **GitHub:** `GET /repos/$GITHUB_REPO/issues/$ISSUE_NUMBER` → use `.title`
40
+ - **Jira:** `GET $JIRA_BASE_URL/rest/api/3/issue/$KEY?fields=summary` → use `.fields.summary`
41
+ - **Linear:** `issues(filter: { identifier: { eq: "$KEY" } }) { nodes { title } }` → use `nodes[0].title`
42
+ - **Azure DevOps:** `GET https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$ID?fields=System.Title&api-version=7.1` → use `.fields["System.Title"]`
43
+ - **Shortcut:** `GET https://api.app.shortcut.com/api/v3/stories/$STORY_ID` → use `.name`
44
+ - **Notion:** `GET https://api.notion.com/v1/pages/$PAGE_ID` → extract title from the `title` type property in `properties`
45
+ If fetching fails, show the key without a title: `Auto-selected [{KEY}] (planned {date}). Promote? [Y/n]`
46
+ 5. If user declines:
47
+ ```
48
+ Cancelled.
49
+ ```
50
+ Stop.
51
+ 6. Note that the user has already confirmed — set a flag to skip the Step 4 confirmation.
52
+
53
+ ### If argument provided:
54
+
55
+ Validate the key format per board (case-insensitive):
56
+
57
+ - **GitHub:** `#\d+` or bare number
58
+ - **Jira:** `[A-Za-z][A-Za-z0-9]+-\d+` (e.g. `PROJ-123` or `proj-123`)
59
+ - **Linear:** `[A-Za-z]{1,10}-\d+` (e.g. `ENG-42` or `eng-42`)
60
+ - **Azure DevOps:** `\d+` (bare number, e.g. `42` — work item IDs are always numeric)
61
+ - **Shortcut:** `[A-Za-z]{1,5}-\d+` or bare number (e.g. `SC-123` or `123` — Shortcut story IDs are numeric, prefixed identifiers are optional)
62
+ - **Notion:** UUID format (`[a-f0-9]{32}` or with dashes) or `notion-[a-f0-9]{8}` short key
63
+
64
+ If invalid format:
65
+
66
+ ```
67
+ Invalid ticket key: {input}. Expected format: {board-specific example}.
68
+ ```
69
+
70
+ Stop.
71
+
72
+ Proceed with that key.
73
+
74
+ ---
75
+
76
+ ## Step 3 — Fetch the plan comment
77
+
78
+ Detect board from `.clancy/.env` and fetch comments for the specified ticket.
79
+
80
+ ### Jira
81
+
82
+ ```bash
83
+ RESPONSE=$(curl -s \
84
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
85
+ -H "Accept: application/json" \
86
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/comment")
87
+ ```
88
+
89
+ Search for the most recent comment containing an ADF heading node with text `Clancy Implementation Plan`. **Capture the comment `id`** for later editing in Step 5b.
90
+
91
+ ### GitHub
92
+
93
+ First, determine the issue number from the ticket key (strip the `#` prefix if present):
94
+
95
+ ```bash
96
+ RESPONSE=$(curl -s \
97
+ -H "Authorization: Bearer $GITHUB_TOKEN" \
98
+ -H "Accept: application/vnd.github+json" \
99
+ -H "X-GitHub-Api-Version: 2022-11-28" \
100
+ "https://api.github.com/repos/$GITHUB_REPO/issues/$ISSUE_NUMBER/comments?per_page=100")
101
+ ```
102
+
103
+ Search for the most recent comment body containing `## Clancy Implementation Plan`. **Capture the comment `id`** for later editing in Step 5b.
104
+
105
+ ### Linear
106
+
107
+ Use the filter-based query (preferred over `issueSearch`):
108
+
109
+ ```graphql
110
+ query {
111
+ issues(filter: { identifier: { eq: "$KEY" } }) {
112
+ nodes {
113
+ id
114
+ identifier
115
+ title
116
+ description
117
+ comments {
118
+ nodes {
119
+ id
120
+ body
121
+ createdAt
122
+ user {
123
+ id
124
+ }
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+ ```
131
+
132
+ If the filter-based query returns no results, fall back to `issueSearch`:
133
+
134
+ ```graphql
135
+ query {
136
+ issueSearch(query: "$IDENTIFIER", first: 5) {
137
+ nodes {
138
+ id
139
+ identifier
140
+ title
141
+ description
142
+ comments {
143
+ nodes {
144
+ id
145
+ body
146
+ createdAt
147
+ user {
148
+ id
149
+ }
150
+ }
151
+ }
152
+ }
153
+ }
154
+ }
155
+ ```
156
+
157
+ **Important:** `issueSearch` is a fuzzy text search. After fetching results, verify the returned issue's `identifier` field exactly matches the provided key (case-insensitive). If no exact match is found in the results, report: `Issue {KEY} not found. Check the identifier and try again.`
158
+
159
+ Search the comments for the most recent one containing `## Clancy Implementation Plan`. **Capture the comment `id`** and the existing comment `body` for later editing in Step 5b. Also capture the issue's internal `id` (UUID) for transitions in Step 6.
160
+
161
+ ### Azure DevOps
162
+
163
+ Fetch work item comments:
164
+
165
+ ```bash
166
+ RESPONSE=$(curl -s \
167
+ -u ":$AZDO_PAT" \
168
+ -H "Accept: application/json" \
169
+ "https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$WORK_ITEM_ID/comments?api-version=7.1-preview.4")
170
+ ```
171
+
172
+ Search `comments` array for the most recent comment where the `text` field (HTML) contains `Clancy Implementation Plan` (as an `<h2>` heading or plain text). **Capture the comment `id`** for later editing in Step 5b. Also fetch the work item title and description:
173
+
174
+ ```bash
175
+ WORK_ITEM=$(curl -s \
176
+ -u ":$AZDO_PAT" \
177
+ -H "Accept: application/json" \
178
+ "https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$WORK_ITEM_ID?fields=System.Title,System.Description&api-version=7.1")
179
+ ```
180
+
181
+ ### Shortcut
182
+
183
+ Fetch story comments:
184
+
185
+ ```bash
186
+ RESPONSE=$(curl -s \
187
+ -H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
188
+ "https://api.app.shortcut.com/api/v3/stories/$STORY_ID/comments")
189
+ ```
190
+
191
+ Search for the most recent comment where `text` contains `## Clancy Implementation Plan`. **Capture the comment `id`** for later editing in Step 5b. Also fetch the story for its title and description:
192
+
193
+ ```bash
194
+ STORY=$(curl -s \
195
+ -H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
196
+ "https://api.app.shortcut.com/api/v3/stories/$STORY_ID")
197
+ ```
198
+
199
+ ### Notion
200
+
201
+ Fetch page comments:
202
+
203
+ ```bash
204
+ RESPONSE=$(curl -s \
205
+ -H "Authorization: Bearer $NOTION_TOKEN" \
206
+ -H "Notion-Version: 2022-06-28" \
207
+ "https://api.notion.com/v1/comments?block_id=$PAGE_ID")
208
+ ```
209
+
210
+ Search `results` array for the most recent comment where `rich_text` content contains `Clancy Implementation Plan`. **Capture the comment `id`** for later reference in Step 5b. Also fetch the page for its title and content:
211
+
212
+ ```bash
213
+ PAGE=$(curl -s \
214
+ -H "Authorization: Bearer $NOTION_TOKEN" \
215
+ -H "Notion-Version: 2022-06-28" \
216
+ "https://api.notion.com/v1/pages/$PAGE_ID")
217
+ ```
218
+
219
+ **Notion limitation:** Comments use `rich_text` arrays. Search each comment's `rich_text[].text.content` for the plan marker.
220
+
221
+ If no plan comment is found:
222
+
223
+ ```
224
+ No Clancy plan found for {KEY}. Run /clancy:plan first.
225
+ ```
226
+
227
+ Stop.
228
+
229
+ ---
230
+
231
+ ## Step 3b — Check for existing plan in description
232
+
233
+ Before confirming, check if the ticket description already contains `## Clancy Implementation Plan`.
234
+
235
+ If it does:
236
+
237
+ ```
238
+ This ticket's description already contains a Clancy plan.
239
+ Continuing will add a duplicate.
240
+
241
+ [1] Continue anyway
242
+ [2] Cancel
243
+ ```
244
+
245
+ If the user picks [2], stop: `Cancelled. No changes made.`
246
+
247
+ ---
248
+
249
+ ## Step 4 — Confirm
250
+
251
+ **If the user already confirmed via auto-select in Step 2, SKIP this step entirely** (avoid double-confirmation).
252
+
253
+ **AFK mode:** If running in AFK mode (`--afk` flag or `CLANCY_MODE=afk`), skip the confirmation prompt and auto-confirm. Display the summary for logging purposes but proceed without waiting for input.
254
+
255
+ Display a summary and ask for confirmation:
256
+
257
+ ```
258
+ Clancy — Approve Plan
259
+
260
+ [{KEY}] {Title}
261
+ Size: {S/M/L} | {N} affected files
262
+ Planned: {date from plan}
263
+
264
+ Promote this plan to the ticket description? [Y/n]
265
+ ```
266
+
267
+ If the user declines (interactive only), stop:
268
+
269
+ ```
270
+ Cancelled. No changes made.
271
+ ```
272
+
273
+ ---
274
+
275
+ ## Step 5 — Update ticket description
276
+
277
+ Append the plan below the existing description with a separator. Never overwrite the original description.
278
+
279
+ The updated description follows this format:
280
+
281
+ ```
282
+ {existing description}
283
+
284
+ ---
285
+
286
+ {full plan content}
287
+ ```
288
+
289
+ ### Jira — PUT issue
290
+
291
+ Fetch the current description first:
292
+
293
+ ```bash
294
+ CURRENT=$(curl -s \
295
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
296
+ -H "Accept: application/json" \
297
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY?fields=description")
298
+ ```
299
+
300
+ Merge the existing ADF description with a `rule` node (horizontal rule) and the plan content as new ADF nodes. Then update:
301
+
302
+ ```bash
303
+ curl -s \
304
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
305
+ -X PUT \
306
+ -H "Content-Type: application/json" \
307
+ -H "Accept: application/json" \
308
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY" \
309
+ -d '{"fields": {"description": <merged ADF>}}'
310
+ ```
311
+
312
+ If ADF construction fails for the plan content, wrap the plan in a `codeBlock` node as fallback.
313
+
314
+ ### GitHub — PATCH issue
315
+
316
+ Fetch the current body:
317
+
318
+ ```bash
319
+ CURRENT=$(curl -s \
320
+ -H "Authorization: Bearer $GITHUB_TOKEN" \
321
+ -H "Accept: application/vnd.github+json" \
322
+ -H "X-GitHub-Api-Version: 2022-11-28" \
323
+ "https://api.github.com/repos/$GITHUB_REPO/issues/$ISSUE_NUMBER")
324
+ ```
325
+
326
+ Append the plan:
327
+
328
+ ```bash
329
+ curl -s \
330
+ -H "Authorization: Bearer $GITHUB_TOKEN" \
331
+ -H "Accept: application/vnd.github+json" \
332
+ -H "X-GitHub-Api-Version: 2022-11-28" \
333
+ -X PATCH \
334
+ "https://api.github.com/repos/$GITHUB_REPO/issues/$ISSUE_NUMBER" \
335
+ -d '{"body": "<existing body>\n\n---\n\n<plan>"}'
336
+ ```
337
+
338
+ ### Linear — issueUpdate mutation
339
+
340
+ Fetch the current description:
341
+
342
+ ```graphql
343
+ query {
344
+ issue(id: "$ISSUE_ID") {
345
+ description
346
+ }
347
+ }
348
+ ```
349
+
350
+ Update with appended plan:
351
+
352
+ ```graphql
353
+ mutation {
354
+ issueUpdate(
355
+ id: "$ISSUE_ID"
356
+ input: { description: "<existing>\n\n---\n\n<plan>" }
357
+ ) {
358
+ success
359
+ }
360
+ }
361
+ ```
362
+
363
+ ### Azure DevOps — PATCH work item
364
+
365
+ Fetch the current description:
366
+
367
+ ```bash
368
+ CURRENT=$(curl -s \
369
+ -u ":$AZDO_PAT" \
370
+ -H "Accept: application/json" \
371
+ "https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$WORK_ITEM_ID?fields=System.Description&api-version=7.1")
372
+ ```
373
+
374
+ Azure DevOps descriptions are **HTML**. Append the plan (converted to HTML) with a horizontal rule separator:
375
+
376
+ ```bash
377
+ curl -s \
378
+ -u ":$AZDO_PAT" \
379
+ -X PATCH \
380
+ -H "Content-Type: application/json-patch+json" \
381
+ "https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$WORK_ITEM_ID?api-version=7.1" \
382
+ -d '[{"op": "replace", "path": "/fields/System.Description", "value": "<existing HTML>\n<hr>\n<plan as HTML>"}]'
383
+ ```
384
+
385
+ Convert the plan markdown to HTML using the same conversion rules as Step 5 in plan.md (headings, lists, tables, bold, code → HTML equivalents). If HTML construction is too complex, wrap the plan in `<pre>` tags as fallback.
386
+
387
+ ### Shortcut — PUT story
388
+
389
+ Fetch the current description:
390
+
391
+ ```bash
392
+ CURRENT=$(curl -s \
393
+ -H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
394
+ "https://api.app.shortcut.com/api/v3/stories/$STORY_ID")
395
+ ```
396
+
397
+ Append the plan (Shortcut descriptions use markdown):
398
+
399
+ ```bash
400
+ curl -s \
401
+ -H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
402
+ -H "Content-Type: application/json" \
403
+ -X PUT \
404
+ "https://api.app.shortcut.com/api/v3/stories/$STORY_ID" \
405
+ -d '{"description": "<existing description>\n\n---\n\n<plan>"}'
406
+ ```
407
+
408
+ ### Notion — Append blocks to page
409
+
410
+ Notion page "descriptions" are stored as **child blocks**, not a single text property. To append the plan, add new blocks to the page:
411
+
412
+ ```bash
413
+ curl -s \
414
+ -H "Authorization: Bearer $NOTION_TOKEN" \
415
+ -H "Notion-Version: 2022-06-28" \
416
+ -X PATCH \
417
+ "https://api.notion.com/v1/blocks/$PAGE_ID/children" \
418
+ -d '{"children": [
419
+ {"type": "divider", "divider": {}},
420
+ {"type": "heading_2", "heading_2": {"rich_text": [{"type": "text", "text": {"content": "Clancy Implementation Plan"}}]}},
421
+ {"type": "paragraph", "paragraph": {"rich_text": [{"type": "text", "text": {"content": "<plan section text>"}}]}}
422
+ ]}'
423
+ ```
424
+
425
+ Convert the plan into Notion block format:
426
+
427
+ - `## Heading` → `heading_2` block
428
+ - `### Heading` → `heading_3` block
429
+ - Plain text paragraphs → `paragraph` block
430
+ - `- bullet` → `bulleted_list_item` block
431
+ - `- [ ] checkbox` → `to_do` block
432
+ - Tables → not natively supported as blocks; use `paragraph` blocks with monospace formatting, or split into individual `paragraph` blocks per row
433
+ - Code blocks → `code` block with `language: "markdown"`
434
+
435
+ **Notion limitation:** Each `rich_text` block has a **2000-character limit**. Split long sections across multiple blocks. The `children` array can contain up to **100 blocks** per request — if the plan is very large, make multiple PATCH calls.
436
+
437
+ **Notion limitation:** Unlike other boards, this appends to the page body (not the description property). The plan content will appear at the bottom of the page, after existing content, separated by a divider.
438
+
439
+ ---
440
+
441
+ ## Step 5b — Edit plan comment (approval note)
442
+
443
+ After updating the description, edit the original plan comment to prepend an approval note. This is **best-effort** — warn on failure, continue.
444
+
445
+ ### GitHub
446
+
447
+ ```bash
448
+ curl -s \
449
+ -H "Authorization: Bearer $GITHUB_TOKEN" \
450
+ -H "Accept: application/vnd.github+json" \
451
+ -H "X-GitHub-Api-Version: 2022-11-28" \
452
+ -H "Content-Type: application/json" \
453
+ -X PATCH \
454
+ "https://api.github.com/repos/$GITHUB_REPO/issues/comments/$COMMENT_ID" \
455
+ -d '{"body": "> **Plan approved and promoted to description** -- {YYYY-MM-DD}\n\n{existing_comment_body}"}'
456
+ ```
457
+
458
+ ### Jira
459
+
460
+ ```bash
461
+ curl -s \
462
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
463
+ -X PUT \
464
+ -H "Content-Type: application/json" \
465
+ -H "Accept: application/json" \
466
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/comment/$COMMENT_ID" \
467
+ -d '{
468
+ "body": {
469
+ "version": 1,
470
+ "type": "doc",
471
+ "content": [
472
+ {"type": "paragraph", "content": [
473
+ {"type": "text", "text": "Plan approved and promoted to description -- {YYYY-MM-DD}.",
474
+ "marks": [{"type": "strong"}]}
475
+ ]},
476
+ <...existing ADF content nodes...>
477
+ ]
478
+ }
479
+ }'
480
+ ```
481
+
482
+ ### Linear
483
+
484
+ ```graphql
485
+ mutation {
486
+ commentUpdate(
487
+ id: "$COMMENT_ID"
488
+ input: {
489
+ body: "> **Plan approved and promoted to description** -- {YYYY-MM-DD}\n\n{existing_comment_body}"
490
+ }
491
+ ) {
492
+ success
493
+ }
494
+ }
495
+ ```
496
+
497
+ ### Azure DevOps
498
+
499
+ ```bash
500
+ curl -s \
501
+ -u ":$AZDO_PAT" \
502
+ -X PATCH \
503
+ -H "Content-Type: application/json" \
504
+ "https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$WORK_ITEM_ID/comments/$COMMENT_ID?api-version=7.1-preview.4" \
505
+ -d '{"text": "<p><strong>Plan approved and promoted to description -- {YYYY-MM-DD}.</strong></p><existing HTML comment>"}'
506
+ ```
507
+
508
+ ### Shortcut
509
+
510
+ ```bash
511
+ curl -s \
512
+ -H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
513
+ -H "Content-Type: application/json" \
514
+ -X PUT \
515
+ "https://api.app.shortcut.com/api/v3/stories/$STORY_ID/comments/$COMMENT_ID" \
516
+ -d '{"text": "> **Plan approved and promoted to description** -- {YYYY-MM-DD}\n\n{existing_comment_text}"}'
517
+ ```
518
+
519
+ ### Notion
520
+
521
+ **Notion limitation:** The Notion API does **not support editing comments**. There is no PATCH/PUT endpoint for comments. Instead, post a new comment with the approval note:
522
+
523
+ ```bash
524
+ curl -s \
525
+ -H "Authorization: Bearer $NOTION_TOKEN" \
526
+ -H "Notion-Version: 2022-06-28" \
527
+ -X POST \
528
+ "https://api.notion.com/v1/comments" \
529
+ -d '{"parent": {"page_id": "$PAGE_ID"}, "rich_text": [{"type": "text", "text": {"content": "Plan approved and promoted to page content — {YYYY-MM-DD}."}, "annotations": {"bold": true}}]}'
530
+ ```
531
+
532
+ This posts a new comment rather than editing the original plan comment. The approval note references the plan being promoted to the page body (not description, since Notion uses blocks).
533
+
534
+ On failure for any platform:
535
+
536
+ ```
537
+ Could not update plan comment. The plan is still promoted to the description.
538
+ ```
539
+
540
+ ---
541
+
542
+ ## Step 6 — Post-approval label transition
543
+
544
+ Transition the ticket from the planning queue to the implementation queue via pipeline labels. This is **best-effort** — warn on failure, continue.
545
+
546
+ **Crash safety:** Add the new label BEFORE removing the old one. A ticket briefly has two labels (harmless) rather than zero labels (ticket lost).
547
+
548
+ **This label transition is mandatory — always apply and remove.** Use `CLANCY_LABEL_BUILD` from `.clancy/.env` if set, otherwise `clancy:build`. Use `CLANCY_LABEL_PLAN` from `.clancy/.env` if set, otherwise `clancy:plan`. Ensure the build label exists on the board (create if missing), add it to the ticket, then remove the plan label.
549
+
550
+ **If build label creation fails** (GitHub/Linear/Shortcut require explicit creation): warn and **do not remove the plan label**. The ticket must keep at least one pipeline label — removing the plan label without a build label would orphan the ticket from both queues.
551
+
552
+ ### GitHub
553
+
554
+ 1. **Add build label** (ensure it exists first):
555
+
556
+ ```bash
557
+ # Ensure label exists (ignore 422 = already exists)
558
+ curl -s \
559
+ -H "Authorization: Bearer $GITHUB_TOKEN" \
560
+ -H "Accept: application/vnd.github+json" \
561
+ -H "X-GitHub-Api-Version: 2022-11-28" \
562
+ -H "Content-Type: application/json" \
563
+ -X POST \
564
+ "https://api.github.com/repos/$GITHUB_REPO/labels" \
565
+ -d '{"name": "$CLANCY_LABEL_BUILD", "color": "0075ca"}'
566
+
567
+ # Add to issue
568
+ curl -s \
569
+ -H "Authorization: Bearer $GITHUB_TOKEN" \
570
+ -H "Accept: application/vnd.github+json" \
571
+ -H "X-GitHub-Api-Version: 2022-11-28" \
572
+ -H "Content-Type: application/json" \
573
+ -X POST \
574
+ "https://api.github.com/repos/$GITHUB_REPO/issues/$ISSUE_NUMBER/labels" \
575
+ -d '{"labels": ["$CLANCY_LABEL_BUILD"]}'
576
+ ```
577
+
578
+ 2. **Remove plan label:**
579
+ ```bash
580
+ curl -s \
581
+ -H "Authorization: Bearer $GITHUB_TOKEN" \
582
+ -H "Accept: application/vnd.github+json" \
583
+ -H "X-GitHub-Api-Version: 2022-11-28" \
584
+ -X DELETE \
585
+ "https://api.github.com/repos/$GITHUB_REPO/issues/$ISSUE_NUMBER/labels/$(echo $CLANCY_LABEL_PLAN | jq -Rr @uri)"
586
+ ```
587
+ Ignore 404 (label not on issue).
588
+
589
+ ### Jira
590
+
591
+ 1. **Add build label** (Jira auto-creates labels):
592
+
593
+ ```bash
594
+ # Fetch current labels
595
+ CURRENT_LABELS=$(curl -s \
596
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
597
+ -H "Accept: application/json" \
598
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY?fields=labels" | jq -r '.fields.labels')
599
+
600
+ # Add build label
601
+ UPDATED_LABELS=$(echo "$CURRENT_LABELS" | jq --arg build "$CLANCY_LABEL_BUILD" '. + [$build] | unique')
602
+
603
+ curl -s \
604
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
605
+ -X PUT \
606
+ -H "Content-Type: application/json" \
607
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY" \
608
+ -d "{\"fields\": {\"labels\": $UPDATED_LABELS}}"
609
+ ```
610
+
611
+ 2. **Remove plan label:**
612
+
613
+ ```bash
614
+ # Re-fetch labels (may have changed), remove plan label
615
+ CURRENT_LABELS=$(curl -s \
616
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
617
+ -H "Accept: application/json" \
618
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY?fields=labels" | jq -r '.fields.labels')
619
+
620
+ UPDATED_LABELS=$(echo "$CURRENT_LABELS" | jq --arg plan "$CLANCY_LABEL_PLAN" '[.[] | select(. != $plan)]')
621
+
622
+ curl -s \
623
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
624
+ -X PUT \
625
+ -H "Content-Type: application/json" \
626
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY" \
627
+ -d "{\"fields\": {\"labels\": $UPDATED_LABELS}}"
628
+ ```
629
+
630
+ 3. **Status transition** (only if `CLANCY_STATUS_PLANNED` is set — skip if unset):
631
+
632
+ ```bash
633
+ # Fetch transitions
634
+ curl -s \
635
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
636
+ -H "Accept: application/json" \
637
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/transitions"
638
+
639
+ # Find matching transition and execute
640
+ curl -s \
641
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
642
+ -X POST \
643
+ -H "Content-Type: application/json" \
644
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/transitions" \
645
+ -d '{"transition": {"id": "$TRANSITION_ID"}}'
646
+ ```
647
+
648
+ On failure:
649
+
650
+ ```
651
+ Could not transition ticket. Move it manually to your implementation queue.
652
+ ```
653
+
654
+ ### Linear
655
+
656
+ 1. **Add build label** (ensure it exists, then add):
657
+
658
+ ```graphql
659
+ # Ensure label exists — check team labels, workspace labels, create if missing
660
+ mutation {
661
+ issueLabelCreate(input: {
662
+ teamId: "$LINEAR_TEAM_ID"
663
+ name: "$CLANCY_LABEL_BUILD"
664
+ color: "#0075ca"
665
+ }) { success issueLabel { id } }
666
+ }
667
+
668
+ # Fetch current label IDs on the issue, add build label ID
669
+ mutation {
670
+ issueUpdate(
671
+ id: "$ISSUE_UUID"
672
+ input: { labelIds: [...currentLabelIds, buildLabelId] }
673
+ ) { success }
674
+ }
675
+ ```
676
+
677
+ 2. **Remove plan label:**
678
+
679
+ ```graphql
680
+ # Fetch current label IDs, filter out plan label ID
681
+ mutation {
682
+ issueUpdate(
683
+ id: "$ISSUE_UUID"
684
+ input: { labelIds: [currentLabelIds, without, planLabelId] }
685
+ ) {
686
+ success
687
+ }
688
+ }
689
+ ```
690
+
691
+ 3. **State transition** (always):
692
+
693
+ ```graphql
694
+ # Resolve "unstarted" state
695
+ query {
696
+ workflowStates(
697
+ filter: {
698
+ team: { id: { eq: "$LINEAR_TEAM_ID" } }
699
+ type: { eq: "unstarted" }
700
+ }
701
+ ) {
702
+ nodes {
703
+ id
704
+ name
705
+ }
706
+ }
707
+ }
708
+
709
+ # Transition
710
+ mutation {
711
+ issueUpdate(id: "$ISSUE_UUID", input: { stateId: "$UNSTARTED_STATE_ID" }) {
712
+ success
713
+ }
714
+ }
715
+ ```
716
+
717
+ If no `unstarted` state found: warn, skip transition.
718
+
719
+ ### Azure DevOps
720
+
721
+ Azure DevOps uses **tags** (semicolon-delimited string field) instead of labels, and **board columns/states** for transitions.
722
+
723
+ 1. **Add build tag:**
724
+
725
+ ```bash
726
+ # Fetch current tags
727
+ CURRENT=$(curl -s \
728
+ -u ":$AZDO_PAT" \
729
+ -H "Accept: application/json" \
730
+ "https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$WORK_ITEM_ID?fields=System.Tags&api-version=7.1")
731
+
732
+ # Append build tag (semicolon-delimited)
733
+ # If existing tags are "clancy:plan; bug-fix", new value is "clancy:plan; bug-fix; clancy:build"
734
+ curl -s \
735
+ -u ":$AZDO_PAT" \
736
+ -X PATCH \
737
+ -H "Content-Type: application/json-patch+json" \
738
+ "https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$WORK_ITEM_ID?api-version=7.1" \
739
+ -d '[{"op": "replace", "path": "/fields/System.Tags", "value": "<existing tags>; $CLANCY_LABEL_BUILD"}]'
740
+ ```
741
+
742
+ 2. **Remove plan tag:**
743
+
744
+ ```bash
745
+ # Re-fetch tags, remove plan tag from the semicolon-delimited string
746
+ # E.g., "clancy:plan; bug-fix; clancy:build" → "bug-fix; clancy:build"
747
+ curl -s \
748
+ -u ":$AZDO_PAT" \
749
+ -X PATCH \
750
+ -H "Content-Type: application/json-patch+json" \
751
+ "https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$WORK_ITEM_ID?api-version=7.1" \
752
+ -d '[{"op": "replace", "path": "/fields/System.Tags", "value": "<tags without plan tag>"}]'
753
+ ```
754
+
755
+ 3. **State transition** (only if `CLANCY_STATUS_PLANNED` is set — skip if unset):
756
+
757
+ ```bash
758
+ curl -s \
759
+ -u ":$AZDO_PAT" \
760
+ -X PATCH \
761
+ -H "Content-Type: application/json-patch+json" \
762
+ "https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$WORK_ITEM_ID?api-version=7.1" \
763
+ -d '[{"op": "replace", "path": "/fields/System.State", "value": "$CLANCY_STATUS_PLANNED"}]'
764
+ ```
765
+
766
+ ### Shortcut
767
+
768
+ Shortcut uses **labels** and **workflow state transitions**.
769
+
770
+ 1. **Add build label:**
771
+
772
+ ```bash
773
+ # Resolve build label ID (create if missing)
774
+ LABELS=$(curl -s \
775
+ -H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
776
+ "https://api.app.shortcut.com/api/v3/labels")
777
+
778
+ # Find or create the build label, get its ID
779
+ # Then add to story:
780
+ curl -s \
781
+ -H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
782
+ -H "Content-Type: application/json" \
783
+ -X PUT \
784
+ "https://api.app.shortcut.com/api/v3/stories/$STORY_ID" \
785
+ -d '{"labels": [{"name": "$CLANCY_LABEL_BUILD"}, ...existing_labels]}'
786
+ ```
787
+
788
+ 2. **Remove plan label:**
789
+
790
+ ```bash
791
+ # Fetch current story labels, filter out plan label, update
792
+ curl -s \
793
+ -H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
794
+ -H "Content-Type: application/json" \
795
+ -X PUT \
796
+ "https://api.app.shortcut.com/api/v3/stories/$STORY_ID" \
797
+ -d '{"labels": [labels_without_plan_label]}'
798
+ ```
799
+
800
+ 3. **Workflow state transition:**
801
+
802
+ ```bash
803
+ # Resolve the "Unstarted" or "Ready for Development" state ID from workflows
804
+ WORKFLOWS=$(curl -s \
805
+ -H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
806
+ "https://api.app.shortcut.com/api/v3/workflows")
807
+
808
+ # Find state with type "unstarted", get its ID
809
+ curl -s \
810
+ -H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
811
+ -H "Content-Type: application/json" \
812
+ -X PUT \
813
+ "https://api.app.shortcut.com/api/v3/stories/$STORY_ID" \
814
+ -d '{"workflow_state_id": $UNSTARTED_STATE_ID}'
815
+ ```
816
+
817
+ If no suitable state found: warn, skip transition.
818
+
819
+ ### Notion
820
+
821
+ Notion uses **multi-select properties** for labels and **status properties** for transitions.
822
+
823
+ 1. **Add build label** (add to multi-select property):
824
+
825
+ ```bash
826
+ # Fetch current page properties
827
+ PAGE=$(curl -s \
828
+ -H "Authorization: Bearer $NOTION_TOKEN" \
829
+ -H "Notion-Version: 2022-06-28" \
830
+ "https://api.notion.com/v1/pages/$PAGE_ID")
831
+
832
+ # Update multi-select property to include build label
833
+ curl -s \
834
+ -H "Authorization: Bearer $NOTION_TOKEN" \
835
+ -H "Notion-Version: 2022-06-28" \
836
+ -X PATCH \
837
+ "https://api.notion.com/v1/pages/$PAGE_ID" \
838
+ -d '{"properties": {"$CLANCY_NOTION_LABELS": {"multi_select": [existing_options, {"name": "$CLANCY_LABEL_BUILD"}]}}}'
839
+ ```
840
+
841
+ 2. **Remove plan label** (update multi-select without plan label):
842
+
843
+ ```bash
844
+ curl -s \
845
+ -H "Authorization: Bearer $NOTION_TOKEN" \
846
+ -H "Notion-Version: 2022-06-28" \
847
+ -X PATCH \
848
+ "https://api.notion.com/v1/pages/$PAGE_ID" \
849
+ -d '{"properties": {"$CLANCY_NOTION_LABELS": {"multi_select": [options_without_plan_label]}}}'
850
+ ```
851
+
852
+ 3. **Status transition** (only if `CLANCY_STATUS_PLANNED` is set):
853
+
854
+ ```bash
855
+ curl -s \
856
+ -H "Authorization: Bearer $NOTION_TOKEN" \
857
+ -H "Notion-Version: 2022-06-28" \
858
+ -X PATCH \
859
+ "https://api.notion.com/v1/pages/$PAGE_ID" \
860
+ -d '{"properties": {"$CLANCY_NOTION_STATUS": {"status": {"name": "$CLANCY_STATUS_PLANNED"}}}}'
861
+ ```
862
+
863
+ On failure:
864
+
865
+ ```
866
+ Could not transition ticket. Move it manually to your implementation queue.
867
+ ```
868
+
869
+ ---
870
+
871
+ ## Step 7 — Confirm and log
872
+
873
+ On success, display a board-specific message:
874
+
875
+ **GitHub:**
876
+
877
+ ```
878
+ Plan promoted. Label swapped: {CLANCY_LABEL_PLAN} → {CLANCY_LABEL_BUILD}. Ready for /clancy:implement.
879
+
880
+ "Book 'em, Lou." — The ticket is ready for /clancy:implement.
881
+ ```
882
+
883
+ **Jira (with transition):**
884
+
885
+ ```
886
+ Plan promoted. Ticket transitioned to {CLANCY_STATUS_PLANNED}.
887
+
888
+ "Book 'em, Lou." -- The ticket is ready for /clancy:implement.
889
+ ```
890
+
891
+ **Jira (no transition configured):**
892
+
893
+ ```
894
+ Plan promoted. Move [{KEY}] to your implementation queue for /clancy:implement.
895
+
896
+ "Book 'em, Lou." -- The ticket is ready for /clancy:implement.
897
+ ```
898
+
899
+ **Linear:**
900
+
901
+ ```
902
+ Plan promoted. Moved to unstarted. Ready for /clancy:implement.
903
+
904
+ "Book 'em, Lou." -- The ticket is ready for /clancy:implement.
905
+ ```
906
+
907
+ **Azure DevOps (with transition):**
908
+
909
+ ```
910
+ Plan promoted. Work item transitioned to {CLANCY_STATUS_PLANNED}.
911
+
912
+ "Book 'em, Lou." -- The ticket is ready for /clancy:implement.
913
+ ```
914
+
915
+ **Azure DevOps (no transition configured):**
916
+
917
+ ```
918
+ Plan promoted. Move work item {ID} to your implementation queue for /clancy:implement.
919
+
920
+ "Book 'em, Lou." -- The ticket is ready for /clancy:implement.
921
+ ```
922
+
923
+ **Shortcut:**
924
+
925
+ ```
926
+ Plan promoted. Moved to unstarted. Ready for /clancy:implement.
927
+
928
+ "Book 'em, Lou." -- The ticket is ready for /clancy:implement.
929
+ ```
930
+
931
+ **Notion (with transition):**
932
+
933
+ ```
934
+ Plan promoted to page content. Status updated to {CLANCY_STATUS_PLANNED}.
935
+
936
+ "Book 'em, Lou." -- The ticket is ready for /clancy:implement.
937
+ ```
938
+
939
+ **Notion (no transition configured):**
940
+
941
+ ```
942
+ Plan promoted to page content. Move the page to your implementation queue for /clancy:implement.
943
+
944
+ "Book 'em, Lou." -- The ticket is ready for /clancy:implement.
945
+ ```
946
+
947
+ Append to `.clancy/progress.txt`:
948
+
949
+ ```
950
+ YYYY-MM-DD HH:MM | {KEY} | APPROVE_PLAN | —
951
+ ```
952
+
953
+ On failure:
954
+
955
+ ```
956
+ Failed to update description for [{KEY}]. Check your board permissions.
957
+ ```
958
+
959
+ ---
960
+
961
+ ## Notes
962
+
963
+ - This command only appends -- it never overwrites the existing ticket description
964
+ - If the ticket has multiple plan comments, the most recent one is used
965
+ - The plan content is taken verbatim from the comment -- no regeneration
966
+ - Step 3b checks for existing plans in the description to prevent accidental duplication
967
+ - The ticket key is case-insensitive -- accept `PROJ-123`, `proj-123`, or `#123` (GitHub)
968
+ - Step 5b edits the plan comment with an approval note -- this is best-effort and does not block the workflow
969
+ - Step 6 transitions the ticket to the implementation queue -- this is best-effort and board-specific
970
+ - The `## Clancy Implementation Plan` marker in comments is used by both `/clancy:plan` (to detect existing plans) and `/clancy:approve-plan` (to find the plan to promote)