@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.
@@ -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,6 @@
1
+ #!/usr/bin/env node
2
+ // PostToolUse hook — non-blocking service health pass-through.
3
+ 'use strict';
4
+ process.stdin.resume();
5
+ process.stdin.on('data', () => {});
6
+ process.stdin.on('end', () => { process.exit(0); });
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/rdc-skills",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
4
4
  "description": "RDC typed-agent dispatch skill suite for Claude Code \u00e2\u20ac\u201d plan, build, review, overnight builds",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -155,23 +155,22 @@ function cleanUserSkills(userSkillsDir) {
155
155
  }
156
156
 
157
157
  // ── Stale hook cleanup ────────────────────────────────────────────────────────
158
- // Remove hook files from ~/.claude/hooks/ that are no longer in the source hooks/ dir.
159
- // Prevents orphaned hooks (removed from source but still installed) from misfiring.
160
- function cleanStaleHooks(hooksDstDir, hooksSrcDir) {
161
- if (!fs.existsSync(hooksDstDir) || !fs.existsSync(hooksSrcDir)) return 0;
162
- const sourceFiles = new Set(fs.readdirSync(hooksSrcDir).filter(f => f.endsWith('.js')));
163
- // These hooks were written by us (rdc-skills) safe to remove if no longer in source.
164
- // We only manage our own hooks; never touch hooks we didn't install.
165
- const knownHooks = [
166
- 'check-cwd.js', 'check-stale-work-items.js', 'check-services.js',
167
- 'precompact-log.js', 'postcompact-log.js', 'restart-brief.js',
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 knownHooks) {
173
- if (!sourceFiles.has(f) && fs.existsSync(path.join(hooksDstDir, f))) {
174
- try { fs.unlinkSync(path.join(hooksDstDir, f)); removed++; } catch {}
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, hooksSrc);
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