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 +16 -3
- package/bin/install.js +62 -0
- package/hooks/clancy-check-update.js +71 -0
- package/hooks/clancy-context-monitor.js +101 -0
- package/hooks/clancy-statusline.js +82 -0
- package/package.json +2 -1
- package/src/commands/dry-run.md +14 -0
- package/src/commands/help.md +2 -1
- package/src/commands/once.md +4 -0
- package/src/templates/.env.example.jira +6 -0
- package/src/templates/.env.example.linear +6 -0
- package/src/templates/CLAUDE.md +12 -5
- package/src/templates/scripts/clancy-once-github.sh +27 -3
- package/src/templates/scripts/clancy-once-linear.sh +71 -3
- package/src/templates/scripts/clancy-once.sh +66 -3
- package/src/workflows/init.md +1 -1
- package/src/workflows/once.md +16 -0
- package/src/workflows/scaffold.md +176 -9
- package/src/workflows/settings.md +73 -1
- package/src/workflows/uninstall.md +26 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Autonomous, board-driven development for Claude Code.**
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/chief-clancy) [](./LICENSE) [](https://www.npmjs.com/package/chief-clancy) [](./LICENSE) [](./test/) [](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.
|
|
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
|
-
#
|
|
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
|
|
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`.
|
package/src/commands/help.md
CHANGED
|
@@ -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:
|
|
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
|
|
package/src/commands/once.md
CHANGED
|
@@ -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
|
package/src/templates/CLAUDE.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
package/src/workflows/init.md
CHANGED
|
@@ -420,4 +420,4 @@ Clancy is ready.
|
|
|
420
420
|
- Config: `.clancy/.env`
|
|
421
421
|
- CLAUDE.md: updated
|
|
422
422
|
|
|
423
|
-
Run `/clancy:
|
|
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.
|
package/src/workflows/once.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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` →
|
|
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
|
|
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
|
|