@lifeaitools/rdc-skills 0.35.13 → 0.35.15
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.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rdc",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.15",
|
|
4
4
|
"description": "RDC typed-agent dispatch skill suite for Claude Code — plan, build, review, overnight unattended builds with work-item tracking and TDD enforcement.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "LIFEAI",
|
|
@@ -25,6 +25,33 @@
|
|
|
25
25
|
*/
|
|
26
26
|
const hookLog = require('./hook-logger');
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The mitigator — OPTIONAL, because this hook crosses a repo boundary.
|
|
30
|
+
*
|
|
31
|
+
* EVERY BLOCK ROUTES THROUGH THE MITIGATOR (operator: "any block"). Audited
|
|
32
|
+
* 2026-08-30: 9 PreToolUse hooks wired, only the two guard dispatchers called
|
|
33
|
+
* mitigate(), so seven families refused with no repeat count and no
|
|
34
|
+
* classification — the signal that caught rtp-steering-grep being wrong in
|
|
35
|
+
* BOTH directions.
|
|
36
|
+
*
|
|
37
|
+
* This file ships in rdc-skills and installs to ~/.claude/hooks. The mitigator
|
|
38
|
+
* lives in lifeai-env, which is a SEPARATE repo that a consumer of this package
|
|
39
|
+
* may not have. So the import is best-effort and its absence is not an error:
|
|
40
|
+
* the block still lands, it simply arrives unannotated. A refusal that depended
|
|
41
|
+
* on a sibling repo being installed would be worse than no annotation at all.
|
|
42
|
+
*/
|
|
43
|
+
let mitigator = null;
|
|
44
|
+
async function loadMitigator() {
|
|
45
|
+
try {
|
|
46
|
+
const path = require('node:path');
|
|
47
|
+
const fs = require('node:fs');
|
|
48
|
+
const envRoot = process.env.LIFEAI_ENV || 'C:/Dev/lifeai-env';
|
|
49
|
+
const file = path.join(envRoot, 'hooks', 'lib', 'guard-mitigator.mjs');
|
|
50
|
+
if (!fs.existsSync(file)) return; // no lifeai-env here — that is fine
|
|
51
|
+
mitigator = await import(require('node:url').pathToFileURL(file).href);
|
|
52
|
+
} catch { /* annotation is never load-bearing */ }
|
|
53
|
+
}
|
|
54
|
+
|
|
28
55
|
function readStdin() {
|
|
29
56
|
return new Promise((resolve) => {
|
|
30
57
|
let input = '';
|
|
@@ -37,8 +64,28 @@ function readStdin() {
|
|
|
37
64
|
|
|
38
65
|
function block(message, details = {}) {
|
|
39
66
|
hookLog('foreground-process-gate', 'PreToolUse', 'block', details);
|
|
67
|
+
|
|
68
|
+
let mitigation = '';
|
|
69
|
+
try {
|
|
70
|
+
if (mitigator) {
|
|
71
|
+
// details.kind already distinguishes the two refusals, so the rule id is
|
|
72
|
+
// taken from it rather than parsed out of the message — guard-mitigator
|
|
73
|
+
// keys on (rule id, argv shape) and explicitly forbids reading wording.
|
|
74
|
+
mitigation = mitigator.mitigationLine(mitigator.mitigate(
|
|
75
|
+
{
|
|
76
|
+
rule: `foreground-process-gate-${details.kind || 'unknown'}`,
|
|
77
|
+
cmd: blockSubject,
|
|
78
|
+
cwd: process.cwd(),
|
|
79
|
+
dir: mitigator.blockLedgerDir(),
|
|
80
|
+
sessionId: process.env.LIFEAI_SESSION_ID,
|
|
81
|
+
},
|
|
82
|
+
{ evaluate: null }, // a headed-browser refusal has no command to re-evaluate
|
|
83
|
+
));
|
|
84
|
+
}
|
|
85
|
+
} catch { /* annotation is never load-bearing */ }
|
|
86
|
+
|
|
40
87
|
process.stdout.write(JSON.stringify({
|
|
41
|
-
systemMessage: `HARD BLOCK — Foreground process launch rejected.\n\n${message}`,
|
|
88
|
+
systemMessage: `HARD BLOCK — Foreground process launch rejected.\n\n${message}${mitigation ? `\n\n${mitigation}` : ''}`,
|
|
42
89
|
}));
|
|
43
90
|
process.exit(1);
|
|
44
91
|
}
|
|
@@ -72,12 +119,20 @@ function checkPlaywright(command) {
|
|
|
72
119
|
}
|
|
73
120
|
}
|
|
74
121
|
|
|
122
|
+
let blockSubject = '';
|
|
123
|
+
|
|
75
124
|
async function main() {
|
|
76
125
|
let raw;
|
|
77
126
|
try { raw = JSON.parse(await readStdin()); } catch { process.exit(0); }
|
|
78
127
|
const command = toolText(raw);
|
|
79
128
|
if (!command) pass({ reason: 'no-command' });
|
|
80
129
|
|
|
130
|
+
// Preload BEFORE any check can block, so block() stays synchronous. The
|
|
131
|
+
// subject is the command itself, so repeated attempts at the SAME shape
|
|
132
|
+
// register as repeats rather than as unrelated events.
|
|
133
|
+
await loadMitigator();
|
|
134
|
+
blockSubject = command;
|
|
135
|
+
|
|
81
136
|
checkPlaywright(command);
|
|
82
137
|
|
|
83
138
|
pass({ reason: 'clean' });
|
|
@@ -53,6 +53,54 @@ const os = require('os');
|
|
|
53
53
|
const { execFileSync } = require('child_process');
|
|
54
54
|
const hookLog = require('./hook-logger');
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* The mitigator — OPTIONAL, because this hook crosses a repo boundary.
|
|
58
|
+
*
|
|
59
|
+
* EVERY BLOCK ROUTES THROUGH THE MITIGATOR (operator: "any block"). Audited
|
|
60
|
+
* 2026-08-30: 9 PreToolUse hooks wired, only the two guard dispatchers called
|
|
61
|
+
* mitigate(), so seven families refused with no repeat count and no
|
|
62
|
+
* classification.
|
|
63
|
+
*
|
|
64
|
+
* This file is the reason that matters most. Its unreadable-message refusal
|
|
65
|
+
* fired TWICE in one session on `git commit -F "$SP/msg.txt"` — a path the
|
|
66
|
+
* check cannot resolve because it holds a shell variable, not because anything
|
|
67
|
+
* was wrong with the message. Two identical refusals, no repeat count, nothing
|
|
68
|
+
* to distinguish "you keep doing this" from "this rule keeps being wrong". That
|
|
69
|
+
* is exactly the signal the mitigator exists to raise, and this hook could not
|
|
70
|
+
* raise it.
|
|
71
|
+
*
|
|
72
|
+
* Ships in rdc-skills and installs to ~/.claude/hooks, where lifeai-env may not
|
|
73
|
+
* exist, so the import is best-effort and its absence is not an error.
|
|
74
|
+
*/
|
|
75
|
+
let mitigator = null;
|
|
76
|
+
let blockSubject = '';
|
|
77
|
+
async function loadMitigator() {
|
|
78
|
+
try {
|
|
79
|
+
const envRoot = process.env.LIFEAI_ENV || 'C:/Dev/lifeai-env';
|
|
80
|
+
const file = path.join(envRoot, 'hooks', 'lib', 'guard-mitigator.mjs');
|
|
81
|
+
if (!fs.existsSync(file)) return; // no lifeai-env here — that is fine
|
|
82
|
+
mitigator = await import(require('node:url').pathToFileURL(file).href);
|
|
83
|
+
} catch { /* annotation is never load-bearing */ }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** One emit path for both refusals, so an annotation cannot be added to one and forgotten on the other. */
|
|
87
|
+
function denyWith(rule, reason) {
|
|
88
|
+
let mitigation = '';
|
|
89
|
+
try {
|
|
90
|
+
if (mitigator) {
|
|
91
|
+
mitigation = mitigator.mitigationLine(mitigator.mitigate(
|
|
92
|
+
{ rule, cmd: blockSubject, cwd: process.cwd(), dir: mitigator.blockLedgerDir(), sessionId: process.env.LIFEAI_SESSION_ID },
|
|
93
|
+
{ evaluate: null }, // a commit-message refusal has no command to re-evaluate
|
|
94
|
+
));
|
|
95
|
+
}
|
|
96
|
+
} catch { /* annotation is never load-bearing */ }
|
|
97
|
+
process.stdout.write(JSON.stringify({
|
|
98
|
+
decision: 'block',
|
|
99
|
+
reason: mitigation ? `${reason}\n\n${mitigation}` : reason,
|
|
100
|
+
}));
|
|
101
|
+
return process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
|
|
56
104
|
const MARKER_FILE = path.join(
|
|
57
105
|
process.env.USERPROFILE || process.env.HOME || os.homedir(),
|
|
58
106
|
'.claude',
|
|
@@ -330,14 +378,13 @@ async function preToolUse(raw) {
|
|
|
330
378
|
// for the wrong thing.
|
|
331
379
|
if (!readable) {
|
|
332
380
|
hookLog('require-work-item', 'PreToolUse', 'block-unreadable', {});
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
381
|
+
return denyWith(
|
|
382
|
+
'require-work-item-unreadable',
|
|
383
|
+
'⛔ [require-work-item] The commit message arrives on stdin, so this check cannot read it '
|
|
336
384
|
+ 'and will not guess. Nothing is wrong with your message — it is unverifiable from here. '
|
|
337
385
|
+ 'Re-run with the message where the check can see it: `git commit -F <path>` (a file), '
|
|
338
386
|
+ 'a heredoc (`git commit -F - <<\'EOF\' … EOF`), or `-m "<type>(<scope>): …"`.',
|
|
339
|
-
|
|
340
|
-
return process.exit(0);
|
|
387
|
+
);
|
|
341
388
|
}
|
|
342
389
|
|
|
343
390
|
if (CONVENTIONAL_TYPES.test(msg.trim()) || UUID_PATTERN.test(msg) || ISSUE_REF.test(msg)) {
|
|
@@ -349,14 +396,13 @@ async function preToolUse(raw) {
|
|
|
349
396
|
// or none declared at all) requires a work item and none was referenced —
|
|
350
397
|
// this used to print a sentence and let the commit through regardless.
|
|
351
398
|
hookLog('require-work-item', 'PreToolUse', 'block', { msg: msg.slice(0, 80) });
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
399
|
+
return denyWith(
|
|
400
|
+
'require-work-item',
|
|
401
|
+
'⛔ [require-work-item] No work item reference or conventional commit type, and the current '
|
|
355
402
|
+ 'flow requires one (build/refactor/overnight, or no flow declared). Add a Work-Item UUID or '
|
|
356
403
|
+ '#issue ref to the message, use fix(<scope>): ..., or declare a flow that does not need one '
|
|
357
404
|
+ '(rdc-flow.mjs: plan/design/collab) if this genuinely ships no trackable work.',
|
|
358
|
-
|
|
359
|
-
return process.exit(0);
|
|
405
|
+
);
|
|
360
406
|
}
|
|
361
407
|
|
|
362
408
|
// ---------------------------------------------------------------------------
|
|
@@ -375,6 +421,12 @@ async function main() {
|
|
|
375
421
|
let raw;
|
|
376
422
|
try { raw = JSON.parse(input); } catch { return process.exit(0); }
|
|
377
423
|
|
|
424
|
+
// Preload BEFORE any refusal can fire, so denyWith() stays synchronous. The
|
|
425
|
+
// subject is the command, so repeated refusals of the SAME shape register as
|
|
426
|
+
// repeats — which is the whole point for this hook in particular.
|
|
427
|
+
await loadMitigator();
|
|
428
|
+
blockSubject = String(raw?.tool_input?.command || '');
|
|
429
|
+
|
|
378
430
|
const event = raw.hook_event_name || raw.hookEventName || 'PreToolUse';
|
|
379
431
|
|
|
380
432
|
if (event === 'PostToolUse') {
|