@chief-clancy/plan 0.2.0 → 0.4.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/README.md +37 -1
- package/bin/plan.js +5 -2
- package/dist/installer/install.d.ts.map +1 -1
- package/dist/installer/install.js +6 -2
- package/dist/installer/install.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/approve-plan.md +23 -0
- package/src/commands/commands.test.ts +1 -1
- package/src/workflows/approve-plan.md +1199 -0
- package/src/workflows/workflows.test.ts +300 -1
|
@@ -0,0 +1,1199 @@
|
|
|
1
|
+
# Clancy Approve Plan Workflow
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Approve a Clancy implementation plan. Behaviour depends on the install context:
|
|
6
|
+
|
|
7
|
+
- **Standalone mode** (no `.clancy/.env`): write a local `.clancy/plans/{stem}.approved` marker file with the plan's SHA-256 and approval timestamp. The marker is the gate `/clancy:implement-from` checks before applying changes
|
|
8
|
+
- **Standalone+board mode** (`.clancy/.env` present, no full pipeline): with a board ticket key, run the existing comment-to-description transport flow; with a plan-file stem, write the local marker (board push lands in PR 9)
|
|
9
|
+
- **Terminal mode** (full pipeline installed): existing behaviour — promote an approved plan from a ticket comment to the ticket description and transition the ticket to the implementation queue
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Step 1 — Preflight checks
|
|
14
|
+
|
|
15
|
+
### 1. Detect installation context
|
|
16
|
+
|
|
17
|
+
Check for `.clancy/.env`:
|
|
18
|
+
|
|
19
|
+
- **Absent** → **standalone mode**. No board credentials. Board ticket arguments are blocked; only plan-file stems are accepted
|
|
20
|
+
- **Present** → continue to `.clancy/clancy-implement.js` check below
|
|
21
|
+
|
|
22
|
+
If `.clancy/.env` is present, check for `.clancy/clancy-implement.js`:
|
|
23
|
+
|
|
24
|
+
- **Present** → **terminal mode**. Full Clancy pipeline installed
|
|
25
|
+
- **Absent** → **standalone+board mode**. Board credentials available via `/clancy:board-setup`. Board ticket arguments work via the existing transport flow. Plan-file stems write the local marker
|
|
26
|
+
|
|
27
|
+
### 2. Terminal-mode preflight (skip in standalone mode and standalone+board mode)
|
|
28
|
+
|
|
29
|
+
If in **terminal mode** (`.clancy/.env` present AND `.clancy/clancy-implement.js` present):
|
|
30
|
+
|
|
31
|
+
a. Source `.clancy/.env` and check board credentials are present.
|
|
32
|
+
|
|
33
|
+
b. Check `CLANCY_ROLES` includes `planner` (or env var is unset, which indicates a global install where all roles are available). If `CLANCY_ROLES` is set but does not include `planner`:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
The Planner role is not enabled. Add "planner" to CLANCY_ROLES in .clancy/.env or run /clancy:settings.
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Stop.
|
|
40
|
+
|
|
41
|
+
### 3. Standalone-mode preflight (only in standalone mode)
|
|
42
|
+
|
|
43
|
+
If in **standalone mode** (no `.clancy/.env`), check that `.clancy/plans/` exists. If not:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
No local plans found. Run /clancy:plan --from .clancy/briefs/<brief>.md first.
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Stop.
|
|
50
|
+
|
|
51
|
+
### 4. Standalone+board preflight (only in standalone+board mode)
|
|
52
|
+
|
|
53
|
+
If in **standalone+board mode**, source `.clancy/.env` for board credentials. Both plan-file stems and board ticket arguments are valid in this mode — Step 2 routes between them.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Step 2 — Resolve target
|
|
58
|
+
|
|
59
|
+
The argument can be either a **plan-file stem** (e.g. `add-dark-mode-2`, matching a file at `.clancy/plans/{stem}.md`) or a **board ticket key** (e.g. `PROJ-123`, `#42`). Resolution depends on the installation mode detected in Step 1.
|
|
60
|
+
|
|
61
|
+
### Standalone mode
|
|
62
|
+
|
|
63
|
+
In standalone mode, the argument must be a plan-file stem. Board ticket keys are not valid here because there are no board credentials.
|
|
64
|
+
|
|
65
|
+
**With argument:** look up `.clancy/plans/{arg}.md`. If the file does not exist:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
Plan file not found: .clancy/plans/{arg}.md. Plan stems include the row number (e.g. `add-dark-mode-2`). Run /clancy:plan --list to see available plans.
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Stop. Do not attempt to interpret the argument as a ticket key in standalone mode.
|
|
72
|
+
|
|
73
|
+
**No argument:** auto-select the oldest unapproved local plan.
|
|
74
|
+
|
|
75
|
+
1. Scan `.clancy/plans/` for `.md` files
|
|
76
|
+
2. **Filter to plan files only**: a file qualifies as a plan if it contains the literal heading `## Clancy Implementation Plan` (the marker written by Step 4f / 5a of `plan.md`). Files without this heading are scratch / notes / drafts and are silently skipped — they are not approvable
|
|
77
|
+
3. For each remaining file, check whether a sibling `.approved` marker exists at the same path with the `.approved` suffix. The unapproved set is qualifying files with no sibling marker
|
|
78
|
+
4. Sort the unapproved set by the `**Planned:**` header date (ascending). Tie-break by Plan ID (alphabetical ascending). Files with a missing or unparseable `**Planned:**` date sort **last** (after all dated plans), then by Plan ID alphabetically among themselves. Mirrors `plan.md` Step 8 inventory's deterministic ordering
|
|
79
|
+
|
|
80
|
+
If the unapproved set is empty:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
No local plans awaiting approval. Run /clancy:plan --from .clancy/briefs/<brief>.md first, or all existing plans are already approved.
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Stop. If non-empty, auto-select the first entry. Confirm with the user (skipped in `--afk` mode):
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
Auto-selected {stem} (planned {date}). Approve this plan? [Y/n]
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
If declined: `Cancelled.` Stop.
|
|
93
|
+
|
|
94
|
+
### Standalone+board and terminal modes
|
|
95
|
+
|
|
96
|
+
In these modes the argument may be either a plan-file stem or a board ticket key. **Try plan-file lookup first (does `.clancy/plans/{arg}.md` exist?)**, then fall back to ticket-key validation. The plan stem wins over ticket key on collision (e.g. if `PROJ-123.md` exists in `.clancy/plans/` AND `PROJ-123` is a valid ticket key, the plan stem wins). Document the collision rule explicitly so users are not surprised.
|
|
97
|
+
|
|
98
|
+
**With argument that resolves to a plan file:** continue to Step 4 (Confirm), then Step 4a (Write local marker). The board push offer for plan-file-stem mode is deferred to a future PR — for now the local marker is the only side effect.
|
|
99
|
+
|
|
100
|
+
**With argument that does not resolve to a plan file:** validate as a ticket key per the board configured in `.clancy/.env` (case-insensitive):
|
|
101
|
+
|
|
102
|
+
- **GitHub:** `#\d+` or bare number
|
|
103
|
+
- **Jira:** `[A-Za-z][A-Za-z0-9]+-\d+` (e.g. `PROJ-123` or `proj-123`)
|
|
104
|
+
- **Linear:** `[A-Za-z]{1,10}-\d+` (e.g. `ENG-42` or `eng-42`)
|
|
105
|
+
- **Azure DevOps:** `\d+` (bare number, e.g. `42` — work item IDs are always numeric)
|
|
106
|
+
- **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)
|
|
107
|
+
- **Notion:** UUID format (`[a-f0-9]{32}` or with dashes) or `notion-[a-f0-9]{8}` short key
|
|
108
|
+
|
|
109
|
+
If invalid format:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
Invalid ticket key: {input}. Expected format: {board-specific example}.
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Stop.
|
|
116
|
+
|
|
117
|
+
If valid: proceed with that key. The board transport flow runs (Steps 3-7 below) — this is the existing behaviour, unchanged from before PR 7b.
|
|
118
|
+
|
|
119
|
+
**No argument:**
|
|
120
|
+
|
|
121
|
+
- **Standalone+board and terminal:** scan `.clancy/progress.txt` for entries matching `| PLAN |` or `| REVISED |` that have no subsequent `| APPROVE_PLAN |` for the same key. Sort by timestamp ascending (oldest first).
|
|
122
|
+
- If 0 found:
|
|
123
|
+
```
|
|
124
|
+
No planned tickets awaiting approval. Run /clancy:plan first.
|
|
125
|
+
```
|
|
126
|
+
Stop.
|
|
127
|
+
- If 1+ found, auto-select the oldest. Show:
|
|
128
|
+
```
|
|
129
|
+
Auto-selected [{KEY}] {Title} (planned {date}). Promote this plan? [Y/n]
|
|
130
|
+
```
|
|
131
|
+
To resolve the title, fetch the ticket from the board:
|
|
132
|
+
- **GitHub:** `GET /repos/$GITHUB_REPO/issues/$ISSUE_NUMBER` → use `.title`
|
|
133
|
+
- **Jira:** `GET $JIRA_BASE_URL/rest/api/3/issue/$KEY?fields=summary` → use `.fields.summary`
|
|
134
|
+
- **Linear:** `issues(filter: { identifier: { eq: "$KEY" } }) { nodes { title } }` → use `nodes[0].title`
|
|
135
|
+
- **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"]`
|
|
136
|
+
- **Shortcut:** `GET https://api.app.shortcut.com/api/v3/stories/$STORY_ID` → use `.name`
|
|
137
|
+
- **Notion:** `GET https://api.notion.com/v1/pages/$PAGE_ID` → extract title from the `title` type property in `properties`
|
|
138
|
+
If fetching fails, show the key without a title: `Auto-selected [{KEY}] (planned {date}). Promote? [Y/n]`
|
|
139
|
+
- If user declines:
|
|
140
|
+
```
|
|
141
|
+
Cancelled.
|
|
142
|
+
```
|
|
143
|
+
Stop.
|
|
144
|
+
- Note that the user has already confirmed — set a flag to skip the Step 4 confirmation.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Step 3 — Fetch the plan comment
|
|
149
|
+
|
|
150
|
+
Detect board from `.clancy/.env` and fetch comments for the specified ticket.
|
|
151
|
+
|
|
152
|
+
### Jira
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
RESPONSE=$(curl -s \
|
|
156
|
+
-u "$JIRA_USER:$JIRA_API_TOKEN" \
|
|
157
|
+
-H "Accept: application/json" \
|
|
158
|
+
"$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/comment")
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
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.
|
|
162
|
+
|
|
163
|
+
### GitHub
|
|
164
|
+
|
|
165
|
+
First, determine the issue number from the ticket key (strip the `#` prefix if present):
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
RESPONSE=$(curl -s \
|
|
169
|
+
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
|
170
|
+
-H "Accept: application/vnd.github+json" \
|
|
171
|
+
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
172
|
+
"https://api.github.com/repos/$GITHUB_REPO/issues/$ISSUE_NUMBER/comments?per_page=100")
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Search for the most recent comment body containing `## Clancy Implementation Plan`. **Capture the comment `id`** for later editing in Step 5b.
|
|
176
|
+
|
|
177
|
+
### Linear
|
|
178
|
+
|
|
179
|
+
Use the filter-based query (preferred over `issueSearch`):
|
|
180
|
+
|
|
181
|
+
```graphql
|
|
182
|
+
query {
|
|
183
|
+
issues(filter: { identifier: { eq: "$KEY" } }) {
|
|
184
|
+
nodes {
|
|
185
|
+
id
|
|
186
|
+
identifier
|
|
187
|
+
title
|
|
188
|
+
description
|
|
189
|
+
comments {
|
|
190
|
+
nodes {
|
|
191
|
+
id
|
|
192
|
+
body
|
|
193
|
+
createdAt
|
|
194
|
+
user {
|
|
195
|
+
id
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
If the filter-based query returns no results, fall back to `issueSearch`:
|
|
205
|
+
|
|
206
|
+
```graphql
|
|
207
|
+
query {
|
|
208
|
+
issueSearch(query: "$IDENTIFIER", first: 5) {
|
|
209
|
+
nodes {
|
|
210
|
+
id
|
|
211
|
+
identifier
|
|
212
|
+
title
|
|
213
|
+
description
|
|
214
|
+
comments {
|
|
215
|
+
nodes {
|
|
216
|
+
id
|
|
217
|
+
body
|
|
218
|
+
createdAt
|
|
219
|
+
user {
|
|
220
|
+
id
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**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.`
|
|
230
|
+
|
|
231
|
+
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.
|
|
232
|
+
|
|
233
|
+
### Azure DevOps
|
|
234
|
+
|
|
235
|
+
Fetch work item comments:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
RESPONSE=$(curl -s \
|
|
239
|
+
-u ":$AZDO_PAT" \
|
|
240
|
+
-H "Accept: application/json" \
|
|
241
|
+
"https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$WORK_ITEM_ID/comments?api-version=7.1-preview.4")
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
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:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
WORK_ITEM=$(curl -s \
|
|
248
|
+
-u ":$AZDO_PAT" \
|
|
249
|
+
-H "Accept: application/json" \
|
|
250
|
+
"https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$WORK_ITEM_ID?fields=System.Title,System.Description&api-version=7.1")
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Shortcut
|
|
254
|
+
|
|
255
|
+
Fetch story comments:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
RESPONSE=$(curl -s \
|
|
259
|
+
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
|
|
260
|
+
"https://api.app.shortcut.com/api/v3/stories/$STORY_ID/comments")
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
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:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
STORY=$(curl -s \
|
|
267
|
+
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
|
|
268
|
+
"https://api.app.shortcut.com/api/v3/stories/$STORY_ID")
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Notion
|
|
272
|
+
|
|
273
|
+
Fetch page comments:
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
RESPONSE=$(curl -s \
|
|
277
|
+
-H "Authorization: Bearer $NOTION_TOKEN" \
|
|
278
|
+
-H "Notion-Version: 2022-06-28" \
|
|
279
|
+
"https://api.notion.com/v1/comments?block_id=$PAGE_ID")
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
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:
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
PAGE=$(curl -s \
|
|
286
|
+
-H "Authorization: Bearer $NOTION_TOKEN" \
|
|
287
|
+
-H "Notion-Version: 2022-06-28" \
|
|
288
|
+
"https://api.notion.com/v1/pages/$PAGE_ID")
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**Notion limitation:** Comments use `rich_text` arrays. Search each comment's `rich_text[].text.content` for the plan marker.
|
|
292
|
+
|
|
293
|
+
If no plan comment is found:
|
|
294
|
+
|
|
295
|
+
```
|
|
296
|
+
No Clancy plan found for {KEY}. Run /clancy:plan first.
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Stop.
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Step 3b — Check for existing plan in description
|
|
304
|
+
|
|
305
|
+
Before confirming, check if the ticket description already contains `## Clancy Implementation Plan`.
|
|
306
|
+
|
|
307
|
+
If it does:
|
|
308
|
+
|
|
309
|
+
```
|
|
310
|
+
This ticket's description already contains a Clancy plan.
|
|
311
|
+
Continuing will add a duplicate.
|
|
312
|
+
|
|
313
|
+
[1] Continue anyway
|
|
314
|
+
[2] Cancel
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
If the user picks [2], stop: `Cancelled. No changes made.`
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Step 4 — Confirm
|
|
322
|
+
|
|
323
|
+
**If the user already confirmed via auto-select in Step 2, SKIP this step entirely** (avoid double-confirmation).
|
|
324
|
+
|
|
325
|
+
**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.
|
|
326
|
+
|
|
327
|
+
Display a summary and ask for confirmation:
|
|
328
|
+
|
|
329
|
+
```
|
|
330
|
+
Clancy — Approve Plan
|
|
331
|
+
|
|
332
|
+
[{KEY}] {Title}
|
|
333
|
+
Size: {S/M/L} | {N} affected files
|
|
334
|
+
Planned: {date from plan}
|
|
335
|
+
|
|
336
|
+
Promote this plan to the ticket description? [Y/n]
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
If the user declines (interactive only), stop:
|
|
340
|
+
|
|
341
|
+
```
|
|
342
|
+
Cancelled. No changes made.
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
For **plan-file stem mode** (Step 2 resolved the argument to a local plan file), the summary shows the plan stem instead of `[{KEY}] {Title}`:
|
|
346
|
+
|
|
347
|
+
```
|
|
348
|
+
Clancy — Approve Plan (local)
|
|
349
|
+
|
|
350
|
+
{stem}
|
|
351
|
+
Size: {S/M/L} | {N} affected files
|
|
352
|
+
Planned: {date from plan}
|
|
353
|
+
|
|
354
|
+
Approve this plan? [Y/n]
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
After confirmation in plan-file stem mode, jump to Step 4a (local marker write). For board ticket key mode, continue to Step 5 below.
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Step 4a — Write local marker
|
|
362
|
+
|
|
363
|
+
Run this step instead of Steps 5, 5b, 6 when the resolved argument was a plan-file stem (standalone mode, or standalone+board / terminal mode where Step 2 found a matching plan file). Write a `.clancy/plans/{stem}.approved` marker that gates `/clancy:implement-from`.
|
|
364
|
+
|
|
365
|
+
### Compute the SHA-256
|
|
366
|
+
|
|
367
|
+
**Order of operations** (do these in order, exactly):
|
|
368
|
+
|
|
369
|
+
1. Read the plan file at `.clancy/plans/{stem}.md` from disk into memory as bytes.
|
|
370
|
+
2. Compute the SHA-256 hash of those bytes — no normalisation (no line-ending fix, no trailing-whitespace strip, no BOM removal). Hex-encode lowercase.
|
|
371
|
+
3. **Then** (only after the hash is computed) open the `.approved` marker for exclusive create as described below.
|
|
372
|
+
|
|
373
|
+
The `.approved` file is **never** included in the hash — only `.clancy/plans/{stem}.md` is hashed, and only its on-disk byte content at the moment of step 1. PR 8's `/clancy:implement-from` re-reads the same plan file, hashes it the same way, and compares to the `sha256=` value stored in the marker. Any divergence (re-edit, line-ending change, trailing whitespace tweak) blocks implementation until re-approval.
|
|
374
|
+
|
|
375
|
+
### Write the marker file with O_EXCL
|
|
376
|
+
|
|
377
|
+
Open `.clancy/plans/{stem}.approved` for **exclusive create** (Node `fs.openSync(path, 'wx')`, equivalent to `open(2)` with `O_EXCL`). Write the marker body as plain text:
|
|
378
|
+
|
|
379
|
+
```
|
|
380
|
+
sha256={hex sha256 of the plan file at approval time}
|
|
381
|
+
approved_at={ISO 8601 UTC timestamp, e.g. 2026-04-08T22:30:00Z}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
Two `key=value` lines, each terminated with `\n`. No JSON, no extra whitespace, no comments. PR 8 parses this with a tolerant `^(sha256|approved_at)=(.+)$` regex per line.
|
|
385
|
+
|
|
386
|
+
### Handle EEXIST (already-approved)
|
|
387
|
+
|
|
388
|
+
If the exclusive create fails with `EEXIST`, the marker already exists — the plan was previously approved. Stop with:
|
|
389
|
+
|
|
390
|
+
```
|
|
391
|
+
Plan already approved: {stem}
|
|
392
|
+
Marker: .clancy/plans/{stem}.approved
|
|
393
|
+
|
|
394
|
+
To re-approve (e.g. after revising the plan):
|
|
395
|
+
Delete .clancy/plans/{stem}.approved manually, then re-run /clancy:approve-plan {stem}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
A `--fresh` flag for `/clancy:approve-plan` is not implemented in this release. Manual deletion is the supported re-approval path.
|
|
399
|
+
|
|
400
|
+
### Marker is the gate for /clancy:implement-from
|
|
401
|
+
|
|
402
|
+
The `.approved` marker is the gate `/clancy:implement-from` checks before applying changes. PR 8 reads the marker, hashes the current plan file, and compares to the stored `sha256`. Match → proceed; mismatch → block with a "plan changed since approval" error. This is why the SHA must be computed over the plan file content (not just touched as an empty file).
|
|
403
|
+
|
|
404
|
+
### After writing the marker
|
|
405
|
+
|
|
406
|
+
After the marker is written successfully, update the source brief file's marker comment (Step 4b below), then jump to Step 7 (Confirm and log). Steps 5, 5b, and 6 (board transport) are skipped entirely in plan-file stem mode.
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## Step 4b — Update brief marker (best-effort)
|
|
411
|
+
|
|
412
|
+
After Step 4a writes the local plan marker, update the source brief file's planned-rows marker so `/clancy:plan --list` and the brief's display logic know which rows have been approved. This step is best-effort: any failure here logs a warning but does NOT roll back the `.clancy/plans/{stem}.approved` marker.
|
|
413
|
+
|
|
414
|
+
### Resolve the source brief filename
|
|
415
|
+
|
|
416
|
+
Read the plan file at `.clancy/plans/{stem}.md` and extract the `**Brief:**` header line (e.g. `**Brief:** 2026-04-01-add-dark-mode.md`). The value is the brief filename relative to `.clancy/briefs/`. If the line is absent or empty, warn and skip the rest of Step 4b:
|
|
417
|
+
|
|
418
|
+
```
|
|
419
|
+
⚠ Plan {stem} has no **Brief:** header — cannot update brief marker. Continuing without brief update.
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Resolve the row number
|
|
423
|
+
|
|
424
|
+
Extract the row number from the plan file's `**Row:**` header line (e.g. `**Row:** #2 — Add toggle component`). The number after `#` and before the em-dash (U+2014, U+2013, or hyphen) is the row. If absent, warn and skip Step 4b.
|
|
425
|
+
|
|
426
|
+
### Find and update the marker
|
|
427
|
+
|
|
428
|
+
Open `.clancy/briefs/{brief-filename}` and find the marker comment matching this tolerant regex (line-anchored, allows missing-or-present `approved:` prefix, allows arbitrary whitespace):
|
|
429
|
+
|
|
430
|
+
```
|
|
431
|
+
^<!--\s*(?:approved:([\d,]*)\s+)?planned:([\d,]+)\s*-->\s*$
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
This matches all of:
|
|
435
|
+
|
|
436
|
+
- `<!-- planned:1,2,3 -->` (no approved prefix yet — current state from PR 6b)
|
|
437
|
+
- `<!-- approved:1 planned:1,2,3 -->` (PR 7b adds the approved prefix)
|
|
438
|
+
- `<!-- approved: planned:1,2,3 -->` (empty approved list — should not happen but handle gracefully)
|
|
439
|
+
- `<!--planned:1,2,3-->` (no surrounding spaces — hand-edited)
|
|
440
|
+
|
|
441
|
+
If no marker line matches, warn and skip — do not synthesise a marker. The brief should already have one written by `/clancy:plan --from`.
|
|
442
|
+
|
|
443
|
+
**Reversed-order markers** (`<!-- planned:1,2 approved:1 -->` — `planned:` first, `approved:` second) do NOT match this regex and fall through to the warn-and-skip branch. The canonical ordering is enforced on every write, so a brief that drifts into reversed order requires manual correction. If you see the warning "no marker found" but the brief clearly has a marker, check the order — `approved:` must come before `planned:`.
|
|
444
|
+
|
|
445
|
+
**Code-fence false positives:** the regex is line-anchored but does NOT track fenced-code-block context. If a brief file embeds an example marker inside a triple-backtick block (e.g. documentation about how markers work), the regex may match the example. The first match wins, so authors should keep the real marker as the first marker line in the file. The `## Feedback` detector in `plan.md` Step 3a uses code-fence-aware parsing for the same reason — apply that pattern manually if a brief becomes complex enough to need it.
|
|
446
|
+
|
|
447
|
+
If a marker matches, parse the existing `approved:` and `planned:` row lists. Add the current row number to the `approved:` list (deduped, sorted ascending). Reconstruct the marker with the canonical ordering: `approved:` first, `planned:` second, single space between fields and inside the comment. Example:
|
|
448
|
+
|
|
449
|
+
- Before: `<!-- planned:1,2,3 -->`, current row = `2`
|
|
450
|
+
- After: `<!-- approved:2 planned:1,2,3 -->`
|
|
451
|
+
- Before: `<!-- approved:1 planned:1,2,3 -->`, current row = `2`
|
|
452
|
+
- After: `<!-- approved:1,2 planned:1,2,3 -->`
|
|
453
|
+
|
|
454
|
+
Write the updated brief file back. The read-modify-write is not concurrency-safe — running multiple `/clancy:approve-plan` commands against the same brief in parallel may produce duplicate or missing entries. Single-user local flow is assumed (mirrors the concurrency note in [`plan.md` Step 3a](./plan.md)).
|
|
455
|
+
|
|
456
|
+
### Best-effort failure handling
|
|
457
|
+
|
|
458
|
+
If any step in 4b fails (file not found, marker not found, write error, regex mismatch), log a warning but do NOT roll back the `.clancy/plans/{stem}.approved` marker. The local marker is the source of truth — the brief marker is metadata for display and `/clancy:plan --list`. Example warning:
|
|
459
|
+
|
|
460
|
+
```
|
|
461
|
+
⚠ Failed to update brief marker for {stem}: {reason}
|
|
462
|
+
The plan is still approved. The .clancy/plans/{stem}.approved marker is in place.
|
|
463
|
+
You can manually update .clancy/briefs/{brief}.md if needed.
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
After Step 4b completes (successfully or with a warning), jump to Step 7 (Confirm and log). Skip Steps 5, 5b, and 6 entirely.
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## Step 5 — Update ticket description
|
|
471
|
+
|
|
472
|
+
Append the plan below the existing description with a separator. Never overwrite the original description.
|
|
473
|
+
|
|
474
|
+
The updated description follows this format:
|
|
475
|
+
|
|
476
|
+
```
|
|
477
|
+
{existing description}
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
{full plan content}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Jira — PUT issue
|
|
485
|
+
|
|
486
|
+
Fetch the current description first:
|
|
487
|
+
|
|
488
|
+
```bash
|
|
489
|
+
CURRENT=$(curl -s \
|
|
490
|
+
-u "$JIRA_USER:$JIRA_API_TOKEN" \
|
|
491
|
+
-H "Accept: application/json" \
|
|
492
|
+
"$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY?fields=description")
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Merge the existing ADF description with a `rule` node (horizontal rule) and the plan content as new ADF nodes. Then update:
|
|
496
|
+
|
|
497
|
+
```bash
|
|
498
|
+
curl -s \
|
|
499
|
+
-u "$JIRA_USER:$JIRA_API_TOKEN" \
|
|
500
|
+
-X PUT \
|
|
501
|
+
-H "Content-Type: application/json" \
|
|
502
|
+
-H "Accept: application/json" \
|
|
503
|
+
"$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY" \
|
|
504
|
+
-d '{"fields": {"description": <merged ADF>}}'
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
If ADF construction fails for the plan content, wrap the plan in a `codeBlock` node as fallback.
|
|
508
|
+
|
|
509
|
+
### GitHub — PATCH issue
|
|
510
|
+
|
|
511
|
+
Fetch the current body:
|
|
512
|
+
|
|
513
|
+
```bash
|
|
514
|
+
CURRENT=$(curl -s \
|
|
515
|
+
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
|
516
|
+
-H "Accept: application/vnd.github+json" \
|
|
517
|
+
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
518
|
+
"https://api.github.com/repos/$GITHUB_REPO/issues/$ISSUE_NUMBER")
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
Append the plan:
|
|
522
|
+
|
|
523
|
+
```bash
|
|
524
|
+
curl -s \
|
|
525
|
+
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
|
526
|
+
-H "Accept: application/vnd.github+json" \
|
|
527
|
+
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
528
|
+
-X PATCH \
|
|
529
|
+
"https://api.github.com/repos/$GITHUB_REPO/issues/$ISSUE_NUMBER" \
|
|
530
|
+
-d '{"body": "<existing body>\n\n---\n\n<plan>"}'
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### Linear — issueUpdate mutation
|
|
534
|
+
|
|
535
|
+
Fetch the current description:
|
|
536
|
+
|
|
537
|
+
```graphql
|
|
538
|
+
query {
|
|
539
|
+
issue(id: "$ISSUE_ID") {
|
|
540
|
+
description
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
Update with appended plan:
|
|
546
|
+
|
|
547
|
+
```graphql
|
|
548
|
+
mutation {
|
|
549
|
+
issueUpdate(
|
|
550
|
+
id: "$ISSUE_ID"
|
|
551
|
+
input: { description: "<existing>\n\n---\n\n<plan>" }
|
|
552
|
+
) {
|
|
553
|
+
success
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### Azure DevOps — PATCH work item
|
|
559
|
+
|
|
560
|
+
Fetch the current description:
|
|
561
|
+
|
|
562
|
+
```bash
|
|
563
|
+
CURRENT=$(curl -s \
|
|
564
|
+
-u ":$AZDO_PAT" \
|
|
565
|
+
-H "Accept: application/json" \
|
|
566
|
+
"https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$WORK_ITEM_ID?fields=System.Description&api-version=7.1")
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
Azure DevOps descriptions are **HTML**. Append the plan (converted to HTML) with a horizontal rule separator:
|
|
570
|
+
|
|
571
|
+
```bash
|
|
572
|
+
curl -s \
|
|
573
|
+
-u ":$AZDO_PAT" \
|
|
574
|
+
-X PATCH \
|
|
575
|
+
-H "Content-Type: application/json-patch+json" \
|
|
576
|
+
"https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$WORK_ITEM_ID?api-version=7.1" \
|
|
577
|
+
-d '[{"op": "replace", "path": "/fields/System.Description", "value": "<existing HTML>\n<hr>\n<plan as HTML>"}]'
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
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.
|
|
581
|
+
|
|
582
|
+
### Shortcut — PUT story
|
|
583
|
+
|
|
584
|
+
Fetch the current description:
|
|
585
|
+
|
|
586
|
+
```bash
|
|
587
|
+
CURRENT=$(curl -s \
|
|
588
|
+
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
|
|
589
|
+
"https://api.app.shortcut.com/api/v3/stories/$STORY_ID")
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
Append the plan (Shortcut descriptions use markdown):
|
|
593
|
+
|
|
594
|
+
```bash
|
|
595
|
+
curl -s \
|
|
596
|
+
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
|
|
597
|
+
-H "Content-Type: application/json" \
|
|
598
|
+
-X PUT \
|
|
599
|
+
"https://api.app.shortcut.com/api/v3/stories/$STORY_ID" \
|
|
600
|
+
-d '{"description": "<existing description>\n\n---\n\n<plan>"}'
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### Notion — Append blocks to page
|
|
604
|
+
|
|
605
|
+
Notion page "descriptions" are stored as **child blocks**, not a single text property. To append the plan, add new blocks to the page:
|
|
606
|
+
|
|
607
|
+
```bash
|
|
608
|
+
curl -s \
|
|
609
|
+
-H "Authorization: Bearer $NOTION_TOKEN" \
|
|
610
|
+
-H "Notion-Version: 2022-06-28" \
|
|
611
|
+
-X PATCH \
|
|
612
|
+
"https://api.notion.com/v1/blocks/$PAGE_ID/children" \
|
|
613
|
+
-d '{"children": [
|
|
614
|
+
{"type": "divider", "divider": {}},
|
|
615
|
+
{"type": "heading_2", "heading_2": {"rich_text": [{"type": "text", "text": {"content": "Clancy Implementation Plan"}}]}},
|
|
616
|
+
{"type": "paragraph", "paragraph": {"rich_text": [{"type": "text", "text": {"content": "<plan section text>"}}]}}
|
|
617
|
+
]}'
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
Convert the plan into Notion block format:
|
|
621
|
+
|
|
622
|
+
- `## Heading` → `heading_2` block
|
|
623
|
+
- `### Heading` → `heading_3` block
|
|
624
|
+
- Plain text paragraphs → `paragraph` block
|
|
625
|
+
- `- bullet` → `bulleted_list_item` block
|
|
626
|
+
- `- [ ] checkbox` → `to_do` block
|
|
627
|
+
- Tables → not natively supported as blocks; use `paragraph` blocks with monospace formatting, or split into individual `paragraph` blocks per row
|
|
628
|
+
- Code blocks → `code` block with `language: "markdown"`
|
|
629
|
+
|
|
630
|
+
**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.
|
|
631
|
+
|
|
632
|
+
**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.
|
|
633
|
+
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
## Step 5b — Edit plan comment (approval note)
|
|
637
|
+
|
|
638
|
+
After updating the description, edit the original plan comment to prepend an approval note. This is **best-effort** — warn on failure, continue.
|
|
639
|
+
|
|
640
|
+
### GitHub
|
|
641
|
+
|
|
642
|
+
```bash
|
|
643
|
+
curl -s \
|
|
644
|
+
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
|
645
|
+
-H "Accept: application/vnd.github+json" \
|
|
646
|
+
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
647
|
+
-H "Content-Type: application/json" \
|
|
648
|
+
-X PATCH \
|
|
649
|
+
"https://api.github.com/repos/$GITHUB_REPO/issues/comments/$COMMENT_ID" \
|
|
650
|
+
-d '{"body": "> **Plan approved and promoted to description** -- {YYYY-MM-DD}\n\n{existing_comment_body}"}'
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### Jira
|
|
654
|
+
|
|
655
|
+
```bash
|
|
656
|
+
curl -s \
|
|
657
|
+
-u "$JIRA_USER:$JIRA_API_TOKEN" \
|
|
658
|
+
-X PUT \
|
|
659
|
+
-H "Content-Type: application/json" \
|
|
660
|
+
-H "Accept: application/json" \
|
|
661
|
+
"$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/comment/$COMMENT_ID" \
|
|
662
|
+
-d '{
|
|
663
|
+
"body": {
|
|
664
|
+
"version": 1,
|
|
665
|
+
"type": "doc",
|
|
666
|
+
"content": [
|
|
667
|
+
{"type": "paragraph", "content": [
|
|
668
|
+
{"type": "text", "text": "Plan approved and promoted to description -- {YYYY-MM-DD}.",
|
|
669
|
+
"marks": [{"type": "strong"}]}
|
|
670
|
+
]},
|
|
671
|
+
<...existing ADF content nodes...>
|
|
672
|
+
]
|
|
673
|
+
}
|
|
674
|
+
}'
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### Linear
|
|
678
|
+
|
|
679
|
+
```graphql
|
|
680
|
+
mutation {
|
|
681
|
+
commentUpdate(
|
|
682
|
+
id: "$COMMENT_ID"
|
|
683
|
+
input: {
|
|
684
|
+
body: "> **Plan approved and promoted to description** -- {YYYY-MM-DD}\n\n{existing_comment_body}"
|
|
685
|
+
}
|
|
686
|
+
) {
|
|
687
|
+
success
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
### Azure DevOps
|
|
693
|
+
|
|
694
|
+
```bash
|
|
695
|
+
curl -s \
|
|
696
|
+
-u ":$AZDO_PAT" \
|
|
697
|
+
-X PATCH \
|
|
698
|
+
-H "Content-Type: application/json" \
|
|
699
|
+
"https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$WORK_ITEM_ID/comments/$COMMENT_ID?api-version=7.1-preview.4" \
|
|
700
|
+
-d '{"text": "<p><strong>Plan approved and promoted to description -- {YYYY-MM-DD}.</strong></p><existing HTML comment>"}'
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
### Shortcut
|
|
704
|
+
|
|
705
|
+
```bash
|
|
706
|
+
curl -s \
|
|
707
|
+
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
|
|
708
|
+
-H "Content-Type: application/json" \
|
|
709
|
+
-X PUT \
|
|
710
|
+
"https://api.app.shortcut.com/api/v3/stories/$STORY_ID/comments/$COMMENT_ID" \
|
|
711
|
+
-d '{"text": "> **Plan approved and promoted to description** -- {YYYY-MM-DD}\n\n{existing_comment_text}"}'
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
### Notion
|
|
715
|
+
|
|
716
|
+
**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:
|
|
717
|
+
|
|
718
|
+
```bash
|
|
719
|
+
curl -s \
|
|
720
|
+
-H "Authorization: Bearer $NOTION_TOKEN" \
|
|
721
|
+
-H "Notion-Version: 2022-06-28" \
|
|
722
|
+
-X POST \
|
|
723
|
+
"https://api.notion.com/v1/comments" \
|
|
724
|
+
-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}}]}'
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
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).
|
|
728
|
+
|
|
729
|
+
On failure for any platform:
|
|
730
|
+
|
|
731
|
+
```
|
|
732
|
+
Could not update plan comment. The plan is still promoted to the description.
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
---
|
|
736
|
+
|
|
737
|
+
## Step 6 — Post-approval label transition
|
|
738
|
+
|
|
739
|
+
Transition the ticket from the planning queue to the implementation queue via pipeline labels. This is **best-effort** — warn on failure, continue.
|
|
740
|
+
|
|
741
|
+
**Crash safety:** Add the new label BEFORE removing the old one. A ticket briefly has two labels (harmless) rather than zero labels (ticket lost).
|
|
742
|
+
|
|
743
|
+
**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.
|
|
744
|
+
|
|
745
|
+
**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.
|
|
746
|
+
|
|
747
|
+
### GitHub
|
|
748
|
+
|
|
749
|
+
1. **Add build label** (ensure it exists first):
|
|
750
|
+
|
|
751
|
+
```bash
|
|
752
|
+
# Ensure label exists (ignore 422 = already exists)
|
|
753
|
+
curl -s \
|
|
754
|
+
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
|
755
|
+
-H "Accept: application/vnd.github+json" \
|
|
756
|
+
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
757
|
+
-H "Content-Type: application/json" \
|
|
758
|
+
-X POST \
|
|
759
|
+
"https://api.github.com/repos/$GITHUB_REPO/labels" \
|
|
760
|
+
-d '{"name": "$CLANCY_LABEL_BUILD", "color": "0075ca"}'
|
|
761
|
+
|
|
762
|
+
# Add to issue
|
|
763
|
+
curl -s \
|
|
764
|
+
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
|
765
|
+
-H "Accept: application/vnd.github+json" \
|
|
766
|
+
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
767
|
+
-H "Content-Type: application/json" \
|
|
768
|
+
-X POST \
|
|
769
|
+
"https://api.github.com/repos/$GITHUB_REPO/issues/$ISSUE_NUMBER/labels" \
|
|
770
|
+
-d '{"labels": ["$CLANCY_LABEL_BUILD"]}'
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
2. **Remove plan label:**
|
|
774
|
+
```bash
|
|
775
|
+
curl -s \
|
|
776
|
+
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
|
777
|
+
-H "Accept: application/vnd.github+json" \
|
|
778
|
+
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
779
|
+
-X DELETE \
|
|
780
|
+
"https://api.github.com/repos/$GITHUB_REPO/issues/$ISSUE_NUMBER/labels/$(echo $CLANCY_LABEL_PLAN | jq -Rr @uri)"
|
|
781
|
+
```
|
|
782
|
+
Ignore 404 (label not on issue).
|
|
783
|
+
|
|
784
|
+
### Jira
|
|
785
|
+
|
|
786
|
+
1. **Add build label** (Jira auto-creates labels):
|
|
787
|
+
|
|
788
|
+
```bash
|
|
789
|
+
# Fetch current labels
|
|
790
|
+
CURRENT_LABELS=$(curl -s \
|
|
791
|
+
-u "$JIRA_USER:$JIRA_API_TOKEN" \
|
|
792
|
+
-H "Accept: application/json" \
|
|
793
|
+
"$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY?fields=labels" | jq -r '.fields.labels')
|
|
794
|
+
|
|
795
|
+
# Add build label
|
|
796
|
+
UPDATED_LABELS=$(echo "$CURRENT_LABELS" | jq --arg build "$CLANCY_LABEL_BUILD" '. + [$build] | unique')
|
|
797
|
+
|
|
798
|
+
curl -s \
|
|
799
|
+
-u "$JIRA_USER:$JIRA_API_TOKEN" \
|
|
800
|
+
-X PUT \
|
|
801
|
+
-H "Content-Type: application/json" \
|
|
802
|
+
"$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY" \
|
|
803
|
+
-d "{\"fields\": {\"labels\": $UPDATED_LABELS}}"
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
2. **Remove plan label:**
|
|
807
|
+
|
|
808
|
+
```bash
|
|
809
|
+
# Re-fetch labels (may have changed), remove plan label
|
|
810
|
+
CURRENT_LABELS=$(curl -s \
|
|
811
|
+
-u "$JIRA_USER:$JIRA_API_TOKEN" \
|
|
812
|
+
-H "Accept: application/json" \
|
|
813
|
+
"$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY?fields=labels" | jq -r '.fields.labels')
|
|
814
|
+
|
|
815
|
+
UPDATED_LABELS=$(echo "$CURRENT_LABELS" | jq --arg plan "$CLANCY_LABEL_PLAN" '[.[] | select(. != $plan)]')
|
|
816
|
+
|
|
817
|
+
curl -s \
|
|
818
|
+
-u "$JIRA_USER:$JIRA_API_TOKEN" \
|
|
819
|
+
-X PUT \
|
|
820
|
+
-H "Content-Type: application/json" \
|
|
821
|
+
"$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY" \
|
|
822
|
+
-d "{\"fields\": {\"labels\": $UPDATED_LABELS}}"
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
3. **Status transition** (only if `CLANCY_STATUS_PLANNED` is set — skip if unset):
|
|
826
|
+
|
|
827
|
+
```bash
|
|
828
|
+
# Fetch transitions
|
|
829
|
+
curl -s \
|
|
830
|
+
-u "$JIRA_USER:$JIRA_API_TOKEN" \
|
|
831
|
+
-H "Accept: application/json" \
|
|
832
|
+
"$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/transitions"
|
|
833
|
+
|
|
834
|
+
# Find matching transition and execute
|
|
835
|
+
curl -s \
|
|
836
|
+
-u "$JIRA_USER:$JIRA_API_TOKEN" \
|
|
837
|
+
-X POST \
|
|
838
|
+
-H "Content-Type: application/json" \
|
|
839
|
+
"$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/transitions" \
|
|
840
|
+
-d '{"transition": {"id": "$TRANSITION_ID"}}'
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
On failure:
|
|
844
|
+
|
|
845
|
+
```
|
|
846
|
+
Could not transition ticket. Move it manually to your implementation queue.
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
### Linear
|
|
850
|
+
|
|
851
|
+
1. **Add build label** (ensure it exists, then add):
|
|
852
|
+
|
|
853
|
+
```graphql
|
|
854
|
+
# Ensure label exists — check team labels, workspace labels, create if missing
|
|
855
|
+
mutation {
|
|
856
|
+
issueLabelCreate(input: {
|
|
857
|
+
teamId: "$LINEAR_TEAM_ID"
|
|
858
|
+
name: "$CLANCY_LABEL_BUILD"
|
|
859
|
+
color: "#0075ca"
|
|
860
|
+
}) { success issueLabel { id } }
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
# Fetch current label IDs on the issue, add build label ID
|
|
864
|
+
mutation {
|
|
865
|
+
issueUpdate(
|
|
866
|
+
id: "$ISSUE_UUID"
|
|
867
|
+
input: { labelIds: [...currentLabelIds, buildLabelId] }
|
|
868
|
+
) { success }
|
|
869
|
+
}
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
2. **Remove plan label:**
|
|
873
|
+
|
|
874
|
+
```graphql
|
|
875
|
+
# Fetch current label IDs, filter out plan label ID
|
|
876
|
+
mutation {
|
|
877
|
+
issueUpdate(
|
|
878
|
+
id: "$ISSUE_UUID"
|
|
879
|
+
input: { labelIds: [currentLabelIds, without, planLabelId] }
|
|
880
|
+
) {
|
|
881
|
+
success
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
3. **State transition** (always):
|
|
887
|
+
|
|
888
|
+
```graphql
|
|
889
|
+
# Resolve "unstarted" state
|
|
890
|
+
query {
|
|
891
|
+
workflowStates(
|
|
892
|
+
filter: {
|
|
893
|
+
team: { id: { eq: "$LINEAR_TEAM_ID" } }
|
|
894
|
+
type: { eq: "unstarted" }
|
|
895
|
+
}
|
|
896
|
+
) {
|
|
897
|
+
nodes {
|
|
898
|
+
id
|
|
899
|
+
name
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
# Transition
|
|
905
|
+
mutation {
|
|
906
|
+
issueUpdate(id: "$ISSUE_UUID", input: { stateId: "$UNSTARTED_STATE_ID" }) {
|
|
907
|
+
success
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
If no `unstarted` state found: warn, skip transition.
|
|
913
|
+
|
|
914
|
+
### Azure DevOps
|
|
915
|
+
|
|
916
|
+
Azure DevOps uses **tags** (semicolon-delimited string field) instead of labels, and **board columns/states** for transitions.
|
|
917
|
+
|
|
918
|
+
1. **Add build tag:**
|
|
919
|
+
|
|
920
|
+
```bash
|
|
921
|
+
# Fetch current tags
|
|
922
|
+
CURRENT=$(curl -s \
|
|
923
|
+
-u ":$AZDO_PAT" \
|
|
924
|
+
-H "Accept: application/json" \
|
|
925
|
+
"https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$WORK_ITEM_ID?fields=System.Tags&api-version=7.1")
|
|
926
|
+
|
|
927
|
+
# Append build tag (semicolon-delimited)
|
|
928
|
+
# If existing tags are "clancy:plan; bug-fix", new value is "clancy:plan; bug-fix; clancy:build"
|
|
929
|
+
curl -s \
|
|
930
|
+
-u ":$AZDO_PAT" \
|
|
931
|
+
-X PATCH \
|
|
932
|
+
-H "Content-Type: application/json-patch+json" \
|
|
933
|
+
"https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$WORK_ITEM_ID?api-version=7.1" \
|
|
934
|
+
-d '[{"op": "replace", "path": "/fields/System.Tags", "value": "<existing tags>; $CLANCY_LABEL_BUILD"}]'
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
2. **Remove plan tag:**
|
|
938
|
+
|
|
939
|
+
```bash
|
|
940
|
+
# Re-fetch tags, remove plan tag from the semicolon-delimited string
|
|
941
|
+
# E.g., "clancy:plan; bug-fix; clancy:build" → "bug-fix; clancy:build"
|
|
942
|
+
curl -s \
|
|
943
|
+
-u ":$AZDO_PAT" \
|
|
944
|
+
-X PATCH \
|
|
945
|
+
-H "Content-Type: application/json-patch+json" \
|
|
946
|
+
"https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$WORK_ITEM_ID?api-version=7.1" \
|
|
947
|
+
-d '[{"op": "replace", "path": "/fields/System.Tags", "value": "<tags without plan tag>"}]'
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
3. **State transition** (only if `CLANCY_STATUS_PLANNED` is set — skip if unset):
|
|
951
|
+
|
|
952
|
+
```bash
|
|
953
|
+
curl -s \
|
|
954
|
+
-u ":$AZDO_PAT" \
|
|
955
|
+
-X PATCH \
|
|
956
|
+
-H "Content-Type: application/json-patch+json" \
|
|
957
|
+
"https://dev.azure.com/$AZDO_ORG/$AZDO_PROJECT/_apis/wit/workitems/$WORK_ITEM_ID?api-version=7.1" \
|
|
958
|
+
-d '[{"op": "replace", "path": "/fields/System.State", "value": "$CLANCY_STATUS_PLANNED"}]'
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
### Shortcut
|
|
962
|
+
|
|
963
|
+
Shortcut uses **labels** and **workflow state transitions**.
|
|
964
|
+
|
|
965
|
+
1. **Add build label:**
|
|
966
|
+
|
|
967
|
+
```bash
|
|
968
|
+
# Resolve build label ID (create if missing)
|
|
969
|
+
LABELS=$(curl -s \
|
|
970
|
+
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
|
|
971
|
+
"https://api.app.shortcut.com/api/v3/labels")
|
|
972
|
+
|
|
973
|
+
# Find or create the build label, get its ID
|
|
974
|
+
# Then add to story:
|
|
975
|
+
curl -s \
|
|
976
|
+
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
|
|
977
|
+
-H "Content-Type: application/json" \
|
|
978
|
+
-X PUT \
|
|
979
|
+
"https://api.app.shortcut.com/api/v3/stories/$STORY_ID" \
|
|
980
|
+
-d '{"labels": [{"name": "$CLANCY_LABEL_BUILD"}, ...existing_labels]}'
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
2. **Remove plan label:**
|
|
984
|
+
|
|
985
|
+
```bash
|
|
986
|
+
# Fetch current story labels, filter out plan label, update
|
|
987
|
+
curl -s \
|
|
988
|
+
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
|
|
989
|
+
-H "Content-Type: application/json" \
|
|
990
|
+
-X PUT \
|
|
991
|
+
"https://api.app.shortcut.com/api/v3/stories/$STORY_ID" \
|
|
992
|
+
-d '{"labels": [labels_without_plan_label]}'
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
3. **Workflow state transition:**
|
|
996
|
+
|
|
997
|
+
```bash
|
|
998
|
+
# Resolve the "Unstarted" or "Ready for Development" state ID from workflows
|
|
999
|
+
WORKFLOWS=$(curl -s \
|
|
1000
|
+
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
|
|
1001
|
+
"https://api.app.shortcut.com/api/v3/workflows")
|
|
1002
|
+
|
|
1003
|
+
# Find state with type "unstarted", get its ID
|
|
1004
|
+
curl -s \
|
|
1005
|
+
-H "Shortcut-Token: $SHORTCUT_API_TOKEN" \
|
|
1006
|
+
-H "Content-Type: application/json" \
|
|
1007
|
+
-X PUT \
|
|
1008
|
+
"https://api.app.shortcut.com/api/v3/stories/$STORY_ID" \
|
|
1009
|
+
-d '{"workflow_state_id": $UNSTARTED_STATE_ID}'
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
If no suitable state found: warn, skip transition.
|
|
1013
|
+
|
|
1014
|
+
### Notion
|
|
1015
|
+
|
|
1016
|
+
Notion uses **multi-select properties** for labels and **status properties** for transitions.
|
|
1017
|
+
|
|
1018
|
+
1. **Add build label** (add to multi-select property):
|
|
1019
|
+
|
|
1020
|
+
```bash
|
|
1021
|
+
# Fetch current page properties
|
|
1022
|
+
PAGE=$(curl -s \
|
|
1023
|
+
-H "Authorization: Bearer $NOTION_TOKEN" \
|
|
1024
|
+
-H "Notion-Version: 2022-06-28" \
|
|
1025
|
+
"https://api.notion.com/v1/pages/$PAGE_ID")
|
|
1026
|
+
|
|
1027
|
+
# Update multi-select property to include build label
|
|
1028
|
+
curl -s \
|
|
1029
|
+
-H "Authorization: Bearer $NOTION_TOKEN" \
|
|
1030
|
+
-H "Notion-Version: 2022-06-28" \
|
|
1031
|
+
-X PATCH \
|
|
1032
|
+
"https://api.notion.com/v1/pages/$PAGE_ID" \
|
|
1033
|
+
-d '{"properties": {"$CLANCY_NOTION_LABELS": {"multi_select": [existing_options, {"name": "$CLANCY_LABEL_BUILD"}]}}}'
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
2. **Remove plan label** (update multi-select without plan label):
|
|
1037
|
+
|
|
1038
|
+
```bash
|
|
1039
|
+
curl -s \
|
|
1040
|
+
-H "Authorization: Bearer $NOTION_TOKEN" \
|
|
1041
|
+
-H "Notion-Version: 2022-06-28" \
|
|
1042
|
+
-X PATCH \
|
|
1043
|
+
"https://api.notion.com/v1/pages/$PAGE_ID" \
|
|
1044
|
+
-d '{"properties": {"$CLANCY_NOTION_LABELS": {"multi_select": [options_without_plan_label]}}}'
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
3. **Status transition** (only if `CLANCY_STATUS_PLANNED` is set):
|
|
1048
|
+
|
|
1049
|
+
```bash
|
|
1050
|
+
curl -s \
|
|
1051
|
+
-H "Authorization: Bearer $NOTION_TOKEN" \
|
|
1052
|
+
-H "Notion-Version: 2022-06-28" \
|
|
1053
|
+
-X PATCH \
|
|
1054
|
+
"https://api.notion.com/v1/pages/$PAGE_ID" \
|
|
1055
|
+
-d '{"properties": {"$CLANCY_NOTION_STATUS": {"status": {"name": "$CLANCY_STATUS_PLANNED"}}}}'
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
On failure:
|
|
1059
|
+
|
|
1060
|
+
```
|
|
1061
|
+
Could not transition ticket. Move it manually to your implementation queue.
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
---
|
|
1065
|
+
|
|
1066
|
+
## Step 7 — Confirm and log
|
|
1067
|
+
|
|
1068
|
+
**Mode gate (read first):** if Steps 4a/4b ran (the resolved argument was a plan-file stem), skip the entire "board-specific success message" and "board-mode progress.txt entry" blocks below and jump straight to the **Local mode (Step 4a / 4b path)** subsection further down. The board-success-message text only applies when Steps 5/5b/6 ran for a board ticket key. Do NOT render both — exactly one branch executes per approval.
|
|
1069
|
+
|
|
1070
|
+
On success in **board ticket mode**, display a board-specific message:
|
|
1071
|
+
|
|
1072
|
+
**GitHub:**
|
|
1073
|
+
|
|
1074
|
+
```
|
|
1075
|
+
Plan promoted. Label swapped: {CLANCY_LABEL_PLAN} → {CLANCY_LABEL_BUILD}. Ready for /clancy:implement.
|
|
1076
|
+
|
|
1077
|
+
"Book 'em, Lou." — The ticket is ready for /clancy:implement.
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
**Jira (with transition):**
|
|
1081
|
+
|
|
1082
|
+
```
|
|
1083
|
+
Plan promoted. Ticket transitioned to {CLANCY_STATUS_PLANNED}.
|
|
1084
|
+
|
|
1085
|
+
"Book 'em, Lou." -- The ticket is ready for /clancy:implement.
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
**Jira (no transition configured):**
|
|
1089
|
+
|
|
1090
|
+
```
|
|
1091
|
+
Plan promoted. Move [{KEY}] to your implementation queue for /clancy:implement.
|
|
1092
|
+
|
|
1093
|
+
"Book 'em, Lou." -- The ticket is ready for /clancy:implement.
|
|
1094
|
+
```
|
|
1095
|
+
|
|
1096
|
+
**Linear:**
|
|
1097
|
+
|
|
1098
|
+
```
|
|
1099
|
+
Plan promoted. Moved to unstarted. Ready for /clancy:implement.
|
|
1100
|
+
|
|
1101
|
+
"Book 'em, Lou." -- The ticket is ready for /clancy:implement.
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
**Azure DevOps (with transition):**
|
|
1105
|
+
|
|
1106
|
+
```
|
|
1107
|
+
Plan promoted. Work item transitioned to {CLANCY_STATUS_PLANNED}.
|
|
1108
|
+
|
|
1109
|
+
"Book 'em, Lou." -- The ticket is ready for /clancy:implement.
|
|
1110
|
+
```
|
|
1111
|
+
|
|
1112
|
+
**Azure DevOps (no transition configured):**
|
|
1113
|
+
|
|
1114
|
+
```
|
|
1115
|
+
Plan promoted. Move work item {ID} to your implementation queue for /clancy:implement.
|
|
1116
|
+
|
|
1117
|
+
"Book 'em, Lou." -- The ticket is ready for /clancy:implement.
|
|
1118
|
+
```
|
|
1119
|
+
|
|
1120
|
+
**Shortcut:**
|
|
1121
|
+
|
|
1122
|
+
```
|
|
1123
|
+
Plan promoted. Moved to unstarted. Ready for /clancy:implement.
|
|
1124
|
+
|
|
1125
|
+
"Book 'em, Lou." -- The ticket is ready for /clancy:implement.
|
|
1126
|
+
```
|
|
1127
|
+
|
|
1128
|
+
**Notion (with transition):**
|
|
1129
|
+
|
|
1130
|
+
```
|
|
1131
|
+
Plan promoted to page content. Status updated to {CLANCY_STATUS_PLANNED}.
|
|
1132
|
+
|
|
1133
|
+
"Book 'em, Lou." -- The ticket is ready for /clancy:implement.
|
|
1134
|
+
```
|
|
1135
|
+
|
|
1136
|
+
**Notion (no transition configured):**
|
|
1137
|
+
|
|
1138
|
+
```
|
|
1139
|
+
Plan promoted to page content. Move the page to your implementation queue for /clancy:implement.
|
|
1140
|
+
|
|
1141
|
+
"Book 'em, Lou." -- The ticket is ready for /clancy:implement.
|
|
1142
|
+
```
|
|
1143
|
+
|
|
1144
|
+
Append to `.clancy/progress.txt` for **board mode**:
|
|
1145
|
+
|
|
1146
|
+
```
|
|
1147
|
+
YYYY-MM-DD HH:MM | {KEY} | APPROVE_PLAN | —
|
|
1148
|
+
```
|
|
1149
|
+
|
|
1150
|
+
On board failure:
|
|
1151
|
+
|
|
1152
|
+
```
|
|
1153
|
+
Failed to update description for [{KEY}]. Check your board permissions.
|
|
1154
|
+
```
|
|
1155
|
+
|
|
1156
|
+
### Local mode (Step 4a / 4b path)
|
|
1157
|
+
|
|
1158
|
+
For **plan-file stem mode** (Step 4a wrote a `.approved` marker), display:
|
|
1159
|
+
|
|
1160
|
+
```
|
|
1161
|
+
Clancy — Approve Plan (local)
|
|
1162
|
+
|
|
1163
|
+
✅ Approved {stem}
|
|
1164
|
+
Marker: .clancy/plans/{stem}.approved
|
|
1165
|
+
sha256: {first 12 hex chars}…
|
|
1166
|
+
|
|
1167
|
+
Next: /clancy:implement-from .clancy/plans/{stem}.md
|
|
1168
|
+
|
|
1169
|
+
"Book 'em, Lou."
|
|
1170
|
+
```
|
|
1171
|
+
|
|
1172
|
+
**Conditional Brief line:** if Step 4b actually resolved AND updated the brief marker (i.e. the `**Brief:**`/`**Row:**` headers were present, the marker regex matched, and the write succeeded), insert this line between `sha256:` and the blank line above `Next:`:
|
|
1173
|
+
|
|
1174
|
+
```
|
|
1175
|
+
Brief: .clancy/briefs/{brief}.md (row #{N} marked approved)
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
Do NOT print the Brief line when Step 4b warned and skipped (missing headers, no matching marker, or write error). In that case, also print the warning that Step 4b emitted under the success block but do not change the exit status — the plan IS approved regardless of whether the brief marker was updated.
|
|
1179
|
+
|
|
1180
|
+
Append to `.clancy/progress.txt`:
|
|
1181
|
+
|
|
1182
|
+
```
|
|
1183
|
+
YYYY-MM-DD HH:MM | {stem} | LOCAL_APPROVE_PLAN | sha256={first 12 hex}
|
|
1184
|
+
```
|
|
1185
|
+
|
|
1186
|
+
The `LOCAL_APPROVE_PLAN` token mirrors the `LOCAL_PLAN` / `LOCAL_REVISED` convention used by `/clancy:plan --from` (see [`plan.md` Step 6](./plan.md)). PR 8's `/clancy:implement-from` does NOT scan progress.txt for approval state — it reads the `.clancy/plans/{stem}.approved` marker directly. The log entry is for human audit only.
|
|
1187
|
+
|
|
1188
|
+
---
|
|
1189
|
+
|
|
1190
|
+
## Notes
|
|
1191
|
+
|
|
1192
|
+
- This command only appends -- it never overwrites the existing ticket description
|
|
1193
|
+
- If the ticket has multiple plan comments, the most recent one is used
|
|
1194
|
+
- The plan content is taken verbatim from the comment -- no regeneration
|
|
1195
|
+
- Step 3b checks for existing plans in the description to prevent accidental duplication
|
|
1196
|
+
- The ticket key is case-insensitive -- accept `PROJ-123`, `proj-123`, or `#123` (GitHub)
|
|
1197
|
+
- Step 5b edits the plan comment with an approval note -- this is best-effort and does not block the workflow
|
|
1198
|
+
- Step 6 transitions the ticket to the implementation queue -- this is best-effort and board-specific
|
|
1199
|
+
- 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)
|