chief-clancy 0.1.7 → 0.2.0-beta.2
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 +18 -7
- package/bin/install.js +61 -109
- 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 -42
- package/src/workflows/uninstall.md +26 -2
- package/src/workflows/update.md +0 -28
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` |
|
|
@@ -188,7 +192,7 @@ Clancy also merges a section into your `CLAUDE.md` (or creates one) that tells C
|
|
|
188
192
|
|
|
189
193
|
## Optional enhancements
|
|
190
194
|
|
|
191
|
-
Set during `/clancy:init` advanced setup, or by editing `.clancy/.env` directly.
|
|
195
|
+
Set during `/clancy:init` advanced setup, or by editing `.clancy/.env` directly.
|
|
192
196
|
|
|
193
197
|
### Figma MCP
|
|
194
198
|
|
|
@@ -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
|
```
|
|
@@ -405,8 +418,6 @@ lsof -ti:5173 | xargs kill -9 # replace 5173 with your PLAYWRIGHT_DEV_PORT
|
|
|
405
418
|
|
|
406
419
|
Or directly: `npx chief-clancy@latest`
|
|
407
420
|
|
|
408
|
-
The update workflow shows what's changed (changelog diff) and asks for confirmation before overwriting. If you've customised any command or workflow files, they're automatically backed up to `.claude/clancy/local-patches/` before the update — check there to reapply your changes afterwards.
|
|
409
|
-
|
|
410
421
|
---
|
|
411
422
|
|
|
412
423
|
**Uninstalling?**
|
|
@@ -415,7 +426,7 @@ The update workflow shows what's changed (changelog diff) and asks for confirmat
|
|
|
415
426
|
/clancy:uninstall
|
|
416
427
|
```
|
|
417
428
|
|
|
418
|
-
Removes slash commands from your chosen location.
|
|
429
|
+
Removes slash commands from your chosen location. Optionally removes `.clancy/` (credentials and docs). Never touches `CLAUDE.md`.
|
|
419
430
|
|
|
420
431
|
---
|
|
421
432
|
|
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) {
|
|
@@ -48,13 +49,6 @@ async function choose(question, options, defaultChoice = 1) {
|
|
|
48
49
|
return raw.trim() || String(defaultChoice);
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
const crypto = require('crypto');
|
|
52
|
-
|
|
53
|
-
function fileHash(filePath) {
|
|
54
|
-
const content = fs.readFileSync(filePath);
|
|
55
|
-
return crypto.createHash('sha256').digest('hex', content);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
52
|
function copyDir(src, dest) {
|
|
59
53
|
// Use lstatSync (not statSync) to detect symlinks — statSync follows them and misreports
|
|
60
54
|
if (fs.existsSync(dest)) {
|
|
@@ -71,73 +65,6 @@ function copyDir(src, dest) {
|
|
|
71
65
|
}
|
|
72
66
|
}
|
|
73
67
|
|
|
74
|
-
/**
|
|
75
|
-
* Build a manifest of installed files with SHA-256 hashes.
|
|
76
|
-
* Format: { "relative/path.md": "<sha256>", ... }
|
|
77
|
-
*/
|
|
78
|
-
function buildManifest(baseDir) {
|
|
79
|
-
const manifest = {};
|
|
80
|
-
function walk(dir, prefix) {
|
|
81
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
82
|
-
const full = path.join(dir, entry.name);
|
|
83
|
-
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
84
|
-
if (entry.isDirectory()) {
|
|
85
|
-
walk(full, rel);
|
|
86
|
-
} else {
|
|
87
|
-
const content = fs.readFileSync(full);
|
|
88
|
-
manifest[rel] = crypto.createHash('sha256').update(content).digest('hex');
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
walk(baseDir, '');
|
|
93
|
-
return manifest;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Detect files modified by the user since last install by comparing
|
|
98
|
-
* current file hashes against the stored manifest. Returns array of
|
|
99
|
-
* { rel, absPath } for modified files.
|
|
100
|
-
*/
|
|
101
|
-
function detectModifiedFiles(baseDir, manifestPath) {
|
|
102
|
-
if (!fs.existsSync(manifestPath)) return [];
|
|
103
|
-
let manifest;
|
|
104
|
-
try {
|
|
105
|
-
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
106
|
-
} catch { return []; }
|
|
107
|
-
|
|
108
|
-
const modified = [];
|
|
109
|
-
for (const [rel, hash] of Object.entries(manifest)) {
|
|
110
|
-
const absPath = path.join(baseDir, rel);
|
|
111
|
-
if (!fs.existsSync(absPath)) continue;
|
|
112
|
-
const content = fs.readFileSync(absPath);
|
|
113
|
-
const currentHash = crypto.createHash('sha256').update(content).digest('hex');
|
|
114
|
-
if (currentHash !== hash) {
|
|
115
|
-
modified.push({ rel, absPath });
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return modified;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Back up modified files to a patches directory alongside the install.
|
|
123
|
-
* Returns the backup directory path if any files were backed up.
|
|
124
|
-
*/
|
|
125
|
-
function backupModifiedFiles(modified, patchesDir) {
|
|
126
|
-
if (modified.length === 0) return null;
|
|
127
|
-
fs.mkdirSync(patchesDir, { recursive: true });
|
|
128
|
-
for (const { rel, absPath } of modified) {
|
|
129
|
-
const backupPath = path.join(patchesDir, rel);
|
|
130
|
-
fs.mkdirSync(path.dirname(backupPath), { recursive: true });
|
|
131
|
-
fs.copyFileSync(absPath, backupPath);
|
|
132
|
-
}
|
|
133
|
-
// Write metadata so /clancy:update workflow knows what was backed up
|
|
134
|
-
fs.writeFileSync(
|
|
135
|
-
path.join(patchesDir, 'backup-meta.json'),
|
|
136
|
-
JSON.stringify({ backed_up: modified.map(m => m.rel), date: new Date().toISOString() }, null, 2)
|
|
137
|
-
);
|
|
138
|
-
return patchesDir;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
68
|
async function main() {
|
|
142
69
|
console.log('');
|
|
143
70
|
console.log(blue(' ██████╗██╗ █████╗ ███╗ ██╗ ██████╗██╗ ██╗'));
|
|
@@ -189,42 +116,14 @@ async function main() {
|
|
|
189
116
|
console.log(dim(` Installing to: ${dest}`));
|
|
190
117
|
|
|
191
118
|
try {
|
|
192
|
-
// Determine manifest and patches paths (sibling to commands dir)
|
|
193
|
-
const claudeDir = path.dirname(path.dirname(dest)); // .claude/ (parent of commands/)
|
|
194
|
-
const manifestPath = path.join(claudeDir, 'clancy', 'manifest.json');
|
|
195
|
-
const patchesDir = path.join(claudeDir, 'clancy', 'local-patches');
|
|
196
|
-
|
|
197
119
|
if (fs.existsSync(dest) || fs.existsSync(workflowsDest)) {
|
|
198
120
|
console.log('');
|
|
199
|
-
|
|
200
|
-
// Detect user-modified files before overwriting
|
|
201
|
-
const modified = detectModifiedFiles(dest, manifestPath);
|
|
202
|
-
const modifiedWorkflows = detectModifiedFiles(workflowsDest, manifestPath.replace('manifest.json', 'workflows-manifest.json'));
|
|
203
|
-
const allModified = [...modified, ...modifiedWorkflows];
|
|
204
|
-
|
|
205
|
-
if (allModified.length > 0) {
|
|
206
|
-
console.log(blue(' Modified files detected:'));
|
|
207
|
-
for (const { rel } of allModified) {
|
|
208
|
-
console.log(` ${dim('•')} ${rel}`);
|
|
209
|
-
}
|
|
210
|
-
console.log('');
|
|
211
|
-
console.log(dim(' These will be backed up to .claude/clancy/local-patches/'));
|
|
212
|
-
console.log(dim(' before overwriting. You can reapply them after the update.'));
|
|
213
|
-
console.log('');
|
|
214
|
-
}
|
|
215
|
-
|
|
216
121
|
const overwrite = await ask(blue(` Commands already exist at ${dest}. Overwrite? [y/N] `));
|
|
217
122
|
if (!overwrite.trim().toLowerCase().startsWith('y')) {
|
|
218
123
|
console.log('\n Aborted. No files changed.');
|
|
219
124
|
rl.close();
|
|
220
125
|
process.exit(0);
|
|
221
126
|
}
|
|
222
|
-
|
|
223
|
-
// Back up modified files before overwriting
|
|
224
|
-
if (allModified.length > 0) {
|
|
225
|
-
backupModifiedFiles(allModified, patchesDir);
|
|
226
|
-
console.log(green(`\n ✓ ${allModified.length} modified file(s) backed up to local-patches/`));
|
|
227
|
-
}
|
|
228
127
|
}
|
|
229
128
|
|
|
230
129
|
copyDir(COMMANDS_SRC, dest);
|
|
@@ -251,13 +150,65 @@ async function main() {
|
|
|
251
150
|
// Write VERSION file so /clancy:doctor and /clancy:update can read the installed version
|
|
252
151
|
fs.writeFileSync(path.join(dest, 'VERSION'), PKG.version);
|
|
253
152
|
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
+
}
|
|
261
212
|
|
|
262
213
|
console.log('');
|
|
263
214
|
console.log(green(' ✓ Clancy installed successfully.'));
|
|
@@ -273,6 +224,7 @@ async function main() {
|
|
|
273
224
|
['/clancy:map-codebase', 'Scan codebase with 5 parallel agents'],
|
|
274
225
|
['/clancy:run', 'Run Clancy in loop mode'],
|
|
275
226
|
['/clancy:once', 'Pick up one ticket and stop'],
|
|
227
|
+
['/clancy:dry-run', 'Preview next ticket without making changes'],
|
|
276
228
|
['/clancy:status', 'Show next tickets without running'],
|
|
277
229
|
['/clancy:review', 'Score next ticket and get recommendations'],
|
|
278
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.
|
|
3
|
+
"version": "0.2.0-beta.2",
|
|
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.
|