@orderful/droid 0.42.1 → 0.44.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.
@@ -0,0 +1,166 @@
1
+ # Release Workflows
2
+
3
+ Detailed step-by-step procedures for each `/release` subcommand. All commands use `gh` CLI directly.
4
+
5
+ ## Common Setup (every command)
6
+
7
+ ```bash
8
+ # 1. Read config
9
+ SLACK_CHANNEL=$(droid config --get tools.release.slack_channel)
10
+ # Default to #release-management if not set
11
+ SLACK_CHANNEL="${SLACK_CHANNEL:-#release-management}"
12
+
13
+ # 2. Get repos and filter for release repos (those with release_branch set)
14
+ droid config --get repos
15
+ # Parse JSON output — release repos have release_branch defined
16
+ ```
17
+
18
+ Detect repo from cwd or ask user. Extract `release_branch`, `production_branch` (default: `master`), and the GitHub `owner/repo` slug from the repo's remote.
19
+
20
+ ```bash
21
+ # Get owner/repo from git remote
22
+ git -C {repo_path} remote get-url origin
23
+ # Parse to extract owner/repo (e.g., "Orderful/orderful-workspace")
24
+ ```
25
+
26
+ ---
27
+
28
+ ## `/release start [repo]`
29
+
30
+ Create a release PR and notify Slack.
31
+
32
+ ### Steps
33
+
34
+ 1. **Detect repo** — match cwd or ask user to pick from release repos
35
+
36
+ 2. **Check for existing release PR:**
37
+ ```bash
38
+ gh pr list --search "[RELEASE]" --state open --base {production_branch} --head {release_branch} --json number,title,url --repo {owner}/{repo}
39
+ ```
40
+ If one exists, show it and ask: "A release PR already exists. Open it instead?"
41
+
42
+ 3. **Generate release notes** — compare branches to find PRs included in this release:
43
+ ```bash
44
+ # Get commits in release_branch that aren't in production_branch
45
+ gh api repos/{owner}/{repo}/compare/{production_branch}...{release_branch} --jq '.commits[].commit.message'
46
+ ```
47
+ Extract PR numbers from commit messages (e.g. `(#1234)`) and format as a bulleted list for the PR body.
48
+
49
+ **Do NOT use** `gh pr list --base {release_branch} --state merged` — that returns ALL historically merged PRs, not just the ones since the last release.
50
+
51
+ 4. **Ask risk level** — use AskUserQuestion:
52
+ - Low Risk (routine release, no breaking changes)
53
+ - High Risk (breaking changes, data migrations, or high-traffic feature)
54
+
55
+ 5. **Create the release PR:**
56
+ ```bash
57
+ gh pr create \
58
+ --base {production_branch} \
59
+ --head {release_branch} \
60
+ --title "[RELEASE] {repo_name}" \
61
+ --label "READY" \
62
+ --body "{release_pr_body}" \
63
+ --repo {owner}/{repo}
64
+ ```
65
+ See `templates.md` for the PR body template.
66
+
67
+ 6. **Post to Slack:**
68
+ ```bash
69
+ node -e 'process.stdout.write(JSON.stringify({
70
+ channel: "{slack_channel}",
71
+ text: "{slack_message}",
72
+ unfurl_links: false
73
+ }))' | droid integrations slack post
74
+ ```
75
+ See `templates.md` for the Slack message template (release started).
76
+
77
+ 7. **Confirm to user** — show PR URL and Slack post confirmation.
78
+
79
+ ---
80
+
81
+ ## `/release merge [repo]`
82
+
83
+ Merge the release PR if all checks are green, then notify Slack.
84
+
85
+ ### Steps
86
+
87
+ 1. **Detect repo** — match cwd or ask user
88
+
89
+ 2. **Find the open release PR:**
90
+ ```bash
91
+ gh pr list --search "[RELEASE]" --state open --base {production_branch} --head {release_branch} --json number,title,url,statusCheckRollup --repo {owner}/{repo}
92
+ ```
93
+ If no open release PR found, error: "No open release PR found. Run `/release start` first."
94
+
95
+ 3. **Check all status checks:**
96
+ Parse `statusCheckRollup` from the PR data. Every check must have `conclusion: "SUCCESS"` or `state: "SUCCESS"`.
97
+
98
+ If any checks are pending: "CI checks are still running on PR #{number}. Wait for them to finish."
99
+ If any checks are failing: "CI checks are failing on PR #{number}. Fix them before merging." Show the failing checks.
100
+
101
+ 4. **Confirm with user** — use AskUserQuestion:
102
+ "All checks are green on PR #{number}. Merge `{release_branch}` → `{production_branch}`?"
103
+
104
+ 5. **Merge the PR:**
105
+ ```bash
106
+ gh pr merge {pr_number} --merge --repo {owner}/{repo}
107
+ ```
108
+
109
+ 6. **Post to Slack** — release merged notification (see `templates.md`).
110
+
111
+ 7. **Confirm to user** — show merge confirmation and Slack post confirmation.
112
+
113
+ ---
114
+
115
+ ## `/release status`
116
+
117
+ Check release status across all configured release repos.
118
+
119
+ ### Steps
120
+
121
+ 1. **Get all release repos** from config
122
+
123
+ 2. **For each release repo**, gather:
124
+
125
+ **Open release PRs:**
126
+ ```bash
127
+ gh pr list --search "[RELEASE]" --state open --json number,title,url,statusCheckRollup --repo {owner}/{repo}
128
+ ```
129
+
130
+ 3. **Format and display** as a summary:
131
+
132
+ ```
133
+ {repo_name}
134
+ Release PR: #{number} — {CI status} (green/pending/failing)
135
+ ```
136
+
137
+ If no open release PR: "No active release"
138
+ If no release repos configured: "No release repos configured"
139
+
140
+ ---
141
+
142
+ ## `/release complete [repo]`
143
+
144
+ Close out a release — notify Slack.
145
+
146
+ ### Steps
147
+
148
+ 1. **Detect repo** — match cwd or ask user
149
+
150
+ 2. **Find the release PR:**
151
+ ```bash
152
+ # Check merged first
153
+ gh pr list --search "[RELEASE]" --state merged --base {production_branch} --head {release_branch} --json number,title,url,mergedAt --limit 1 --repo {owner}/{repo}
154
+ ```
155
+ If no merged PR found, check open:
156
+ ```bash
157
+ gh pr list --search "[RELEASE]" --state open --base {production_branch} --head {release_branch} --json number,title,url --repo {owner}/{repo}
158
+ ```
159
+ - If PR is still open (not merged): warn "Release PR #{number} is still open. Merge it first, or complete anyway?"
160
+ - If no PR found at all: warn "No release PR found. Post completion anyway?"
161
+
162
+ 3. **Post to Slack** — release complete notification (see `templates.md`).
163
+
164
+ 4. **Confirm to user:**
165
+ - "Release complete for `{repo_name}`"
166
+ - Show Slack message confirmation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orderful/droid",
3
- "version": "0.42.1",
3
+ "version": "0.44.0",
4
4
  "description": "AI workflow toolkit for sharing skills, commands, and agents across the team",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,6 +40,10 @@ export async function reposListCommand(options?: ReposListOptions): Promise<void
40
40
  if (repo.description) {
41
41
  console.log(chalk.gray(` ${repo.description}`));
42
42
  }
43
+ if (repo.release_branch) {
44
+ const prod = repo.production_branch || 'master';
45
+ console.log(chalk.gray(` Release: ${repo.release_branch} → ${prod}`));
46
+ }
43
47
  console.log();
44
48
  }
45
49
  }
@@ -93,6 +97,29 @@ export async function reposAddCommand(
93
97
  repoDescription = answers.description || undefined;
94
98
  }
95
99
 
100
+ // Optional release fields
101
+ const releaseAnswers = await inquirer.prompt([
102
+ {
103
+ type: 'input',
104
+ name: 'release_branch',
105
+ message: 'Release branch (optional, e.g. "dev"):',
106
+ },
107
+ ]);
108
+ const releaseBranch = releaseAnswers.release_branch || undefined;
109
+
110
+ let productionBranch: string | undefined;
111
+ if (releaseBranch) {
112
+ const prodAnswers = await inquirer.prompt([
113
+ {
114
+ type: 'input',
115
+ name: 'production_branch',
116
+ message: 'Production branch:',
117
+ default: 'master',
118
+ },
119
+ ]);
120
+ productionBranch = prodAnswers.production_branch || 'master';
121
+ }
122
+
96
123
  // At this point, repoName and repoPath should be defined
97
124
  if (!repoName || !repoPath) {
98
125
  console.error(chalk.red('Name and path are required'));
@@ -103,6 +130,8 @@ export async function reposAddCommand(
103
130
  name: repoName,
104
131
  path: repoPath,
105
132
  description: repoDescription,
133
+ ...(releaseBranch && { release_branch: releaseBranch }),
134
+ ...(productionBranch && { production_branch: productionBranch }),
106
135
  };
107
136
 
108
137
  addRepo(repo);
@@ -10,7 +10,15 @@ export interface ReposManagementScreenProps {
10
10
  onCancel: () => void;
11
11
  }
12
12
 
13
- type Screen = 'list' | 'add-name' | 'add-path' | 'add-desc' | 'confirm-delete';
13
+ type Screen =
14
+ | 'list'
15
+ | 'add-name'
16
+ | 'add-path'
17
+ | 'add-desc'
18
+ | 'repo-actions'
19
+ | 'edit-release-branch'
20
+ | 'edit-production-branch'
21
+ | 'confirm-delete';
14
22
 
15
23
  export function ReposManagementScreen({ onComplete: _onComplete, onCancel }: ReposManagementScreenProps) {
16
24
  const [repos, setRepos] = useState<RepoConfig[]>(() => getRepos());
@@ -23,8 +31,11 @@ export function ReposManagementScreen({ onComplete: _onComplete, onCancel }: Rep
23
31
  const [newRepoPath, setNewRepoPath] = useState('');
24
32
  const [newRepoDesc, setNewRepoDesc] = useState('');
25
33
 
26
- // Delete repo state
27
- const [repoToDelete, setRepoToDelete] = useState<string | null>(null);
34
+ // Edit/delete repo state
35
+ const [activeRepo, setActiveRepo] = useState<RepoConfig | null>(null);
36
+ const [actionIndex, setActionIndex] = useState(0);
37
+ const [editReleaseBranch, setEditReleaseBranch] = useState('');
38
+ const [editProductionBranch, setEditProductionBranch] = useState('');
28
39
 
29
40
  const handleAddRepo = () => {
30
41
  if (!newRepoName || !newRepoPath) {
@@ -53,7 +64,7 @@ export function ReposManagementScreen({ onComplete: _onComplete, onCancel }: Rep
53
64
  removeRepo(repoName);
54
65
  setRepos(getRepos());
55
66
  setMessage({ text: `Removed ${repoName}`, type: 'success' });
56
- setRepoToDelete(null);
67
+ setActiveRepo(null);
57
68
  setScreen('list');
58
69
 
59
70
  // Adjust selected index if needed
@@ -62,6 +73,25 @@ export function ReposManagementScreen({ onComplete: _onComplete, onCancel }: Rep
62
73
  }
63
74
  };
64
75
 
76
+ const handleSaveReleaseBranches = () => {
77
+ if (!activeRepo) return;
78
+
79
+ const updated: RepoConfig = {
80
+ ...activeRepo,
81
+ release_branch: editReleaseBranch || undefined,
82
+ production_branch: editReleaseBranch ? (editProductionBranch || 'master') : undefined,
83
+ };
84
+
85
+ addRepo(updated);
86
+ setRepos(getRepos());
87
+ const label = editReleaseBranch
88
+ ? `Set release: ${editReleaseBranch} → ${editProductionBranch || 'master'}`
89
+ : 'Cleared release branches';
90
+ setMessage({ text: `${activeRepo.name}: ${label}`, type: 'success' });
91
+ setActiveRepo(null);
92
+ setScreen('list');
93
+ };
94
+
65
95
  // List screen navigation
66
96
  useInput((input, key) => {
67
97
  if (message) setMessage(null);
@@ -86,13 +116,42 @@ export function ReposManagementScreen({ onComplete: _onComplete, onCancel }: Rep
86
116
  // "Add New Repo" selected
87
117
  setScreen('add-name');
88
118
  } else if (selectedIndex < repos.length) {
89
- // Repo selected - prompt to delete
90
- setRepoToDelete(repos[selectedIndex].name);
91
- setScreen('confirm-delete');
119
+ // Repo selected - show action menu
120
+ setActiveRepo(repos[selectedIndex]);
121
+ setActionIndex(0);
122
+ setScreen('repo-actions');
92
123
  }
93
124
  }
94
125
  }, { isActive: screen === 'list' });
95
126
 
127
+ // Repo action menu
128
+ useInput((input, key) => {
129
+ if (key.escape) {
130
+ setActiveRepo(null);
131
+ setScreen('list');
132
+ return;
133
+ }
134
+
135
+ if (key.upArrow) {
136
+ setActionIndex((prev) => Math.max(0, prev - 1));
137
+ }
138
+ if (key.downArrow) {
139
+ setActionIndex((prev) => Math.min(1, prev + 1));
140
+ }
141
+
142
+ if (key.return && activeRepo) {
143
+ if (actionIndex === 0) {
144
+ // Edit release branches
145
+ setEditReleaseBranch(activeRepo.release_branch || '');
146
+ setEditProductionBranch(activeRepo.production_branch || '');
147
+ setScreen('edit-release-branch');
148
+ } else {
149
+ // Delete
150
+ setScreen('confirm-delete');
151
+ }
152
+ }
153
+ }, { isActive: screen === 'repo-actions' });
154
+
96
155
  // Add name input
97
156
  useInput((input, key) => {
98
157
  if (key.escape) {
@@ -117,14 +176,27 @@ export function ReposManagementScreen({ onComplete: _onComplete, onCancel }: Rep
117
176
  }
118
177
  }, { isActive: screen === 'add-desc' });
119
178
 
179
+ // Edit release branch input
180
+ useInput((input, key) => {
181
+ if (key.escape) {
182
+ setScreen('repo-actions');
183
+ }
184
+ }, { isActive: screen === 'edit-release-branch' });
185
+
186
+ // Edit production branch input
187
+ useInput((input, key) => {
188
+ if (key.escape) {
189
+ setScreen('edit-release-branch');
190
+ }
191
+ }, { isActive: screen === 'edit-production-branch' });
192
+
120
193
  // Confirm delete
121
194
  useInput((input, key) => {
122
195
  if (key.escape || input === 'n') {
123
- setRepoToDelete(null);
124
- setScreen('list');
196
+ setScreen('repo-actions');
125
197
  }
126
- if (input === 'y' && repoToDelete) {
127
- handleDeleteRepo(repoToDelete);
198
+ if (input === 'y' && activeRepo) {
199
+ handleDeleteRepo(activeRepo.name);
128
200
  }
129
201
  }, { isActive: screen === 'confirm-delete' });
130
202
 
@@ -206,14 +278,98 @@ export function ReposManagementScreen({ onComplete: _onComplete, onCancel }: Rep
206
278
  );
207
279
  }
208
280
 
209
- if (screen === 'confirm-delete' && repoToDelete) {
210
- const repo = repos.find((r) => r.name === repoToDelete);
281
+ if (screen === 'repo-actions' && activeRepo) {
282
+ const actions = ['Edit release branches', 'Remove repository'];
283
+ return (
284
+ <Box flexDirection="column" padding={2}>
285
+ <Text color={colors.text} bold>{activeRepo.name}</Text>
286
+ <Box marginTop={0}>
287
+ <Text color={colors.textDim}>{activeRepo.path}</Text>
288
+ </Box>
289
+ {activeRepo.release_branch && (
290
+ <Box marginTop={0}>
291
+ <Text color={colors.textDim}>Release: {activeRepo.release_branch} → {activeRepo.production_branch || 'master'}</Text>
292
+ </Box>
293
+ )}
294
+
295
+ <Box flexDirection="column" marginTop={1}>
296
+ {actions.map((action, index) => (
297
+ <Box key={action}>
298
+ <Text
299
+ color={actionIndex === index ? colors.primary : (index === 1 ? colors.error : colors.text)}
300
+ bold={actionIndex === index}
301
+ >
302
+ {actionIndex === index ? '> ' : ' '}
303
+ {action}
304
+ </Text>
305
+ </Box>
306
+ ))}
307
+ </Box>
308
+
309
+ <Box marginTop={2}>
310
+ <Text color={colors.textDim}>↑↓ navigate · enter select · esc back</Text>
311
+ </Box>
312
+ </Box>
313
+ );
314
+ }
315
+
316
+ if (screen === 'edit-release-branch' && activeRepo) {
317
+ return (
318
+ <Box flexDirection="column" padding={2}>
319
+ <Text color={colors.text} bold>Edit Release Branches — {activeRepo.name}</Text>
320
+ <Box marginTop={1}>
321
+ <Text color={colors.textDim}>Release branch (e.g. &quot;dev&quot;, blank to clear): </Text>
322
+ <TextInput
323
+ value={editReleaseBranch}
324
+ onChange={setEditReleaseBranch}
325
+ onSubmit={() => {
326
+ if (editReleaseBranch) {
327
+ setScreen('edit-production-branch');
328
+ } else {
329
+ handleSaveReleaseBranches();
330
+ }
331
+ }}
332
+ placeholder={activeRepo.release_branch || 'dev'}
333
+ />
334
+ </Box>
335
+ <Box marginTop={1}>
336
+ <Text color={colors.textDim}>enter {editReleaseBranch ? 'next' : 'clear & save'} · esc back</Text>
337
+ </Box>
338
+ </Box>
339
+ );
340
+ }
341
+
342
+ if (screen === 'edit-production-branch' && activeRepo) {
343
+ return (
344
+ <Box flexDirection="column" padding={2}>
345
+ <Text color={colors.text} bold>Edit Release Branches — {activeRepo.name}</Text>
346
+ <Box marginTop={1}>
347
+ <Text color={colors.textDim}>Release branch: </Text>
348
+ <Text color={colors.text}>{editReleaseBranch}</Text>
349
+ </Box>
350
+ <Box marginTop={1}>
351
+ <Text color={colors.textDim}>Production branch: </Text>
352
+ <TextInput
353
+ value={editProductionBranch}
354
+ onChange={setEditProductionBranch}
355
+ onSubmit={handleSaveReleaseBranches}
356
+ placeholder="master"
357
+ />
358
+ </Box>
359
+ <Box marginTop={1}>
360
+ <Text color={colors.textDim}>enter save · esc back</Text>
361
+ </Box>
362
+ </Box>
363
+ );
364
+ }
365
+
366
+ if (screen === 'confirm-delete' && activeRepo) {
211
367
  return (
212
368
  <Box flexDirection="column" padding={2}>
213
369
  <Text color={colors.text} bold>Remove Repository</Text>
214
370
  <Box marginTop={1} flexDirection="column">
215
- <Text color={colors.text}>{repo?.name}</Text>
216
- <Text color={colors.textDim}>{repo?.path}</Text>
371
+ <Text color={colors.text}>{activeRepo.name}</Text>
372
+ <Text color={colors.textDim}>{activeRepo.path}</Text>
217
373
  </Box>
218
374
  <Box marginTop={2}>
219
375
  <Text color={colors.error}>Remove this repository from the registry?</Text>
@@ -263,6 +419,11 @@ export function ReposManagementScreen({ onComplete: _onComplete, onCancel }: Rep
263
419
  <Text color={colors.textDim}>{repo.description}</Text>
264
420
  </Box>
265
421
  )}
422
+ {repo.release_branch && (
423
+ <Box paddingLeft={2}>
424
+ <Text color={colors.textDim}>Release: {repo.release_branch} → {repo.production_branch || 'master'}</Text>
425
+ </Box>
426
+ )}
266
427
  </Box>
267
428
  ))
268
429
  )}
@@ -282,7 +443,7 @@ export function ReposManagementScreen({ onComplete: _onComplete, onCancel }: Rep
282
443
  <Box marginTop={2}>
283
444
  <Text color={colors.textDim}>
284
445
  {repos.length > 0 && selectedIndex < repos.length
285
- ? 'enter remove · esc back'
446
+ ? 'enter edit · esc back'
286
447
  : '↑↓ navigate · enter select · esc back'}
287
448
  </Text>
288
449
  </Box>
@@ -36,6 +36,11 @@ export function ReposViewerScreen({ onClose }: ReposViewerScreenProps) {
36
36
  <Text color={colors.textDim}>{repo.description}</Text>
37
37
  </Box>
38
38
  )}
39
+ {repo.release_branch && (
40
+ <Box marginTop={0}>
41
+ <Text color={colors.textDim}>Release: {repo.release_branch} → {repo.production_branch || 'master'}</Text>
42
+ </Box>
43
+ )}
39
44
  </Box>
40
45
  ))}
41
46
  </Box>
package/src/lib/types.ts CHANGED
@@ -53,6 +53,8 @@ export interface RepoConfig {
53
53
  name: string;
54
54
  path: string;
55
55
  description?: string;
56
+ release_branch?: string; // e.g., "dev" — source branch for releases
57
+ production_branch?: string; // e.g., "master" — target branch for releases
56
58
  }
57
59
 
58
60
  // ToolConfig is flexible - each tool defines its own keys
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "droid-release",
3
+ "version": "0.1.0",
4
+ "description": "Release ceremony automation — create release PRs, check status, notify Slack.",
5
+ "author": {
6
+ "name": "Orderful",
7
+ "url": "https://github.com/orderful"
8
+ },
9
+ "repository": "https://github.com/orderful/droid",
10
+ "license": "MIT",
11
+ "keywords": [
12
+ "droid",
13
+ "ai",
14
+ "release"
15
+ ],
16
+ "skills": [
17
+ "./skills/release/SKILL.md"
18
+ ],
19
+ "commands": [
20
+ "./commands/release.md"
21
+ ]
22
+ }
@@ -0,0 +1,21 @@
1
+ name: release
2
+ description: "Release ceremony automation — create release PRs, check status, notify Slack."
3
+ version: 0.1.0
4
+ status: alpha
5
+
6
+ includes:
7
+ skills:
8
+ - name: release
9
+ required: true
10
+ commands:
11
+ - name: release
12
+ is_alias: false
13
+ agents: []
14
+
15
+ dependencies: []
16
+
17
+ config_schema:
18
+ slack_channel:
19
+ type: string
20
+ description: "Slack channel for release notifications"
21
+ default: "#release-management"
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: release
3
+ description: "Release ceremony automation"
4
+ argument-hint: "[start [repo] | status | complete [repo]]"
5
+ ---
6
+
7
+ # /release
8
+
9
+ **User invoked:** `/release $ARGUMENTS`
10
+
11
+ **Your task:** Invoke the **release skill** with these arguments.
12
+
13
+ ## Examples
14
+
15
+ - `/release start` → Create release PR for current repo
16
+ - `/release start orderful-workspace` → Create release PR for a specific repo
17
+ - `/release status` → Check CI state across release repos
18
+ - `/release complete` → Post "release complete" to Slack
19
+
20
+ ## Quick Reference
21
+
22
+ ```
23
+ /release start [repo] # Create release PR + notify Slack
24
+ /release status # CI state across repos
25
+ /release complete [repo] # Notify Slack, close out release
26
+ ```
27
+
28
+ See the **release skill** for complete documentation.
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: release
3
+ description: "Release ceremony automation for dev-to-master releases. Use when starting a release, merging a release, checking release status, or completing a release. User prompts like 'start a release', 'merge the release', 'release status', 'release complete'."
4
+ argument-hint: "[start [repo] | merge [repo] | status | complete [repo]]"
5
+ allowed-tools: [Read, Write, Glob, Grep, Bash, Edit]
6
+ ---
7
+
8
+ # Release Skill
9
+
10
+ Automate dev → master release ceremonies: create release PRs, notify Slack, and track status.
11
+
12
+ ## When to Use
13
+
14
+ - User wants to start a release (create PR from dev → master)
15
+ - User asks about release status (open PRs, CI checks)
16
+ - User says "release complete" or wants to close out a release
17
+ - Natural language like "start a release", "what's the release status?"
18
+
19
+ ## When NOT to Use
20
+
21
+ - Deploying to environments (this is about merging dev → master, not deployment)
22
+ - Feature branch management (this is for release branches only)
23
+ - Hotfixes or cherry-picks (manual process)
24
+
25
+ ## Prerequisites
26
+
27
+ 1. **`gh` CLI** — authenticated with access to target repos
28
+ 2. **Slack integration** — `droid integrations slack post` configured (optional, falls back to terminal)
29
+
30
+ ## Configuration
31
+
32
+ Read config at the start of every command:
33
+
34
+ ```bash
35
+ droid config --get tools.release
36
+ droid config --get repos
37
+ ```
38
+
39
+ - **Repos:** Filter `repos` array for entries with `release_branch` set — these are release repos
40
+ - **Slack channel:** From `tools.release.slack_channel` (default: `#release-management`)
41
+ - **Repo detection:** Match cwd against repo paths, or ask user if ambiguous
42
+
43
+ If no repos have `release_branch` set, tell user:
44
+ > "No release repos configured. Run `droid repos add` and set a release branch to get started."
45
+
46
+ ## Commands
47
+
48
+ | Command | Action |
49
+ |---------|--------|
50
+ | `/release start [repo]` | Create release PR + notify Slack |
51
+ | `/release merge [repo]` | Merge release PR (only if checks pass) + notify Slack |
52
+ | `/release status` | Check open release PRs + CI state |
53
+ | `/release complete [repo]` | Post completion to Slack |
54
+
55
+ See `references/workflows.md` for detailed step-by-step procedures and exact `gh` commands.
56
+
57
+ See `references/templates.md` for Slack message and PR body templates.
58
+
59
+ ## Repo Detection
60
+
61
+ 1. Get cwd and match against configured repo paths
62
+ 2. If match found and repo has `release_branch`, use it
63
+ 3. If no match or multiple release repos, ask user to pick
64
+ 4. Use `release_branch` as source and `production_branch` (default: `master`) as target
65
+
66
+ ## Error Handling
67
+
68
+ | Error | Action |
69
+ |-------|--------|
70
+ | No release repos configured | Suggest `droid repos add` with release branch |
71
+ | `gh` CLI not authenticated | Suggest `gh auth login` |
72
+ | Slack not configured | Print message to terminal, suggest `droid integrations setup slack` |
73
+ | Release PR already exists | Show existing PR, ask if user wants to proceed |
74
+ | CI checks failing | Show status, do not auto-merge |