@lifeaitools/rdc-skills 0.9.29 → 0.9.31

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.31",
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/rdc-skills",
3
- "version": "0.9.29",
3
+ "version": "0.9.31",
4
4
  "description": "RDC typed-agent dispatch skill suite for Claude Code — plan, build, review, overnight builds",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -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');
@@ -0,0 +1,217 @@
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."
4
+ ---
5
+
6
+ > **⚠️ OUTPUT CONTRACT (READ FIRST):** `guides/output-contract.md`
7
+ > Checklist-only output. No tool-call narration. No raw MCP/JSON/log dumps.
8
+ > One checklist upfront, updated in place, shown again at end with a 1-line verdict.
9
+
10
+ # terminal-config — Windows Terminal & Cell Startup Reference
11
+
12
+ ## When to Use
13
+ - Before modifying any terminal setting, profile, or keybinding
14
+ - When setting up a new profile (Claude, Codex, cell cells)
15
+ - When cell startup is broken (wrong `--append-system-prompt`, wrong cwd, wrong CELL_ROLE)
16
+ - When keybindings conflict or are missing
17
+
18
+ ## ⛔ ABSOLUTE RULES — READ BEFORE TOUCHING ANYTHING
19
+
20
+ 1. **NEVER change profile GUIDs** — Windows Terminal uses these as identity. Changing a GUID orphans all pinned shortcuts and window layouts.
21
+ 2. **NEVER remove `"id": null` keybinding entries** — these intentionally UNBIND enter/shift-enter/ctrl-enter so Claude Code's interactive prompt works without the terminal swallowing keystrokes.
22
+ 3. **NEVER change `firstWindowPreference`** — it's `persistedWindowLayout`, which restores Dave's exact pane/tab layout on restart.
23
+ 4. **NEVER change `defaultProfile`** — it's `{574e775e-...}` (PowerShell Core). Changing it breaks new-tab behavior.
24
+ 5. **ALWAYS read the file before editing** — never write from memory.
25
+ 6. **ALWAYS validate JSON** before saving — a syntax error silently resets all settings on next Terminal launch (no error shown, settings wiped).
26
+
27
+ ---
28
+
29
+ ## File Locations
30
+
31
+ | File | Path | Purpose |
32
+ |------|------|---------|
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` |
35
+ | **Cell state dir** | `C:\Dev\regen-root\.cell-state\` | PID lockfiles per cell (`sv.lock`, `cell-portal.lock`, etc.) |
36
+ | **Claude keybindings** | `C:\Users\DaveLadouceur\.claude\keybindings.json` | Claude Code keybindings (separate from Terminal) |
37
+ | **Claude settings** | `C:\Dev\regen-root\.claude\settings.json` | Project-level Claude Code settings, hooks, permissions |
38
+ | **Claude user settings** | `C:\Users\DaveLadouceur\.claude\settings.json` | User-level Claude Code settings |
39
+
40
+ ---
41
+
42
+ ## Profile Inventory
43
+
44
+ Every profile has a fixed GUID. Do not change them.
45
+
46
+ | GUID | Name | Tab Color | Purpose |
47
+ |------|------|-----------|---------|
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` |
52
+ | `{767642b0-606c-468c-89a0-4573df6fcdaf}` | VS Code | `#2D5986` (blue) | Launches `code-safe C:\Dev\regen-root` |
53
+ | `{6cbd327a-5b5e-4e83-96f6-041615382d36}` | PowerShell (Admin) | `#1E3A5F` (dark blue) | Elevated PowerShell — uses `LIFEAI Slate` color scheme |
54
+ | `{0caa0dad-35be-5f56-a8ff-afceeeaa6101}` | Command Prompt | — | Legacy CMD — visible but rarely used |
55
+ | `{574e775e...}` and others | `hidden: true` | — | Windows PowerShell, Azure Shell, VS DevTools — all hidden |
56
+
57
+ ### Profile commandlines
58
+
59
+ ```
60
+ SV / Claude: cmd.exe /k "C:\Dev\regen-root\scripts\cell-init.cmd" sv
61
+ Codex: powershell.exe -NoLogo -NoExit -Command codex
62
+ VS Code: cmd.exe /c start "" code-safe C:\Dev\regen-root
63
+ Admin PS: powershell.exe -NoLogo (+ elevate: true)
64
+ ```
65
+
66
+ All cell profiles set `"startingDirectory": "C:\\Dev\\regen-root"`.
67
+
68
+ ---
69
+
70
+ ## Keybinding Map
71
+
72
+ ### Critical: intentionally unbound keys (never restore these)
73
+
74
+ ```json
75
+ { "id": null, "keys": "shift+enter" }
76
+ { "id": null, "keys": "ctrl+enter" }
77
+ { "id": null, "keys": "enter" }
78
+ ```
79
+
80
+ **Why:** Without these null bindings, Windows Terminal intercepts Enter/Shift-Enter/Ctrl-Enter before Claude Code's interactive prompt sees them. Sessions break silently.
81
+
82
+ ### Pane navigation (safe to change)
83
+
84
+ | Keys | Action |
85
+ |------|--------|
86
+ | `ctrl+alt+v` | Split pane right (duplicate) |
87
+ | `ctrl+alt+h` | Split pane down (duplicate) |
88
+ | `ctrl+alt+left/right/up/down` | Move focus between panes |
89
+ | `ctrl+alt+shift+left/right` | Resize pane |
90
+ | `ctrl+shift+z` | Toggle pane zoom |
91
+ | `ctrl+shift+w` | Close pane |
92
+ | `alt+shift+d` | Split pane auto (duplicate) |
93
+
94
+ ### Standard overrides
95
+
96
+ | Keys | Action |
97
+ |------|--------|
98
+ | `ctrl+c` | Copy (singleLine: false) |
99
+ | `ctrl+v` | Paste |
100
+ | `ctrl+shift+f` | Find |
101
+
102
+ ---
103
+
104
+ ## Color Schemes
105
+
106
+ Four custom schemes live in the `schemes` array. Do not rename them — profiles reference by name.
107
+
108
+ | Name | Background | Used by |
109
+ |------|-----------|---------|
110
+ | `Dimidium` | `#282A36` (dark purple) | Default for all cell profiles |
111
+ | `LIFEAI Claude` | `#1A0F0A` (dark brown) | Available, not currently assigned |
112
+ | `LIFEAI Dark` | `#0D1F17` (dark green) | Available, not currently assigned |
113
+ | `LIFEAI Slate` | `#0F1923` (dark blue) | PowerShell Admin profile |
114
+ | `Dracula` | `#282A36` | Available, not currently assigned |
115
+
116
+ ---
117
+
118
+ ## Global Settings (safe reference)
119
+
120
+ ```json
121
+ "copyOnSelect": true // highlight = copied. Don't change — muscle memory.
122
+ "copyFormatting": "all" // preserves ANSI colors on copy
123
+ "firstWindowPreference": "persistedWindowLayout" // restores last layout on open. NEVER CHANGE.
124
+ "defaultProfile": "{574e775e-4f2a-5b96-ac1e-a2962a402336}" // PowerShell Core. NEVER CHANGE.
125
+ "theme": "dark"
126
+ "showTabsFullscreen": true
127
+ "initialRows": 40
128
+ ```
129
+
130
+ Font defaults (in `profiles.defaults`):
131
+ ```json
132
+ "face": "Consolas"
133
+ "size": 12
134
+ "cellHeight": "1.2"
135
+ "cellWidth": "0.6"
136
+ "weight": "normal"
137
+ ```
138
+
139
+ ---
140
+
141
+ ## Cell Startup Sequencing
142
+
143
+ `cell-init.cmd` is the startup script for all cell profiles. It:
144
+
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
147
+ 3. Prints a colored banner
148
+ 4. Writes a PID lockfile to `.cell-state\<role>.lock`
149
+ 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>"`
152
+
153
+ ### Cell role → prompt scope mapping
154
+
155
+ | Role arg | Claude system prompt |
156
+ |----------|---------------------|
157
+ | `sv` | "You are the SUPERVISOR cell. You have full repo access. Coordinate across all packages and apps." |
158
+ | `cell-portal` | Portal cell — frontend apps only (apps/prt, apps/rdc, packages/ui, models/) |
159
+ | `cell-data` | Data cell — packages/supabase, virtue-engine, pal, hail, daf-intelligence |
160
+ | `cell-cs2` | CS2 cell — packages/cs2, quad-pixel, planetary-ontology, models/ |
161
+ | `cell-mktg` | Marketing cell — rdc-marketing-engine, canvas, sites/, email-templates |
162
+ | `cell-infra` | Infra cell — Coolify, CI/CD, root config, scripts/ |
163
+ | `specialist` | Specialist cell — repo-wide reviews, cleanup, docs, audits |
164
+
165
+ ---
166
+
167
+ ## Safe Edit Procedure
168
+
169
+ ### When adding or modifying a profile
170
+
171
+ 1. Read the current file first (never edit from memory)
172
+ 2. Generate a new GUID with: `[System.Guid]::NewGuid().ToString('B')` (PowerShell) — wrap in `{}`
173
+ 3. Copy the SV profile as a template
174
+ 4. Set `commandline`, `startingDirectory`, `environment`, `tabColor`, `icon`, `name`
175
+ 5. Do NOT set `guid` to an existing value
176
+ 6. Validate JSON: `Get-Content settings.json | ConvertFrom-Json` (errors = syntax problem)
177
+ 7. Save and reopen Terminal to verify
178
+
179
+ ### When adding a keybinding
180
+
181
+ 1. Add to the `keybindings` array
182
+ 2. Check for conflicts with the null-bound keys (`enter`, `shift+enter`, `ctrl+enter`) — NEVER override them
183
+ 3. Validate JSON before saving
184
+
185
+ ### When editing cell startup
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
190
+
191
+ ---
192
+
193
+ ## Validation Commands
194
+
195
+ ```powershell
196
+ # Validate JSON syntax before saving
197
+ Get-Content "$env:LOCALAPPDATA\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json" | ConvertFrom-Json
198
+
199
+ # Check cell lockfiles
200
+ Get-Content C:\Dev\regen-root\.cell-state\sv.lock
201
+
202
+ # Generate a new GUID for a profile
203
+ "{$([System.Guid]::NewGuid().ToString())}"
204
+ ```
205
+
206
+ ---
207
+
208
+ ## WezTerm Alternative
209
+
210
+ If Windows Terminal settings keep getting corrupted or misedited, **WezTerm** is the recommended migration:
211
+ - Config is a Lua file (`~/.wezterm.lua` or `C:\Users\<user>\.config\wezterm\wezterm.lua`)
212
+ - Committed to git — every change is a reviewable diff, not an opaque JSON blob
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>`
215
+ - Download: https://wezfurlong.org/wezterm/installation.html
216
+
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.