@lifeaitools/rdc-skills 0.9.34 → 0.9.35

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.9.34",
3
+ "version": "0.9.35",
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",
package/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## v0.9.35 — RDC output-contract hook enforcement
11
+
12
+ ### Added
13
+
14
+ - **RDC slash-command marker hook.** `rdc-invocation-marker.js` marks direct `/rdc:*` skill invocations through `UserPromptExpansion`, with a `UserPromptSubmit` fallback for older runtimes, and injects the output/engineering contract reminder.
15
+ - **RDC Stop gate.** `rdc-output-contract-gate.js` blocks completion for active RDC invocations until the final assistant message includes both a checklist row and a verdict line.
16
+ - **Installer wiring and self-test coverage.** The Node and PowerShell installers now wire the RDC marker/gate hooks, and `self-test` fails when required hook files or installer references are missing.
17
+
18
+ ---
19
+
10
20
  ## 0.6.1 — Tier 2 stability + rdc:watch session watcher
11
21
 
12
22
  ### Added
package/README.md CHANGED
@@ -176,7 +176,7 @@ python3 -c "import json; print(json.load(open('package.json'))['version'])"
176
176
  /rdc:help
177
177
  ```
178
178
 
179
- Current version: **v0.9.25**
179
+ Current version: **v0.9.35**
180
180
 
181
181
  ## Quick Start
182
182
 
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * UserPromptExpansion/UserPromptSubmit hook — mark active rdc:* invocations.
4
+ *
5
+ * This does not enforce compliance. It primes the turn with the RDC contract
6
+ * and leaves a session marker for rdc-output-contract-gate.js to enforce at
7
+ * Stop time.
8
+ */
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const os = require('os');
13
+ const path = require('path');
14
+ const hookLog = require('./hook-logger');
15
+
16
+ const RDC_COMMANDS = new Set([
17
+ 'build',
18
+ 'co-develop',
19
+ 'collab',
20
+ 'deploy',
21
+ 'design',
22
+ 'fixit',
23
+ 'fs-mcp',
24
+ 'handoff',
25
+ 'help',
26
+ 'overnight',
27
+ 'plan',
28
+ 'preplan',
29
+ 'prototype',
30
+ 'release',
31
+ 'report',
32
+ 'review',
33
+ 'self-test',
34
+ 'status',
35
+ 'terminal-config',
36
+ 'watch',
37
+ 'workitems',
38
+ ]);
39
+
40
+ function readStdin() {
41
+ return new Promise((resolve) => {
42
+ let input = '';
43
+ process.stdin.setEncoding('utf8');
44
+ process.stdin.on('data', (chunk) => { input += chunk; });
45
+ process.stdin.on('end', () => resolve(input));
46
+ process.stdin.resume();
47
+ });
48
+ }
49
+
50
+ function markerDir() {
51
+ return path.join(os.homedir(), '.claude', 'rdc-active');
52
+ }
53
+
54
+ function markerPath(sessionId) {
55
+ const safe = String(sessionId || 'unknown').replace(/[^a-zA-Z0-9_.-]/g, '_');
56
+ return path.join(markerDir(), `${safe}.json`);
57
+ }
58
+
59
+ function normalizeCommandName(value) {
60
+ return String(value || '')
61
+ .trim()
62
+ .replace(/^\/+/, '')
63
+ .replace(/^rdc[:-]/i, '')
64
+ .toLowerCase();
65
+ }
66
+
67
+ function detectRdc(raw) {
68
+ const event = raw.hook_event_name || '';
69
+ if (event === 'UserPromptExpansion') {
70
+ if (raw.command_source && raw.command_source !== 'plugin') return null;
71
+ const command = normalizeCommandName(raw.command_name);
72
+ if (command === 'help' && !/^\/rdc[:-]help\b/i.test(String(raw.prompt || ''))) return null;
73
+ if (RDC_COMMANDS.has(command)) return command;
74
+ return null;
75
+ }
76
+
77
+ if (event === 'UserPromptSubmit') {
78
+ const prompt = String(raw.prompt || '').trim();
79
+ const m = prompt.match(/^\/(?:rdc[:-])?([a-z][a-z0-9-]*)\b/i);
80
+ if (!m) return null;
81
+ const command = normalizeCommandName(m[1]);
82
+ if (RDC_COMMANDS.has(command)) return command;
83
+ }
84
+
85
+ return null;
86
+ }
87
+
88
+ function writeMarker(raw, command) {
89
+ fs.mkdirSync(markerDir(), { recursive: true });
90
+ const marker = {
91
+ session_id: raw.session_id || null,
92
+ command,
93
+ command_name: raw.command_name || null,
94
+ command_args: raw.command_args || null,
95
+ prompt: raw.prompt || null,
96
+ cwd: raw.cwd || null,
97
+ transcript_path: raw.transcript_path || null,
98
+ started_at: new Date().toISOString(),
99
+ hook_event_name: raw.hook_event_name || null,
100
+ };
101
+ fs.writeFileSync(markerPath(raw.session_id), JSON.stringify(marker, null, 2));
102
+ return marker;
103
+ }
104
+
105
+ function outputContext(eventName, command) {
106
+ const additionalContext = [
107
+ `RDC CONTRACT ACTIVE for /${command}.`,
108
+ 'Before responding, follow the project-local guide contracts:',
109
+ '- .rdc/guides/output-contract.md: show one checklist for this invocation, update it as work progresses, and end with the required verdict line.',
110
+ '- .rdc/guides/engineering-behavior.md: assumptions, scope, evidence, deviations, blockers, and verification must be explicit.',
111
+ 'Runtime enforcement is on emitted artifacts: the final assistant message must contain at least one checklist row and a verdict line.',
112
+ ].join('\n');
113
+
114
+ process.stdout.write(JSON.stringify({
115
+ hookSpecificOutput: {
116
+ hookEventName: eventName,
117
+ additionalContext,
118
+ },
119
+ }));
120
+ }
121
+
122
+ async function main() {
123
+ let raw;
124
+ try { raw = JSON.parse(await readStdin()); } catch { process.exit(0); }
125
+ const command = detectRdc(raw);
126
+ if (!command) process.exit(0);
127
+
128
+ try {
129
+ const marker = writeMarker(raw, command);
130
+ hookLog('rdc-invocation-marker', raw.hook_event_name || 'unknown', 'marked', {
131
+ command,
132
+ session_id: marker.session_id,
133
+ });
134
+ outputContext(raw.hook_event_name || 'UserPromptExpansion', command);
135
+ } catch (e) {
136
+ hookLog('rdc-invocation-marker', raw.hook_event_name || 'unknown', 'error', {
137
+ command,
138
+ error: e.message,
139
+ });
140
+ }
141
+ }
142
+
143
+ main().catch(() => process.exit(0));
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Stop hook — enforce visible RDC output contract artifacts.
4
+ *
5
+ * Triggered only when rdc-invocation-marker.js has marked the session. It checks
6
+ * positive output patterns only: at least one checklist row and one verdict
7
+ * line. It intentionally does not police forbidden phrases.
8
+ */
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const os = require('os');
13
+ const path = require('path');
14
+ const hookLog = require('./hook-logger');
15
+
16
+ function readStdin() {
17
+ return new Promise((resolve) => {
18
+ let input = '';
19
+ process.stdin.setEncoding('utf8');
20
+ process.stdin.on('data', (chunk) => { input += chunk; });
21
+ process.stdin.on('end', () => resolve(input));
22
+ process.stdin.resume();
23
+ });
24
+ }
25
+
26
+ function markerPath(sessionId) {
27
+ const safe = String(sessionId || 'unknown').replace(/[^a-zA-Z0-9_.-]/g, '_');
28
+ return path.join(os.homedir(), '.claude', 'rdc-active', `${safe}.json`);
29
+ }
30
+
31
+ function readMarker(sessionId) {
32
+ const p = markerPath(sessionId);
33
+ if (!fs.existsSync(p)) return null;
34
+ try {
35
+ return { path: p, data: JSON.parse(fs.readFileSync(p, 'utf8')) };
36
+ } catch {
37
+ return { path: p, data: { command: 'unknown' } };
38
+ }
39
+ }
40
+
41
+ function hasChecklist(text) {
42
+ return /(?:^|\n)\s*(?:[-*]\s*)?\[(?: |x|X|~|!|-)\]\s+\S/m.test(text || '');
43
+ }
44
+
45
+ function hasVerdict(text) {
46
+ return /(?:^|\n)\s*(?:✅|⚠️|❌)\s+\S/m.test(text || '');
47
+ }
48
+
49
+ function block(reason, details = {}) {
50
+ hookLog('rdc-output-contract-gate', 'Stop', 'block', details);
51
+ process.stdout.write(JSON.stringify({ decision: 'block', reason }));
52
+ process.exit(0);
53
+ }
54
+
55
+ function pass(marker) {
56
+ try { fs.unlinkSync(marker.path); } catch {}
57
+ hookLog('rdc-output-contract-gate', 'Stop', 'pass', {
58
+ command: marker.data.command || null,
59
+ });
60
+ process.exit(0);
61
+ }
62
+
63
+ async function main() {
64
+ let raw;
65
+ try { raw = JSON.parse(await readStdin()); } catch { process.exit(0); }
66
+ const marker = readMarker(raw.session_id);
67
+ if (!marker) process.exit(0);
68
+
69
+ const message = String(raw.last_assistant_message || '');
70
+ const checklist = hasChecklist(message);
71
+ const verdict = hasVerdict(message);
72
+ if (checklist && verdict) pass(marker);
73
+
74
+ const command = marker.data.command || 'rdc';
75
+ const missing = [];
76
+ if (!checklist) missing.push('a visible checklist row like `[ ] Step` or `[x] Step`');
77
+ if (!verdict) missing.push('a final verdict line beginning with ✅, ⚠️, or ❌');
78
+
79
+ block(
80
+ `RDC output contract incomplete for /${command}: missing ${missing.join(' and ')}. Continue the response by rendering the RDC checklist and verdict required by .rdc/guides/output-contract.md. Do not restart the task; correct the visible output contract.`,
81
+ { command, checklist, verdict, stop_hook_active: raw.stop_hook_active === true },
82
+ );
83
+ }
84
+
85
+ main().catch((e) => block(`RDC output contract gate crashed: ${e.message}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/rdc-skills",
3
- "version": "0.9.34",
3
+ "version": "0.9.35",
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.34",
22
+ "version": "0.9.35",
23
23
  "commands": "commands/"
24
24
  },
25
25
  "scripts": {
@@ -557,6 +557,12 @@ function buildHooksConfig(hooksDir) {
557
557
  return entry;
558
558
  };
559
559
  return {
560
+ UserPromptExpansion: [{ hooks: [
561
+ cmd('rdc-invocation-marker.js', 'Marking RDC slash command...'),
562
+ ]}],
563
+ UserPromptSubmit: [{ hooks: [
564
+ cmd('rdc-invocation-marker.js', 'Marking RDC prompt...'),
565
+ ]}],
560
566
  SessionStart: [{ hooks: [
561
567
  cmd('check-cwd.js'),
562
568
  cmd('check-stale-work-items.js', 'Checking for stale work items...'),
@@ -582,6 +588,7 @@ function buildHooksConfig(hooksDir) {
582
588
  ]}],
583
589
  Stop: [{ hooks: [
584
590
  cmd('rate-limit-retry.js', 'Checking for rate limits...'),
591
+ cmd('rdc-output-contract-gate.js', 'Checking RDC output contract...'),
585
592
  cmd('post-work-check.js', 'Checking for undocumented work...'),
586
593
  cmd('no-stop-open-epics.js', 'Checking for open epics...'),
587
594
  ]}],
@@ -82,6 +82,20 @@ if ($SkipHooks) {
82
82
  $hooksBase = $hooksDir.Replace("\", "/")
83
83
 
84
84
  $hooksConfig = [PSCustomObject]@{
85
+ UserPromptExpansion = @(
86
+ [PSCustomObject]@{
87
+ hooks = @(
88
+ [PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/rdc-invocation-marker.js`""; statusMessage = "Marking RDC slash command..." }
89
+ )
90
+ }
91
+ )
92
+ UserPromptSubmit = @(
93
+ [PSCustomObject]@{
94
+ hooks = @(
95
+ [PSCustomObject]@{ type = "command"; command = "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$hooksBase/run-hidden-hook.ps1`" `"$hooksBase/rdc-invocation-marker.js`""; statusMessage = "Marking RDC prompt..." }
96
+ )
97
+ }
98
+ )
85
99
  SessionStart = @(
86
100
  [PSCustomObject]@{
87
101
  hooks = @(
@@ -130,6 +144,7 @@ if ($SkipHooks) {
130
144
  [PSCustomObject]@{
131
145
  hooks = @(
132
146
  [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..." },
147
+ [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..." },
133
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..." },
134
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..." }
135
150
  )
@@ -699,6 +699,7 @@ function auditOrphanHooks(results) {
699
699
  })
700
700
  .map((f) => join(SKILLS_DIR, f, "SKILL.md")),
701
701
  join(REPO_ROOT, ".claude", "settings.json"),
702
+ join(REPO_ROOT, "scripts", "install-rdc-skills.js"),
702
703
  PLUGIN_MANIFEST,
703
704
  ];
704
705
  for (const src of sources) {
@@ -729,6 +730,78 @@ function auditOrphanHooks(results) {
729
730
  return findings;
730
731
  }
731
732
 
733
+ function auditRequiredHookWiring() {
734
+ const findings = [];
735
+ const required = [
736
+ {
737
+ file: join(REPO_ROOT, "scripts", "install-rdc-skills.js"),
738
+ label: "scripts/install-rdc-skills.js",
739
+ tokens: [
740
+ "UserPromptExpansion",
741
+ "UserPromptSubmit",
742
+ "Stop",
743
+ "rdc-invocation-marker.js",
744
+ "rdc-output-contract-gate.js",
745
+ ],
746
+ },
747
+ {
748
+ file: join(REPO_ROOT, "scripts", "install.ps1"),
749
+ label: "scripts/install.ps1",
750
+ tokens: [
751
+ "UserPromptExpansion",
752
+ "UserPromptSubmit",
753
+ "Stop",
754
+ "rdc-invocation-marker.js",
755
+ "rdc-output-contract-gate.js",
756
+ ],
757
+ },
758
+ ];
759
+
760
+ for (const hookFile of [
761
+ "rdc-invocation-marker.js",
762
+ "rdc-output-contract-gate.js",
763
+ ]) {
764
+ if (!existsSync(join(HOOKS_DIR, hookFile))) {
765
+ findings.push({
766
+ level: "error",
767
+ code: "required-hook-missing",
768
+ message: `hooks/${hookFile} is required for RDC output-contract enforcement`,
769
+ });
770
+ }
771
+ }
772
+
773
+ for (const target of required) {
774
+ if (!existsSync(target.file)) {
775
+ findings.push({
776
+ level: "error",
777
+ code: "hook-installer-missing",
778
+ message: `${target.label} not found for RDC hook wiring audit`,
779
+ });
780
+ continue;
781
+ }
782
+ let text = "";
783
+ try { text = readFileSync(target.file, "utf8"); } catch (e) {
784
+ findings.push({
785
+ level: "error",
786
+ code: "hook-installer-unreadable",
787
+ message: `${target.label} unreadable: ${e.message}`,
788
+ });
789
+ continue;
790
+ }
791
+ for (const token of target.tokens) {
792
+ if (!text.includes(token)) {
793
+ findings.push({
794
+ level: "error",
795
+ code: "required-hook-wiring-missing",
796
+ message: `${target.label} must reference ${token}`,
797
+ });
798
+ }
799
+ }
800
+ }
801
+
802
+ return findings;
803
+ }
804
+
732
805
  // ─── Tier 2 behavioral runner ──────────────────────────────────────────────
733
806
 
734
807
  async function runPool(items, concurrency, worker) {
@@ -1143,14 +1216,16 @@ function main() {
1143
1216
  // Cross-skill checks — deferred until all audits complete
1144
1217
  let duplicateFindings = [];
1145
1218
  let orphanHookFindings = [];
1219
+ let requiredHookFindings = [];
1146
1220
  if (!ONLY_SKILL) {
1147
1221
  duplicateFindings = auditDuplicates(results);
1148
1222
  orphanHookFindings = auditOrphanHooks(results);
1223
+ requiredHookFindings = auditRequiredHookWiring();
1149
1224
  }
1150
1225
 
1151
- if (duplicateFindings.length + orphanHookFindings.length > 0) {
1226
+ if (duplicateFindings.length + orphanHookFindings.length + requiredHookFindings.length > 0) {
1152
1227
  console.log("\nglobal findings\n");
1153
- for (const f of [...duplicateFindings, ...orphanHookFindings]) {
1228
+ for (const f of [...duplicateFindings, ...orphanHookFindings, ...requiredHookFindings]) {
1154
1229
  console.log(` [${f.level}] ${f.code}: ${f.message}`);
1155
1230
  }
1156
1231
  }
@@ -1166,12 +1241,14 @@ function main() {
1166
1241
  ...manifestAudit.findings.filter((f) => f.level === "error"),
1167
1242
  ...duplicateFindings.filter((f) => f.level === "error"),
1168
1243
  ...orphanHookFindings.filter((f) => f.level === "error"),
1244
+ ...requiredHookFindings.filter((f) => f.level === "error"),
1169
1245
  ...(guideValidatorResult ? guideValidatorResult.errors : []),
1170
1246
  ];
1171
1247
  const globalWarnings = [
1172
1248
  ...manifestAudit.findings.filter((f) => f.level === "warn"),
1173
1249
  ...duplicateFindings.filter((f) => f.level === "warn"),
1174
1250
  ...orphanHookFindings.filter((f) => f.level === "warn"),
1251
+ ...requiredHookFindings.filter((f) => f.level === "warn"),
1175
1252
  ...(guideValidatorResult ? guideValidatorResult.warnings : []),
1176
1253
  ];
1177
1254