@lifeaitools/rdc-skills 0.9.30 → 0.9.32

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.28",
3
+ "version": "0.9.32",
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",
@@ -120,11 +120,25 @@ Severity rules:
120
120
 
121
121
  `--fix` auto-remediates only: missing watch_paths, registry row updates, CF cache purges. Never touches env vars, DNS, or container config without explicit confirmation.
122
122
 
123
+ ## Coolify Access — clauth + REST API
124
+
125
+ All Coolify operations use the clauth daemon and the Coolify REST API directly.
126
+ There is no Coolify MCP server — do not reference `@masonator/coolify-mcp`.
127
+
128
+ ```bash
129
+ _COOLIFY=$(curl -s http://127.0.0.1:52437/v/coolify-api)
130
+ curl -s -H "Authorization: Bearer $_COOLIFY" https://deploy.regendevcorp.com/api/v1/applications
131
+ ```
132
+
133
+ If clauth daemon is not responding:
134
+ ```
135
+ BLOCKED: clauth daemon not responding. Run scripts\restart-clauth.bat, unlock at http://127.0.0.1:52437
136
+ ```
137
+
123
138
  ## References
124
139
 
125
140
  - Type-specific checklists + DNS tree + gate commands: `docs/runbooks/coolify-deploy-checklist.md`
126
141
  - Rules / registry RPCs / hard limits: `.claude/rules/coolify-deployment.md`
127
- - MCP server: `@masonator/coolify-mcp` (38 tools)
128
142
  - Infrastructure constants:
129
143
  ```
130
144
  Server UUID: ih386anenvvvn6fy1umtyow0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/rdc-skills",
3
- "version": "0.9.30",
3
+ "version": "0.9.32",
4
4
  "description": "RDC typed-agent dispatch skill suite for Claude Code — plan, build, review, overnight builds",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -0,0 +1,15 @@
1
+ # Bad Infrastructure Guide — FIXTURE (contains banned terms for self-test)
2
+ > This file is used by the rdc:self-test guide-content validator fixture test.
3
+ > It intentionally contains banned terms in positive-instruction context.
4
+
5
+ ## Coolify Access
6
+
7
+ All Coolify operations via `@masonator/coolify-mcp` (38 tools, pre-authenticated).
8
+
9
+ Use the `@masonator/coolify-mcp` MCP tool to deploy apps.
10
+
11
+ You can also use brand-studio for design token work via @regen/brand-studio.
12
+
13
+ ## Clauth Key
14
+
15
+ Use `curl -s http://127.0.0.1:52437/v/nonexistent-key` for that service.
@@ -0,0 +1,16 @@
1
+ # Good Infrastructure Guide — FIXTURE (clean, no banned terms)
2
+ > This file is used by the rdc:self-test guide-content validator fixture test.
3
+ > It is a clean guide with no banned terms.
4
+
5
+ ## Coolify Access
6
+
7
+ There is no Coolify MCP server. Do not reference `@masonator/coolify-mcp`.
8
+ Never use brand-studio — the canonical name is Studio.
9
+ The @regen/brand-studio package does not exist — use @regen/studio.
10
+
11
+ All Coolify operations use the clauth daemon + REST API:
12
+
13
+ ```bash
14
+ _COOLIFY=$(curl -s http://127.0.0.1:52437/v/coolify-api)
15
+ curl -s -H "Authorization: Bearer $_COOLIFY" https://deploy.regendevcorp.com/api/v1/applications
16
+ ```
@@ -140,30 +140,53 @@ function buildPluginCache(cacheDir, version, gitSha) {
140
140
  // Claude Code loads that directory AND the plugin cache, so any rdc skills left
141
141
  // there produce duplicate registrations and break the resolver.
142
142
  // This function nukes any entry whose frontmatter name starts with "rdc:".
143
+ // Scans BOTH the immediate dir and nested .md files (e.g. `user/skill.md`,
144
+ // `user/rdc-build/SKILL.md`) so pre-plugin orphans are caught regardless of
145
+ // naming convention.
143
146
  function cleanUserSkills(userSkillsDir) {
144
147
  if (!fs.existsSync(userSkillsDir)) return 0;
145
148
  let removed = 0;
146
149
  for (const entry of fs.readdirSync(userSkillsDir, { withFileTypes: true })) {
147
150
  const candidate = path.join(userSkillsDir, entry.name);
148
- let skillFile = null;
149
151
  if (entry.isDirectory()) {
150
- // New format: <name>/SKILL.md or <name>/skill.md
152
+ // Subdir form: <name>/SKILL.md or <name>/skill.md
153
+ let skillFile = null;
151
154
  for (const sf of ['SKILL.md', 'skill.md']) {
152
155
  const p = path.join(candidate, sf);
153
156
  if (fs.existsSync(p)) { skillFile = p; break; }
154
157
  }
155
- } else if (entry.isFile() && entry.name.endsWith('.md') && entry.name !== 'skill.md' && entry.name !== 'SKILL.md' && entry.name !== 'README.md') {
156
- // Old format: rdc-deploy.md flat file
157
- skillFile = candidate;
158
+ if (!skillFile) continue;
159
+ const fm = readFrontmatter(skillFile);
160
+ if (fm.name && fm.name.startsWith('rdc:')) {
161
+ try { fs.rmSync(candidate, { recursive: true, force: true }); removed++; } catch {}
162
+ }
163
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
164
+ // ANY .md file at this level — including skill.md / SKILL.md / README.md
165
+ // if their frontmatter declares an rdc:* skill. A previous version skipped
166
+ // those names; that left an orphan rdc:build copy at user/skill.md which
167
+ // registered as a duplicate "user" skill.
168
+ const fm = readFrontmatter(candidate);
169
+ if (fm.name && fm.name.startsWith('rdc:')) {
170
+ try { fs.unlinkSync(candidate); removed++; } catch {}
171
+ }
158
172
  }
159
- if (!skillFile) continue;
160
- const fm = readFrontmatter(skillFile);
161
- if (fm.name && fm.name.startsWith('rdc:')) {
162
- try {
163
- if (entry.isDirectory()) fs.rmSync(candidate, { recursive: true, force: true });
164
- else fs.unlinkSync(candidate);
165
- removed++;
166
- } catch {}
173
+ }
174
+ return removed;
175
+ }
176
+
177
+ // Scrub legacy rdc orphans from ~/.claude/skills/ (top-level), not just user/.
178
+ // Some older installs landed flat skill files alongside the plugin tree.
179
+ function cleanGlobalSkillsRoot(skillsDir) {
180
+ if (!fs.existsSync(skillsDir)) return 0;
181
+ let removed = 0;
182
+ for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
183
+ if (entry.name === 'user') continue; // handled separately
184
+ const candidate = path.join(skillsDir, entry.name);
185
+ if (entry.isFile() && entry.name.endsWith('.md')) {
186
+ const fm = readFrontmatter(candidate);
187
+ if (fm.name && fm.name.startsWith('rdc:')) {
188
+ try { fs.unlinkSync(candidate); removed++; } catch {}
189
+ }
167
190
  }
168
191
  }
169
192
  return removed;
@@ -192,11 +215,14 @@ function cleanStaleHooks(hooksDstDir) {
192
215
  }
193
216
 
194
217
  // ── Cache flush helper ────────────────────────────────────────────────────────
195
- function flushOldCaches(cacheBase, keepVersion) {
218
+ // Only `latest/` is ever kept. Earlier versions wrote BOTH `<version>/` and
219
+ // `latest/`, which caused the plugin loader to scan and register every rdc:*
220
+ // skill twice (once per directory). The fix is permanent single-dir layout.
221
+ function flushOldCaches(cacheBase /* keepVersion intentionally unused */) {
196
222
  if (!fs.existsSync(cacheBase)) return 0;
197
223
  let flushed = 0;
198
224
  for (const entry of fs.readdirSync(cacheBase)) {
199
- if (entry === keepVersion || entry === 'latest') continue;
225
+ if (entry === 'latest') continue;
200
226
  try {
201
227
  fs.rmSync(path.join(cacheBase, entry), { recursive: true, force: true });
202
228
  flushed++;
@@ -211,7 +237,6 @@ function registerCLI(version, gitSha) {
211
237
  const mktDir = path.join(pluginDir, 'marketplaces', MARKETPLACE);
212
238
  const mktPlugDir = path.join(mktDir, '.claude-plugin');
213
239
  const cacheBase = path.join(pluginDir, 'cache', MARKETPLACE, 'rdc-skills');
214
- const cacheDir = path.join(cacheBase, version);
215
240
  const latestDir = path.join(cacheBase, 'latest');
216
241
 
217
242
  // 1. Marketplace manifest
@@ -224,12 +249,11 @@ function registerCLI(version, gitSha) {
224
249
  knownMp[MARKETPLACE] = { source: { source: 'github', repo: 'LIFEAI/rdc-skills' }, installLocation: mktDir, lastUpdated: new Date().toISOString() };
225
250
  writeJson(kmpPath, knownMp, 4);
226
251
 
227
- // 3. Flush stale version caches, then write versioned + stable latest
228
- const flushed = flushOldCaches(cacheBase, version);
252
+ // 3. Flush every cache dir except `latest/`, then rewrite `latest/`. We
253
+ // intentionally do NOT keep a versioned dir — the plugin loader registers
254
+ // every dir it finds, so two dirs = duplicate skills.
255
+ const flushed = flushOldCaches(cacheBase);
229
256
  if (flushed > 0) info(` flushed : ${flushed} stale cache dir(s)`);
230
- buildPluginCache(cacheDir, version, gitSha);
231
- // Write to stable 'latest/' so open terminals can pick up changes if they
232
- // re-read installed_plugins.json between skill invocations.
233
257
  if (fs.existsSync(latestDir)) fs.rmSync(latestDir, { recursive: true, force: true });
234
258
  buildPluginCache(latestDir, version, gitSha);
235
259
 
@@ -312,7 +336,6 @@ function registerCowork(version, gitSha) {
312
336
  for (const { dir, settingsFile } of bases) {
313
337
  const pluginsDir = path.join(dir, 'cowork_plugins');
314
338
  const cacheBase = path.join(pluginsDir, 'cache', MARKETPLACE, 'rdc-skills');
315
- const cacheDir = path.join(cacheBase, version);
316
339
  const latestDir = path.join(cacheBase, 'latest');
317
340
  const mktDir = path.join(pluginsDir, 'marketplaces', MARKETPLACE);
318
341
  const mktPlugDir = path.join(mktDir, '.claude-plugin');
@@ -327,9 +350,8 @@ function registerCowork(version, gitSha) {
327
350
  knownMp[MARKETPLACE] = { source: { source: 'github', repo: 'LIFEAI/rdc-skills' }, installLocation: mktDir, lastUpdated: new Date().toISOString() };
328
351
  writeJson(kmpPath, knownMp, 4);
329
352
 
330
- // Flush stale caches, write versioned + stable latest
331
- flushOldCaches(cacheBase, version);
332
- buildPluginCache(cacheDir, version, gitSha);
353
+ // Flush all non-latest caches and rewrite `latest/` only — single-dir layout
354
+ flushOldCaches(cacheBase);
333
355
  if (fs.existsSync(latestDir)) fs.rmSync(latestDir, { recursive: true, force: true });
334
356
  buildPluginCache(latestDir, version, gitSha);
335
357
 
@@ -622,6 +644,10 @@ async function main() {
622
644
  const userSkillsDir = path.join(claudeHome, 'skills', 'user');
623
645
  const purged = cleanUserSkills(userSkillsDir);
624
646
  if (purged > 0) ok(`[0.5a] Skills cleanup — removed ${purged} stale rdc: skill(s) from skills/user/`);
647
+ // Also scan the parent ~/.claude/skills/ for any flat-file rdc orphans.
648
+ const skillsRoot = path.join(claudeHome, 'skills');
649
+ const rootPurged = cleanGlobalSkillsRoot(skillsRoot);
650
+ if (rootPurged > 0) ok(`[0.5a] Skills cleanup — removed ${rootPurged} stale rdc: file(s) from skills/`);
625
651
  }
626
652
 
627
653
  // 0.5b. Stale hook cleanup — remove hooks we no longer ship
@@ -742,6 +768,53 @@ async function main() {
742
768
  console.log(' \x1b[36mPreflight:\x1b[0m');
743
769
  runPreflight();
744
770
 
771
+ // 6.5 Post-install verification — duplication guard
772
+ console.log('');
773
+ console.log(' \x1b[36mPost-install verification:\x1b[0m');
774
+ let verifyFailed = false;
775
+ {
776
+ const cacheBase = path.join(claudeHome, 'plugins', 'cache', MARKETPLACE, 'rdc-skills');
777
+ const cacheDirs = fs.existsSync(cacheBase) ? fs.readdirSync(cacheBase) : [];
778
+ if (cacheDirs.length === 1 && cacheDirs[0] === 'latest') {
779
+ ok(`plugin cache : 1 dir (latest/)`);
780
+ } else {
781
+ fail(`plugin cache : expected exactly [latest/], found [${cacheDirs.join(', ')}]`);
782
+ verifyFailed = true;
783
+ }
784
+ const ipPath = path.join(claudeHome, 'plugins', 'installed_plugins.json');
785
+ const installed = readJson(ipPath, { plugins: {} });
786
+ const rdcEntries = installed.plugins[PLUGIN_KEY] || [];
787
+ if (rdcEntries.length === 1) {
788
+ ok(`installed_plugins: 1 entry for ${PLUGIN_KEY}`);
789
+ } else {
790
+ fail(`installed_plugins: expected 1 entry, found ${rdcEntries.length}`);
791
+ verifyFailed = true;
792
+ }
793
+ const userSkillsDir = path.join(claudeHome, 'skills', 'user');
794
+ const stillThere = fs.existsSync(userSkillsDir)
795
+ ? fs.readdirSync(userSkillsDir).filter(f => {
796
+ const p = path.join(userSkillsDir, f);
797
+ const skillFile = fs.statSync(p).isDirectory()
798
+ ? ['SKILL.md','skill.md'].map(s => path.join(p, s)).find(fs.existsSync)
799
+ : (f.endsWith('.md') ? p : null);
800
+ if (!skillFile) return false;
801
+ const fm = readFrontmatter(skillFile);
802
+ return fm.name && fm.name.startsWith('rdc:');
803
+ })
804
+ : [];
805
+ if (stillThere.length === 0) {
806
+ ok(`skills/user/ : no rdc: orphans`);
807
+ } else {
808
+ fail(`skills/user/ : still has rdc: orphans: ${stillThere.join(', ')}`);
809
+ verifyFailed = true;
810
+ }
811
+ }
812
+ if (verifyFailed) {
813
+ console.log('');
814
+ fail('Post-install verification FAILED — duplicates or orphans remain. Investigate.');
815
+ process.exit(2);
816
+ }
817
+
745
818
  // Done
746
819
  console.log('');
747
820
  console.log(' \x1b[32mDone!\x1b[0m');
@@ -49,6 +49,181 @@ const HOOKS_DIR = join(REPO_ROOT, "hooks");
49
49
  const PLUGIN_MANIFEST = join(REPO_ROOT, ".claude-plugin", "plugin.json");
50
50
  const PACKAGE_JSON = join(REPO_ROOT, "package.json");
51
51
 
52
+ // ─── Guide-content validator constants ───────────────────────────────────────
53
+ // Path to the regen-root project (sibling of rdc-skills).
54
+ // Override with REGEN_ROOT env var when running from a non-standard location.
55
+ const REGEN_ROOT = process.env.REGEN_ROOT || "C:/Dev/regen-root";
56
+
57
+ // Terms that must NOT appear in guide/rule files as positive instructions.
58
+ // A line is flagged only when it does NOT contain a known negation pattern.
59
+ const GUIDE_BANNED_TERMS = [
60
+ "@masonator/coolify-mcp",
61
+ "@masonator",
62
+ "coolify-mcp",
63
+ "@regen/brand-studio",
64
+ "brand-studio",
65
+ ];
66
+
67
+ // Negation patterns — if a line contains one of these, the banned-term
68
+ // occurrence is an explicit "don't use" warning and should NOT be flagged.
69
+ const GUIDE_NEGATION_PATTERNS = [
70
+ /\bdo not\b/i,
71
+ /\bnever\b/i,
72
+ /\bno such\b/i,
73
+ /\bdoes not exist\b/i,
74
+ /\bbanned\b/i,
75
+ /\bnot reference\b/i,
76
+ /\bnot use\b/i,
77
+ /\bavoid\b/i,
78
+ /\bremoved\b/i,
79
+ /\bdeprecated\b/i,
80
+ // Markdown table row showing a WRONG→CORRECT mapping (naming-corrections.md pattern)
81
+ /^\|[^|]*WRONG[^|]*\|/i,
82
+ // A table row where the term is in the WRONG column (first data column after the | WRONG | header)
83
+ // Heuristic: line starts with | and the term appears before the first CORRECT value
84
+ /^\|\s*(Brand Studio|brand-studio|@regen\/brand-studio|@masonator[^ |]*|coolify-mcp)[^|]*\|\s*\*\*/,
85
+ ];
86
+
87
+ // Known valid clauth key names. Extend when new keys are added to the vault.
88
+ // Source: C:/Dev/regen-root/.rdc/guides/agent-bootstrap.md credential table
89
+ // + C:/Dev/regen-root/.claude/rules/clauth-endpoints.md
90
+ const KNOWN_CLAUTH_KEYS = new Set([
91
+ "coolify-api",
92
+ "cloudflare",
93
+ "npm",
94
+ "supabase",
95
+ "supabase-anon",
96
+ "supabase-db",
97
+ "r2-access-key-id",
98
+ "r2-secret-key",
99
+ "anthropic",
100
+ "openai",
101
+ "github",
102
+ "github-token",
103
+ "vultr-dev-ssh",
104
+ "mcp-web-research-secret",
105
+ "mcp-regen-media-secret",
106
+ "web-research",
107
+ "regen-media",
108
+ ]);
109
+
110
+ /**
111
+ * Audit a single guide or rule file for banned terms and clauth key validity.
112
+ *
113
+ * Returns an array of finding objects:
114
+ * { level: 'error'|'warn', code: string, file: string, line: number, message: string }
115
+ */
116
+ function auditGuideFile(filepath, relPath) {
117
+ const findings = [];
118
+ let text;
119
+ try {
120
+ text = readFileSync(filepath, "utf8");
121
+ } catch (e) {
122
+ findings.push({ level: "error", code: "guide-unreadable", file: relPath, line: 0, message: `cannot read: ${e.message}` });
123
+ return findings;
124
+ }
125
+
126
+ const lines = text.split(/\r?\n/);
127
+ lines.forEach((line, i) => {
128
+ const lineNo = i + 1;
129
+
130
+ // Check banned terms (skip negation lines and comment lines)
131
+ for (const term of GUIDE_BANNED_TERMS) {
132
+ if (line.includes(term)) {
133
+ const isNegated = GUIDE_NEGATION_PATTERNS.some((re) => re.test(line));
134
+ if (!isNegated) {
135
+ findings.push({
136
+ level: "error",
137
+ code: "guide-banned-term",
138
+ file: relPath,
139
+ line: lineNo,
140
+ message: `banned term "${term}" found as positive instruction — replace with clauth+REST pattern`,
141
+ });
142
+ }
143
+ }
144
+ }
145
+
146
+ // Check clauth /v/<key> references — flag unknown key names
147
+ // Pattern: /v/some-key-name (standalone path, not inside a larger URL path)
148
+ const clauthKeyRe = /\bhttp:\/\/127\.0\.0\.1:52437\/v\/([\w-]+)/g;
149
+ let m;
150
+ while ((m = clauthKeyRe.exec(line)) !== null) {
151
+ const key = m[1];
152
+ if (!KNOWN_CLAUTH_KEYS.has(key)) {
153
+ findings.push({
154
+ level: "warn",
155
+ code: "guide-clauth-key-unknown",
156
+ file: relPath,
157
+ line: lineNo,
158
+ message: `clauth key "${key}" not in known key list — verify it exists in the vault`,
159
+ });
160
+ }
161
+ }
162
+ });
163
+
164
+ return findings;
165
+ }
166
+
167
+ /**
168
+ * Run the guide-content validator across:
169
+ * 1. C:/Dev/rdc-skills/guides/**\/*.md (base guides shipped with the plugin)
170
+ * 2. REGEN_ROOT/.rdc/guides/**\/*.md (project-level agent guides)
171
+ * 3. REGEN_ROOT/.claude/rules/**\/*.md (project auto-loaded rules)
172
+ *
173
+ * Also scans fixture guides under scripts/fixtures/guides/ when GUIDE_FIXTURE_DIR is set
174
+ * (used by the self-test's own test suite).
175
+ *
176
+ * Returns { findings[], ok, stats }
177
+ */
178
+ function runGuideContentValidator({ fixtureDir } = {}) {
179
+ const allFindings = [];
180
+ let filesScanned = 0;
181
+
182
+ function scanDir(dir, prefix) {
183
+ if (!existsSync(dir)) return;
184
+ let entries;
185
+ try { entries = readdirSync(dir); } catch { return; }
186
+ for (const entry of entries) {
187
+ const fullPath = join(dir, entry);
188
+ let stat;
189
+ try { stat = statSync(fullPath); } catch { continue; }
190
+ if (stat.isDirectory()) {
191
+ scanDir(fullPath, `${prefix}/${entry}`);
192
+ } else if (entry.endsWith(".md")) {
193
+ const relPath = `${prefix}/${entry}`;
194
+ const findings = auditGuideFile(fullPath, relPath);
195
+ allFindings.push(...findings);
196
+ filesScanned++;
197
+ }
198
+ }
199
+ }
200
+
201
+ // 1. Base guides in rdc-skills repo
202
+ scanDir(GUIDES_DIR, "guides");
203
+
204
+ // 2. Regen-root agent guides
205
+ scanDir(join(REGEN_ROOT, ".rdc", "guides"), "regen-root/.rdc/guides");
206
+
207
+ // 3. Regen-root auto-loaded rules
208
+ scanDir(join(REGEN_ROOT, ".claude", "rules"), "regen-root/.claude/rules");
209
+
210
+ // 4. Fixture directory (for self-test's own test assertions)
211
+ if (fixtureDir && existsSync(fixtureDir)) {
212
+ scanDir(fixtureDir, "fixtures");
213
+ }
214
+
215
+ const errors = allFindings.filter((f) => f.level === "error");
216
+ const warnings = allFindings.filter((f) => f.level === "warn");
217
+
218
+ return {
219
+ findings: allFindings,
220
+ errors,
221
+ warnings,
222
+ ok: errors.length === 0,
223
+ stats: { files_scanned: filesScanned, errors: errors.length, warnings: warnings.length },
224
+ };
225
+ }
226
+
52
227
  const STANDARD_BANNER = [
53
228
  "> **⚠️ OUTPUT CONTRACT (READ FIRST):** `guides/output-contract.md`",
54
229
  "> Checklist-only output. No tool-call narration. No raw MCP/JSON/log dumps.",
@@ -775,6 +950,8 @@ function main() {
775
950
  agentResults = agentFiles.map((f) => auditAgentGuide(join(AGENT_GUIDES_DIR, f)));
776
951
  }
777
952
 
953
+ const guideValidatorResultJson = !ONLY_SKILL ? runGuideContentValidator() : null;
954
+
778
955
  const failed = results.filter((r) => r.errors.length > 0);
779
956
  const warned = results.filter((r) => r.warnings.length > 0 && r.errors.length === 0);
780
957
  const clean = results.filter((r) => r.errors.length === 0 && r.warnings.length === 0);
@@ -785,11 +962,13 @@ function main() {
785
962
  ...manifestAudit.findings.filter((f) => f.level === "error"),
786
963
  ...duplicateFindings.filter((f) => f.level === "error"),
787
964
  ...orphanHookFindings.filter((f) => f.level === "error"),
965
+ ...(guideValidatorResultJson ? guideValidatorResultJson.errors : []),
788
966
  ];
789
967
  const globalWarnings = [
790
968
  ...manifestAudit.findings.filter((f) => f.level === "warn"),
791
969
  ...duplicateFindings.filter((f) => f.level === "warn"),
792
970
  ...orphanHookFindings.filter((f) => f.level === "warn"),
971
+ ...(guideValidatorResultJson ? guideValidatorResultJson.warnings : []),
793
972
  ];
794
973
  const fail =
795
974
  failed.length > 0 ||
@@ -819,6 +998,13 @@ function main() {
819
998
  findings: manifestAudit.findings,
820
999
  },
821
1000
  global_findings: [...duplicateFindings, ...orphanHookFindings],
1001
+ guide_content_validator: guideValidatorResultJson
1002
+ ? {
1003
+ ok: guideValidatorResultJson.ok,
1004
+ stats: guideValidatorResultJson.stats,
1005
+ findings: guideValidatorResultJson.findings,
1006
+ }
1007
+ : null,
822
1008
  results: results.map((r) => ({
823
1009
  skill: r.skill,
824
1010
  file: r.file,
@@ -918,6 +1104,28 @@ function main() {
918
1104
  }
919
1105
  }
920
1106
 
1107
+ // Guide-content validation — check .rdc/guides + .claude/rules for banned terms and invalid clauth keys
1108
+ let guideValidatorResult = null;
1109
+ if (!ONLY_SKILL) {
1110
+ guideValidatorResult = runGuideContentValidator();
1111
+ if (guideValidatorResult.stats.files_scanned > 0) {
1112
+ console.log(`\nguide-content validator (${guideValidatorResult.stats.files_scanned} files)\n`);
1113
+ console.log("─".repeat(80));
1114
+ if (guideValidatorResult.findings.length === 0) {
1115
+ console.log(" ✓ no banned terms or invalid clauth keys found");
1116
+ } else {
1117
+ for (const f of guideValidatorResult.findings) {
1118
+ const tag = f.level === "error" ? "FAIL" : "WARN";
1119
+ console.log(` [${tag}] ${f.file}:${f.line} ${f.code} ${f.message}`);
1120
+ }
1121
+ }
1122
+ console.log("─".repeat(80));
1123
+ const gErrors = guideValidatorResult.errors.length;
1124
+ const gWarnings = guideValidatorResult.warnings.length;
1125
+ console.log(`total: ${guideValidatorResult.stats.files_scanned} | errors: ${gErrors} | warnings: ${gWarnings}`);
1126
+ }
1127
+ }
1128
+
921
1129
  // Cross-skill checks — deferred until all audits complete
922
1130
  let duplicateFindings = [];
923
1131
  let orphanHookFindings = [];
@@ -944,11 +1152,13 @@ function main() {
944
1152
  ...manifestAudit.findings.filter((f) => f.level === "error"),
945
1153
  ...duplicateFindings.filter((f) => f.level === "error"),
946
1154
  ...orphanHookFindings.filter((f) => f.level === "error"),
1155
+ ...(guideValidatorResult ? guideValidatorResult.errors : []),
947
1156
  ];
948
1157
  const globalWarnings = [
949
1158
  ...manifestAudit.findings.filter((f) => f.level === "warn"),
950
1159
  ...duplicateFindings.filter((f) => f.level === "warn"),
951
1160
  ...orphanHookFindings.filter((f) => f.level === "warn"),
1161
+ ...(guideValidatorResult ? guideValidatorResult.warnings : []),
952
1162
  ];
953
1163
 
954
1164
  const fail =
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env node
2
+ // test-guide-validator.mjs — Unit test for the guide-content validator
3
+ //
4
+ // Proves:
5
+ // 1. The validator flags guides containing banned terms in positive-instruction context
6
+ // 2. The validator passes guides that mention banned terms only as warnings/negations
7
+ // 3. The validator flags unknown clauth key names
8
+ // 4. The validator passes known clauth key names
9
+ //
10
+ // Usage:
11
+ // node scripts/test-guide-validator.mjs
12
+ //
13
+ // Exit codes:
14
+ // 0 = all assertions pass
15
+ // 1 = one or more assertions failed
16
+
17
+ import { dirname, join, resolve } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ const __dirname = dirname(fileURLToPath(import.meta.url));
21
+ const REPO_ROOT = resolve(__dirname, "..");
22
+
23
+ // Import the validator functions from self-test.mjs by running it in test mode.
24
+ // Since self-test.mjs is not a module that exports cleanly, we replicate the
25
+ // validator logic here using the same constants and algorithms.
26
+ // (The shared implementation lives in runGuideContentValidator / auditGuideFile.)
27
+ //
28
+ // We import via dynamic import of the self-test module's exported helpers —
29
+ // but since self-test.mjs has no exports (it runs immediately), we inline
30
+ // the equivalent test using the same fixture files it targets.
31
+
32
+ // ─── Inline validator (mirrors self-test.mjs logic) ────────────────────────
33
+ import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
34
+
35
+ const GUIDE_BANNED_TERMS = [
36
+ "@masonator/coolify-mcp",
37
+ "@masonator",
38
+ "coolify-mcp",
39
+ "@regen/brand-studio",
40
+ "brand-studio",
41
+ ];
42
+
43
+ const GUIDE_NEGATION_PATTERNS = [
44
+ /\bdo not\b/i,
45
+ /\bnever\b/i,
46
+ /\bno such\b/i,
47
+ /\bdoes not exist\b/i,
48
+ /\bbanned\b/i,
49
+ /\bnot reference\b/i,
50
+ /\bnot use\b/i,
51
+ /\bavoid\b/i,
52
+ /\bremoved\b/i,
53
+ /\bdeprecated\b/i,
54
+ // Markdown table row showing a WRONG→CORRECT mapping (naming-corrections.md pattern)
55
+ /^\|[^|]*WRONG[^|]*\|/i,
56
+ // A table row where the term is in the WRONG column (first data column after the | WRONG | header)
57
+ /^\|\s*(Brand Studio|brand-studio|@regen\/brand-studio|@masonator[^ |]*|coolify-mcp)[^|]*\|\s*\*\*/,
58
+ ];
59
+
60
+ const KNOWN_CLAUTH_KEYS = new Set([
61
+ "coolify-api",
62
+ "cloudflare",
63
+ "npm",
64
+ "supabase",
65
+ "supabase-anon",
66
+ "supabase-db",
67
+ "r2-access-key-id",
68
+ "r2-secret-key",
69
+ "anthropic",
70
+ "openai",
71
+ "github",
72
+ "github-token",
73
+ "vultr-dev-ssh",
74
+ "mcp-web-research-secret",
75
+ "mcp-regen-media-secret",
76
+ "web-research",
77
+ "regen-media",
78
+ ]);
79
+
80
+ function auditGuideFile(filepath, relPath) {
81
+ const findings = [];
82
+ let text;
83
+ try {
84
+ text = readFileSync(filepath, "utf8");
85
+ } catch (e) {
86
+ findings.push({ level: "error", code: "guide-unreadable", file: relPath, line: 0, message: `cannot read: ${e.message}` });
87
+ return findings;
88
+ }
89
+ const lines = text.split(/\r?\n/);
90
+ lines.forEach((line, i) => {
91
+ const lineNo = i + 1;
92
+ for (const term of GUIDE_BANNED_TERMS) {
93
+ if (line.includes(term)) {
94
+ const isNegated = GUIDE_NEGATION_PATTERNS.some((re) => re.test(line));
95
+ if (!isNegated) {
96
+ findings.push({ level: "error", code: "guide-banned-term", file: relPath, line: lineNo, message: `banned term "${term}"` });
97
+ }
98
+ }
99
+ }
100
+ const clauthKeyRe = /\bhttp:\/\/127\.0\.0\.1:52437\/v\/([\w-]+)/g;
101
+ let m;
102
+ while ((m = clauthKeyRe.exec(line)) !== null) {
103
+ const key = m[1];
104
+ if (!KNOWN_CLAUTH_KEYS.has(key)) {
105
+ findings.push({ level: "warn", code: "guide-clauth-key-unknown", file: relPath, line: lineNo, message: `unknown clauth key "${key}"` });
106
+ }
107
+ }
108
+ });
109
+ return findings;
110
+ }
111
+
112
+ function scanDir(dir, prefix) {
113
+ const findings = [];
114
+ if (!existsSync(dir)) return findings;
115
+ let entries;
116
+ try { entries = readdirSync(dir); } catch { return findings; }
117
+ for (const entry of entries) {
118
+ const fullPath = join(dir, entry);
119
+ let stat;
120
+ try { stat = statSync(fullPath); } catch { continue; }
121
+ if (stat.isDirectory()) {
122
+ findings.push(...scanDir(fullPath, `${prefix}/${entry}`));
123
+ } else if (entry.endsWith(".md")) {
124
+ findings.push(...auditGuideFile(fullPath, `${prefix}/${entry}`));
125
+ }
126
+ }
127
+ return findings;
128
+ }
129
+
130
+ // ─── Test runner ─────────────────────────────────────────────────────────────
131
+
132
+ let passed = 0;
133
+ let failed = 0;
134
+
135
+ function assert(description, condition, detail = "") {
136
+ if (condition) {
137
+ console.log(` ✓ ${description}`);
138
+ passed++;
139
+ } else {
140
+ console.error(` ✗ FAIL: ${description}${detail ? ` — ${detail}` : ""}`);
141
+ failed++;
142
+ }
143
+ }
144
+
145
+ console.log("\nrdc:self-test — guide-content validator unit tests\n");
146
+
147
+ // ─── Test 1: bad-guide.md should produce errors for banned terms ─────────────
148
+ console.log("Test 1: bad-guide.md flags banned terms");
149
+ const BAD_FIXTURE = join(REPO_ROOT, "scripts/fixtures/guides/bad-guide.md");
150
+ const badFindings = auditGuideFile(BAD_FIXTURE, "fixtures/guides/bad-guide.md");
151
+ const badErrors = badFindings.filter((f) => f.level === "error" && f.code === "guide-banned-term");
152
+ assert("bad-guide.md produces at least 2 banned-term errors", badErrors.length >= 2, `got ${badErrors.length}`);
153
+ assert("flags @masonator/coolify-mcp", badErrors.some((f) => f.message.includes("@masonator/coolify-mcp")));
154
+ assert("flags brand-studio or @regen/brand-studio", badErrors.some((f) => f.message.includes("brand-studio")));
155
+
156
+ // ─── Test 2: bad-guide.md flags unknown clauth key ───────────────────────────
157
+ console.log("\nTest 2: bad-guide.md flags unknown clauth key");
158
+ const badKeyWarnings = badFindings.filter((f) => f.code === "guide-clauth-key-unknown");
159
+ assert("bad-guide.md warns on unknown clauth key 'nonexistent-key'", badKeyWarnings.length >= 1, `got ${badKeyWarnings.length}`);
160
+ assert("warning message references nonexistent-key", badKeyWarnings.some((f) => f.message.includes("nonexistent-key")));
161
+
162
+ // ─── Test 3: good-guide.md produces no errors (negated mentions are OK) ──────
163
+ console.log("\nTest 3: good-guide.md passes (negated mentions are not flagged)");
164
+ const GOOD_FIXTURE = join(REPO_ROOT, "scripts/fixtures/guides-clean/good-guide.md");
165
+ const goodFindings = auditGuideFile(GOOD_FIXTURE, "fixtures/guides-clean/good-guide.md");
166
+ const goodErrors = goodFindings.filter((f) => f.level === "error");
167
+ assert("good-guide.md produces 0 banned-term errors", goodErrors.length === 0, `got ${goodErrors.length}: ${goodErrors.map((f) => f.message).join("; ")}`);
168
+
169
+ // good-guide uses coolify-api which is a known key — no key warnings
170
+ const goodKeyWarnings = goodFindings.filter((f) => f.code === "guide-clauth-key-unknown");
171
+ assert("good-guide.md produces 0 unknown-key warnings", goodKeyWarnings.length === 0, `got ${goodKeyWarnings.length}`);
172
+
173
+ // ─── Test 4: scan fixture directory returns combined findings ─────────────────
174
+ console.log("\nTest 4: scanDir finds findings across fixture dirs");
175
+ const BAD_DIR = join(REPO_ROOT, "scripts/fixtures/guides");
176
+ const dirFindings = scanDir(BAD_DIR, "fixtures");
177
+ const dirErrors = dirFindings.filter((f) => f.level === "error");
178
+ assert("scanDir on bad fixture dir returns errors", dirErrors.length >= 2, `got ${dirErrors.length}`);
179
+
180
+ const GOOD_DIR = join(REPO_ROOT, "scripts/fixtures/guides-clean");
181
+ const cleanFindings = scanDir(GOOD_DIR, "fixtures");
182
+ const cleanErrors = cleanFindings.filter((f) => f.level === "error");
183
+ assert("scanDir on clean fixture dir returns 0 errors", cleanErrors.length === 0, `got ${cleanErrors.length}`);
184
+
185
+ // ─── Summary ─────────────────────────────────────────────────────────────────
186
+ console.log(`\n${"─".repeat(60)}`);
187
+ console.log(`guide-validator tests: ${passed} passed, ${failed} failed`);
188
+ if (failed > 0) {
189
+ console.error(`\n❌ FAIL — ${failed} assertion(s) did not pass`);
190
+ process.exit(1);
191
+ } else {
192
+ console.log(`\n✓ PASS — all guide-content validator assertions passed`);
193
+ process.exit(0);
194
+ }
@@ -134,11 +134,57 @@ Severity rules:
134
134
 
135
135
  `--fix` auto-remediates only: missing watch_paths, registry row updates, CF cache purges. Never touches env vars, DNS, or container config without explicit confirmation.
136
136
 
137
+ ## Coolify Access — clauth + REST API
138
+
139
+ All Coolify operations use the clauth daemon and the Coolify REST API directly.
140
+ There is no Coolify MCP server. Do not reference `@masonator/coolify-mcp`.
141
+
142
+ ```bash
143
+ # Get token (plain text — no JSON parsing needed)
144
+ _COOLIFY=$(curl -s http://127.0.0.1:52437/v/coolify-api)
145
+
146
+ # List applications
147
+ curl -s -H "Authorization: Bearer $_COOLIFY" \
148
+ https://deploy.regendevcorp.com/api/v1/applications
149
+
150
+ # Get application details
151
+ curl -s -H "Authorization: Bearer $_COOLIFY" \
152
+ https://deploy.regendevcorp.com/api/v1/applications/<uuid>
153
+
154
+ # Deploy (trigger)
155
+ curl -s -X POST -H "Authorization: Bearer $_COOLIFY" \
156
+ https://deploy.regendevcorp.com/api/v1/applications/<uuid>/deploy
157
+
158
+ # Get deployment logs
159
+ curl -s -H "Authorization: Bearer $_COOLIFY" \
160
+ https://deploy.regendevcorp.com/api/v1/deployments/<deployment-id>
161
+
162
+ # Set env var
163
+ curl -s -X POST -H "Authorization: Bearer $_COOLIFY" \
164
+ -H "Content-Type: application/json" \
165
+ -d '{"key":"<KEY>","value":"<VALUE>"}' \
166
+ https://deploy.regendevcorp.com/api/v1/applications/<uuid>/envs
167
+
168
+ # Set watch_paths
169
+ curl -s -X PATCH -H "Authorization: Bearer $_COOLIFY" \
170
+ -H "Content-Type: application/json" \
171
+ -d '{"watch_paths":"apps/<name>/**\npackages/**"}' \
172
+ https://deploy.regendevcorp.com/api/v1/applications/<uuid>
173
+ ```
174
+
175
+ **Never print `$_COOLIFY` to stdout.** Inline from clauth only — do not assign raw strings.
176
+
177
+ If clauth daemon is not responding (`curl -s http://127.0.0.1:52437/ping` fails):
178
+ ```
179
+ BLOCKED: clauth daemon is not responding.
180
+ Fix: Run C:\Dev\regen-root\scripts\restart-clauth.bat, then unlock at http://127.0.0.1:52437
181
+ I cannot proceed until this is resolved.
182
+ ```
183
+
137
184
  ## References
138
185
 
139
186
  - Type-specific checklists + DNS tree + gate commands: `docs/runbooks/coolify-deploy-checklist.md`
140
187
  - Rules / registry RPCs / hard limits: `.claude/context/coolify-deployment.md`
141
- - MCP server: `@masonator/coolify-mcp` (38 tools)
142
188
  - Infrastructure constants:
143
189
  ```
144
190
  Server UUID: ih386anenvvvn6fy1umtyow0
@@ -1,13 +1,13 @@
1
1
  ---
2
- name: terminal-config
3
- description: "Read and safely modify Windows Terminal settings and cell startup sequencing. Contains the canonical file locations, profile GUIDs, keybinding map, and what NEVER to change."
2
+ name: rdc:terminal-config
3
+ description: "Usage `rdc:terminal-config <task>` — read and safely modify Windows Terminal settings and cell startup sequencing. Contains canonical file locations, profile GUIDs, keybinding map, and what NEVER to change."
4
4
  ---
5
5
 
6
6
  > **⚠️ OUTPUT CONTRACT (READ FIRST):** `guides/output-contract.md`
7
7
  > Checklist-only output. No tool-call narration. No raw MCP/JSON/log dumps.
8
8
  > One checklist upfront, updated in place, shown again at end with a 1-line verdict.
9
9
 
10
- # terminal-config — Windows Terminal & Cell Startup Reference
10
+ # rdc:terminal-config — Windows Terminal & Cell Startup Reference
11
11
 
12
12
  ## When to Use
13
13
  - Before modifying any terminal setting, profile, or keybinding
@@ -31,7 +31,7 @@ description: "Read and safely modify Windows Terminal settings and cell startup
31
31
  | File | Path | Purpose |
32
32
  |------|------|---------|
33
33
  | **Terminal settings** | `C:\Users\DaveLadouceur\AppData\Local\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json` | Main config — profiles, keybindings, color schemes |
34
- | **Cell init script** | `C:\Dev\regen-root\scripts\cell-init.cmd` | Launched by each cell profile. Sets CELL_ROLE, prints banner, runs `claude --append-system-prompt` |
34
+ | **Cell init script** | `C:\Dev\regen-root\scripts\cell-init.ps1` | Launched by each cell profile (PowerShell 7). Sets CELL_ROLE, prints banner, runs `claude --append-system-prompt` |
35
35
  | **Cell state dir** | `C:\Dev\regen-root\.cell-state\` | PID lockfiles per cell (`sv.lock`, `cell-portal.lock`, etc.) |
36
36
  | **Claude keybindings** | `C:\Users\DaveLadouceur\.claude\keybindings.json` | Claude Code keybindings (separate from Terminal) |
37
37
  | **Claude settings** | `C:\Dev\regen-root\.claude\settings.json` | Project-level Claude Code settings, hooks, permissions |
@@ -46,9 +46,9 @@ Every profile has a fixed GUID. Do not change them.
46
46
  | GUID | Name | Tab Color | Purpose |
47
47
  |------|------|-----------|---------|
48
48
  | `{574e775e-4f2a-5b96-ac1e-a2962a402336}` | PowerShell | — | **Default profile** — PowerShell 7 Core |
49
- | `{e7c1128b-e51f-4eba-bcdb-85f550c31b97}` | SV | `#1A6B3C` (green) | Supervisor cell — full repo access, runs `cell-init.cmd sv` |
50
- | `{190a2f39-f6ad-4638-8fef-e4c7aa58c349}` | Claude | `#B91C1C` (red) | Claude conversation cell, runs `cell-init.cmd sv` |
51
- | `{2d0248ef-7bb7-4b9e-b381-c4aa1f570f49}` | Codex | `#0F766E` (teal) | Codex AI cell — runs `powershell -NoLogo -NoExit -Command codex` |
49
+ | `{e7c1128b-e51f-4eba-bcdb-85f550c31b97}` | SV | `#1A6B3C` (green) | Supervisor cell — full repo access, runs `cell-init.ps1 sv` |
50
+ | `{190a2f39-f6ad-4638-8fef-e4c7aa58c349}` | Claude | `#B91C1C` (red) | Claude conversation cell, runs `cell-init.ps1 sv` |
51
+ | `{2d0248ef-7bb7-4b9e-b381-c4aa1f570f49}` | Codex | `#0F766E` (teal) | Codex AI cell — runs `pwsh.exe -NoLogo -NoExit -Command codex` |
52
52
  | `{767642b0-606c-468c-89a0-4573df6fcdaf}` | VS Code | `#2D5986` (blue) | Launches `code-safe C:\Dev\regen-root` |
53
53
  | `{6cbd327a-5b5e-4e83-96f6-041615382d36}` | PowerShell (Admin) | `#1E3A5F` (dark blue) | Elevated PowerShell — uses `LIFEAI Slate` color scheme |
54
54
  | `{0caa0dad-35be-5f56-a8ff-afceeeaa6101}` | Command Prompt | — | Legacy CMD — visible but rarely used |
@@ -57,13 +57,14 @@ Every profile has a fixed GUID. Do not change them.
57
57
  ### Profile commandlines
58
58
 
59
59
  ```
60
- SV / Claude: cmd.exe /k "C:\Dev\regen-root\scripts\cell-init.cmd" sv
61
- Codex: powershell.exe -NoLogo -NoExit -Command codex
60
+ SV / Claude: pwsh.exe -NoLogo -NoExit -ExecutionPolicy Bypass -File "C:\Dev\regen-root\scripts\cell-init.ps1" sv
61
+ Codex: pwsh.exe -NoLogo -NoExit -Command codex
62
62
  VS Code: cmd.exe /c start "" code-safe C:\Dev\regen-root
63
63
  Admin PS: powershell.exe -NoLogo (+ elevate: true)
64
64
  ```
65
65
 
66
66
  All cell profiles set `"startingDirectory": "C:\\Dev\\regen-root"`.
67
+ SV / Claude / Codex all run on **PowerShell 7 (`pwsh.exe`)** — never bare `powershell.exe` (that resolves to legacy Windows PowerShell 5.1).
67
68
 
68
69
  ---
69
70
 
@@ -140,15 +141,14 @@ Font defaults (in `profiles.defaults`):
140
141
 
141
142
  ## Cell Startup Sequencing
142
143
 
143
- `cell-init.cmd` is the startup script for all cell profiles. It:
144
+ `cell-init.ps1` is the PowerShell 7 startup script for all cell profiles. It:
144
145
 
145
- 1. Accepts `ROLE` arg (`sv`, `cell-portal`, `cell-data`, `cell-cs2`, `cell-mktg`, `cell-infra`, `specialist`)
146
- 2. Looks up `LABEL`, `COLOR`, `SCOPE`, `PATHS`, `PROMPT_SCOPE` from a role table
146
+ 1. Accepts `Role` arg (`sv`, `cell-portal`, `cell-data`, `cell-cs2`, `cell-mktg`, `cell-infra`, `specialist`)
147
+ 2. Looks up `Label`, `Color`, `Scope`, `Paths`, `Prompt` from the `$roles` hashtable
147
148
  3. Prints a colored banner
148
149
  4. Writes a PID lockfile to `.cell-state\<role>.lock`
149
150
  5. Runs `git log` filtered to that cell's path scope
150
- 6. Opens VS Code for the project
151
- 7. Launches: `claude --append-system-prompt "<PROMPT_SCOPE>"`
151
+ 6. Launches: `claude --append-system-prompt "<Prompt>"`
152
152
 
153
153
  ### Cell role → prompt scope mapping
154
154
 
@@ -184,9 +184,9 @@ Font defaults (in `profiles.defaults`):
184
184
 
185
185
  ### When editing cell startup
186
186
 
187
- Edit `C:\Dev\regen-root\scripts\cell-init.cmd` — it's in the repo, so changes are tracked.
188
- - Adding a new role: add a new `if "%ROLE%"=="..."` block with all five vars
189
- - Changing scope: update `PATHS` and `PROMPT_SCOPE` only — don't touch banner/lockfile/git logic
187
+ Edit `C:\Dev\regen-root\scripts\cell-init.ps1` — it's in the repo, so changes are tracked.
188
+ - Adding a new role: add a new entry to the `$roles` hashtable with all five keys (`Label`, `Color`, `Scope`, `Paths`, `Prompt`)
189
+ - Changing scope: update `Paths` and `Prompt` only — don't touch banner/lockfile/git logic
190
190
 
191
191
  ---
192
192
 
@@ -211,7 +211,7 @@ If Windows Terminal settings keep getting corrupted or misedited, **WezTerm** is
211
211
  - Config is a Lua file (`~/.wezterm.lua` or `C:\Users\<user>\.config\wezterm\wezterm.lua`)
212
212
  - Committed to git — every change is a reviewable diff, not an opaque JSON blob
213
213
  - Startup layouts (tabs, panes, commands) are defined programmatically
214
- - Equivalent to current setup: `wezterm.mux.spawn_window()` per cell with `args` set to `cmd.exe /k cell-init.cmd <role>`
214
+ - Equivalent to current setup: `wezterm.mux.spawn_window()` per cell with `args` set to `pwsh.exe -NoLogo -NoExit -File cell-init.ps1 <role>`
215
215
  - Download: https://wezfurlong.org/wezterm/installation.html
216
216
 
217
217
  Migration path: copy existing color schemes as `wezterm.color.get_default_colors()` override tables, map keybindings to `config.keys` array, define tab bar with cell roles.