chief-clancy 0.1.4 → 0.2.0-beta.1

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Autonomous, board-driven development for Claude Code.**
4
4
 
5
- [![npm](https://img.shields.io/npm/v/chief-clancy?color=cb3837)](https://www.npmjs.com/package/chief-clancy) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE) [![Tests](https://img.shields.io/badge/tests-34%20passing-brightgreen)](./test/) [![GitHub Stars](https://img.shields.io/github/stars/Pushedskydiver/clancy?style=flat)](https://github.com/Pushedskydiver/clancy/stargazers)
5
+ [![npm](https://img.shields.io/npm/v/chief-clancy?color=cb3837)](https://www.npmjs.com/package/chief-clancy) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE) [![Tests](https://img.shields.io/badge/tests-55%20passing-brightgreen)](./test/) [![GitHub Stars](https://img.shields.io/github/stars/Pushedskydiver/clancy?style=flat)](https://github.com/Pushedskydiver/clancy/stargazers)
6
6
 
7
7
  ```bash
8
8
  npx chief-clancy
@@ -130,10 +130,13 @@ npx chief-clancy
130
130
  # 3. Scan your codebase (or say yes during init)
131
131
  /clancy:map-codebase
132
132
 
133
- # 4. Watch your first ticket
133
+ # 4. Preview the first ticket (no changes made)
134
+ /clancy:dry-run
135
+
136
+ # 5. Watch your first ticket
134
137
  /clancy:once
135
138
 
136
- # 5. Go AFK
139
+ # 6. Go AFK
137
140
  /clancy:run
138
141
  ```
139
142
 
@@ -147,6 +150,7 @@ npx chief-clancy
147
150
  | `/clancy:run` | Loop mode — processes tickets until queue is empty or MAX_ITERATIONS hit |
148
151
  | `/clancy:run 20` | Same, override MAX_ITERATIONS to 20 for this session |
149
152
  | `/clancy:once` | Pick up one ticket and stop |
153
+ | `/clancy:dry-run` | Preview next ticket without making changes — no git ops, no Claude call |
150
154
  | `/clancy:status` | Show next tickets without running — read-only |
151
155
  | `/clancy:review` | Score next ticket (0–100%) with actionable recommendations |
152
156
  | `/clancy:logs` | Format and display `.clancy/progress.txt` |
@@ -215,6 +219,15 @@ PLAYWRIGHT_STARTUP_WAIT=15
215
219
 
216
220
  After implementing a UI ticket, Clancy starts the dev server or Storybook, screenshots, assesses visually, checks the console, and fixes anything wrong before committing.
217
221
 
222
+ ### Status transitions
223
+
224
+ ```
225
+ CLANCY_STATUS_IN_PROGRESS="In Progress"
226
+ CLANCY_STATUS_DONE="Done"
227
+ ```
228
+
229
+ Clancy automatically moves tickets through your board when it picks up and completes them. Set these to the exact column name shown in your Jira or Linear board. Best-effort — a failed transition never stops the run. Configurable via `/clancy:settings`.
230
+
218
231
  ### Notifications
219
232
 
220
233
  ```
package/bin/install.js CHANGED
@@ -8,6 +8,7 @@ const readline = require('readline');
8
8
  const PKG = require('../package.json');
9
9
  const COMMANDS_SRC = path.join(__dirname, '..', 'src', 'commands');
10
10
  const WORKFLOWS_SRC = path.join(__dirname, '..', 'src', 'workflows');
11
+ const HOOKS_SRC = path.join(__dirname, '..', 'hooks');
11
12
 
12
13
  const homeDir = process.env.HOME || process.env.USERPROFILE;
13
14
  if (!homeDir) {
@@ -149,6 +150,66 @@ async function main() {
149
150
  // Write VERSION file so /clancy:doctor and /clancy:update can read the installed version
150
151
  fs.writeFileSync(path.join(dest, 'VERSION'), PKG.version);
151
152
 
153
+ // Install hooks and register them in Claude settings.json
154
+ const claudeConfigDir = dest === GLOBAL_DEST
155
+ ? path.join(homeDir, '.claude')
156
+ : path.join(process.cwd(), '.claude');
157
+ const hooksInstallDir = path.join(claudeConfigDir, 'hooks');
158
+ const settingsFile = path.join(claudeConfigDir, 'settings.json');
159
+
160
+ const hookFiles = [
161
+ 'clancy-check-update.js',
162
+ 'clancy-statusline.js',
163
+ 'clancy-context-monitor.js',
164
+ ];
165
+
166
+ try {
167
+ fs.mkdirSync(hooksInstallDir, { recursive: true });
168
+ for (const f of hookFiles) {
169
+ fs.copyFileSync(path.join(HOOKS_SRC, f), path.join(hooksInstallDir, f));
170
+ }
171
+ // Force CommonJS resolution for hook files — projects with "type":"module"
172
+ // in their package.json would otherwise treat .js files as ESM, breaking require().
173
+ fs.writeFileSync(
174
+ path.join(hooksInstallDir, 'package.json'),
175
+ JSON.stringify({ type: 'commonjs' }, null, 2) + '\n'
176
+ );
177
+
178
+ // Merge hooks into settings.json without clobbering existing config
179
+ let settings = {};
180
+ if (fs.existsSync(settingsFile)) {
181
+ try { settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8')); } catch {}
182
+ }
183
+ if (!settings.hooks) settings.hooks = {};
184
+
185
+ // Helper: add a hook command to an event array if not already present
186
+ function registerHook(event, command) {
187
+ if (!settings.hooks[event]) settings.hooks[event] = [];
188
+ const already = settings.hooks[event].some(
189
+ h => h.hooks && h.hooks.some(hh => hh.command === command)
190
+ );
191
+ if (!already) {
192
+ settings.hooks[event].push({ hooks: [{ type: 'command', command }] });
193
+ }
194
+ }
195
+
196
+ const updateScript = path.join(hooksInstallDir, 'clancy-check-update.js');
197
+ const statuslineScript = path.join(hooksInstallDir, 'clancy-statusline.js');
198
+ const monitorScript = path.join(hooksInstallDir, 'clancy-context-monitor.js');
199
+
200
+ registerHook('SessionStart', `node ${updateScript}`);
201
+ registerHook('PostToolUse', `node ${monitorScript}`);
202
+
203
+ // Statusline: registered as top-level key, not inside hooks
204
+ if (!settings.statusLine) {
205
+ settings.statusLine = { type: 'command', command: `node ${statuslineScript}` };
206
+ }
207
+
208
+ fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
209
+ } catch {
210
+ // Hook registration is best-effort — don't fail the install over it
211
+ }
212
+
152
213
  console.log('');
153
214
  console.log(green(' ✓ Clancy installed successfully.'));
154
215
  console.log('');
@@ -163,6 +224,7 @@ async function main() {
163
224
  ['/clancy:map-codebase', 'Scan codebase with 5 parallel agents'],
164
225
  ['/clancy:run', 'Run Clancy in loop mode'],
165
226
  ['/clancy:once', 'Pick up one ticket and stop'],
227
+ ['/clancy:dry-run', 'Preview next ticket without making changes'],
166
228
  ['/clancy:status', 'Show next tickets without running'],
167
229
  ['/clancy:review', 'Score next ticket and get recommendations'],
168
230
  ['/clancy:logs', 'Display progress log'],
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ // SessionStart hook: check for Clancy updates in the background.
3
+ // Spawns a detached child process to hit npm, writes result to cache.
4
+ // Claude reads the cache via CLAUDE.md instruction — no output from this script.
5
+
6
+ 'use strict';
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+ const { spawn } = require('child_process');
12
+
13
+ const homeDir = os.homedir();
14
+ const cwd = process.cwd();
15
+
16
+ // Resolve the Clancy install dir (local takes priority over global).
17
+ function findInstallDir() {
18
+ const localVersion = path.join(cwd, '.claude', 'commands', 'clancy', 'VERSION');
19
+ const globalVersion = path.join(homeDir, '.claude', 'commands', 'clancy', 'VERSION');
20
+ if (fs.existsSync(localVersion)) return path.dirname(localVersion);
21
+ if (fs.existsSync(globalVersion)) return path.dirname(globalVersion);
22
+ return null;
23
+ }
24
+
25
+ const installDir = findInstallDir();
26
+ if (!installDir) process.exit(0); // Clancy not installed — nothing to check
27
+
28
+ const cacheDir = path.join(homeDir, '.claude', 'cache');
29
+ const cacheFile = path.join(cacheDir, 'clancy-update-check.json');
30
+ const versionFile = path.join(installDir, 'VERSION');
31
+
32
+ if (!fs.existsSync(cacheDir)) {
33
+ try { fs.mkdirSync(cacheDir, { recursive: true }); } catch { process.exit(0); }
34
+ }
35
+
36
+ // Spawn a detached background process to do the actual npm check.
37
+ // This script returns immediately so it never delays session start.
38
+ const child = spawn(process.execPath, ['-e', `
39
+ const fs = require('fs');
40
+ const { execSync } = require('child_process');
41
+
42
+ const cacheFile = ${JSON.stringify(cacheFile)};
43
+ const versionFile = ${JSON.stringify(versionFile)};
44
+
45
+ let installed = '0.0.0';
46
+ try { installed = fs.readFileSync(versionFile, 'utf8').trim(); } catch {}
47
+
48
+ let latest = null;
49
+ try {
50
+ latest = execSync('npm view chief-clancy version', {
51
+ encoding: 'utf8',
52
+ timeout: 10000,
53
+ windowsHide: true,
54
+ }).trim();
55
+ } catch {}
56
+
57
+ const result = {
58
+ update_available: Boolean(latest && installed !== latest),
59
+ installed,
60
+ latest: latest || 'unknown',
61
+ checked: Math.floor(Date.now() / 1000),
62
+ };
63
+
64
+ try { fs.writeFileSync(cacheFile, JSON.stringify(result)); } catch {}
65
+ `], {
66
+ stdio: 'ignore',
67
+ windowsHide: true,
68
+ detached: true,
69
+ });
70
+
71
+ child.unref();
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ // Clancy Context Monitor — PostToolUse hook.
3
+ // Reads context metrics from the bridge file written by clancy-statusline.js
4
+ // and injects warnings into Claude's conversation when context runs low.
5
+ //
6
+ // Thresholds:
7
+ // WARNING (remaining <= 35%): wrap up analysis, move to implementation
8
+ // CRITICAL (remaining <= 25%): commit current work, log to .clancy/progress.txt, stop
9
+ //
10
+ // Debounce: 5 tool uses between warnings; severity escalation bypasses debounce.
11
+
12
+ 'use strict';
13
+
14
+ const fs = require('fs');
15
+ const os = require('os');
16
+ const path = require('path');
17
+
18
+ const WARNING_THRESHOLD = 35;
19
+ const CRITICAL_THRESHOLD = 25;
20
+ const STALE_SECONDS = 60;
21
+ const DEBOUNCE_CALLS = 5;
22
+
23
+ let input = '';
24
+ const stdinTimeout = setTimeout(() => process.exit(0), 3000);
25
+ process.stdin.setEncoding('utf8');
26
+ process.stdin.on('data', chunk => { input += chunk; });
27
+ process.stdin.on('end', () => {
28
+ clearTimeout(stdinTimeout);
29
+ try {
30
+ const data = JSON.parse(input);
31
+ const session = data.session_id;
32
+ if (!session) process.exit(0);
33
+
34
+ const bridgePath = path.join(os.tmpdir(), `clancy-ctx-${session}.json`);
35
+ if (!fs.existsSync(bridgePath)) process.exit(0); // no statusline data yet
36
+
37
+ const metrics = JSON.parse(fs.readFileSync(bridgePath, 'utf8'));
38
+ const now = Math.floor(Date.now() / 1000);
39
+
40
+ // Ignore stale metrics (statusline may not have run this session)
41
+ if (metrics.timestamp && (now - metrics.timestamp) > STALE_SECONDS) process.exit(0);
42
+
43
+ const remaining = metrics.remaining_percentage;
44
+ const usedPct = metrics.used_pct;
45
+
46
+ if (remaining > WARNING_THRESHOLD) process.exit(0);
47
+
48
+ // Debounce
49
+ const warnPath = path.join(os.tmpdir(), `clancy-ctx-${session}-warned.json`);
50
+ let warnData = { callsSinceWarn: 0, lastLevel: null };
51
+ let firstWarn = true;
52
+
53
+ if (fs.existsSync(warnPath)) {
54
+ try { warnData = JSON.parse(fs.readFileSync(warnPath, 'utf8')); firstWarn = false; } catch {}
55
+ }
56
+
57
+ warnData.callsSinceWarn = (warnData.callsSinceWarn || 0) + 1;
58
+
59
+ const isCritical = remaining <= CRITICAL_THRESHOLD;
60
+ const currentLevel = isCritical ? 'critical' : 'warning';
61
+ const severityEscalated = currentLevel === 'critical' && warnData.lastLevel === 'warning';
62
+
63
+ if (!firstWarn && warnData.callsSinceWarn < DEBOUNCE_CALLS && !severityEscalated) {
64
+ fs.writeFileSync(warnPath, JSON.stringify(warnData));
65
+ process.exit(0);
66
+ }
67
+
68
+ warnData.callsSinceWarn = 0;
69
+ warnData.lastLevel = currentLevel;
70
+ fs.writeFileSync(warnPath, JSON.stringify(warnData));
71
+
72
+ let message;
73
+ if (isCritical) {
74
+ message =
75
+ `CONTEXT CRITICAL: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
76
+ 'Context is nearly exhausted. Stop reading files and wrap up immediately:\n' +
77
+ '1. Commit whatever work is staged on the current feature branch\n' +
78
+ '2. Append a WIP entry to .clancy/progress.txt: ' +
79
+ 'YYYY-MM-DD HH:MM | TICKET-KEY | Summary | WIP — context exhausted\n' +
80
+ '3. Inform the user what was completed and what remains.\n' +
81
+ 'Do NOT start any new work.';
82
+ } else {
83
+ message =
84
+ `CONTEXT WARNING: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
85
+ 'Context is getting limited. Stop exploring and move to implementation. ' +
86
+ 'Avoid reading additional files unless strictly necessary. ' +
87
+ 'Commit completed work as soon as it is ready.';
88
+ }
89
+
90
+ const output = {
91
+ hookSpecificOutput: {
92
+ hookEventName: 'PostToolUse',
93
+ additionalContext: message,
94
+ },
95
+ };
96
+
97
+ process.stdout.write(JSON.stringify(output));
98
+ } catch {
99
+ process.exit(0);
100
+ }
101
+ });
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ // Clancy Statusline hook — registered as the Claude Code statusline.
3
+ // Two jobs:
4
+ // 1. Write context metrics to a bridge file so the PostToolUse context
5
+ // monitor can read them (the statusline is the only hook that receives
6
+ // context_window data directly).
7
+ // 2. Output a statusline string showing context usage and update status.
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const os = require('os');
14
+
15
+ const homeDir = os.homedir();
16
+
17
+ let input = '';
18
+ const stdinTimeout = setTimeout(() => process.exit(0), 3000);
19
+ process.stdin.setEncoding('utf8');
20
+ process.stdin.on('data', chunk => { input += chunk; });
21
+ process.stdin.on('end', () => {
22
+ clearTimeout(stdinTimeout);
23
+ try {
24
+ const data = JSON.parse(input);
25
+ const session = data.session_id || '';
26
+ const remaining = data.context_window?.remaining_percentage;
27
+
28
+ // Write bridge file for the context monitor PostToolUse hook
29
+ if (session && remaining != null) {
30
+ try {
31
+ const bridgePath = path.join(os.tmpdir(), `clancy-ctx-${session}.json`);
32
+ // Claude Code reserves ~16.5% for autocompact buffer.
33
+ // Normalise to show 100% at the usable limit (same as GSD).
34
+ const AUTO_COMPACT_BUFFER_PCT = 16.5;
35
+ const usableRemaining = Math.max(0, ((remaining - AUTO_COMPACT_BUFFER_PCT) / (100 - AUTO_COMPACT_BUFFER_PCT)) * 100);
36
+ const used = Math.max(0, Math.min(100, Math.round(100 - usableRemaining)));
37
+ fs.writeFileSync(bridgePath, JSON.stringify({
38
+ session_id: session,
39
+ remaining_percentage: remaining,
40
+ used_pct: used,
41
+ timestamp: Math.floor(Date.now() / 1000),
42
+ }));
43
+ } catch { /* bridge is best-effort */ }
44
+ }
45
+
46
+ // Build statusline output
47
+ const parts = [];
48
+
49
+ // Update available?
50
+ const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(homeDir, '.claude');
51
+ const cacheFile = path.join(claudeDir, 'cache', 'clancy-update-check.json');
52
+ try {
53
+ const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
54
+ if (cache.update_available) {
55
+ parts.push('\x1b[33m⬆ /clancy:update\x1b[0m');
56
+ }
57
+ } catch { /* cache missing is normal */ }
58
+
59
+ // Context bar
60
+ if (remaining != null) {
61
+ const AUTO_COMPACT_BUFFER_PCT = 16.5;
62
+ const usableRemaining = Math.max(0, ((remaining - AUTO_COMPACT_BUFFER_PCT) / (100 - AUTO_COMPACT_BUFFER_PCT)) * 100);
63
+ const used = Math.max(0, Math.min(100, Math.round(100 - usableRemaining)));
64
+ const filled = Math.floor(used / 10);
65
+ const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
66
+
67
+ let coloredBar;
68
+ if (used < 50) coloredBar = `\x1b[32m${bar} ${used}%\x1b[0m`;
69
+ else if (used < 65) coloredBar = `\x1b[33m${bar} ${used}%\x1b[0m`;
70
+ else if (used < 80) coloredBar = `\x1b[38;5;208m${bar} ${used}%\x1b[0m`;
71
+ else coloredBar = `\x1b[5;31m💀 ${bar} ${used}%\x1b[0m`;
72
+
73
+ parts.push(`\x1b[2mClancy\x1b[0m ${coloredBar}`);
74
+ } else {
75
+ parts.push('\x1b[2mClancy\x1b[0m');
76
+ }
77
+
78
+ process.stdout.write(parts.join(' │ '));
79
+ } catch {
80
+ process.exit(0);
81
+ }
82
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chief-clancy",
3
- "version": "0.1.4",
3
+ "version": "0.2.0-beta.1",
4
4
  "description": "Autonomous, board-driven development for Claude Code — scaffolds docs, integrates Kanban boards, runs tickets in a loop.",
5
5
  "keywords": [
6
6
  "claude",
@@ -29,6 +29,7 @@
29
29
  "main": "bin/install.js",
30
30
  "files": [
31
31
  "bin/",
32
+ "hooks/",
32
33
  "src/",
33
34
  "registry/"
34
35
  ],
@@ -0,0 +1,14 @@
1
+ # /clancy:dry-run
2
+
3
+ Preview which ticket Clancy would pick up next — no changes made.
4
+
5
+ Shows:
6
+ - The ticket that would be picked up (key, summary, epic)
7
+ - The target branch and feature branch that would be created
8
+ - Full preflight checks — catches config issues early
9
+
10
+ Nothing is written, no git operations run, Claude is not invoked.
11
+
12
+ @.claude/clancy/workflows/once.md
13
+
14
+ Run the once workflow with `--dry-run` as documented above. Treat this invocation as if the user passed `--dry-run`.
@@ -20,6 +20,7 @@ integration, structured codebase docs, and a git workflow built for team develop
20
20
  | `/clancy:run` | Run in loop mode until queue is empty or MAX_ITERATIONS hit |
21
21
  | `/clancy:run 20` | Same, but override MAX_ITERATIONS to 20 for this session |
22
22
  | `/clancy:once` | Pick up one ticket and stop — good for first runs and debugging |
23
+ | `/clancy:dry-run` | Preview next ticket without making any changes |
23
24
  | `/clancy:status` | Show next tickets without running — read-only board check |
24
25
  | `/clancy:review` | Score next ticket (0–100%) with actionable recommendations |
25
26
  | `/clancy:logs` | Format and display .clancy/progress.txt |
@@ -35,7 +36,7 @@ integration, structured codebase docs, and a git workflow built for team develop
35
36
 
36
37
  1. Run `/clancy:init` to connect your Kanban board and scaffold .clancy/
37
38
  2. Run `/clancy:map-codebase` to generate codebase docs (or say yes during init)
38
- 3. Run `/clancy:once` to watch your first ticket — then go AFK with `/clancy:run`
39
+ 3. Run `/clancy:dry-run` to preview the first ticket, then `/clancy:once` to run it — then go AFK with `/clancy:run`
39
40
 
40
41
  Clancy picks one ticket per loop, fresh context every iteration. No context rot.
41
42
 
@@ -8,6 +8,10 @@ Good for:
8
8
  - Debugging a specific ticket
9
9
  - When you only have time for one ticket
10
10
 
11
+ Pass `--dry-run` to preview what Clancy would do without making any changes:
12
+ - Shows the ticket, epic, target branch, and feature branch
13
+ - Exits before any git operations or Claude invocation
14
+
11
15
  @.claude/clancy/workflows/once.md
12
16
 
13
17
  Run one ticket as documented in the workflow above.
@@ -48,6 +48,12 @@ MAX_ITERATIONS=5
48
48
  # PLAYWRIGHT_STORYBOOK_PORT=6006
49
49
  # PLAYWRIGHT_STARTUP_WAIT=15
50
50
 
51
+ # ─── Optional: Status transitions ────────────────────────────────────────────
52
+ # Move tickets automatically when Clancy picks up or completes them.
53
+ # Set to the exact status name shown in your Jira board column header.
54
+ # CLANCY_STATUS_IN_PROGRESS="In Progress"
55
+ # CLANCY_STATUS_DONE="Done"
56
+
51
57
  # ─── Optional: Notifications ──────────────────────────────────────────────────
52
58
  # Webhook URL for Slack or Teams notifications on ticket completion
53
59
  # CLANCY_NOTIFY_WEBHOOK=https://hooks.slack.com/services/your/webhook/url
@@ -38,6 +38,12 @@ MAX_ITERATIONS=20
38
38
  # PLAYWRIGHT_STORYBOOK_PORT=6006
39
39
  # PLAYWRIGHT_STARTUP_WAIT=15
40
40
 
41
+ # ─── Optional: Status transitions ────────────────────────────────────────────
42
+ # Move issues automatically when Clancy picks up or completes them.
43
+ # Set to the exact workflow state name shown in your Linear board column header.
44
+ # CLANCY_STATUS_IN_PROGRESS="In Progress"
45
+ # CLANCY_STATUS_DONE="Done"
46
+
41
47
  # ─── Optional: Notifications ──────────────────────────────────────────────────
42
48
  # Webhook URL for Slack or Teams notifications on ticket completion
43
49
  # CLANCY_NOTIFY_WEBHOOK=https://hooks.slack.com/services/your/webhook/url
@@ -3,19 +3,26 @@
3
3
 
4
4
  This project uses Clancy for autonomous ticket-driven development.
5
5
 
6
+ ### Version check
7
+ At the start of each session, read `~/.claude/cache/clancy-update-check.json` (use the Bash tool). If it exists and `update_available` is true, display exactly:
8
+ `⚠ Clancy update available: v{installed} → v{latest}. Run /clancy:update to upgrade.`
9
+ Then continue normally. If the file is missing or unreadable, continue silently.
10
+
6
11
  ### Docs
7
- Before every run, read all docs in `.clancy/docs/`:
12
+ Before every run, read these core docs from `.clancy/docs/`:
8
13
  - STACK.md — tech stack and dependencies
9
- - INTEGRATIONS.md — external services and APIs
10
14
  - ARCHITECTURE.md — system design and data flow
11
15
  - CONVENTIONS.md — code style and patterns
12
- - TESTING.md — test approach and coverage expectations
13
16
  - GIT.md — branching, commit format, merge strategy
14
- - DESIGN-SYSTEM.md — tokens, components, visual conventions
15
- - ACCESSIBILITY.md — WCAG requirements and ARIA patterns
16
17
  - DEFINITION-OF-DONE.md — checklist before marking a ticket complete
17
18
  - CONCERNS.md — known risks, tech debt, things to avoid
18
19
 
20
+ Also read these if relevant to the ticket:
21
+ - INTEGRATIONS.md — if the ticket touches external APIs, services, or authentication
22
+ - TESTING.md — if the ticket involves tests, specs, or coverage requirements
23
+ - DESIGN-SYSTEM.md — if the ticket touches UI, components, styles, or visual design
24
+ - ACCESSIBILITY.md — if the ticket touches accessibility, ARIA, or WCAG requirements
25
+
19
26
  ### Executability check
20
27
 
21
28
  Before any git operation, branch creation, or code change — assess whether this ticket can be implemented entirely as a code change committed to this repo.
@@ -3,6 +3,15 @@
3
3
  # This means any command that fails will stop the script immediately rather than silently continuing.
4
4
  set -euo pipefail
5
5
 
6
+ # Parse flags — must happen before preflight so --dry-run works without side effects.
7
+ DRY_RUN=false
8
+ for arg in "$@"; do
9
+ case "$arg" in
10
+ --dry-run) DRY_RUN=true ;;
11
+ esac
12
+ done
13
+ readonly DRY_RUN
14
+
6
15
  # ─── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────
7
16
  #
8
17
  # Board: GitHub Issues
@@ -136,17 +145,31 @@ TICKET_BRANCH="feature/issue-${ISSUE_NUMBER}"
136
145
  if [ "$MILESTONE" != "none" ]; then
137
146
  MILESTONE_SLUG=$(echo "$MILESTONE" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd '[:alnum:]-')
138
147
  TARGET_BRANCH="milestone/${MILESTONE_SLUG}"
139
- git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
140
- || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
141
148
  else
142
149
  TARGET_BRANCH="$BASE_BRANCH"
143
150
  fi
144
151
 
152
+ # ─── DRY RUN ───────────────────────────────────────────────────────────────────
153
+
154
+ if [ "$DRY_RUN" = "true" ]; then
155
+ echo ""
156
+ echo "── Dry run ──────────────────────────────────────"
157
+ echo " Issue: [#${ISSUE_NUMBER}] $TITLE"
158
+ echo " Milestone: $MILESTONE"
159
+ echo " Target branch: $TARGET_BRANCH"
160
+ echo " Feature branch: $TICKET_BRANCH"
161
+ echo "─────────────────────────────────────────────────"
162
+ echo " No changes made. Remove --dry-run to run for real."
163
+ exit 0
164
+ fi
165
+
145
166
  # ─── IMPLEMENT ─────────────────────────────────────────────────────────────────
146
167
 
147
168
  echo "Picking up: [#${ISSUE_NUMBER}] $TITLE"
148
169
  echo "Milestone: $MILESTONE | Target branch: $TARGET_BRANCH"
149
170
 
171
+ git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
172
+ || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
150
173
  git checkout "$TARGET_BRANCH"
151
174
  # -B creates the branch if it doesn't exist, or resets it to HEAD if it does.
152
175
  # This handles retries cleanly without failing on an already-existing branch.
@@ -172,7 +195,8 @@ If you must SKIP this issue:
172
195
  4. Stop — no branches, no file changes, no git operations.
173
196
 
174
197
  If the issue IS implementable, continue:
175
- 1. Read ALL docs in .clancy/docs/ especially GIT.md for branching and commit conventions
198
+ 1. Read core docs in .clancy/docs/: STACK.md, ARCHITECTURE.md, CONVENTIONS.md, GIT.md, DEFINITION-OF-DONE.md, CONCERNS.md
199
+ Also read if relevant to this ticket: INTEGRATIONS.md (external APIs/services/auth), TESTING.md (tests/specs/coverage), DESIGN-SYSTEM.md (UI/components/styles), ACCESSIBILITY.md (accessibility/ARIA/WCAG)
176
200
  2. Follow the conventions in GIT.md exactly
177
201
  3. Implement the issue fully
178
202
  4. Commit your work following the conventions in GIT.md
@@ -3,6 +3,15 @@
3
3
  # This means any command that fails will stop the script immediately rather than silently continuing.
4
4
  set -euo pipefail
5
5
 
6
+ # Parse flags — must happen before preflight so --dry-run works without side effects.
7
+ DRY_RUN=false
8
+ for arg in "$@"; do
9
+ case "$arg" in
10
+ --dry-run) DRY_RUN=true ;;
11
+ esac
12
+ done
13
+ readonly DRY_RUN
14
+
6
15
  # ─── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────
7
16
  #
8
17
  # Board: Linear
@@ -153,6 +162,7 @@ if [ "$NODE_COUNT" -eq 0 ]; then
153
162
  exit 0
154
163
  fi
155
164
 
165
+ ISSUE_ID=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].id')
156
166
  IDENTIFIER=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].identifier')
157
167
  TITLE=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].title')
158
168
  DESCRIPTION=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].description // "No description"')
@@ -172,22 +182,58 @@ TICKET_BRANCH="feature/$(echo "$IDENTIFIER" | tr '[:upper:]' '[:lower:]')"
172
182
  # BASE_BRANCH if it doesn't exist yet). Otherwise branch from BASE_BRANCH directly.
173
183
  if [ "$PARENT_ID" != "none" ]; then
174
184
  TARGET_BRANCH="epic/$(echo "$PARENT_ID" | tr '[:upper:]' '[:lower:]')"
175
- git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
176
- || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
177
185
  else
178
186
  TARGET_BRANCH="$BASE_BRANCH"
179
187
  fi
180
188
 
189
+ # ─── DRY RUN ───────────────────────────────────────────────────────────────────
190
+
191
+ if [ "$DRY_RUN" = "true" ]; then
192
+ echo ""
193
+ echo "── Dry run ──────────────────────────────────────"
194
+ echo " Issue: [$IDENTIFIER] $TITLE"
195
+ echo " Epic: $EPIC_INFO"
196
+ echo " Target branch: $TARGET_BRANCH"
197
+ echo " Feature branch: $TICKET_BRANCH"
198
+ echo "─────────────────────────────────────────────────"
199
+ echo " No changes made. Remove --dry-run to run for real."
200
+ exit 0
201
+ fi
202
+
181
203
  # ─── IMPLEMENT ─────────────────────────────────────────────────────────────────
182
204
 
183
205
  echo "Picking up: [$IDENTIFIER] $TITLE"
184
206
  echo "Epic: $EPIC_INFO | Target branch: $TARGET_BRANCH"
185
207
 
208
+ git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
209
+ || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
186
210
  git checkout "$TARGET_BRANCH"
187
211
  # -B creates the branch if it doesn't exist, or resets it to HEAD if it does.
188
212
  # This handles retries cleanly without failing on an already-existing branch.
189
213
  git checkout -B "$TICKET_BRANCH"
190
214
 
215
+ # Transition issue to In Progress (best-effort — never fails the run).
216
+ # Queries team workflow states by type "started", picks the first match.
217
+ if [ -n "${CLANCY_STATUS_IN_PROGRESS:-}" ]; then
218
+ STATE_RESP=$(curl -s -X POST https://api.linear.app/graphql \
219
+ -H "Content-Type: application/json" \
220
+ -H "Authorization: $LINEAR_API_KEY" \
221
+ -d "$(jq -n --arg teamId "$LINEAR_TEAM_ID" --arg name "$CLANCY_STATUS_IN_PROGRESS" \
222
+ '{"query": "query($teamId: String!, $name: String!) { workflowStates(filter: { team: { id: { eq: $teamId } } name: { eq: $name } }) { nodes { id } } }", "variables": {"teamId": $teamId, "name": $name}}')")
223
+ IN_PROGRESS_STATE_ID=$(echo "$STATE_RESP" | jq -r '.data.workflowStates.nodes[0].id // empty')
224
+ if [ -n "$IN_PROGRESS_STATE_ID" ]; then
225
+ curl -s -X POST https://api.linear.app/graphql \
226
+ -H "Content-Type: application/json" \
227
+ -H "Authorization: $LINEAR_API_KEY" \
228
+ -d "$(jq -n --arg issueId "$ISSUE_ID" --arg stateId "$IN_PROGRESS_STATE_ID" \
229
+ '{"query": "mutation($issueId: String!, $stateId: String!) { issueUpdate(id: $issueId, input: { stateId: $stateId }) { success } }", "variables": {"issueId": $issueId, "stateId": $stateId}}')" \
230
+ >/dev/null 2>&1 || true
231
+ echo " → Transitioned to $CLANCY_STATUS_IN_PROGRESS"
232
+ else
233
+ echo " ⚠ Workflow state '$CLANCY_STATUS_IN_PROGRESS' not found — check CLANCY_STATUS_IN_PROGRESS in .clancy/.env."
234
+ fi
235
+ fi
236
+
191
237
  PROMPT="You are implementing Linear issue $IDENTIFIER.
192
238
 
193
239
  Title: $TITLE
@@ -208,7 +254,8 @@ If you must SKIP this issue:
208
254
  4. Stop — no branches, no file changes, no git operations.
209
255
 
210
256
  If the issue IS implementable, continue:
211
- 1. Read ALL docs in .clancy/docs/ especially GIT.md for branching and commit conventions
257
+ 1. Read core docs in .clancy/docs/: STACK.md, ARCHITECTURE.md, CONVENTIONS.md, GIT.md, DEFINITION-OF-DONE.md, CONCERNS.md
258
+ Also read if relevant to this ticket: INTEGRATIONS.md (external APIs/services/auth), TESTING.md (tests/specs/coverage), DESIGN-SYSTEM.md (UI/components/styles), ACCESSIBILITY.md (accessibility/ARIA/WCAG)
212
259
  2. Follow the conventions in GIT.md exactly
213
260
  3. Implement the issue fully
214
261
  4. Commit your work following the conventions in GIT.md
@@ -232,6 +279,27 @@ fi
232
279
  # Delete ticket branch locally
233
280
  git branch -d "$TICKET_BRANCH"
234
281
 
282
+ # Transition issue to Done (best-effort — never fails the run).
283
+ if [ -n "${CLANCY_STATUS_DONE:-}" ]; then
284
+ STATE_RESP=$(curl -s -X POST https://api.linear.app/graphql \
285
+ -H "Content-Type: application/json" \
286
+ -H "Authorization: $LINEAR_API_KEY" \
287
+ -d "$(jq -n --arg teamId "$LINEAR_TEAM_ID" --arg name "$CLANCY_STATUS_DONE" \
288
+ '{"query": "query($teamId: String!, $name: String!) { workflowStates(filter: { team: { id: { eq: $teamId } } name: { eq: $name } }) { nodes { id } } }", "variables": {"teamId": $teamId, "name": $name}}')")
289
+ DONE_STATE_ID=$(echo "$STATE_RESP" | jq -r '.data.workflowStates.nodes[0].id // empty')
290
+ if [ -n "$DONE_STATE_ID" ]; then
291
+ curl -s -X POST https://api.linear.app/graphql \
292
+ -H "Content-Type: application/json" \
293
+ -H "Authorization: $LINEAR_API_KEY" \
294
+ -d "$(jq -n --arg issueId "$ISSUE_ID" --arg stateId "$DONE_STATE_ID" \
295
+ '{"query": "mutation($issueId: String!, $stateId: String!) { issueUpdate(id: $issueId, input: { stateId: $stateId }) { success } }", "variables": {"issueId": $issueId, "stateId": $stateId}}')" \
296
+ >/dev/null 2>&1 || true
297
+ echo " → Transitioned to $CLANCY_STATUS_DONE"
298
+ else
299
+ echo " ⚠ Workflow state '$CLANCY_STATUS_DONE' not found — check CLANCY_STATUS_DONE in .clancy/.env."
300
+ fi
301
+ fi
302
+
235
303
  # Log progress
236
304
  echo "$(date '+%Y-%m-%d %H:%M') | $IDENTIFIER | $TITLE | DONE" >> .clancy/progress.txt
237
305
 
@@ -3,6 +3,15 @@
3
3
  # This means any command that fails will stop the script immediately rather than silently continuing.
4
4
  set -euo pipefail
5
5
 
6
+ # Parse flags — must happen before preflight so --dry-run works without side effects.
7
+ DRY_RUN=false
8
+ for arg in "$@"; do
9
+ case "$arg" in
10
+ --dry-run) DRY_RUN=true ;;
11
+ esac
12
+ done
13
+ readonly DRY_RUN
14
+
6
15
  # ─── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────
7
16
  #
8
17
  # Board: Jira
@@ -178,22 +187,56 @@ TICKET_BRANCH="feature/$(echo "$TICKET_KEY" | tr '[:upper:]' '[:lower:]')"
178
187
  # BASE_BRANCH if it doesn't exist yet). Otherwise branch from BASE_BRANCH directly.
179
188
  if [ "$EPIC_INFO" != "none" ]; then
180
189
  TARGET_BRANCH="epic/$(echo "$EPIC_INFO" | tr '[:upper:]' '[:lower:]')"
181
- git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
182
- || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
183
190
  else
184
191
  TARGET_BRANCH="$BASE_BRANCH"
185
192
  fi
186
193
 
194
+ # ─── DRY RUN ───────────────────────────────────────────────────────────────────
195
+
196
+ if [ "$DRY_RUN" = "true" ]; then
197
+ echo ""
198
+ echo "── Dry run ──────────────────────────────────────"
199
+ echo " Ticket: [$TICKET_KEY] $SUMMARY"
200
+ echo " Epic: $EPIC_INFO"
201
+ echo " Blockers: $BLOCKERS"
202
+ echo " Target branch: $TARGET_BRANCH"
203
+ echo " Feature branch: $TICKET_BRANCH"
204
+ echo "─────────────────────────────────────────────────"
205
+ echo " No changes made. Remove --dry-run to run for real."
206
+ exit 0
207
+ fi
208
+
187
209
  # ─── IMPLEMENT ─────────────────────────────────────────────────────────────────
188
210
 
189
211
  echo "Picking up: [$TICKET_KEY] $SUMMARY"
190
212
  echo "Epic: $EPIC_INFO | Target branch: $TARGET_BRANCH | Blockers: $BLOCKERS"
191
213
 
214
+ git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
215
+ || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
192
216
  git checkout "$TARGET_BRANCH"
193
217
  # -B creates the branch if it doesn't exist, or resets it to HEAD if it does.
194
218
  # This handles retries cleanly without failing on an already-existing branch.
195
219
  git checkout -B "$TICKET_BRANCH"
196
220
 
221
+ # Transition ticket to In Progress (best-effort — never fails the run)
222
+ if [ -n "${CLANCY_STATUS_IN_PROGRESS:-}" ]; then
223
+ TRANSITIONS=$(curl -s \
224
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
225
+ -H "Accept: application/json" \
226
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/transitions")
227
+ IN_PROGRESS_ID=$(echo "$TRANSITIONS" | jq -r \
228
+ --arg name "$CLANCY_STATUS_IN_PROGRESS" \
229
+ '.transitions[] | select(.name == $name) | .id' | head -1)
230
+ if [ -n "$IN_PROGRESS_ID" ]; then
231
+ curl -s -X POST \
232
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
233
+ -H "Content-Type: application/json" \
234
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/transitions" \
235
+ -d "$(jq -n --arg id "$IN_PROGRESS_ID" '{"transition":{"id":$id}}')" >/dev/null 2>&1 || true
236
+ echo " → Transitioned to $CLANCY_STATUS_IN_PROGRESS"
237
+ fi
238
+ fi
239
+
197
240
  PROMPT="You are implementing Jira ticket $TICKET_KEY.
198
241
 
199
242
  Summary: $SUMMARY
@@ -215,7 +258,8 @@ If you must SKIP this ticket:
215
258
  4. Stop — no branches, no file changes, no git operations.
216
259
 
217
260
  If the ticket IS implementable, continue:
218
- 1. Read ALL docs in .clancy/docs/ especially GIT.md for branching and commit conventions
261
+ 1. Read core docs in .clancy/docs/: STACK.md, ARCHITECTURE.md, CONVENTIONS.md, GIT.md, DEFINITION-OF-DONE.md, CONCERNS.md
262
+ Also read if relevant to this ticket: INTEGRATIONS.md (external APIs/services/auth), TESTING.md (tests/specs/coverage), DESIGN-SYSTEM.md (UI/components/styles), ACCESSIBILITY.md (accessibility/ARIA/WCAG)
219
263
  2. Follow the conventions in GIT.md exactly
220
264
  3. Implement the ticket fully
221
265
  4. Commit your work following the conventions in GIT.md
@@ -239,6 +283,25 @@ fi
239
283
  # Delete ticket branch locally (never push deletes)
240
284
  git branch -d "$TICKET_BRANCH"
241
285
 
286
+ # Transition ticket to Done (best-effort — never fails the run)
287
+ if [ -n "${CLANCY_STATUS_DONE:-}" ]; then
288
+ TRANSITIONS=$(curl -s \
289
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
290
+ -H "Accept: application/json" \
291
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/transitions")
292
+ DONE_ID=$(echo "$TRANSITIONS" | jq -r \
293
+ --arg name "$CLANCY_STATUS_DONE" \
294
+ '.transitions[] | select(.name == $name) | .id' | head -1)
295
+ if [ -n "$DONE_ID" ]; then
296
+ curl -s -X POST \
297
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
298
+ -H "Content-Type: application/json" \
299
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/transitions" \
300
+ -d "$(jq -n --arg id "$DONE_ID" '{"transition":{"id":$id}}')" >/dev/null 2>&1 || true
301
+ echo " → Transitioned to $CLANCY_STATUS_DONE"
302
+ fi
303
+ fi
304
+
242
305
  # Log progress
243
306
  echo "$(date '+%Y-%m-%d %H:%M') | $TICKET_KEY | $SUMMARY | DONE" >> .clancy/progress.txt
244
307
 
@@ -420,4 +420,4 @@ Clancy is ready.
420
420
  - Config: `.clancy/.env`
421
421
  - CLAUDE.md: updated
422
422
 
423
- Run `/clancy:once` to pick up your first ticket, or `/clancy:run` to process the full queue.
423
+ Run `/clancy:dry-run` to preview the first ticket without making changes, `/clancy:once` to pick it up, or `/clancy:run` to process the full queue.
@@ -45,6 +45,10 @@ Pick up exactly one ticket from the Kanban board, implement it, commit, squash-m
45
45
 
46
46
  ## Step 2 — Run
47
47
 
48
+ Check if the user passed `--dry-run` as an argument to the slash command.
49
+
50
+ **Without `--dry-run`:**
51
+
48
52
  Display:
49
53
  ```
50
54
  Running Clancy for one ticket.
@@ -55,6 +59,18 @@ Execute:
55
59
  bash .clancy/clancy-once.sh
56
60
  ```
57
61
 
62
+ **With `--dry-run`:**
63
+
64
+ Display:
65
+ ```
66
+ Running Clancy in dry-run mode — no changes will be made.
67
+ ```
68
+
69
+ Execute:
70
+ ```bash
71
+ bash .clancy/clancy-once.sh --dry-run
72
+ ```
73
+
58
74
  Stream output directly — do not buffer or summarise.
59
75
 
60
76
  ---
@@ -304,6 +304,15 @@ Write this file when the chosen board is **Jira**:
304
304
  # This means any command that fails will stop the script immediately rather than silently continuing.
305
305
  set -euo pipefail
306
306
 
307
+ # Parse flags — must happen before preflight so --dry-run works without side effects.
308
+ DRY_RUN=false
309
+ for arg in "$@"; do
310
+ case "$arg" in
311
+ --dry-run) DRY_RUN=true ;;
312
+ esac
313
+ done
314
+ readonly DRY_RUN
315
+
307
316
  # ─── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────
308
317
  #
309
318
  # Board: Jira
@@ -479,22 +488,56 @@ TICKET_BRANCH="feature/$(echo "$TICKET_KEY" | tr '[:upper:]' '[:lower:]')"
479
488
  # BASE_BRANCH if it doesn't exist yet). Otherwise branch from BASE_BRANCH directly.
480
489
  if [ "$EPIC_INFO" != "none" ]; then
481
490
  TARGET_BRANCH="epic/$(echo "$EPIC_INFO" | tr '[:upper:]' '[:lower:]')"
482
- git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
483
- || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
484
491
  else
485
492
  TARGET_BRANCH="$BASE_BRANCH"
486
493
  fi
487
494
 
495
+ # ─── DRY RUN ───────────────────────────────────────────────────────────────────
496
+
497
+ if [ "$DRY_RUN" = "true" ]; then
498
+ echo ""
499
+ echo "── Dry run ──────────────────────────────────────"
500
+ echo " Ticket: [$TICKET_KEY] $SUMMARY"
501
+ echo " Epic: $EPIC_INFO"
502
+ echo " Blockers: $BLOCKERS"
503
+ echo " Target branch: $TARGET_BRANCH"
504
+ echo " Feature branch: $TICKET_BRANCH"
505
+ echo "─────────────────────────────────────────────────"
506
+ echo " No changes made. Remove --dry-run to run for real."
507
+ exit 0
508
+ fi
509
+
488
510
  # ─── IMPLEMENT ─────────────────────────────────────────────────────────────────
489
511
 
490
512
  echo "Picking up: [$TICKET_KEY] $SUMMARY"
491
513
  echo "Epic: $EPIC_INFO | Target branch: $TARGET_BRANCH | Blockers: $BLOCKERS"
492
514
 
515
+ git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
516
+ || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
493
517
  git checkout "$TARGET_BRANCH"
494
518
  # -B creates the branch if it doesn't exist, or resets it to HEAD if it does.
495
519
  # This handles retries cleanly without failing on an already-existing branch.
496
520
  git checkout -B "$TICKET_BRANCH"
497
521
 
522
+ # Transition ticket to In Progress (best-effort — never fails the run)
523
+ if [ -n "${CLANCY_STATUS_IN_PROGRESS:-}" ]; then
524
+ TRANSITIONS=$(curl -s \
525
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
526
+ -H "Accept: application/json" \
527
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/transitions")
528
+ IN_PROGRESS_ID=$(echo "$TRANSITIONS" | jq -r \
529
+ --arg name "$CLANCY_STATUS_IN_PROGRESS" \
530
+ '.transitions[] | select(.name == $name) | .id' | head -1)
531
+ if [ -n "$IN_PROGRESS_ID" ]; then
532
+ curl -s -X POST \
533
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
534
+ -H "Content-Type: application/json" \
535
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/transitions" \
536
+ -d "$(jq -n --arg id "$IN_PROGRESS_ID" '{"transition":{"id":$id}}')" >/dev/null 2>&1 || true
537
+ echo " → Transitioned to $CLANCY_STATUS_IN_PROGRESS"
538
+ fi
539
+ fi
540
+
498
541
  PROMPT="You are implementing Jira ticket $TICKET_KEY.
499
542
 
500
543
  Summary: $SUMMARY
@@ -516,7 +559,8 @@ If you must SKIP this ticket:
516
559
  4. Stop — no branches, no file changes, no git operations.
517
560
 
518
561
  If the ticket IS implementable, continue:
519
- 1. Read ALL docs in .clancy/docs/ especially GIT.md for branching and commit conventions
562
+ 1. Read core docs in .clancy/docs/: STACK.md, ARCHITECTURE.md, CONVENTIONS.md, GIT.md, DEFINITION-OF-DONE.md, CONCERNS.md
563
+ Also read if relevant to this ticket: INTEGRATIONS.md (external APIs/services/auth), TESTING.md (tests/specs/coverage), DESIGN-SYSTEM.md (UI/components/styles), ACCESSIBILITY.md (accessibility/ARIA/WCAG)
520
564
  2. Follow the conventions in GIT.md exactly
521
565
  3. Implement the ticket fully
522
566
  4. Commit your work following the conventions in GIT.md
@@ -540,6 +584,25 @@ fi
540
584
  # Delete ticket branch locally (never push deletes)
541
585
  git branch -d "$TICKET_BRANCH"
542
586
 
587
+ # Transition ticket to Done (best-effort — never fails the run)
588
+ if [ -n "${CLANCY_STATUS_DONE:-}" ]; then
589
+ TRANSITIONS=$(curl -s \
590
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
591
+ -H "Accept: application/json" \
592
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/transitions")
593
+ DONE_ID=$(echo "$TRANSITIONS" | jq -r \
594
+ --arg name "$CLANCY_STATUS_DONE" \
595
+ '.transitions[] | select(.name == $name) | .id' | head -1)
596
+ if [ -n "$DONE_ID" ]; then
597
+ curl -s -X POST \
598
+ -u "$JIRA_USER:$JIRA_API_TOKEN" \
599
+ -H "Content-Type: application/json" \
600
+ "$JIRA_BASE_URL/rest/api/3/issue/$TICKET_KEY/transitions" \
601
+ -d "$(jq -n --arg id "$DONE_ID" '{"transition":{"id":$id}}')" >/dev/null 2>&1 || true
602
+ echo " → Transitioned to $CLANCY_STATUS_DONE"
603
+ fi
604
+ fi
605
+
543
606
  # Log progress
544
607
  echo "$(date '+%Y-%m-%d %H:%M') | $TICKET_KEY | $SUMMARY | DONE" >> .clancy/progress.txt
545
608
 
@@ -572,6 +635,15 @@ Write this file when the chosen board is **GitHub Issues**:
572
635
  # This means any command that fails will stop the script immediately rather than silently continuing.
573
636
  set -euo pipefail
574
637
 
638
+ # Parse flags — must happen before preflight so --dry-run works without side effects.
639
+ DRY_RUN=false
640
+ for arg in "$@"; do
641
+ case "$arg" in
642
+ --dry-run) DRY_RUN=true ;;
643
+ esac
644
+ done
645
+ readonly DRY_RUN
646
+
575
647
  # ─── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────
576
648
  #
577
649
  # Board: GitHub Issues
@@ -705,17 +777,31 @@ TICKET_BRANCH="feature/issue-${ISSUE_NUMBER}"
705
777
  if [ "$MILESTONE" != "none" ]; then
706
778
  MILESTONE_SLUG=$(echo "$MILESTONE" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd '[:alnum:]-')
707
779
  TARGET_BRANCH="milestone/${MILESTONE_SLUG}"
708
- git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
709
- || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
710
780
  else
711
781
  TARGET_BRANCH="$BASE_BRANCH"
712
782
  fi
713
783
 
784
+ # ─── DRY RUN ───────────────────────────────────────────────────────────────────
785
+
786
+ if [ "$DRY_RUN" = "true" ]; then
787
+ echo ""
788
+ echo "── Dry run ──────────────────────────────────────"
789
+ echo " Issue: [#${ISSUE_NUMBER}] $TITLE"
790
+ echo " Milestone: $MILESTONE"
791
+ echo " Target branch: $TARGET_BRANCH"
792
+ echo " Feature branch: $TICKET_BRANCH"
793
+ echo "─────────────────────────────────────────────────"
794
+ echo " No changes made. Remove --dry-run to run for real."
795
+ exit 0
796
+ fi
797
+
714
798
  # ─── IMPLEMENT ─────────────────────────────────────────────────────────────────
715
799
 
716
800
  echo "Picking up: [#${ISSUE_NUMBER}] $TITLE"
717
801
  echo "Milestone: $MILESTONE | Target branch: $TARGET_BRANCH"
718
802
 
803
+ git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
804
+ || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
719
805
  git checkout "$TARGET_BRANCH"
720
806
  # -B creates the branch if it doesn't exist, or resets it to HEAD if it does.
721
807
  # This handles retries cleanly without failing on an already-existing branch.
@@ -741,7 +827,8 @@ If you must SKIP this issue:
741
827
  4. Stop — no branches, no file changes, no git operations.
742
828
 
743
829
  If the issue IS implementable, continue:
744
- 1. Read ALL docs in .clancy/docs/ especially GIT.md for branching and commit conventions
830
+ 1. Read core docs in .clancy/docs/: STACK.md, ARCHITECTURE.md, CONVENTIONS.md, GIT.md, DEFINITION-OF-DONE.md, CONCERNS.md
831
+ Also read if relevant to this ticket: INTEGRATIONS.md (external APIs/services/auth), TESTING.md (tests/specs/coverage), DESIGN-SYSTEM.md (UI/components/styles), ACCESSIBILITY.md (accessibility/ARIA/WCAG)
745
832
  2. Follow the conventions in GIT.md exactly
746
833
  3. Implement the issue fully
747
834
  4. Commit your work following the conventions in GIT.md
@@ -806,6 +893,15 @@ Write this file when the chosen board is **Linear**:
806
893
  # This means any command that fails will stop the script immediately rather than silently continuing.
807
894
  set -euo pipefail
808
895
 
896
+ # Parse flags — must happen before preflight so --dry-run works without side effects.
897
+ DRY_RUN=false
898
+ for arg in "$@"; do
899
+ case "$arg" in
900
+ --dry-run) DRY_RUN=true ;;
901
+ esac
902
+ done
903
+ readonly DRY_RUN
904
+
809
905
  # ─── WHAT THIS SCRIPT DOES ─────────────────────────────────────────────────────
810
906
  #
811
907
  # Board: Linear
@@ -956,6 +1052,7 @@ if [ "$NODE_COUNT" -eq 0 ]; then
956
1052
  exit 0
957
1053
  fi
958
1054
 
1055
+ ISSUE_ID=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].id')
959
1056
  IDENTIFIER=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].identifier')
960
1057
  TITLE=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].title')
961
1058
  DESCRIPTION=$(echo "$RESPONSE" | jq -r '.data.viewer.assignedIssues.nodes[0].description // "No description"')
@@ -975,22 +1072,58 @@ TICKET_BRANCH="feature/$(echo "$IDENTIFIER" | tr '[:upper:]' '[:lower:]')"
975
1072
  # BASE_BRANCH if it doesn't exist yet). Otherwise branch from BASE_BRANCH directly.
976
1073
  if [ "$PARENT_ID" != "none" ]; then
977
1074
  TARGET_BRANCH="epic/$(echo "$PARENT_ID" | tr '[:upper:]' '[:lower:]')"
978
- git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
979
- || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
980
1075
  else
981
1076
  TARGET_BRANCH="$BASE_BRANCH"
982
1077
  fi
983
1078
 
1079
+ # ─── DRY RUN ───────────────────────────────────────────────────────────────────
1080
+
1081
+ if [ "$DRY_RUN" = "true" ]; then
1082
+ echo ""
1083
+ echo "── Dry run ──────────────────────────────────────"
1084
+ echo " Issue: [$IDENTIFIER] $TITLE"
1085
+ echo " Epic: $EPIC_INFO"
1086
+ echo " Target branch: $TARGET_BRANCH"
1087
+ echo " Feature branch: $TICKET_BRANCH"
1088
+ echo "─────────────────────────────────────────────────"
1089
+ echo " No changes made. Remove --dry-run to run for real."
1090
+ exit 0
1091
+ fi
1092
+
984
1093
  # ─── IMPLEMENT ─────────────────────────────────────────────────────────────────
985
1094
 
986
1095
  echo "Picking up: [$IDENTIFIER] $TITLE"
987
1096
  echo "Epic: $EPIC_INFO | Target branch: $TARGET_BRANCH"
988
1097
 
1098
+ git show-ref --verify --quiet "refs/heads/$TARGET_BRANCH" \
1099
+ || git checkout -b "$TARGET_BRANCH" "$BASE_BRANCH"
989
1100
  git checkout "$TARGET_BRANCH"
990
1101
  # -B creates the branch if it doesn't exist, or resets it to HEAD if it does.
991
1102
  # This handles retries cleanly without failing on an already-existing branch.
992
1103
  git checkout -B "$TICKET_BRANCH"
993
1104
 
1105
+ # Transition issue to In Progress (best-effort — never fails the run).
1106
+ # Queries team workflow states by type "started", picks the first match.
1107
+ if [ -n "${CLANCY_STATUS_IN_PROGRESS:-}" ]; then
1108
+ STATE_RESP=$(curl -s -X POST https://api.linear.app/graphql \
1109
+ -H "Content-Type: application/json" \
1110
+ -H "Authorization: $LINEAR_API_KEY" \
1111
+ -d "$(jq -n --arg teamId "$LINEAR_TEAM_ID" --arg name "$CLANCY_STATUS_IN_PROGRESS" \
1112
+ '{"query": "query($teamId: String!, $name: String!) { workflowStates(filter: { team: { id: { eq: $teamId } } name: { eq: $name } }) { nodes { id } } }", "variables": {"teamId": $teamId, "name": $name}}')")
1113
+ IN_PROGRESS_STATE_ID=$(echo "$STATE_RESP" | jq -r '.data.workflowStates.nodes[0].id // empty')
1114
+ if [ -n "$IN_PROGRESS_STATE_ID" ]; then
1115
+ curl -s -X POST https://api.linear.app/graphql \
1116
+ -H "Content-Type: application/json" \
1117
+ -H "Authorization: $LINEAR_API_KEY" \
1118
+ -d "$(jq -n --arg issueId "$ISSUE_ID" --arg stateId "$IN_PROGRESS_STATE_ID" \
1119
+ '{"query": "mutation($issueId: String!, $stateId: String!) { issueUpdate(id: $issueId, input: { stateId: $stateId }) { success } }", "variables": {"issueId": $issueId, "stateId": $stateId}}')" \
1120
+ >/dev/null 2>&1 || true
1121
+ echo " → Transitioned to $CLANCY_STATUS_IN_PROGRESS"
1122
+ else
1123
+ echo " ⚠ Workflow state '$CLANCY_STATUS_IN_PROGRESS' not found — check CLANCY_STATUS_IN_PROGRESS in .clancy/.env."
1124
+ fi
1125
+ fi
1126
+
994
1127
  PROMPT="You are implementing Linear issue $IDENTIFIER.
995
1128
 
996
1129
  Title: $TITLE
@@ -1011,7 +1144,8 @@ If you must SKIP this issue:
1011
1144
  4. Stop — no branches, no file changes, no git operations.
1012
1145
 
1013
1146
  If the issue IS implementable, continue:
1014
- 1. Read ALL docs in .clancy/docs/ especially GIT.md for branching and commit conventions
1147
+ 1. Read core docs in .clancy/docs/: STACK.md, ARCHITECTURE.md, CONVENTIONS.md, GIT.md, DEFINITION-OF-DONE.md, CONCERNS.md
1148
+ Also read if relevant to this ticket: INTEGRATIONS.md (external APIs/services/auth), TESTING.md (tests/specs/coverage), DESIGN-SYSTEM.md (UI/components/styles), ACCESSIBILITY.md (accessibility/ARIA/WCAG)
1015
1149
  2. Follow the conventions in GIT.md exactly
1016
1150
  3. Implement the issue fully
1017
1151
  4. Commit your work following the conventions in GIT.md
@@ -1035,6 +1169,27 @@ fi
1035
1169
  # Delete ticket branch locally
1036
1170
  git branch -d "$TICKET_BRANCH"
1037
1171
 
1172
+ # Transition issue to Done (best-effort — never fails the run).
1173
+ if [ -n "${CLANCY_STATUS_DONE:-}" ]; then
1174
+ STATE_RESP=$(curl -s -X POST https://api.linear.app/graphql \
1175
+ -H "Content-Type: application/json" \
1176
+ -H "Authorization: $LINEAR_API_KEY" \
1177
+ -d "$(jq -n --arg teamId "$LINEAR_TEAM_ID" --arg name "$CLANCY_STATUS_DONE" \
1178
+ '{"query": "query($teamId: String!, $name: String!) { workflowStates(filter: { team: { id: { eq: $teamId } } name: { eq: $name } }) { nodes { id } } }", "variables": {"teamId": $teamId, "name": $name}}')")
1179
+ DONE_STATE_ID=$(echo "$STATE_RESP" | jq -r '.data.workflowStates.nodes[0].id // empty')
1180
+ if [ -n "$DONE_STATE_ID" ]; then
1181
+ curl -s -X POST https://api.linear.app/graphql \
1182
+ -H "Content-Type: application/json" \
1183
+ -H "Authorization: $LINEAR_API_KEY" \
1184
+ -d "$(jq -n --arg issueId "$ISSUE_ID" --arg stateId "$DONE_STATE_ID" \
1185
+ '{"query": "mutation($issueId: String!, $stateId: String!) { issueUpdate(id: $issueId, input: { stateId: $stateId }) { success } }", "variables": {"issueId": $issueId, "stateId": $stateId}}')" \
1186
+ >/dev/null 2>&1 || true
1187
+ echo " → Transitioned to $CLANCY_STATUS_DONE"
1188
+ else
1189
+ echo " ⚠ Workflow state '$CLANCY_STATUS_DONE' not found — check CLANCY_STATUS_DONE in .clancy/.env."
1190
+ fi
1191
+ fi
1192
+
1038
1193
  # Log progress
1039
1194
  echo "$(date '+%Y-%m-%d %H:%M') | $IDENTIFIER | $TITLE | DONE" >> .clancy/progress.txt
1040
1195
 
@@ -1234,6 +1389,12 @@ MAX_ITERATIONS=5
1234
1389
  # PLAYWRIGHT_STORYBOOK_PORT=6006
1235
1390
  # PLAYWRIGHT_STARTUP_WAIT=15
1236
1391
 
1392
+ # ─── Optional: Status transitions ────────────────────────────────────────────
1393
+ # Move tickets automatically when Clancy picks up or completes them.
1394
+ # Set to the exact status name shown in your Jira board column header.
1395
+ # CLANCY_STATUS_IN_PROGRESS="In Progress"
1396
+ # CLANCY_STATUS_DONE="Done"
1397
+
1237
1398
  # ─── Optional: Notifications ──────────────────────────────────────────────────
1238
1399
  # Webhook URL for Slack or Teams notifications on ticket completion
1239
1400
  # CLANCY_NOTIFY_WEBHOOK=https://hooks.slack.com/services/your/webhook/url
@@ -1330,6 +1491,12 @@ MAX_ITERATIONS=20
1330
1491
  # PLAYWRIGHT_STORYBOOK_PORT=6006
1331
1492
  # PLAYWRIGHT_STARTUP_WAIT=15
1332
1493
 
1494
+ # ─── Optional: Status transitions ────────────────────────────────────────────
1495
+ # Move issues automatically when Clancy picks up or completes them.
1496
+ # Set to the exact workflow state name shown in your Linear board column header.
1497
+ # CLANCY_STATUS_IN_PROGRESS="In Progress"
1498
+ # CLANCY_STATUS_DONE="Done"
1499
+
1333
1500
  # ─── Optional: Notifications ──────────────────────────────────────────────────
1334
1501
  # Webhook URL for Slack or Teams notifications on ticket completion
1335
1502
  # CLANCY_NOTIFY_WEBHOOK=https://hooks.slack.com/services/your/webhook/url
@@ -46,10 +46,14 @@ Jira
46
46
  [4] Queue status {CLANCY_JQL_STATUS:-To Do}
47
47
  [5] Sprint filter {on if CLANCY_JQL_SPRINT set, else off}
48
48
  [6] Label filter {CLANCY_LABEL if set, else off — only pick up tickets with this label}
49
+ [7] Pickup status {CLANCY_STATUS_IN_PROGRESS if set, else off — move ticket on pickup}
50
+ [8] Done status {CLANCY_STATUS_DONE if set, else off — move ticket on completion}
49
51
 
50
52
  {If Linear:}
51
53
  Linear
52
54
  [4] Label filter {CLANCY_LABEL if set, else off — only pick up issues with this label}
55
+ [5] Pickup status {CLANCY_STATUS_IN_PROGRESS if set, else off — move issue on pickup}
56
+ [6] Done status {CLANCY_STATUS_DONE if set, else off — move issue on completion}
53
57
 
54
58
  Optional enhancements
55
59
  [{N}] Figma MCP {enabled if FIGMA_API_KEY set, else not set}
@@ -62,7 +66,7 @@ Optional enhancements
62
66
  Which setting would you like to change?
63
67
  ```
64
68
 
65
- Number each option sequentially. Show only the board-specific section that matches the configured board. If Jira: show [4] queue status, [5] sprint, [6] label. If Linear: show [4] label. If GitHub: no board-specific options.
69
+ Number each option sequentially. Show only the board-specific section that matches the configured board. If Jira: show [4] queue status, [5] sprint, [6] label, [7] pickup status, [8] done status. If Linear: show [4] label, [5] pickup status, [6] done status. If GitHub: no board-specific options.
66
70
 
67
71
  ---
68
72
 
@@ -164,6 +168,40 @@ If [2]: remove `CLANCY_LABEL` from `.clancy/.env`.
164
168
 
165
169
  ---
166
170
 
171
+ ### [7] Jira In Progress status (Jira only)
172
+
173
+ ```
174
+ Jira In Progress status — current: {value or "off"}
175
+ When set, Clancy moves a ticket to this status when it starts working on it.
176
+ Must match the exact column name shown in your Jira board.
177
+
178
+ [1] Set status name
179
+ [2] Off (do not transition on pickup)
180
+ [3] Cancel
181
+ ```
182
+
183
+ If [1]: prompt `What status name should Clancy use for In Progress? (e.g. In Progress, In Dev, Doing)` then write `CLANCY_STATUS_IN_PROGRESS=<value>` to `.clancy/.env`.
184
+ If [2]: remove `CLANCY_STATUS_IN_PROGRESS` from `.clancy/.env`.
185
+
186
+ ---
187
+
188
+ ### [8] Jira Done status (Jira only)
189
+
190
+ ```
191
+ Jira Done status — current: {value or "off"}
192
+ When set, Clancy moves a ticket to this status after completing it.
193
+ Must match the exact column name shown in your Jira board.
194
+
195
+ [1] Set status name
196
+ [2] Off (do not transition on completion)
197
+ [3] Cancel
198
+ ```
199
+
200
+ If [1]: prompt `What status name should Clancy use for Done? (e.g. Done, Complete, Closed)` then write `CLANCY_STATUS_DONE=<value>` to `.clancy/.env`.
201
+ If [2]: remove `CLANCY_STATUS_DONE` from `.clancy/.env`.
202
+
203
+ ---
204
+
167
205
  ### [4] Linear label filter (Linear only)
168
206
 
169
207
  ```
@@ -181,6 +219,40 @@ If [2]: remove `CLANCY_LABEL` from `.clancy/.env`.
181
219
 
182
220
  ---
183
221
 
222
+ ### [5] Linear In Progress status (Linear only)
223
+
224
+ ```
225
+ Linear In Progress status — current: {value or "off"}
226
+ When set, Clancy moves an issue to this workflow state when it starts working on it.
227
+ Must match the exact state name shown in your Linear board column header.
228
+
229
+ [1] Set state name
230
+ [2] Off (do not transition on pickup)
231
+ [3] Cancel
232
+ ```
233
+
234
+ If [1]: prompt `What workflow state name should Clancy use for In Progress? (e.g. In Progress, In Dev, Doing)` then write `CLANCY_STATUS_IN_PROGRESS=<value>` to `.clancy/.env`.
235
+ If [2]: remove `CLANCY_STATUS_IN_PROGRESS` from `.clancy/.env`.
236
+
237
+ ---
238
+
239
+ ### [6] Linear Done status (Linear only)
240
+
241
+ ```
242
+ Linear Done status — current: {value or "off"}
243
+ When set, Clancy moves an issue to this workflow state after completing it.
244
+ Must match the exact state name shown in your Linear board column header.
245
+
246
+ [1] Set state name
247
+ [2] Off (do not transition on completion)
248
+ [3] Cancel
249
+ ```
250
+
251
+ If [1]: prompt `What workflow state name should Clancy use for Done? (e.g. Done, Complete, Closed)` then write `CLANCY_STATUS_DONE=<value>` to `.clancy/.env`.
252
+ If [2]: remove `CLANCY_STATUS_DONE` from `.clancy/.env`.
253
+
254
+ ---
255
+
184
256
  ### Figma MCP
185
257
 
186
258
  ```
@@ -35,9 +35,33 @@ Continue? (yes / no)
35
35
  ```
36
36
 
37
37
  - `no` → print "Nothing removed." and stop
38
- - `yes` → delete both the commands directory and the workflows directory for the chosen location(s), print "✓ Clancy removed from [location]."
38
+ - `yes` → proceed to remove commands, workflows, hooks, and settings entries (Steps 2a–2c)
39
39
 
40
- If "Both" was chosen in Step 1: confirm once for both, remove all four directories, print two confirmation lines.
40
+ If "Both" was chosen in Step 1: confirm once for both, remove everything for both locations.
41
+
42
+ ### Step 2a — Remove command and workflow directories
43
+
44
+ Delete both the commands directory and the workflows directory for the chosen location(s):
45
+ - Project-local: `.claude/commands/clancy/` and `.claude/clancy/`
46
+ - Global: `~/.claude/commands/clancy/` and `~/.claude/clancy/`
47
+
48
+ Print: `✓ Clancy commands removed from [location].`
49
+
50
+ ### Step 2b — Remove hooks
51
+
52
+ For each location being removed, delete these hook files if they exist:
53
+ - Project-local: `.claude/hooks/clancy-check-update.js`, `.claude/hooks/clancy-statusline.js`, `.claude/hooks/clancy-context-monitor.js`
54
+ - Global: `~/.claude/hooks/clancy-check-update.js`, `~/.claude/hooks/clancy-statusline.js`, `~/.claude/hooks/clancy-context-monitor.js`
55
+
56
+ Then remove the Clancy hook registrations from the corresponding `settings.json` (`.claude/settings.json` for local, `~/.claude/settings.json` for global):
57
+ - Remove any entry in `hooks.SessionStart` whose `command` contains `clancy-check-update`
58
+ - Remove any entry in `hooks.PostToolUse` whose `command` contains `clancy-context-monitor`
59
+ - Remove the `statusLine` key if its `command` value contains `clancy-statusline`
60
+ - If removing an entry leaves a `hooks.SessionStart` or `hooks.PostToolUse` array empty, remove the key entirely
61
+
62
+ Also remove the update check cache if it exists: `~/.claude/cache/clancy-update-check.json`
63
+
64
+ If `settings.json` does not exist or cannot be parsed, skip silently — do not create or overwrite it.
41
65
 
42
66
  ---
43
67