@lifeaitools/rdc-skills 0.9.35 → 0.9.37
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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +28 -0
- package/README.md +22 -1
- package/guides/rdc-skills-startup.md +30 -0
- package/hooks/rdc-invocation-marker.js +17 -3
- package/hooks/rdc-output-contract-gate.js +9 -0
- package/package.json +4 -3
- package/scripts/install-rdc-skills.js +116 -17
- package/scripts/install.ps1 +36 -20
- package/scripts/self-test.mjs +36 -0
- package/scripts/test-rdc-hooks.mjs +145 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## v0.9.37 — RDC hook loop guard and behavior tests
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Stop hook reentry.** `rdc-output-contract-gate.js` now exits cleanly when Claude is already inside an active Stop-hook retry, preventing noncompliant-but-legitimate turns from hard-looping the session.
|
|
15
|
+
- **Marker deduplication.** `rdc-invocation-marker.js` now preserves the original marker when `UserPromptExpansion` and `UserPromptSubmit` both fire for the same RDC command within the same second.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **Hook behavioral tests.** New `scripts/test-rdc-hooks.mjs` executes the marker and Stop hooks against real JSON payloads, and `self-test --strict` now fails if those behavioral checks fail.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## v0.9.36 — Portable installer profiles and startup onboarding
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **Installer profiles.** `--profile core` installs clean-box-safe hooks only; `--profile lifeai` keeps the LIFEAI/regen-root cwd, clauth, Supabase, and overnight hooks. `auto` chooses `lifeai` only when a sibling `regen-root` project is detected.
|
|
28
|
+
- **Managed startup guide.** New `guides/rdc-skills-startup.md` explains what RDC skills add, the active profiles, and where to find skills, commands, guides, hooks, and installer behavior.
|
|
29
|
+
- **Optional startup block writer.** `--project-root <path> --write-startup-blocks` writes `.rdc/guides/rdc-skills-startup.md` plus managed RDC sections in project `CLAUDE.md` and `AGENTS.md`.
|
|
30
|
+
- **Install welcome.** The installer now prints the selected profile, what it enables, and where to look next.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- The default clean-box profile no longer wires regen-root-specific hooks such as cwd lock, Supabase work-item exit gate, rate-limit retry, or overnight open-epic guard.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
10
38
|
## v0.9.35 — RDC output-contract hook enforcement
|
|
11
39
|
|
|
12
40
|
### Added
|
package/README.md
CHANGED
|
@@ -45,6 +45,27 @@ rm -rf ~/.claude/plugins/cache/rdc-skills/rdc-skills/0.7.*
|
|
|
45
45
|
|
|
46
46
|
Use **one method only**: plugin (Option A or B) OR manual install — never both.
|
|
47
47
|
|
|
48
|
+
### Installer profiles
|
|
49
|
+
|
|
50
|
+
`scripts/install-rdc-skills.js` supports profiles:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
node scripts/install-rdc-skills.js --profile core
|
|
54
|
+
node scripts/install-rdc-skills.js --profile lifeai
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- `core` is safe for a clean machine with only rdc-skills installed. It wires portable hooks only: RDC output-contract enforcement, foreground-process policy, commit-message hygiene, and non-blocking work logging.
|
|
58
|
+
- `lifeai` is for the LIFEAI/regen-root workstation. It additionally wires the regen-root cwd lock, clauth/Supabase work-item gates, overnight queue guard, and LIFEAI session hooks.
|
|
59
|
+
- `auto` is the default. It chooses `lifeai` only when a sibling `regen-root` project is detected; otherwise it chooses `core`.
|
|
60
|
+
|
|
61
|
+
To add managed startup instructions to a project:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
node scripts/install-rdc-skills.js --project-root /path/to/project --write-startup-blocks
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This writes `.rdc/guides/rdc-skills-startup.md` and managed RDC sections in `CLAUDE.md` and `AGENTS.md`.
|
|
68
|
+
|
|
48
69
|
### Legacy install scripts (deprecated since v0.6.0)
|
|
49
70
|
|
|
50
71
|
`scripts/install.sh` and `scripts/install.ps1` copy files into `~/.claude/skills/user/`. These exist only for environments that cannot use the plugin system. Running them alongside the plugin creates duplicates. Do not use them if you have the plugin installed.
|
|
@@ -176,7 +197,7 @@ python3 -c "import json; print(json.load(open('package.json'))['version'])"
|
|
|
176
197
|
/rdc:help
|
|
177
198
|
```
|
|
178
199
|
|
|
179
|
-
Current version: **v0.9.
|
|
200
|
+
Current version: **v0.9.36**
|
|
180
201
|
|
|
181
202
|
## Quick Start
|
|
182
203
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# RDC Skills Startup Contract
|
|
2
|
+
> Managed by `rdc-skills`. Keep local project-specific details in adjacent project guides.
|
|
3
|
+
|
|
4
|
+
## What RDC Skills Adds
|
|
5
|
+
|
|
6
|
+
- Slash commands for the RDC workflow: plan, build, review, report, design, deploy, release, status, and work item operations.
|
|
7
|
+
- Output-contract enforcement for active `/rdc:*` turns: visible checklist rows plus a final verdict line.
|
|
8
|
+
- Engineering behavior guidance: small scoped changes, explicit assumptions, evidence for completed work, and honest blockers.
|
|
9
|
+
- Optional project integrations for work items, credentials, deployments, and release automation.
|
|
10
|
+
|
|
11
|
+
## Agent Startup Rules
|
|
12
|
+
|
|
13
|
+
1. Read the active project instructions first (`CLAUDE.md` for Claude Code, `AGENTS.md` for Codex).
|
|
14
|
+
2. For any `/rdc:*` invocation, follow `.rdc/guides/output-contract.md` and `.rdc/guides/engineering-behavior.md`.
|
|
15
|
+
3. Do not treat skill prose as proof. Completed work needs evidence: command output, test result, route probe, screenshot, SQL result, or source citation.
|
|
16
|
+
4. If a project has its own approval gates, architecture rules, or credential model, those project rules override generic RDC defaults.
|
|
17
|
+
5. When an RDC skill cannot access the required project services, stop with a specific blocker instead of inventing a fallback.
|
|
18
|
+
|
|
19
|
+
## Profiles
|
|
20
|
+
|
|
21
|
+
- `core`: portable defaults for a clean machine. No regen-root cwd lock, clauth requirement, Supabase exit gate, or LIFEAI deployment assumption.
|
|
22
|
+
- `lifeai`: LIFEAI/regen-root defaults. Enables project-specific hooks and workflows for clauth, Supabase work items, deployment, and overnight queue behavior.
|
|
23
|
+
|
|
24
|
+
## Where To Look
|
|
25
|
+
|
|
26
|
+
- Skills: `skills/<name>/SKILL.md`
|
|
27
|
+
- Commands: `commands/<name>.md`
|
|
28
|
+
- Guides: `guides/*.md` and project copies under `.rdc/guides/`
|
|
29
|
+
- Hooks: `hooks/*.js`
|
|
30
|
+
- Installer: `scripts/install-rdc-skills.js`
|
|
@@ -76,7 +76,7 @@ function detectRdc(raw) {
|
|
|
76
76
|
|
|
77
77
|
if (event === 'UserPromptSubmit') {
|
|
78
78
|
const prompt = String(raw.prompt || '').trim();
|
|
79
|
-
const m = prompt.match(/^\/
|
|
79
|
+
const m = prompt.match(/^\/rdc[:-]([a-z][a-z0-9-]*)\b/i);
|
|
80
80
|
if (!m) return null;
|
|
81
81
|
const command = normalizeCommandName(m[1]);
|
|
82
82
|
if (RDC_COMMANDS.has(command)) return command;
|
|
@@ -87,6 +87,20 @@ function detectRdc(raw) {
|
|
|
87
87
|
|
|
88
88
|
function writeMarker(raw, command) {
|
|
89
89
|
fs.mkdirSync(markerDir(), { recursive: true });
|
|
90
|
+
const p = markerPath(raw.session_id);
|
|
91
|
+
if (fs.existsSync(p)) {
|
|
92
|
+
try {
|
|
93
|
+
const existing = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
94
|
+
const started = Date.parse(existing.started_at || '');
|
|
95
|
+
const isRecentDuplicate = Number.isFinite(started) &&
|
|
96
|
+
Date.now() - started <= 1000 &&
|
|
97
|
+
existing.session_id === (raw.session_id || null) &&
|
|
98
|
+
existing.command === command;
|
|
99
|
+
if (isRecentDuplicate) {
|
|
100
|
+
return { ...existing, deduped: true };
|
|
101
|
+
}
|
|
102
|
+
} catch {}
|
|
103
|
+
}
|
|
90
104
|
const marker = {
|
|
91
105
|
session_id: raw.session_id || null,
|
|
92
106
|
command,
|
|
@@ -98,7 +112,7 @@ function writeMarker(raw, command) {
|
|
|
98
112
|
started_at: new Date().toISOString(),
|
|
99
113
|
hook_event_name: raw.hook_event_name || null,
|
|
100
114
|
};
|
|
101
|
-
fs.writeFileSync(
|
|
115
|
+
fs.writeFileSync(p, JSON.stringify(marker, null, 2));
|
|
102
116
|
return marker;
|
|
103
117
|
}
|
|
104
118
|
|
|
@@ -127,7 +141,7 @@ async function main() {
|
|
|
127
141
|
|
|
128
142
|
try {
|
|
129
143
|
const marker = writeMarker(raw, command);
|
|
130
|
-
hookLog('rdc-invocation-marker', raw.hook_event_name || 'unknown', 'marked', {
|
|
144
|
+
hookLog('rdc-invocation-marker', raw.hook_event_name || 'unknown', marker.deduped ? 'deduped' : 'marked', {
|
|
131
145
|
command,
|
|
132
146
|
session_id: marker.session_id,
|
|
133
147
|
});
|
|
@@ -60,6 +60,14 @@ function pass(marker) {
|
|
|
60
60
|
process.exit(0);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
function passReentry(marker) {
|
|
64
|
+
try { fs.unlinkSync(marker.path); } catch {}
|
|
65
|
+
hookLog('rdc-output-contract-gate', 'Stop', 'pass-reentry', {
|
|
66
|
+
command: marker.data.command || null,
|
|
67
|
+
});
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
|
|
63
71
|
async function main() {
|
|
64
72
|
let raw;
|
|
65
73
|
try { raw = JSON.parse(await readStdin()); } catch { process.exit(0); }
|
|
@@ -70,6 +78,7 @@ async function main() {
|
|
|
70
78
|
const checklist = hasChecklist(message);
|
|
71
79
|
const verdict = hasVerdict(message);
|
|
72
80
|
if (checklist && verdict) pass(marker);
|
|
81
|
+
if (raw.stop_hook_active === true) passReentry(marker);
|
|
73
82
|
|
|
74
83
|
const command = marker.data.command || 'rdc';
|
|
75
84
|
const missing = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lifeaitools/rdc-skills",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.37",
|
|
4
4
|
"description": "RDC typed-agent dispatch skill suite for Claude Code - plan, build, review, overnight builds",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"type": "plugin",
|
|
20
20
|
"skills": "skills/",
|
|
21
21
|
"guides": "guides/",
|
|
22
|
-
"version": "0.9.
|
|
22
|
+
"version": "0.9.37",
|
|
23
23
|
"commands": "commands/"
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"uninstall:win": "powershell -ExecutionPolicy Bypass -File scripts/uninstall.ps1",
|
|
28
28
|
"uninstall:unix": "bash scripts/uninstall.sh",
|
|
29
29
|
"validate": "node tests/validate-skills.js",
|
|
30
|
-
"rdc-design": "node scripts/rdc-design-cli.mjs"
|
|
30
|
+
"rdc-design": "node scripts/rdc-design-cli.mjs",
|
|
31
|
+
"test:hooks": "node scripts/test-rdc-hooks.mjs"
|
|
31
32
|
}
|
|
32
33
|
}
|
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
* Usage:
|
|
6
6
|
* node scripts/install-rdc-skills.js ← standard
|
|
7
7
|
* node scripts/install-rdc-skills.js --skip-hooks ← skip hook wiring
|
|
8
|
+
* node scripts/install-rdc-skills.js --profile core ← clean-box portable hooks
|
|
9
|
+
* node scripts/install-rdc-skills.js --profile lifeai ← LIFEAI/regen-root hooks
|
|
8
10
|
* node scripts/install-rdc-skills.js --claude-home <path> ← custom CLI home
|
|
9
11
|
* node scripts/install-rdc-skills.js --codex-root <path> ← also install to .agents/skills/user/
|
|
10
12
|
* node scripts/install-rdc-skills.js --codex-skill-dir <path> ← also install to a Codex skill dir
|
|
13
|
+
* node scripts/install-rdc-skills.js --project-root <path> --write-startup-blocks
|
|
11
14
|
* node scripts/install-rdc-skills.js --migrate <path> ← migrate docs/ → .rdc/
|
|
12
15
|
*
|
|
13
16
|
* What it does:
|
|
@@ -32,11 +35,16 @@ const { execSync } = require('child_process');
|
|
|
32
35
|
// ── Args ──────────────────────────────────────────────────────────────────────
|
|
33
36
|
const args = process.argv.slice(2);
|
|
34
37
|
const skipHooks = args.includes('--skip-hooks');
|
|
38
|
+
const profileIdx = args.indexOf('--profile');
|
|
39
|
+
const profileArg = profileIdx >= 0 ? String(args[profileIdx + 1] || '').toLowerCase() : 'auto';
|
|
35
40
|
const homeIdx = args.indexOf('--claude-home');
|
|
36
41
|
const claudeHome = homeIdx >= 0 ? args[homeIdx + 1] : path.join(os.homedir(), '.claude');
|
|
37
42
|
const migrateIdx = args.indexOf('--migrate');
|
|
38
43
|
const doMigrate = migrateIdx >= 0;
|
|
39
44
|
const migratePath = doMigrate ? (args[migrateIdx + 1] || process.cwd()) : null;
|
|
45
|
+
const projectIdx = args.indexOf('--project-root');
|
|
46
|
+
const projectRootArg = projectIdx >= 0 ? path.resolve(args[projectIdx + 1]) : null;
|
|
47
|
+
const shouldWriteStartupBlocks = args.includes('--write-startup-blocks');
|
|
40
48
|
|
|
41
49
|
const repoRoot = path.resolve(__dirname, '..');
|
|
42
50
|
|
|
@@ -44,6 +52,7 @@ const codexIdx = args.indexOf('--codex-root');
|
|
|
44
52
|
const codexRoot = codexIdx >= 0
|
|
45
53
|
? path.resolve(args[codexIdx + 1])
|
|
46
54
|
: (() => {
|
|
55
|
+
if (projectRootArg) return null;
|
|
47
56
|
const sibling = path.resolve(repoRoot, '..', 'regen-root');
|
|
48
57
|
return fs.existsSync(path.join(sibling, '.agents')) ? sibling : null;
|
|
49
58
|
})();
|
|
@@ -54,6 +63,18 @@ const explicitCodexSkillDir = codexSkillDirIdx >= 0
|
|
|
54
63
|
const hooksSrc = path.join(repoRoot, 'hooks');
|
|
55
64
|
const hooksDst = path.join(claudeHome, 'hooks');
|
|
56
65
|
const settingsPath = path.join(claudeHome, 'settings.json');
|
|
66
|
+
const detectedLifeaiRoot = (() => {
|
|
67
|
+
const sibling = path.resolve(repoRoot, '..', 'regen-root');
|
|
68
|
+
return fs.existsSync(path.join(sibling, 'CLAUDE.md')) && fs.existsSync(path.join(sibling, '.rdc')) ? sibling : null;
|
|
69
|
+
})();
|
|
70
|
+
const installProfile = (() => {
|
|
71
|
+
if (profileArg === 'core' || profileArg === 'lifeai') return profileArg;
|
|
72
|
+
if (profileArg !== 'auto') {
|
|
73
|
+
console.log(` \x1b[33m⚠\x1b[0m Unknown --profile "${profileArg}" — using auto`);
|
|
74
|
+
}
|
|
75
|
+
return detectedLifeaiRoot ? 'lifeai' : 'core';
|
|
76
|
+
})();
|
|
77
|
+
const projectRoot = projectRootArg || codexRoot || detectedLifeaiRoot;
|
|
57
78
|
|
|
58
79
|
const PLUGIN_KEY = 'rdc-skills@rdc-skills';
|
|
59
80
|
const MARKETPLACE = 'rdc-skills';
|
|
@@ -108,6 +129,7 @@ function copyMissingProjectGuides(projectRoot) {
|
|
|
108
129
|
let copied = 0;
|
|
109
130
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
110
131
|
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
132
|
+
if (entry.name === 'rdc-skills-startup.md') continue; // installed only by --write-startup-blocks
|
|
111
133
|
const target = path.join(dst, entry.name);
|
|
112
134
|
if (fs.existsSync(target)) continue;
|
|
113
135
|
fs.copyFileSync(path.join(src, entry.name), target);
|
|
@@ -126,6 +148,53 @@ function writeJson(p, data, indent = 2) {
|
|
|
126
148
|
fs.writeFileSync(p, JSON.stringify(data, null, indent));
|
|
127
149
|
}
|
|
128
150
|
|
|
151
|
+
function upsertManagedBlock(filePath, title, body) {
|
|
152
|
+
const begin = `<!-- BEGIN RDC-SKILLS:${title} -->`;
|
|
153
|
+
const end = `<!-- END RDC-SKILLS:${title} -->`;
|
|
154
|
+
const block = `${begin}\n${body.trim()}\n${end}\n`;
|
|
155
|
+
const current = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
|
|
156
|
+
const re = new RegExp(`${begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\n?`, 'm');
|
|
157
|
+
const next = re.test(current)
|
|
158
|
+
? current.replace(re, block)
|
|
159
|
+
: `${current.replace(/\s*$/, '')}${current.trim() ? '\n\n' : ''}${block}`;
|
|
160
|
+
if (next !== current) {
|
|
161
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
162
|
+
fs.writeFileSync(filePath, next);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function writeStartupBlocks(targetRoot, profile) {
|
|
169
|
+
if (!targetRoot) return { wrote: 0, skipped: 'no-project-root' };
|
|
170
|
+
const startupSrc = path.join(repoRoot, 'guides', 'rdc-skills-startup.md');
|
|
171
|
+
const guidesDir = path.join(targetRoot, '.rdc', 'guides');
|
|
172
|
+
const startupDst = path.join(guidesDir, 'rdc-skills-startup.md');
|
|
173
|
+
fs.mkdirSync(guidesDir, { recursive: true });
|
|
174
|
+
fs.copyFileSync(startupSrc, startupDst);
|
|
175
|
+
|
|
176
|
+
let wrote = 1;
|
|
177
|
+
const claudeBody = [
|
|
178
|
+
'## RDC Skills',
|
|
179
|
+
'',
|
|
180
|
+
'@.rdc/guides/rdc-skills-startup.md',
|
|
181
|
+
'',
|
|
182
|
+
`Installed profile: \`${profile}\`.`,
|
|
183
|
+
'For `/rdc:*` work, follow `.rdc/guides/output-contract.md` and `.rdc/guides/engineering-behavior.md`.',
|
|
184
|
+
].join('\n');
|
|
185
|
+
const agentsBody = [
|
|
186
|
+
'## RDC Skills',
|
|
187
|
+
'',
|
|
188
|
+
'Read `.rdc/guides/rdc-skills-startup.md` before using any `rdc:*` workflow.',
|
|
189
|
+
`Installed profile: \`${profile}\`.`,
|
|
190
|
+
'For `/rdc:*` work, follow `.rdc/guides/output-contract.md` and `.rdc/guides/engineering-behavior.md`.',
|
|
191
|
+
].join('\n');
|
|
192
|
+
|
|
193
|
+
if (upsertManagedBlock(path.join(targetRoot, 'CLAUDE.md'), 'STARTUP', claudeBody)) wrote++;
|
|
194
|
+
if (upsertManagedBlock(path.join(targetRoot, 'AGENTS.md'), 'STARTUP', agentsBody)) wrote++;
|
|
195
|
+
return { wrote, skipped: null };
|
|
196
|
+
}
|
|
197
|
+
|
|
129
198
|
// ── Frontmatter parser ────────────────────────────────────────────────────────
|
|
130
199
|
function readFrontmatter(filePath) {
|
|
131
200
|
try {
|
|
@@ -546,7 +615,7 @@ function buildZip(version) {
|
|
|
546
615
|
}
|
|
547
616
|
|
|
548
617
|
// ── Hook config ───────────────────────────────────────────────────────────────
|
|
549
|
-
function buildHooksConfig(hooksDir) {
|
|
618
|
+
function buildHooksConfig(hooksDir, profile = 'core') {
|
|
550
619
|
const base = hooksDir.replace(/\\/g, '/');
|
|
551
620
|
const cmd = (file, msg) => {
|
|
552
621
|
const command = process.platform === 'win32'
|
|
@@ -556,21 +625,16 @@ function buildHooksConfig(hooksDir) {
|
|
|
556
625
|
if (msg) entry.statusMessage = msg;
|
|
557
626
|
return entry;
|
|
558
627
|
};
|
|
559
|
-
|
|
628
|
+
const config = {
|
|
560
629
|
UserPromptExpansion: [{ hooks: [
|
|
561
630
|
cmd('rdc-invocation-marker.js', 'Marking RDC slash command...'),
|
|
562
631
|
]}],
|
|
563
632
|
UserPromptSubmit: [{ hooks: [
|
|
564
633
|
cmd('rdc-invocation-marker.js', 'Marking RDC prompt...'),
|
|
565
634
|
]}],
|
|
566
|
-
SessionStart: [{ hooks: [
|
|
567
|
-
cmd('check-cwd.js'),
|
|
568
|
-
cmd('check-stale-work-items.js', 'Checking for stale work items...'),
|
|
569
|
-
]}],
|
|
570
635
|
PreToolUse: [
|
|
571
636
|
{ hooks: [
|
|
572
637
|
cmd('foreground-process-gate.js', 'Checking foreground process policy...'),
|
|
573
|
-
cmd('work-item-exit-gate.js', 'Checking work item exit gates...'),
|
|
574
638
|
]},
|
|
575
639
|
{ matcher: 'Bash', hooks: [
|
|
576
640
|
cmd('require-work-item-on-commit.js'),
|
|
@@ -579,20 +643,36 @@ function buildHooksConfig(hooksDir) {
|
|
|
579
643
|
PostToolUse: [{ hooks: [
|
|
580
644
|
cmd('check-services.js'),
|
|
581
645
|
]}],
|
|
582
|
-
PreCompact: [{ hooks: [
|
|
583
|
-
cmd('precompact-log.js'),
|
|
584
|
-
]}],
|
|
585
|
-
PostCompact: [{ hooks: [
|
|
586
|
-
cmd('postcompact-log.js'),
|
|
587
|
-
cmd('restart-brief.js', 'Writing restart brief...'),
|
|
588
|
-
]}],
|
|
589
646
|
Stop: [{ hooks: [
|
|
590
|
-
cmd('rate-limit-retry.js', 'Checking for rate limits...'),
|
|
591
647
|
cmd('rdc-output-contract-gate.js', 'Checking RDC output contract...'),
|
|
592
648
|
cmd('post-work-check.js', 'Checking for undocumented work...'),
|
|
593
|
-
cmd('no-stop-open-epics.js', 'Checking for open epics...'),
|
|
594
649
|
]}],
|
|
595
650
|
};
|
|
651
|
+
|
|
652
|
+
if (profile === 'lifeai') {
|
|
653
|
+
config.SessionStart = [{ hooks: [
|
|
654
|
+
cmd('check-cwd.js'),
|
|
655
|
+
cmd('check-stale-work-items.js', 'Checking for stale work items...'),
|
|
656
|
+
]}];
|
|
657
|
+
config.PreToolUse[0].hooks.push(
|
|
658
|
+
cmd('work-item-exit-gate.js', 'Checking work item exit gates...'),
|
|
659
|
+
);
|
|
660
|
+
config.PreCompact = [{ hooks: [
|
|
661
|
+
cmd('precompact-log.js'),
|
|
662
|
+
]}];
|
|
663
|
+
config.PostCompact = [{ hooks: [
|
|
664
|
+
cmd('postcompact-log.js'),
|
|
665
|
+
cmd('restart-brief.js', 'Writing restart brief...'),
|
|
666
|
+
]}];
|
|
667
|
+
config.Stop[0].hooks.unshift(
|
|
668
|
+
cmd('rate-limit-retry.js', 'Checking for rate limits...'),
|
|
669
|
+
);
|
|
670
|
+
config.Stop[0].hooks.push(
|
|
671
|
+
cmd('no-stop-open-epics.js', 'Checking for open epics...'),
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return config;
|
|
596
676
|
}
|
|
597
677
|
|
|
598
678
|
// ── Preflight ─────────────────────────────────────────────────────────────────
|
|
@@ -682,6 +762,7 @@ async function main() {
|
|
|
682
762
|
console.log('');
|
|
683
763
|
console.log(` CLAUDE_HOME : ${claudeHome}`);
|
|
684
764
|
console.log(` Plugin root : ${repoRoot}`);
|
|
765
|
+
console.log(` Profile : ${installProfile}${profileArg === 'auto' ? ' (auto)' : ''}`);
|
|
685
766
|
console.log('');
|
|
686
767
|
|
|
687
768
|
if (!fs.existsSync(claudeHome)) {
|
|
@@ -822,11 +903,20 @@ async function main() {
|
|
|
822
903
|
info('[4/6] Hook wiring — skipped (--skip-hooks)');
|
|
823
904
|
} else {
|
|
824
905
|
const settings = readJson(settingsPath);
|
|
825
|
-
settings.hooks = buildHooksConfig(hooksDst);
|
|
906
|
+
settings.hooks = buildHooksConfig(hooksDst, installProfile);
|
|
826
907
|
writeJson(settingsPath, settings);
|
|
827
908
|
ok(`[4/6] Hook wiring — ${settingsPath}`);
|
|
828
909
|
}
|
|
829
910
|
|
|
911
|
+
// 4.5 Optional startup blocks
|
|
912
|
+
if (shouldWriteStartupBlocks) {
|
|
913
|
+
const startup = writeStartupBlocks(projectRoot, installProfile);
|
|
914
|
+
if (startup.skipped) warn(`[4.5] Startup — skipped (${startup.skipped})`);
|
|
915
|
+
else ok(`[4.5] Startup — wrote managed startup guide/block(s) under ${projectRoot}`);
|
|
916
|
+
} else {
|
|
917
|
+
info('[4.5] Startup — skipped (use --project-root <path> --write-startup-blocks)');
|
|
918
|
+
}
|
|
919
|
+
|
|
830
920
|
// 5. Zip for claude.ai / distribution
|
|
831
921
|
const zipPath = buildZip(version);
|
|
832
922
|
if (zipPath) {
|
|
@@ -897,6 +987,15 @@ async function main() {
|
|
|
897
987
|
console.log(' Cowork : restart Claude Desktop — /rdc:status in a new Cowork session');
|
|
898
988
|
console.log(' claude.ai : no plugin install needed — use FS MCP to read commands on demand');
|
|
899
989
|
console.log(' dist/rdc-skills-plugin-v' + version + '.zip available if needed');
|
|
990
|
+
console.log('');
|
|
991
|
+
console.log(` Profile: ${installProfile}`);
|
|
992
|
+
if (installProfile === 'core') {
|
|
993
|
+
console.log(' Core hooks are portable: RDC output contract + foreground process + commit-message hygiene.');
|
|
994
|
+
console.log(' Project services are not provisioned. Configure work items, credentials, deploys, and project guides before using infrastructure-heavy skills.');
|
|
995
|
+
} else {
|
|
996
|
+
console.log(' LIFEAI hooks are active: regen-root cwd lock, Supabase work-item gates, clauth-aware checks, and overnight queue guard.');
|
|
997
|
+
}
|
|
998
|
+
console.log(' Startup blocks: run with --project-root <path> --write-startup-blocks to add managed CLAUDE.md/AGENTS.md sections.');
|
|
900
999
|
|
|
901
1000
|
listCommands();
|
|
902
1001
|
|
package/scripts/install.ps1
CHANGED
|
@@ -7,10 +7,14 @@
|
|
|
7
7
|
# Usage:
|
|
8
8
|
# ./install.ps1 # standard install
|
|
9
9
|
# ./install.ps1 -SkipHooks # skip hooks registration (e.g. if you manage hooks manually)
|
|
10
|
+
# ./install.ps1 -Profile core # portable hooks only
|
|
11
|
+
# ./install.ps1 -Profile lifeai # LIFEAI/regen-root hooks
|
|
10
12
|
# ./install.ps1 -ClaudeHome <path> # custom CLAUDE_HOME
|
|
11
13
|
|
|
12
14
|
param(
|
|
13
15
|
[string]$ClaudeHome = "",
|
|
16
|
+
[ValidateSet("auto", "core", "lifeai")]
|
|
17
|
+
[string]$Profile = "auto",
|
|
14
18
|
[switch]$SkipHooks = $false,
|
|
15
19
|
[switch]$Force = $false
|
|
16
20
|
)
|
|
@@ -22,6 +26,12 @@ if (-not $ClaudeHome) {
|
|
|
22
26
|
|
|
23
27
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
24
28
|
$repoRoot = Split-Path -Parent $scriptDir
|
|
29
|
+
$lifeaiRoot = Join-Path (Split-Path -Parent $repoRoot) "regen-root"
|
|
30
|
+
$installProfile = if ($Profile -eq "auto") {
|
|
31
|
+
if ((Test-Path (Join-Path $lifeaiRoot "CLAUDE.md")) -and (Test-Path (Join-Path $lifeaiRoot ".rdc"))) { "lifeai" } else { "core" }
|
|
32
|
+
} else {
|
|
33
|
+
$Profile
|
|
34
|
+
}
|
|
25
35
|
|
|
26
36
|
Write-Host ""
|
|
27
37
|
Write-Host " rdc-skills Installer" -ForegroundColor Green
|
|
@@ -29,6 +39,7 @@ Write-Host " ====================" -ForegroundColor Green
|
|
|
29
39
|
Write-Host ""
|
|
30
40
|
Write-Host " CLAUDE_HOME : $ClaudeHome" -ForegroundColor Cyan
|
|
31
41
|
Write-Host " Plugin root : $repoRoot" -ForegroundColor Cyan
|
|
42
|
+
Write-Host " Profile : $installProfile$(if ($Profile -eq 'auto') { ' (auto)' } else { '' })" -ForegroundColor Cyan
|
|
32
43
|
Write-Host ""
|
|
33
44
|
|
|
34
45
|
if (-not (Test-Path $ClaudeHome)) {
|
|
@@ -96,19 +107,10 @@ if ($SkipHooks) {
|
|
|
96
107
|
)
|
|
97
108
|
}
|
|
98
109
|
)
|
|
99
|
-
SessionStart = @(
|
|
100
|
-
[PSCustomObject]@{
|
|
101
|
-
hooks = @(
|
|
102
|
-
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/check-cwd.js`"" },
|
|
103
|
-
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/check-stale-work-items.js`""; statusMessage = "Checking for stale work items..." }
|
|
104
|
-
)
|
|
105
|
-
}
|
|
106
|
-
)
|
|
107
110
|
PreToolUse = @(
|
|
108
111
|
[PSCustomObject]@{
|
|
109
112
|
hooks = @(
|
|
110
|
-
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/foreground-process-gate.js`""; statusMessage = "Checking foreground process policy..." }
|
|
111
|
-
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/work-item-exit-gate.js`""; statusMessage = "Checking work item exit gates..." }
|
|
113
|
+
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/foreground-process-gate.js`""; statusMessage = "Checking foreground process policy..." }
|
|
112
114
|
)
|
|
113
115
|
},
|
|
114
116
|
[PSCustomObject]@{
|
|
@@ -125,31 +127,42 @@ if ($SkipHooks) {
|
|
|
125
127
|
)
|
|
126
128
|
}
|
|
127
129
|
)
|
|
128
|
-
|
|
130
|
+
Stop = @(
|
|
129
131
|
[PSCustomObject]@{
|
|
130
132
|
hooks = @(
|
|
131
|
-
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/
|
|
133
|
+
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/rdc-output-contract-gate.js`""; statusMessage = "Checking RDC output contract..." },
|
|
134
|
+
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/post-work-check.js`""; statusMessage = "Checking for undocumented work..." }
|
|
132
135
|
)
|
|
133
136
|
}
|
|
134
137
|
)
|
|
135
|
-
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if ($installProfile -eq "lifeai") {
|
|
141
|
+
$hooksConfig | Add-Member -NotePropertyName "SessionStart" -NotePropertyValue @(
|
|
136
142
|
[PSCustomObject]@{
|
|
137
143
|
hooks = @(
|
|
138
|
-
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/
|
|
139
|
-
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/
|
|
144
|
+
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/check-cwd.js`"" },
|
|
145
|
+
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/check-stale-work-items.js`""; statusMessage = "Checking for stale work items..." }
|
|
140
146
|
)
|
|
141
147
|
}
|
|
142
148
|
)
|
|
143
|
-
|
|
149
|
+
$hooksConfig.PreToolUse[0].hooks += [PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/work-item-exit-gate.js`""; statusMessage = "Checking work item exit gates..." }
|
|
150
|
+
$hooksConfig | Add-Member -NotePropertyName "PreCompact" -NotePropertyValue @(
|
|
151
|
+
[PSCustomObject]@{ hooks = @([PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/precompact-log.js`"" }) }
|
|
152
|
+
)
|
|
153
|
+
$hooksConfig | Add-Member -NotePropertyName "PostCompact" -NotePropertyValue @(
|
|
144
154
|
[PSCustomObject]@{
|
|
145
155
|
hooks = @(
|
|
146
|
-
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/
|
|
147
|
-
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/
|
|
148
|
-
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/post-work-check.js`""; statusMessage = "Checking for undocumented work..." },
|
|
149
|
-
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/no-stop-open-epics.js`""; statusMessage = "Checking for open epics..." }
|
|
156
|
+
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/postcompact-log.js`"" },
|
|
157
|
+
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/restart-brief.js`""; statusMessage = "Writing restart brief..." }
|
|
150
158
|
)
|
|
151
159
|
}
|
|
152
160
|
)
|
|
161
|
+
$hooksConfig.Stop[0].hooks = @(
|
|
162
|
+
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/rate-limit-retry.js`""; statusMessage = "Checking for rate limits..." }
|
|
163
|
+
) + $hooksConfig.Stop[0].hooks + @(
|
|
164
|
+
[PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/no-stop-open-epics.js`""; statusMessage = "Checking for open epics..." }
|
|
165
|
+
)
|
|
153
166
|
}
|
|
154
167
|
|
|
155
168
|
# Add/replace hooks key
|
|
@@ -163,6 +176,8 @@ if ($SkipHooks) {
|
|
|
163
176
|
$settings | ConvertTo-Json -Depth 20 | Set-Content $settingsPath -Encoding UTF8
|
|
164
177
|
Write-Host " [3/3] Hook wiring ✓ registered in $settingsPath" -ForegroundColor Green
|
|
165
178
|
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
166
181
|
|
|
167
182
|
# ── Summary ───────────────────────────────────────────────────────────────────
|
|
168
183
|
|
|
@@ -184,3 +199,4 @@ Write-Host " 4. Run /rdc:status in Claude Code to verify"
|
|
|
184
199
|
Write-Host ""
|
|
185
200
|
Write-Host " Docs: https://github.com/LIFEAI/rdc-skills#readme" -ForegroundColor Cyan
|
|
186
201
|
Write-Host ""
|
|
202
|
+
}
|
package/scripts/self-test.mjs
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import { readFileSync, readdirSync, existsSync, writeFileSync, appendFileSync, renameSync, mkdirSync, statSync } from "node:fs";
|
|
32
32
|
import { join, basename, dirname, resolve } from "node:path";
|
|
33
33
|
import { fileURLToPath } from "node:url";
|
|
34
|
+
import { spawnSync } from "node:child_process";
|
|
34
35
|
import { loadAllManifests } from "./lib/manifest-schema.mjs";
|
|
35
36
|
import { runManifest } from "./lib/runner.mjs";
|
|
36
37
|
import {
|
|
@@ -802,6 +803,19 @@ function auditRequiredHookWiring() {
|
|
|
802
803
|
return findings;
|
|
803
804
|
}
|
|
804
805
|
|
|
806
|
+
function runHookBehaviorTests() {
|
|
807
|
+
const result = spawnSync(process.execPath, [join(REPO_ROOT, "scripts", "test-rdc-hooks.mjs")], {
|
|
808
|
+
cwd: REPO_ROOT,
|
|
809
|
+
encoding: "utf8",
|
|
810
|
+
});
|
|
811
|
+
return {
|
|
812
|
+
ok: result.status === 0,
|
|
813
|
+
status: result.status,
|
|
814
|
+
stdout: (result.stdout || "").trim(),
|
|
815
|
+
stderr: (result.stderr || "").trim(),
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
|
|
805
819
|
// ─── Tier 2 behavioral runner ──────────────────────────────────────────────
|
|
806
820
|
|
|
807
821
|
async function runPool(items, concurrency, worker) {
|
|
@@ -1038,6 +1052,7 @@ function main() {
|
|
|
1038
1052
|
}
|
|
1039
1053
|
|
|
1040
1054
|
const guideValidatorResultJson = !ONLY_SKILL ? runGuideContentValidator() : null;
|
|
1055
|
+
const hookBehaviorResultJson = !ONLY_SKILL ? runHookBehaviorTests() : null;
|
|
1041
1056
|
|
|
1042
1057
|
const failed = results.filter((r) => r.errors.length > 0);
|
|
1043
1058
|
const warned = results.filter((r) => r.warnings.length > 0 && r.errors.length === 0);
|
|
@@ -1050,6 +1065,11 @@ function main() {
|
|
|
1050
1065
|
...duplicateFindings.filter((f) => f.level === "error"),
|
|
1051
1066
|
...orphanHookFindings.filter((f) => f.level === "error"),
|
|
1052
1067
|
...(guideValidatorResultJson ? guideValidatorResultJson.errors : []),
|
|
1068
|
+
...(hookBehaviorResultJson && !hookBehaviorResultJson.ok ? [{
|
|
1069
|
+
level: "error",
|
|
1070
|
+
code: "hook-behavior-test-failed",
|
|
1071
|
+
message: hookBehaviorResultJson.stderr || hookBehaviorResultJson.stdout || `exit ${hookBehaviorResultJson.status}`,
|
|
1072
|
+
}] : []),
|
|
1053
1073
|
];
|
|
1054
1074
|
const globalWarnings = [
|
|
1055
1075
|
...manifestAudit.findings.filter((f) => f.level === "warn"),
|
|
@@ -1092,6 +1112,7 @@ function main() {
|
|
|
1092
1112
|
findings: guideValidatorResultJson.findings,
|
|
1093
1113
|
}
|
|
1094
1114
|
: null,
|
|
1115
|
+
hook_behavior_tests: hookBehaviorResultJson,
|
|
1095
1116
|
results: results.map((r) => ({
|
|
1096
1117
|
skill: r.skill,
|
|
1097
1118
|
file: r.file,
|
|
@@ -1217,10 +1238,12 @@ function main() {
|
|
|
1217
1238
|
let duplicateFindings = [];
|
|
1218
1239
|
let orphanHookFindings = [];
|
|
1219
1240
|
let requiredHookFindings = [];
|
|
1241
|
+
let hookBehaviorResult = null;
|
|
1220
1242
|
if (!ONLY_SKILL) {
|
|
1221
1243
|
duplicateFindings = auditDuplicates(results);
|
|
1222
1244
|
orphanHookFindings = auditOrphanHooks(results);
|
|
1223
1245
|
requiredHookFindings = auditRequiredHookWiring();
|
|
1246
|
+
hookBehaviorResult = runHookBehaviorTests();
|
|
1224
1247
|
}
|
|
1225
1248
|
|
|
1226
1249
|
if (duplicateFindings.length + orphanHookFindings.length + requiredHookFindings.length > 0) {
|
|
@@ -1230,6 +1253,14 @@ function main() {
|
|
|
1230
1253
|
}
|
|
1231
1254
|
}
|
|
1232
1255
|
|
|
1256
|
+
if (hookBehaviorResult) {
|
|
1257
|
+
console.log("\nhook behavior tests\n");
|
|
1258
|
+
console.log("─".repeat(80));
|
|
1259
|
+
console.log(hookBehaviorResult.ok ? " pass rdc marker/gate behavioral smoke tests" : " FAIL rdc marker/gate behavioral smoke tests");
|
|
1260
|
+
if (hookBehaviorResult.stdout) console.log(` ${hookBehaviorResult.stdout}`);
|
|
1261
|
+
if (hookBehaviorResult.stderr) console.log(` ${hookBehaviorResult.stderr}`);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1233
1264
|
if (FIXED_FILES.length > 0) {
|
|
1234
1265
|
console.log("\nfixed files (review with git diff):");
|
|
1235
1266
|
for (const p of FIXED_FILES) console.log(` ${p}`);
|
|
@@ -1243,6 +1274,11 @@ function main() {
|
|
|
1243
1274
|
...orphanHookFindings.filter((f) => f.level === "error"),
|
|
1244
1275
|
...requiredHookFindings.filter((f) => f.level === "error"),
|
|
1245
1276
|
...(guideValidatorResult ? guideValidatorResult.errors : []),
|
|
1277
|
+
...(hookBehaviorResult && !hookBehaviorResult.ok ? [{
|
|
1278
|
+
level: "error",
|
|
1279
|
+
code: "hook-behavior-test-failed",
|
|
1280
|
+
message: hookBehaviorResult.stderr || hookBehaviorResult.stdout || `exit ${hookBehaviorResult.status}`,
|
|
1281
|
+
}] : []),
|
|
1246
1282
|
];
|
|
1247
1283
|
const globalWarnings = [
|
|
1248
1284
|
...manifestAudit.findings.filter((f) => f.level === "warn"),
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Behavioral smoke tests for the RDC marker and Stop output-contract hooks.
|
|
3
|
+
|
|
4
|
+
import { mkdtempSync, rmSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join, resolve, dirname } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const REPO_ROOT = resolve(__dirname, "..");
|
|
12
|
+
const MARKER_HOOK = join(REPO_ROOT, "hooks", "rdc-invocation-marker.js");
|
|
13
|
+
const STOP_HOOK = join(REPO_ROOT, "hooks", "rdc-output-contract-gate.js");
|
|
14
|
+
|
|
15
|
+
const tmpHome = mkdtempSync(join(tmpdir(), "rdc-hooks-"));
|
|
16
|
+
const env = { ...process.env, HOME: tmpHome, USERPROFILE: tmpHome };
|
|
17
|
+
const failures = [];
|
|
18
|
+
|
|
19
|
+
function markerPath(sessionId) {
|
|
20
|
+
return join(tmpHome, ".claude", "rdc-active", `${sessionId}.json`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function runHook(script, payload) {
|
|
24
|
+
return spawnSync(process.execPath, [script], {
|
|
25
|
+
input: JSON.stringify(payload),
|
|
26
|
+
encoding: "utf8",
|
|
27
|
+
env,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function assert(name, condition, detail = "") {
|
|
32
|
+
if (!condition) failures.push(`${name}${detail ? `: ${detail}` : ""}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readMarker(sessionId) {
|
|
36
|
+
return JSON.parse(readFileSync(markerPath(sessionId), "utf8"));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const expansion = runHook(MARKER_HOOK, {
|
|
41
|
+
hook_event_name: "UserPromptExpansion",
|
|
42
|
+
session_id: "s-expansion",
|
|
43
|
+
command_source: "plugin",
|
|
44
|
+
command_name: "rdc:design",
|
|
45
|
+
prompt: "/rdc:design palette audit",
|
|
46
|
+
});
|
|
47
|
+
assert("expansion exits zero", expansion.status === 0, expansion.stderr);
|
|
48
|
+
assert("expansion emits context", /RDC CONTRACT ACTIVE/.test(expansion.stdout), expansion.stdout);
|
|
49
|
+
assert("expansion writes marker", existsSync(markerPath("s-expansion")));
|
|
50
|
+
assert("expansion marker event preserved", readMarker("s-expansion").hook_event_name === "UserPromptExpansion");
|
|
51
|
+
|
|
52
|
+
const builtin = runHook(MARKER_HOOK, {
|
|
53
|
+
hook_event_name: "UserPromptExpansion",
|
|
54
|
+
session_id: "s-builtin",
|
|
55
|
+
command_source: "builtin",
|
|
56
|
+
command_name: "help",
|
|
57
|
+
prompt: "/help",
|
|
58
|
+
});
|
|
59
|
+
assert("builtin exits zero", builtin.status === 0, builtin.stderr);
|
|
60
|
+
assert("builtin emits no context", builtin.stdout.trim() === "", builtin.stdout);
|
|
61
|
+
assert("builtin writes no marker", !existsSync(markerPath("s-builtin")));
|
|
62
|
+
|
|
63
|
+
const submit1 = runHook(MARKER_HOOK, {
|
|
64
|
+
hook_event_name: "UserPromptExpansion",
|
|
65
|
+
session_id: "s-dedup",
|
|
66
|
+
command_source: "plugin",
|
|
67
|
+
command_name: "rdc:design",
|
|
68
|
+
prompt: "/rdc:design button audit",
|
|
69
|
+
});
|
|
70
|
+
assert("dedup first mark exits zero", submit1.status === 0, submit1.stderr);
|
|
71
|
+
const first = readMarker("s-dedup");
|
|
72
|
+
const submit2 = runHook(MARKER_HOOK, {
|
|
73
|
+
hook_event_name: "UserPromptSubmit",
|
|
74
|
+
session_id: "s-dedup",
|
|
75
|
+
prompt: "/rdc:design button audit",
|
|
76
|
+
});
|
|
77
|
+
assert("dedup second mark exits zero", submit2.status === 0, submit2.stderr);
|
|
78
|
+
const second = readMarker("s-dedup");
|
|
79
|
+
assert("dedup preserves started_at", second.started_at === first.started_at);
|
|
80
|
+
assert("dedup preserves hook event", second.hook_event_name === "UserPromptExpansion");
|
|
81
|
+
|
|
82
|
+
const genericHelp = runHook(MARKER_HOOK, {
|
|
83
|
+
hook_event_name: "UserPromptSubmit",
|
|
84
|
+
session_id: "s-help",
|
|
85
|
+
prompt: "/help",
|
|
86
|
+
});
|
|
87
|
+
assert("generic help exits zero", genericHelp.status === 0, genericHelp.stderr);
|
|
88
|
+
assert("generic help writes no marker", !existsSync(markerPath("s-help")));
|
|
89
|
+
|
|
90
|
+
const noMarker = runHook(STOP_HOOK, {
|
|
91
|
+
hook_event_name: "Stop",
|
|
92
|
+
session_id: "s-no-marker",
|
|
93
|
+
last_assistant_message: "plain chat",
|
|
94
|
+
});
|
|
95
|
+
assert("stop without marker exits zero", noMarker.status === 0, noMarker.stderr);
|
|
96
|
+
assert("stop without marker is silent", noMarker.stdout.trim() === "", noMarker.stdout);
|
|
97
|
+
|
|
98
|
+
writeFileSync(markerPath("s-block"), JSON.stringify({
|
|
99
|
+
session_id: "s-block",
|
|
100
|
+
command: "design",
|
|
101
|
+
started_at: new Date().toISOString(),
|
|
102
|
+
}, null, 2));
|
|
103
|
+
const blocked = runHook(STOP_HOOK, {
|
|
104
|
+
hook_event_name: "Stop",
|
|
105
|
+
session_id: "s-block",
|
|
106
|
+
last_assistant_message: "No contract artifacts here.",
|
|
107
|
+
});
|
|
108
|
+
assert("noncompliant stop exits zero", blocked.status === 0, blocked.stderr);
|
|
109
|
+
assert("noncompliant stop blocks", /"decision"\s*:\s*"block"/.test(blocked.stdout), blocked.stdout);
|
|
110
|
+
assert("noncompliant stop retains marker", existsSync(markerPath("s-block")));
|
|
111
|
+
|
|
112
|
+
const reentry = runHook(STOP_HOOK, {
|
|
113
|
+
hook_event_name: "Stop",
|
|
114
|
+
session_id: "s-block",
|
|
115
|
+
stop_hook_active: true,
|
|
116
|
+
last_assistant_message: "Still no contract artifacts.",
|
|
117
|
+
});
|
|
118
|
+
assert("stop reentry exits zero", reentry.status === 0, reentry.stderr);
|
|
119
|
+
assert("stop reentry allows silent pass", reentry.stdout.trim() === "", reentry.stdout);
|
|
120
|
+
assert("stop reentry clears marker", !existsSync(markerPath("s-block")));
|
|
121
|
+
|
|
122
|
+
writeFileSync(markerPath("s-pass"), JSON.stringify({
|
|
123
|
+
session_id: "s-pass",
|
|
124
|
+
command: "review",
|
|
125
|
+
started_at: new Date().toISOString(),
|
|
126
|
+
}, null, 2));
|
|
127
|
+
const compliant = runHook(STOP_HOOK, {
|
|
128
|
+
hook_event_name: "Stop",
|
|
129
|
+
session_id: "s-pass",
|
|
130
|
+
last_assistant_message: "[x] Verified hook behavior\n\n✅ Complete in 2s",
|
|
131
|
+
});
|
|
132
|
+
assert("compliant stop exits zero", compliant.status === 0, compliant.stderr);
|
|
133
|
+
assert("compliant stop is silent", compliant.stdout.trim() === "", compliant.stdout);
|
|
134
|
+
assert("compliant stop clears marker", !existsSync(markerPath("s-pass")));
|
|
135
|
+
} finally {
|
|
136
|
+
rmSync(tmpHome, { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (failures.length > 0) {
|
|
140
|
+
console.error("\nrdc hook behavior tests — FAIL\n");
|
|
141
|
+
for (const failure of failures) console.error(` - ${failure}`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log("rdc hook behavior tests — PASS");
|