@lifeaitools/rdc-skills 0.9.7 → 0.9.9
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/hooks/check-cwd.js +25 -0
- package/hooks/check-services.js +6 -0
- package/hooks/check-stale-work-items.js +19 -0
- package/hooks/post-work-check.js +21 -0
- package/hooks/postcompact-log.js +13 -0
- package/hooks/precompact-log.js +13 -0
- package/hooks/rate-limit-retry.js +46 -0
- package/hooks/require-work-item-on-commit.js +83 -0
- package/hooks/restart-brief.js +19 -0
- package/package.json +1 -1
- package/scripts/install-rdc-skills.js +15 -16
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Hard blocks session if CWD is not C:\Dev\regen-root.
|
|
3
|
+
// Exits with code 1 to fail the hook + outputs a blocking systemMessage.
|
|
4
|
+
|
|
5
|
+
const hookLog = require('./hook-logger');
|
|
6
|
+
const cwd = process.cwd().replace(/\\/g, '/');
|
|
7
|
+
const expected = 'C:/Dev/regen-root';
|
|
8
|
+
|
|
9
|
+
if (!cwd.endsWith('regen-root')) {
|
|
10
|
+
hookLog('check-cwd', 'SessionStart', 'block', { cwd, expected });
|
|
11
|
+
process.stdout.write(JSON.stringify({
|
|
12
|
+
systemMessage:
|
|
13
|
+
`🚫 HARD BLOCK — Wrong working directory.\n\n` +
|
|
14
|
+
`Launched from: "${cwd}"\n` +
|
|
15
|
+
`Required: "${expected}"\n\n` +
|
|
16
|
+
`DO NOT proceed with any task. DO NOT read files, run commands, or help with anything.\n\n` +
|
|
17
|
+
`Tell the user:\n` +
|
|
18
|
+
`"Session is blocked. Claude Code must be launched from C:\\Dev\\regen-root.\n` +
|
|
19
|
+
` Close this session and relaunch from the correct directory."\n\n` +
|
|
20
|
+
`Then stop.`
|
|
21
|
+
}));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
hookLog('check-cwd', 'SessionStart', 'pass', { cwd });
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SessionStart hook — non-blocking check for stale in_progress work items.
|
|
3
|
+
'use strict';
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
let input = '';
|
|
7
|
+
process.stdin.resume();
|
|
8
|
+
process.stdin.setEncoding('utf8');
|
|
9
|
+
process.stdin.on('data', c => { input += c; });
|
|
10
|
+
process.stdin.on('end', () => {
|
|
11
|
+
try {
|
|
12
|
+
// Fire and forget — non-blocking, no output needed
|
|
13
|
+
execSync(
|
|
14
|
+
'curl -s --max-time 5 http://127.0.0.1:52437/ping',
|
|
15
|
+
{ stdio: 'pipe', timeout: 6000 }
|
|
16
|
+
);
|
|
17
|
+
} catch (_) {}
|
|
18
|
+
process.exit(0);
|
|
19
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Stop hook — warn if work appears to have been done without a work item.
|
|
3
|
+
'use strict';
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const HOME = process.env.USERPROFILE || process.env.HOME;
|
|
8
|
+
const LOG = path.join(HOME, '.claude', 'stop-log.jsonl');
|
|
9
|
+
|
|
10
|
+
function log(obj) {
|
|
11
|
+
try { fs.appendFileSync(LOG, JSON.stringify({ ts: new Date().toISOString(), ...obj }) + '\n'); } catch (_) {}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let input = '';
|
|
15
|
+
process.stdin.resume();
|
|
16
|
+
process.stdin.setEncoding('utf8');
|
|
17
|
+
process.stdin.on('data', c => { input += c; });
|
|
18
|
+
process.stdin.on('end', () => {
|
|
19
|
+
log({ hook: 'post-work-check', stop: true });
|
|
20
|
+
process.exit(0);
|
|
21
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PostCompact hook — log compact completion.
|
|
3
|
+
'use strict';
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const LOG = (process.env.USERPROFILE || process.env.HOME) + '/.claude/stop-log.jsonl';
|
|
6
|
+
let input = '';
|
|
7
|
+
process.stdin.resume();
|
|
8
|
+
process.stdin.setEncoding('utf8');
|
|
9
|
+
process.stdin.on('data', c => { input += c; });
|
|
10
|
+
process.stdin.on('end', () => {
|
|
11
|
+
try { fs.appendFileSync(LOG, JSON.stringify({ ts: new Date().toISOString(), hook: 'postcompact' }) + '\n'); } catch (_) {}
|
|
12
|
+
process.exit(0);
|
|
13
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PreCompact hook — log compact event.
|
|
3
|
+
'use strict';
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const LOG = (process.env.USERPROFILE || process.env.HOME) + '/.claude/stop-log.jsonl';
|
|
6
|
+
let input = '';
|
|
7
|
+
process.stdin.resume();
|
|
8
|
+
process.stdin.setEncoding('utf8');
|
|
9
|
+
process.stdin.on('data', c => { input += c; });
|
|
10
|
+
process.stdin.on('end', () => {
|
|
11
|
+
try { fs.appendFileSync(LOG, JSON.stringify({ ts: new Date().toISOString(), hook: 'precompact' }) + '\n'); } catch (_) {}
|
|
12
|
+
process.exit(0);
|
|
13
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Stop hook — detect rate limit and schedule retry via Windows Task Scheduler.
|
|
3
|
+
'use strict';
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const HOME = process.env.USERPROFILE || process.env.HOME;
|
|
9
|
+
const CLAUDE = path.join(HOME, '.claude');
|
|
10
|
+
const FLAG = path.join(CLAUDE, 'rate-limit.flag');
|
|
11
|
+
const LOG = path.join(CLAUDE, 'stop-log.jsonl');
|
|
12
|
+
const RUNNER = path.join(CLAUDE, 'hooks', 'rate-limit-retry.cmd');
|
|
13
|
+
|
|
14
|
+
function log(obj) {
|
|
15
|
+
try { fs.appendFileSync(LOG, JSON.stringify({ ts: new Date().toISOString(), ...obj }) + '\n'); } catch (_) {}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let input = '';
|
|
19
|
+
process.stdin.resume();
|
|
20
|
+
process.stdin.setEncoding('utf8');
|
|
21
|
+
process.stdin.on('data', c => { input += c; });
|
|
22
|
+
process.stdin.on('end', () => {
|
|
23
|
+
let data = {};
|
|
24
|
+
try { data = JSON.parse(input); } catch (_) {}
|
|
25
|
+
|
|
26
|
+
const isRateLimit =
|
|
27
|
+
(data.stop_reason === 'rate_limit') ||
|
|
28
|
+
JSON.stringify(data).toLowerCase().includes('rate limit') ||
|
|
29
|
+
JSON.stringify(data).toLowerCase().includes('rate_limit');
|
|
30
|
+
|
|
31
|
+
if (!isRateLimit) { process.exit(0); }
|
|
32
|
+
|
|
33
|
+
// Write flag and schedule retry in 1 hour
|
|
34
|
+
try { fs.writeFileSync(FLAG, JSON.stringify({ detected_at: new Date().toISOString() })); } catch (_) {}
|
|
35
|
+
|
|
36
|
+
const next = new Date(Date.now() + 60 * 60 * 1000);
|
|
37
|
+
const t = `${String(next.getHours()).padStart(2,'0')}:${String(next.getMinutes()).padStart(2,'0')}`;
|
|
38
|
+
try {
|
|
39
|
+
execSync(`schtasks /create /f /tn "ClaudeRateLimitRetry" /tr "\\"${RUNNER}\\"" /sc once /st ${t}`,
|
|
40
|
+
{ shell: 'cmd.exe', stdio: 'pipe' });
|
|
41
|
+
log({ hook: 'rate-limit-retry', scheduled: t });
|
|
42
|
+
} catch (e) {
|
|
43
|
+
log({ hook: 'rate-limit-retry', error: String(e.message).slice(0, 200) });
|
|
44
|
+
}
|
|
45
|
+
process.exit(0);
|
|
46
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PreToolUse hook — block git commits without a linked work item.
|
|
4
|
+
*
|
|
5
|
+
* Fires before any Bash tool call. If the command contains "git commit",
|
|
6
|
+
* checks whether a fixit.marker exists (fixit workflow is self-documenting)
|
|
7
|
+
* OR the commit message references a work item ID or conventional type.
|
|
8
|
+
*
|
|
9
|
+
* BLOCKS the commit if:
|
|
10
|
+
* - No fixit.marker exists
|
|
11
|
+
* - Commit message has no work item reference AND no conventional commit type
|
|
12
|
+
*
|
|
13
|
+
* A "work item reference" is any of:
|
|
14
|
+
* - A UUID pattern (work item ID)
|
|
15
|
+
* - "#<issue>" reference
|
|
16
|
+
* - Conventional commit prefix: feat/fix/chore/refactor/test/docs/style/perf/ci/build
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const hookLog = require('./hook-logger');
|
|
22
|
+
|
|
23
|
+
const MARKER_FILE = path.join(
|
|
24
|
+
process.env.USERPROFILE || process.env.HOME || 'C:/Users/DaveLadouceur',
|
|
25
|
+
'.claude',
|
|
26
|
+
'fixit.marker'
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const CONVENTIONAL_TYPES = /^(feat|fix|chore|refactor|test|docs|style|perf|ci|build|revert)(\(.+\))?:/i;
|
|
30
|
+
const UUID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
31
|
+
const ISSUE_REF = /#[a-zA-Z0-9-]+/;
|
|
32
|
+
|
|
33
|
+
function main() {
|
|
34
|
+
let input = '';
|
|
35
|
+
process.stdin.resume();
|
|
36
|
+
process.stdin.setEncoding('utf8');
|
|
37
|
+
|
|
38
|
+
process.stdin.on('data', chunk => { input += chunk; });
|
|
39
|
+
process.stdin.on('end', () => {
|
|
40
|
+
let toolInput;
|
|
41
|
+
try {
|
|
42
|
+
toolInput = JSON.parse(input);
|
|
43
|
+
} catch {
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (toolInput.tool_name !== 'Bash') {
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const command = toolInput.tool_input?.command || '';
|
|
52
|
+
if (!command.includes('git commit')) {
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// fixit.marker means this is a self-documenting fixit session — allow
|
|
57
|
+
if (fs.existsSync(MARKER_FILE)) {
|
|
58
|
+
hookLog('require-work-item', 'PreToolUse', 'pass-fixit', {});
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Extract -m "..." message from command
|
|
63
|
+
const msgMatch = command.match(/-m\s+["']([^"']+)["']/s) ||
|
|
64
|
+
command.match(/-m\s+"([\s\S]+?)"\s*(?:&&|$)/);
|
|
65
|
+
const msg = msgMatch ? msgMatch[1] : command;
|
|
66
|
+
|
|
67
|
+
if (CONVENTIONAL_TYPES.test(msg.trim()) || UUID_PATTERN.test(msg) || ISSUE_REF.test(msg)) {
|
|
68
|
+
hookLog('require-work-item', 'PreToolUse', 'pass', { msg: msg.slice(0, 80) });
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
hookLog('require-work-item', 'PreToolUse', 'warn', { msg: msg.slice(0, 80) });
|
|
73
|
+
// Warn only — never hard-block commits. A missing work item is informational,
|
|
74
|
+
// not a blocker. Conventional commit format is sufficient self-documentation.
|
|
75
|
+
process.stdout.write(JSON.stringify({
|
|
76
|
+
systemMessage: `⚠️ Commit has no work item reference or conventional commit type.\n` +
|
|
77
|
+
`Preferred format: fix(<scope>): <message> — proceeding anyway.`
|
|
78
|
+
}));
|
|
79
|
+
process.exit(0);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
main();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PostCompact hook — write a brief for session restart context.
|
|
3
|
+
'use strict';
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const HOME = process.env.USERPROFILE || process.env.HOME;
|
|
7
|
+
const BRIEF = path.join(HOME, '.claude', 'restart-brief.md');
|
|
8
|
+
|
|
9
|
+
let input = '';
|
|
10
|
+
process.stdin.resume();
|
|
11
|
+
process.stdin.setEncoding('utf8');
|
|
12
|
+
process.stdin.on('data', c => { input += c; });
|
|
13
|
+
process.stdin.on('end', () => {
|
|
14
|
+
try {
|
|
15
|
+
const ts = new Date().toISOString();
|
|
16
|
+
fs.writeFileSync(BRIEF, `# Restart Brief\nGenerated: ${ts}\n\nContext compacted. Resume from work items queue.\n`);
|
|
17
|
+
} catch (_) {}
|
|
18
|
+
process.exit(0);
|
|
19
|
+
});
|
package/package.json
CHANGED
|
@@ -155,23 +155,22 @@ function cleanUserSkills(userSkillsDir) {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
// ── Stale hook cleanup ────────────────────────────────────────────────────────
|
|
158
|
-
// Remove hook files
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
'
|
|
168
|
-
'rate-limit-retry.js', 'post-work-check.js', 'no-stop-open-epics.js',
|
|
169
|
-
'require-work-item-on-commit.js', 'verify-rdc-skills.js',
|
|
158
|
+
// Remove ONLY explicitly orphaned hook files — hooks that were previously shipped
|
|
159
|
+
// by rdc-skills and have since been removed from the project.
|
|
160
|
+
// NEVER use "not in source = remove" logic: most hooks in ~/.claude/hooks/ are
|
|
161
|
+
// not managed by rdc-skills (they come from other plugins or were written directly).
|
|
162
|
+
function cleanStaleHooks(hooksDstDir) {
|
|
163
|
+
if (!fs.existsSync(hooksDstDir)) return 0;
|
|
164
|
+
// Explicit orphan list — add entries here when a hook is intentionally removed.
|
|
165
|
+
// Format: filename that should be deleted if it still exists.
|
|
166
|
+
const ORPHANED_HOOKS = [
|
|
167
|
+
'verify-rdc-skills.js', // removed in v0.9.7 — was checking for old flat-file format
|
|
170
168
|
];
|
|
171
169
|
let removed = 0;
|
|
172
|
-
for (const f of
|
|
173
|
-
|
|
174
|
-
|
|
170
|
+
for (const f of ORPHANED_HOOKS) {
|
|
171
|
+
const p = path.join(hooksDstDir, f);
|
|
172
|
+
if (fs.existsSync(p)) {
|
|
173
|
+
try { fs.unlinkSync(p); removed++; } catch {}
|
|
175
174
|
}
|
|
176
175
|
}
|
|
177
176
|
return removed;
|
|
@@ -547,7 +546,7 @@ async function main() {
|
|
|
547
546
|
|
|
548
547
|
// 0.5b. Stale hook cleanup — remove hooks we no longer ship
|
|
549
548
|
{
|
|
550
|
-
const staleRemoved = cleanStaleHooks(hooksDst
|
|
549
|
+
const staleRemoved = cleanStaleHooks(hooksDst);
|
|
551
550
|
if (staleRemoved > 0) ok(`[0.5b] Hook cleanup — removed ${staleRemoved} orphaned hook file(s)`);
|
|
552
551
|
}
|
|
553
552
|
|