@lifeaitools/rdc-skills 0.35.21 → 0.35.23

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,256 @@
1
+ #!/usr/bin/env node
2
+ // rdc-design-compare-cli.mjs — dispatches the SAME design brief to a Claude-side
3
+ // pass and a Codex-side pass, in parallel, and writes both raw outputs plus a
4
+ // machine-readable report. This is the real dispatch mechanism behind
5
+ // `rdc:design compare` / `--compare` (see skills/design/SKILL.md §Design Compare).
6
+ //
7
+ // It reuses the fleet's existing dual-engine spawn pattern — the same one
8
+ // scripts/lib/runner.mjs already uses to run `claude --print` and
9
+ // `codex exec --json` against a prompt for skill acceptance tests, and the
10
+ // same read-only-sandbox shape CDE's codex-cli-implementor.ts uses for a
11
+ // fresh planning turn (`codex exec --sandbox read-only --json <prompt>`).
12
+ //
13
+ // Both engines are told explicitly this is a proposal-only pass; the Codex
14
+ // side additionally enforces that at the sandbox level (`--sandbox read-only`),
15
+ // not just by prompt instruction. Neither side is expected to mutate files.
16
+ //
17
+ // The script does NOT synthesize the comparison itself — it hands both raw
18
+ // outputs to the calling agent, which reads claude.md + codex.md and writes
19
+ // the structured comparison table per skills/design/SKILL.md. A script
20
+ // diffing two paragraphs of prose cannot judge design tradeoffs; an agent can.
21
+
22
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
23
+ import { dirname, join, resolve } from "node:path";
24
+ import { fileURLToPath } from "node:url";
25
+ import { spawnHiddenShell } from "./lib/runner.mjs";
26
+
27
+ const __dirname = dirname(fileURLToPath(import.meta.url));
28
+ const repoRoot = resolve(__dirname, "..");
29
+ const skillRoot = join(repoRoot, "skills", "design");
30
+ const reportsRoot = join(repoRoot, ".rdc", "reports", "rdc-design-cli");
31
+
32
+ const DEFAULT_TIMEOUT_MS = Number(process.env.RDC_DESIGN_COMPARE_TIMEOUT_MS ?? "240000");
33
+
34
+ const commandRefs = {
35
+ studio: ["studio-model", "ownership"],
36
+ tokens: ["studio-model", "ownership"],
37
+ palette: ["studio-model", "rampa", "ownership"],
38
+ theme: ["studio-model", "rampa", "ownership"],
39
+ colorize: ["rampa", "studio-model", "ownership"],
40
+ audit: ["studio-model", "ownership"],
41
+ critique: ["studio-model", "ownership"],
42
+ polish: ["studio-model", "ownership"],
43
+ craft: ["studio-model", "rampa", "ownership"],
44
+ prototype: ["studio-model", "rampa", "ownership"],
45
+ };
46
+
47
+ function parseArgs(argv) {
48
+ const out = { json: false, out: null, command: "craft", rest: [] };
49
+ for (let i = 0; i < argv.length; i++) {
50
+ const a = argv[i];
51
+ if (a === "--json") { out.json = true; continue; }
52
+ if (a === "--out") { out.out = argv[++i]; continue; }
53
+ if (a === "--command") { out.command = argv[++i]; continue; }
54
+ out.rest.push(a);
55
+ }
56
+ return out;
57
+ }
58
+
59
+ function readText(path) {
60
+ return readFileSync(path, "utf8").replace(/\r\n/g, "\n");
61
+ }
62
+
63
+ function stripFrontmatter(text) {
64
+ return text.replace(/^---\n[\s\S]*?\n---\n?/, "").trim();
65
+ }
66
+
67
+ function loadReferences(names) {
68
+ return names.map((name) => {
69
+ const path = join(skillRoot, "reference", `${name}.md`);
70
+ if (!existsSync(path)) throw new Error(`Missing reference: ${path}`);
71
+ return { name, path, text: readText(path) };
72
+ });
73
+ }
74
+
75
+ function buildDesignPrompt(brief, command) {
76
+ const skillPath = join(skillRoot, "SKILL.md");
77
+ if (!existsSync(skillPath)) throw new Error(`Missing skill file: ${skillPath}`);
78
+ const refs = loadReferences(commandRefs[command] || commandRefs.craft);
79
+ const skill = stripFrontmatter(readText(skillPath));
80
+ const refText = refs.map((ref) => `## Reference: ${ref.name}\n\n${ref.text}`).join("\n\n");
81
+
82
+ return [
83
+ "# rdc:design Compare — Independent Design Proposal Pass",
84
+ "",
85
+ `Brief: ${brief}`,
86
+ "",
87
+ "## Operating Instructions",
88
+ "",
89
+ skill,
90
+ "",
91
+ refText,
92
+ "",
93
+ "## Your Task",
94
+ "",
95
+ `Produce an independent design proposal for: ${brief}`,
96
+ "",
97
+ "This is a READ-ONLY proposal pass. Do NOT create, edit, or delete any files, and do not",
98
+ "run any command that mutates the working tree. Respond with a single written design",
99
+ "proposal covering, at minimum: the dominant visual object, information hierarchy,",
100
+ "interaction model, responsive transformation, semantic color / token approach, and",
101
+ "named tradeoffs. Cite real existing tokens, components, or routes where relevant rather",
102
+ "than inventing generic ones.",
103
+ ].join("\n");
104
+ }
105
+
106
+ function extractClaudeFinalText(stdout) {
107
+ let lastAssistantText = "";
108
+ let resultText = "";
109
+ for (const line of stdout.split(/\r?\n/)) {
110
+ const trimmed = line.trim();
111
+ if (!trimmed) continue;
112
+ let evt;
113
+ try {
114
+ evt = JSON.parse(trimmed);
115
+ } catch {
116
+ continue; // a non-JSON transport line is not a provider transcript to retain
117
+ }
118
+ if (evt.type === "assistant" && Array.isArray(evt.message?.content)) {
119
+ const text = evt.message.content
120
+ .filter((b) => b?.type === "text" && typeof b.text === "string")
121
+ .map((b) => b.text)
122
+ .join("\n");
123
+ if (text) lastAssistantText = text;
124
+ }
125
+ if (evt.type === "result" && typeof evt.result === "string") {
126
+ resultText = evt.result;
127
+ }
128
+ }
129
+ return resultText || lastAssistantText || stdout.trim();
130
+ }
131
+
132
+ function extractCodexFinalText(stdout) {
133
+ let final = "";
134
+ for (const line of stdout.split(/\r?\n/)) {
135
+ const trimmed = line.trim();
136
+ if (!trimmed) continue;
137
+ let evt;
138
+ try {
139
+ evt = JSON.parse(trimmed);
140
+ } catch {
141
+ continue;
142
+ }
143
+ if (evt.item?.type === "agent_message" && typeof evt.item.text === "string") final = evt.item.text;
144
+ }
145
+ return final || stdout.trim();
146
+ }
147
+
148
+ async function main() {
149
+ const { json, out, command, rest } = parseArgs(process.argv.slice(2));
150
+ const brief = rest.join(" ").trim();
151
+
152
+ if (!brief) {
153
+ console.error(
154
+ 'rdc-design-compare-cli: a design brief is required, e.g.\n node scripts/rdc-design-compare-cli.mjs "dashboard hero panel for a stewardship dashboard"',
155
+ );
156
+ process.exit(1);
157
+ return;
158
+ }
159
+
160
+ const prompt = buildDesignPrompt(brief, command);
161
+ const claudeBin = process.env.CLAUDE_BIN || "claude";
162
+ const codexBin = process.env.CODEX_BIN || "codex";
163
+ const timeoutMs = DEFAULT_TIMEOUT_MS;
164
+ const env = { ...process.env, RDC_TEST: process.env.RDC_TEST ?? "1" };
165
+
166
+ // The assembled prompt (SKILL.md + references) routinely exceeds Windows' ~8191-char
167
+ // command-line length limit, so it is NEVER passed as an argv element — both engines
168
+ // read it from stdin instead (both support this; verified live against codex-cli 0.146.0
169
+ // and claude-code 2.1.233 on this box before this script shipped).
170
+ //
171
+ // Claude side: no positional prompt arg, no --dangerously-skip-permissions. A
172
+ // non-interactive --print run with no approved tool grants naturally cannot mutate
173
+ // files; the prompt also says not to. cwd = repo root so it can cite real
174
+ // tokens/components if asked.
175
+ const claudeArgs = ["--print", "--output-format", "stream-json", "--verbose"];
176
+
177
+ // Codex side: explicit "-" reads the prompt from stdin. Read-only sandbox is a
178
+ // technical enforcement, not just a prompt instruction — mirrors CDE's
179
+ // readOnlyPlanningArgs in codex-cli-implementor.ts.
180
+ const codexArgs = ["exec", "--ignore-user-config", "--ignore-rules", "--sandbox", "read-only", "--json", "-"];
181
+
182
+ const [claudeRes, codexRes] = await Promise.all([
183
+ spawnHiddenShell(claudeBin, claudeArgs, { cwd: repoRoot, env, timeoutMs, stdin: prompt }),
184
+ spawnHiddenShell(codexBin, codexArgs, { cwd: repoRoot, env, timeoutMs, stdin: prompt }),
185
+ ]);
186
+
187
+ const claudeOk = claudeRes.exit === 0 && !claudeRes.timedOut;
188
+ const codexOk = codexRes.exit === 0 && !codexRes.timedOut;
189
+ const claudeText = claudeOk ? extractClaudeFinalText(claudeRes.stdout) : "";
190
+ const codexText = codexOk ? extractCodexFinalText(codexRes.stdout) : "";
191
+
192
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
193
+ const dir = out ? resolve(out) : join(reportsRoot, `compare-${stamp}`);
194
+ mkdirSync(dir, { recursive: true });
195
+
196
+ const claudePath = join(dir, "claude.md");
197
+ const codexPath = join(dir, "codex.md");
198
+ const reportPath = join(dir, "report.json");
199
+
200
+ writeFileSync(claudePath, claudeText || "(no output — see report.json for exit code / stderr)\n");
201
+ writeFileSync(codexPath, codexText || "(no output — see report.json for exit code / stderr)\n");
202
+
203
+ const report = {
204
+ brief,
205
+ command,
206
+ generated_at: new Date().toISOString(),
207
+ claude: {
208
+ ok: claudeOk,
209
+ exit: claudeRes.exit,
210
+ timed_out: claudeRes.timedOut,
211
+ stderr: claudeRes.stderr.slice(0, 4000),
212
+ chars: claudeText.length,
213
+ },
214
+ codex: {
215
+ ok: codexOk,
216
+ exit: codexRes.exit,
217
+ timed_out: codexRes.timedOut,
218
+ stderr: codexRes.stderr.slice(0, 4000),
219
+ chars: codexText.length,
220
+ },
221
+ paths: { dir, claude_md: claudePath, codex_md: codexPath, report_json: reportPath },
222
+ };
223
+ writeFileSync(reportPath, JSON.stringify(report, null, 2));
224
+
225
+ if (json) {
226
+ console.log(JSON.stringify(report, null, 2));
227
+ } else {
228
+ console.log("rdc-design-compare-cli");
229
+ console.log(`brief: ${brief}`);
230
+ console.log(
231
+ `claude: ${claudeOk ? "ok" : `FAILED (exit ${claudeRes.exit}${claudeRes.timedOut ? ", timed out" : ""})`} — ${claudeText.length} chars`,
232
+ );
233
+ console.log(
234
+ `codex: ${codexOk ? "ok" : `FAILED (exit ${codexRes.exit}${codexRes.timedOut ? ", timed out" : ""})`} — ${codexText.length} chars`,
235
+ );
236
+ console.log(`report: ${reportPath}`);
237
+ console.log(`claude.md: ${claudePath}`);
238
+ console.log(`codex.md: ${codexPath}`);
239
+ if (!claudeOk && !codexOk) {
240
+ console.log("Both engines failed — nothing to compare. See report.json for stderr.");
241
+ } else if (!codexOk) {
242
+ console.log("Codex unreachable/failed — single-engine (Claude) result only. Report this; do not fabricate a second opinion.");
243
+ } else if (!claudeOk) {
244
+ console.log("Claude side failed — single-engine (Codex) result only. Report this; do not fabricate a second opinion.");
245
+ }
246
+ }
247
+
248
+ // Non-zero only when BOTH engines failed — a single-engine fallback is a
249
+ // reportable degraded result, not a hard failure of the compare tool itself.
250
+ process.exit(claudeOk || codexOk ? 0 : 1);
251
+ }
252
+
253
+ main().catch((error) => {
254
+ console.error(`rdc-design-compare-cli error: ${error.message}`);
255
+ process.exit(1);
256
+ });