sneakoscope 1.18.13 → 1.19.0

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.
Files changed (49) hide show
  1. package/README.md +36 -10
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +1 -1
  5. package/dist/.sks-build-stamp.json +4 -4
  6. package/dist/bin/sks.js +1 -1
  7. package/dist/build-manifest.json +11 -9
  8. package/dist/cli/command-registry.d.ts +3 -0
  9. package/dist/cli/command-registry.js +3 -1
  10. package/dist/cli/install-helpers.d.ts +148 -3
  11. package/dist/cli/install-helpers.js +311 -121
  12. package/dist/commands/codex-lb.js +77 -1
  13. package/dist/commands/doctor.js +24 -0
  14. package/dist/commands/mad-sks.d.ts +1 -0
  15. package/dist/commands/mad-sks.js +3 -3
  16. package/dist/commands/versioning.js +2 -1
  17. package/dist/core/agents/agent-effort-policy.d.ts +6 -0
  18. package/dist/core/agents/agent-effort-policy.js +42 -0
  19. package/dist/core/agents/agent-orchestrator.js +9 -5
  20. package/dist/core/agents/agent-roster.d.ts +52 -4
  21. package/dist/core/agents/agent-roster.js +121 -11
  22. package/dist/core/agents/agent-scheduler.d.ts +2 -1
  23. package/dist/core/agents/agent-scheduler.js +7 -5
  24. package/dist/core/agents/agent-schema.d.ts +5 -0
  25. package/dist/core/agents/agent-schema.js +5 -0
  26. package/dist/core/codex/codex-config-eperm-repair.d.ts +1 -0
  27. package/dist/core/codex/codex-config-eperm-repair.js +20 -2
  28. package/dist/core/codex/codex-config-readability.js +31 -1
  29. package/dist/core/codex/codex-project-config-policy.d.ts +23 -0
  30. package/dist/core/codex/codex-project-config-policy.js +191 -8
  31. package/dist/core/commands/basic-cli.js +22 -5
  32. package/dist/core/commands/mad-sks-command.d.ts +1 -0
  33. package/dist/core/commands/mad-sks-command.js +17 -1
  34. package/dist/core/commands/naruto-command.d.ts +2 -0
  35. package/dist/core/commands/naruto-command.js +189 -0
  36. package/dist/core/feature-fixtures.js +3 -0
  37. package/dist/core/fsx.d.ts +1 -1
  38. package/dist/core/fsx.js +1 -1
  39. package/dist/core/init.js +1 -1
  40. package/dist/core/preflight/parallel-preflight-engine.d.ts +1 -0
  41. package/dist/core/routes.js +17 -1
  42. package/dist/core/version-manager.js +1 -1
  43. package/dist/core/version.d.ts +1 -1
  44. package/dist/core/version.js +1 -1
  45. package/dist/core/zellij/zellij-launcher.js +4 -7
  46. package/dist/core/zellij/zellij-layout-builder.js +1 -1
  47. package/dist/scripts/release-parallel-check.js +5 -0
  48. package/package.json +8 -2
  49. package/scripts/codex-config-load-probe.mjs +245 -0
@@ -25,7 +25,7 @@ const DEFAULT_CODEX_APP_PLUGINS = [
25
25
  function packagedSksEntrypoint() {
26
26
  return path.join(packageRoot(), 'dist', 'bin', 'sks.js');
27
27
  }
28
- export async function postinstall({ bootstrap }) {
28
+ export async function postinstall({ bootstrap, args = [] }) {
29
29
  const installRoot = path.resolve(process.env.INIT_CWD || process.cwd());
30
30
  const conflictScan = await scanHarnessConflicts(installRoot);
31
31
  if (conflictScan.hard_block) {
@@ -33,88 +33,104 @@ export async function postinstall({ bootstrap }) {
33
33
  return;
34
34
  }
35
35
  const codexLbConfigSnapshot = await capturePostinstallCodexLbConfigSnapshot();
36
- console.log('\nSKS installed.');
37
- const shim = await ensureSksCommandDuringInstall();
38
- if (shim.status === 'present')
39
- console.log(`SKS command: available (${shim.command ?? 'unknown'}).`);
40
- else if (shim.status === 'repaired')
41
- console.log(`SKS command: stale PATH shim repaired (${shim.command ?? 'unknown'}).`);
42
- else if (shim.status === 'created')
43
- console.log(`SKS command: shim created at ${shim.command ?? 'unknown'}.`);
44
- else if (shim.status === 'created_not_on_path')
45
- console.log(`SKS command: shim created at ${shim.command ?? 'unknown'}. Add ${path.dirname(shim.command ?? '')} to PATH, or run npx -y -p sneakoscope sks.`);
46
- else if (shim.status === 'skipped')
47
- console.log(`SKS command: skipped (${shim.reason}).`);
48
- else
49
- console.log(`SKS command: shim unavailable. Use npx -y -p sneakoscope sks. ${shim.error || ''}`.trim());
50
- const context7Install = await ensureGlobalContext7DuringInstall();
51
- if (context7Install.status === 'present')
52
- console.log('Context7 MCP: already configured for Codex.');
53
- else if (context7Install.status === 'installed')
54
- console.log('Context7 MCP: configured for Codex.');
55
- else if (context7Install.status === 'codex_missing')
56
- console.log('Context7 MCP: Codex CLI missing. Install @openai/codex or set SKS_CODEX_BIN, then run `sks context7 setup --scope global` or `sks setup` in a project.');
57
- else if (context7Install.status === 'skipped')
58
- console.log(`Context7 MCP: skipped (${context7Install.reason}).`);
59
- else if (context7Install.status === 'failed')
60
- console.log(`Context7 MCP: auto setup failed. Run \`sks context7 setup --scope global\` or \`sks setup\`. ${context7Install.error || ''}`.trim());
61
- const fastModeRepair = await ensureGlobalCodexFastModeDuringInstall();
62
- if (fastModeRepair.status === 'updated')
63
- console.log(`Codex App Fast mode: restored in ${fastModeRepair.config_path}.`);
64
- else if (fastModeRepair.status === 'present')
65
- console.log('Codex App Fast mode: config already compatible.');
66
- else if (fastModeRepair.status === 'skipped')
67
- console.log(`Codex App Fast mode: skipped (${fastModeRepair.reason}).`);
68
- else if (fastModeRepair.status === 'failed')
69
- console.log(`Codex App Fast mode: auto repair failed. Run \`sks setup\`. ${fastModeRepair.error || ''}`.trim());
70
- const appProcessRepair = await reconcileCodexAppUpgradeProcesses();
71
- if (appProcessRepair.status === 'repaired')
72
- console.log(`Codex App reconnect repair: stopped ${appProcessRepair.killed.length} stale orphan app-server process(es). Restart Codex App to reconnect cleanly.`);
73
- else if (appProcessRepair.status === 'partial')
74
- console.log(`Codex App reconnect repair: stopped ${appProcessRepair.killed.length} stale orphan app-server process(es); ${(appProcessRepair.failed ?? []).length} could not be stopped. Restart Codex App if reconnecting continues.`);
75
- else if (appProcessRepair.status === 'skipped' && appProcessRepair.reason !== 'platform')
76
- console.log(`Codex App reconnect repair: skipped (${appProcessRepair.reason}).`);
77
- else if (appProcessRepair.status === 'failed')
78
- console.log(`Codex App reconnect repair: skipped (${appProcessRepair.error || appProcessRepair.reason || 'process check failed'}).`);
79
- const globalSkills = await ensureGlobalCodexSkillsDuringInstall();
80
- if (globalSkills.status === 'installed') {
81
- const removed = globalSkills.removed_stale_generated_skills || [];
82
- const cleanup = removed.length ? ` Removed stale generated skill shadow(s): ${removed.join(', ')}.` : '';
83
- console.log(`Codex App global $ skills: installed in ${globalSkills.root} (${globalSkills.installed_count} skills).${cleanup}`);
84
- }
85
- else if (globalSkills.status === 'partial')
86
- console.log(`Codex App global $ skills: partial in ${globalSkills.root}; missing ${(globalSkills.missing_skills ?? []).join(', ')}. Run \`sks doctor --fix\`.`);
87
- else if (globalSkills.status === 'skipped')
88
- console.log(`Codex App global $ skills: skipped (${globalSkills.reason}).`);
89
- else if (globalSkills.status === 'failed')
90
- console.log(`Codex App global $ skills: auto setup failed. Run \`sks doctor --fix\`. ${globalSkills.error || ''}`.trim());
91
- const getdesignSkill = await ensureGlobalGetdesignSkillDuringInstall();
92
- if (getdesignSkill.status === 'installed')
93
- console.log('getdesign Codex skill: installed.');
94
- else if (getdesignSkill.status === 'present')
95
- console.log('getdesign Codex skill: already available.');
96
- else if (getdesignSkill.status === 'skills_cli_missing')
97
- console.log(`getdesign Codex skill: skills CLI missing; generated getdesign-reference skill is installed. Later run \`${getdesignSkill.install}\` if the skills CLI is available.`);
98
- else if (getdesignSkill.status === 'skipped')
99
- console.log(`getdesign Codex skill: skipped (${getdesignSkill.reason}).`);
100
- else if (getdesignSkill.status === 'failed')
101
- console.log(`getdesign Codex skill: auto setup failed; generated getdesign-reference skill remains available. ${getdesignSkill.error || ''}`.trim());
102
- const bootstrapDecision = await postinstallBootstrapDecision(installRoot);
103
- if (bootstrapDecision.run) {
104
- console.log(`SKS bootstrap: ${bootstrapDecision.reason}.`);
105
- await runPostinstallBootstrap(installRoot, bootstrap);
106
- await restorePostinstallCodexLbConfigSnapshot(codexLbConfigSnapshot);
107
- await reportPostinstallCodexLbAuth();
108
- return;
36
+ // A failed setup side-effect must never fail `npm i`. Wrap the whole flow; always
37
+ // restore the codex-lb snapshot in finally (even on the early bootstrap return / on throw).
38
+ try {
39
+ console.log('\nSKS installed.');
40
+ const shim = await ensureSksCommandDuringInstall();
41
+ if (shim.status === 'present')
42
+ console.log(`SKS command: available (${shim.command ?? 'unknown'}).`);
43
+ else if (shim.status === 'repaired')
44
+ console.log(`SKS command: stale PATH shim repaired (${shim.command ?? 'unknown'}).`);
45
+ else if (shim.status === 'created')
46
+ console.log(`SKS command: shim created at ${shim.command ?? 'unknown'}.`);
47
+ else if (shim.status === 'created_not_on_path')
48
+ console.log(`SKS command: shim created at ${shim.command ?? 'unknown'}. Add ${path.dirname(shim.command ?? '')} to PATH, or run npx -y -p sneakoscope sks.`);
49
+ else if (shim.status === 'skipped')
50
+ console.log(`SKS command: skipped (${shim.reason}).`);
51
+ else
52
+ console.log(`SKS command: shim unavailable. Use npx -y -p sneakoscope sks. ${shim.error || ''}`.trim());
53
+ const context7Install = await ensureGlobalContext7DuringInstall();
54
+ if (context7Install.status === 'present')
55
+ console.log('Context7 MCP: already configured for Codex.');
56
+ else if (context7Install.status === 'installed')
57
+ console.log('Context7 MCP: configured for Codex.');
58
+ else if (context7Install.status === 'codex_missing')
59
+ console.log('Context7 MCP: Codex CLI missing. Install @openai/codex or set SKS_CODEX_BIN, then run `sks context7 setup --scope global` or `sks setup` in a project.');
60
+ else if (context7Install.status === 'skipped')
61
+ console.log(`Context7 MCP: skipped (${context7Install.reason}).`);
62
+ else if (context7Install.status === 'failed')
63
+ console.log(`Context7 MCP: auto setup failed. Run \`sks context7 setup --scope global\` or \`sks setup\`. ${context7Install.error || ''}`.trim());
64
+ const fastModeRepair = await ensureGlobalCodexFastModeDuringInstall();
65
+ if (fastModeRepair.status === 'updated')
66
+ console.log(`Codex App Fast mode: updated ${fastModeRepair.config_path}${fastModeRepair.backup_path ? ` (backup ${fastModeRepair.backup_path})` : ''}.`);
67
+ else if (fastModeRepair.status === 'present')
68
+ console.log('Codex App Fast mode: config already compatible.');
69
+ else if (fastModeRepair.status === 'unparseable_config_preserved')
70
+ console.log(`Codex App Fast mode: existing ${fastModeRepair.config_path} is not valid TOML — left untouched, backed up to ${fastModeRepair.backup_path}. Run \`sks doctor --fix\` to recover it.`);
71
+ else if (fastModeRepair.status === 'skipped_unsafe_rewrite')
72
+ console.log(`Codex App Fast mode: skipped (managed rewrite would not parse; ${fastModeRepair.config_path} left untouched).`);
73
+ else if (fastModeRepair.status === 'skipped')
74
+ console.log(`Codex App Fast mode: skipped (${fastModeRepair.reason}).`);
75
+ else if (fastModeRepair.status === 'failed')
76
+ console.log(`Codex App Fast mode: auto repair failed. Run \`sks setup\`. ${fastModeRepair.error || ''}`.trim());
77
+ // Terminating a third-party app's processes during `npm i` is unsafe by default; opt-in only.
78
+ const appProcessRepair = process.env.SKS_POSTINSTALL_RECONCILE_APP_PROCESSES === '1'
79
+ ? await reconcileCodexAppUpgradeProcesses()
80
+ : { status: 'skipped', reason: 'opt_in_required', killed: [] };
81
+ if (appProcessRepair.status === 'repaired')
82
+ console.log(`Codex App reconnect repair: stopped ${appProcessRepair.killed.length} stale orphan app-server process(es). Restart Codex App to reconnect cleanly.`);
83
+ else if (appProcessRepair.status === 'partial')
84
+ console.log(`Codex App reconnect repair: stopped ${appProcessRepair.killed.length} stale orphan app-server process(es); ${(appProcessRepair.failed ?? []).length} could not be stopped. Restart Codex App if reconnecting continues.`);
85
+ else if (appProcessRepair.status === 'skipped' && appProcessRepair.reason === 'opt_in_required')
86
+ console.log('Codex App reconnect repair: not run (set SKS_POSTINSTALL_RECONCILE_APP_PROCESSES=1 to allow postinstall to stop stale orphan app-server processes; otherwise run `sks doctor --fix`).');
87
+ else if (appProcessRepair.status === 'skipped' && appProcessRepair.reason !== 'platform')
88
+ console.log(`Codex App reconnect repair: skipped (${appProcessRepair.reason}).`);
89
+ else if (appProcessRepair.status === 'failed')
90
+ console.log(`Codex App reconnect repair: skipped (${appProcessRepair.error || appProcessRepair.reason || 'process check failed'}).`);
91
+ const globalSkills = await ensureGlobalCodexSkillsDuringInstall();
92
+ if (globalSkills.status === 'installed') {
93
+ const removed = globalSkills.removed_stale_generated_skills || [];
94
+ const cleanup = removed.length ? ` Removed stale generated skill shadow(s): ${removed.join(', ')}.` : '';
95
+ console.log(`Codex App global $ skills: installed in ${globalSkills.root} (${globalSkills.installed_count} skills).${cleanup}`);
96
+ }
97
+ else if (globalSkills.status === 'partial')
98
+ console.log(`Codex App global $ skills: partial in ${globalSkills.root}; missing ${(globalSkills.missing_skills ?? []).join(', ')}. Run \`sks doctor --fix\`.`);
99
+ else if (globalSkills.status === 'skipped')
100
+ console.log(`Codex App global $ skills: skipped (${globalSkills.reason}).`);
101
+ else if (globalSkills.status === 'failed')
102
+ console.log(`Codex App global $ skills: auto setup failed. Run \`sks doctor --fix\`. ${globalSkills.error || ''}`.trim());
103
+ const getdesignSkill = await ensureGlobalGetdesignSkillDuringInstall();
104
+ if (getdesignSkill.status === 'installed')
105
+ console.log('getdesign Codex skill: installed.');
106
+ else if (getdesignSkill.status === 'present')
107
+ console.log('getdesign Codex skill: already available.');
108
+ else if (getdesignSkill.status === 'skills_cli_missing')
109
+ console.log(`getdesign Codex skill: skills CLI missing; generated getdesign-reference skill is installed. Later run \`${getdesignSkill.install}\` if the skills CLI is available.`);
110
+ else if (getdesignSkill.status === 'skipped')
111
+ console.log(`getdesign Codex skill: skipped (${getdesignSkill.reason}).`);
112
+ else if (getdesignSkill.status === 'failed')
113
+ console.log(`getdesign Codex skill: auto setup failed; generated getdesign-reference skill remains available. ${getdesignSkill.error || ''}`.trim());
114
+ const bootstrapDecision = await postinstallBootstrapDecision(installRoot);
115
+ if (bootstrapDecision.run) {
116
+ console.log(`SKS bootstrap: ${bootstrapDecision.reason}.`);
117
+ await runPostinstallBootstrap(installRoot, bootstrap);
118
+ return;
119
+ }
120
+ console.log('\nNext:');
121
+ console.log(' sks bootstrap');
122
+ console.log(`\nSKS bootstrap was not run automatically: ${bootstrapDecision.reason}.`);
123
+ console.log('This initializes the current project, installs SKS Codex App skills, verifies Codex App/Context7 readiness, and checks Zellij runtime dependencies.');
124
+ console.log('Dependency repair: sks bootstrap --yes, sks deps check --yes, or sks --mad --yes. Postinstall reports missing CLI tools but does not mutate Homebrew/npm globals unless SKS_POSTINSTALL_AUTO_INSTALL_CLI_TOOLS=1 is set.');
125
+ console.log('Open runtime after readiness is green: sks\n');
126
+ }
127
+ catch (err) {
128
+ console.log(`\nSKS postinstall: a setup step did not complete; installation continues. Run \`sks doctor --fix\` afterward. (${err?.message || err})`);
129
+ }
130
+ finally {
131
+ await restorePostinstallCodexLbConfigSnapshot(codexLbConfigSnapshot).catch(() => { });
132
+ await reportPostinstallCodexLbAuth().catch(() => { });
109
133
  }
110
- await restorePostinstallCodexLbConfigSnapshot(codexLbConfigSnapshot);
111
- await reportPostinstallCodexLbAuth();
112
- console.log('\nNext:');
113
- console.log(' sks bootstrap');
114
- console.log(`\nSKS bootstrap was not run automatically: ${bootstrapDecision.reason}.`);
115
- console.log('This initializes the current project, installs SKS Codex App skills, verifies Codex App/Context7 readiness, and checks Zellij runtime dependencies.');
116
- console.log('Dependency repair: sks deps check; install Zellij with brew install zellij when needed.');
117
- console.log('Open runtime after readiness is green: sks\n');
118
134
  }
119
135
  async function reportPostinstallCodexLbAuth() {
120
136
  const codexLbAuth = await ensureCodexLbAuthDuringInstall();
@@ -128,6 +144,8 @@ async function reportPostinstallCodexLbAuth() {
128
144
  console.log('codex-lb auth: stored key missing. Run `sks codex-lb setup --host <domain> --api-key <key>` to repair.');
129
145
  else if (codexLbAuth.status === 'missing_base_url')
130
146
  console.log('codex-lb auth: stored key has no recoverable base URL. Run `sks codex-lb reconfigure --host <domain> --api-key <key>` once.');
147
+ else if (codexLbAuth.status === 'not_configured')
148
+ console.log('codex-lb (optional multi-account load balancer): not configured — opt in anytime with `sks codex-lb setup` (your choice; never applied automatically, never edits your Codex config without it). Swap key later: `sks codex-lb set-key`; switch auth: `sks codex-lb use-oauth` / `use-codex-lb`.');
131
149
  else if (codexLbAuth.status && codexLbAuth.status !== 'not_configured')
132
150
  console.log(`codex-lb auth: repair skipped (${codexLbAuth.status}${codexLbAuth.error ? `: ${codexLbAuth.error}` : ''}).`);
133
151
  const reconcile = codexLbAuth.auth_reconcile;
@@ -197,6 +215,12 @@ export async function postinstallBootstrapDecision(root) {
197
215
  const target = candidate ? installRoot : globalSksRoot();
198
216
  if (process.env.SKS_POSTINSTALL_BOOTSTRAP === '1')
199
217
  return { run: true, target, reason: 'forced by SKS_POSTINSTALL_BOOTSTRAP=1' };
218
+ // A global `npm i -g sneakoscope` must NOT initialize whatever project the user's shell
219
+ // happened to be in (that would scribble AGENTS.md/.codex/.agents into an unrelated repo).
220
+ // Only bootstrap the global runtime root; the user runs `sks setup` inside a project explicitly.
221
+ if (process.env.npm_config_global === 'true' && candidate) {
222
+ return { run: true, target: globalSksRoot(), reason: 'global install: bootstrapping global SKS runtime only (run `sks setup` inside a project to initialize it)' };
223
+ }
200
224
  if (candidate)
201
225
  return { run: true, target, reason: 'auto-running sks setup --bootstrap --install-scope global --force' };
202
226
  return { run: true, target, reason: 'no project marker found; auto-running global SKS runtime bootstrap' };
@@ -267,8 +291,8 @@ async function restorePostinstallCodexLbConfigSnapshot(snapshot) {
267
291
  const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(current, snapshot.base_url));
268
292
  const alreadyOk = next === ensureTrailingNewline(current) && codexLbProviderBaseUrl(current);
269
293
  if (!alreadyOk) {
270
- await writeTextAtomic(snapshot.config_path, next);
271
- configRestored = true;
294
+ const safeWrite = await safeWriteCodexConfigToml(snapshot.config_path, current, next, 'codex-lb-restore');
295
+ configRestored = safeWrite.ok && safeWrite.changed === true;
272
296
  }
273
297
  }
274
298
  // Restore auth.json only if bootstrap accidentally wiped or emptied a pre-existing auth.json.
@@ -347,8 +371,10 @@ export async function configureCodexLb(opts = {}) {
347
371
  await ensureDir(path.dirname(configPath));
348
372
  const current = await readText(configPath, '');
349
373
  const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(current, baseUrl, useDefaultProvider));
350
- await writeTextAtomic(configPath, next);
351
- appliedActions.push({ type: 'write_config_provider', target: configPath, ok: true });
374
+ const safeWrite = await safeWriteCodexConfigToml(configPath, current, next, 'codex-lb');
375
+ if (!safeWrite.ok)
376
+ return { ok: false, status: safeWrite.status, config_path: configPath, env_path: envPath, backup_path: safeWrite.backup_path };
377
+ appliedActions.push({ type: 'write_config_provider', target: configPath, ok: true, backup_path: safeWrite.backup_path });
352
378
  if (useDefaultProvider)
353
379
  appliedActions.push({ type: 'select_default_provider', target: configPath, ok: true });
354
380
  if (writeEnvFile) {
@@ -789,8 +815,8 @@ export async function repairCodexLbAuth(opts = {}) {
789
815
  if (status.env_key_configured && status.base_url && (!status.ok || !status.selected || !status.provider_uses_codex_lb_env_auth || legacyAuthMigrated || hasTopLevelCodexModeLock(currentConfig))) {
790
816
  await ensureDir(path.dirname(status.config_path));
791
817
  const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(currentConfig, status.base_url));
792
- await writeTextAtomic(status.config_path, next);
793
- configRepaired = true;
818
+ const safeWrite = await safeWriteCodexConfigToml(status.config_path, currentConfig, next, 'codex-lb-repair');
819
+ configRepaired = safeWrite.ok && safeWrite.changed === true;
794
820
  status = await codexLbStatus(opts);
795
821
  }
796
822
  if (!status.ok) {
@@ -1352,7 +1378,7 @@ async function syncCodexApiKeyLogin(apiKey, opts = {}) {
1352
1378
  return { ok: true, status: 'synced' };
1353
1379
  return { ok: false, status: 'login_failed', error: redactSecretText(login.stderr || login.stdout || 'codex login failed', [apiKey]).trim() };
1354
1380
  }
1355
- function upsertCodexLbConfig(text = '', baseUrl, selectDefault = true) {
1381
+ export function upsertCodexLbConfig(text = '', baseUrl, selectDefault = true) {
1356
1382
  let next = selectDefault
1357
1383
  ? upsertTopLevelTomlString(text, 'model_provider', 'codex-lb')
1358
1384
  : removeTopLevelTomlKeyIfValue(text, 'model_provider', 'codex-lb');
@@ -1442,41 +1468,61 @@ export async function ensureGlobalCodexFastModeDuringInstall(opts = {}) {
1442
1468
  try {
1443
1469
  await ensureDir(path.dirname(configPath));
1444
1470
  const current = await readText(configPath, '');
1471
+ // Safety gate 1: never blind-overwrite an unparseable user config — that would
1472
+ // entrench corruption on the file Codex actually loads. Back it up and bail.
1473
+ if (current.trim()) {
1474
+ const currentSmoke = codexConfigParseSmoke(current);
1475
+ if (!currentSmoke.ok) {
1476
+ const backupPath = await backupCodexConfig(configPath, current, 'unparseable');
1477
+ return { status: 'unparseable_config_preserved', config_path: configPath, backup_path: backupPath, parse_smoke: currentSmoke };
1478
+ }
1479
+ }
1445
1480
  const next = normalizeCodexFastModeUiConfig(current);
1446
1481
  if (next === ensureTrailingNewline(current))
1447
1482
  return { status: 'present', config_path: configPath };
1483
+ // Safety gate 2: never WRITE a config that would not parse.
1484
+ const nextSmoke = codexConfigParseSmoke(next);
1485
+ if (!nextSmoke.ok)
1486
+ return { status: 'skipped_unsafe_rewrite', config_path: configPath, parse_smoke: nextSmoke };
1487
+ // Safety gate 3: back up the user's good config before mutating it.
1488
+ const backupPath = current.trim() ? await backupCodexConfig(configPath, current, 'pre-update') : null;
1448
1489
  await writeTextAtomic(configPath, next);
1449
- return { status: 'updated', config_path: configPath };
1490
+ return { status: 'updated', config_path: configPath, backup_path: backupPath };
1450
1491
  }
1451
1492
  catch (err) {
1452
1493
  return { status: 'failed', config_path: configPath, error: err.message };
1453
1494
  }
1454
1495
  }
1455
1496
  export function normalizeCodexFastModeUiConfig(text = '') {
1456
- let next = removeLegacyTopLevelCodexModeLocks(text);
1497
+ // Run to a fixed point so a second install is a true no-op (idempotent). The per-pass
1498
+ // table/whitespace normalization converges within one extra pass.
1499
+ return normalizeCodexFastModeUiConfigOnce(normalizeCodexFastModeUiConfigOnce(text));
1500
+ }
1501
+ function normalizeCodexFastModeUiConfigOnce(text = '') {
1502
+ // Preserve user-owned top-level scalars (model / service_tier / model_reasoning_effort):
1503
+ // SKS only supplies a default when the user has not chosen one, and never strips the
1504
+ // user's own reasoning effort. SKS continues to manage its own namespaced tables below
1505
+ // ([features], [profiles.sks-*], [user.fast_mode], [plugins]).
1506
+ let next = String(text || '');
1457
1507
  next = removeTomlTableKey(next, 'notice', 'fast_default_opt_out');
1458
1508
  next = removeTomlTableKey(next, 'features', 'codex_hooks');
1459
- next = upsertTopLevelTomlString(next, 'model', 'gpt-5.5');
1460
- next = upsertTopLevelTomlString(next, 'service_tier', 'fast');
1461
- next = upsertTopLevelTomlBoolean(next, 'suppress_unstable_features_warning', true);
1462
- next = upsertTomlTableKey(next, 'features', 'hooks = true');
1463
- next = upsertTomlTableKey(next, 'features', 'remote_control = true');
1464
- next = upsertTomlTableKey(next, 'features', 'multi_agent = true');
1465
- next = upsertTomlTableKey(next, 'features', 'fast_mode = true');
1466
- next = upsertTomlTableKey(next, 'features', 'fast_mode_ui = true');
1467
- next = upsertTomlTableKey(next, 'features', 'codex_git_commit = true');
1468
- next = upsertTomlTableKey(next, 'features', 'computer_use = true');
1469
- next = upsertTomlTableKey(next, 'features', 'browser_use = true');
1470
- next = upsertTomlTableKey(next, 'features', 'browser_use_external = true');
1471
- next = upsertTomlTableKey(next, 'features', 'image_generation = true');
1472
- next = upsertTomlTableKey(next, 'features', 'in_app_browser = true');
1473
- next = upsertTomlTableKey(next, 'features', 'guardian_approval = true');
1474
- next = upsertTomlTableKey(next, 'features', 'tool_suggest = true');
1475
- next = upsertTomlTableKey(next, 'features', 'apps = true');
1476
- next = upsertTomlTableKey(next, 'features', 'plugins = true');
1477
- next = upsertTomlTableKey(next, 'user.fast_mode', 'visible = true');
1478
- next = upsertTomlTableKey(next, 'user.fast_mode', 'enabled = true');
1479
- next = upsertTomlTableKey(next, 'user.fast_mode', 'default_profile = "sks-fast-high"');
1509
+ next = upsertTopLevelTomlStringIfAbsent(next, 'model', 'gpt-5.5');
1510
+ next = upsertTopLevelTomlStringIfAbsent(next, 'service_tier', 'fast');
1511
+ // Codex App feature flags / fast-mode UI / suppress-warning are SET-IF-ABSENT: a fresh
1512
+ // config still gets SKS's defaults, but SKS NEVER overrides (re-enables) a feature the
1513
+ // user disabled in the App, and never rejects-then-hides UI by forcing an unrecognized
1514
+ // flag on an older App build. This is what stops SKS from "removing/blocking" the App UI.
1515
+ next = upsertTopLevelTomlBooleanIfAbsent(next, 'suppress_unstable_features_warning', true);
1516
+ for (const featureLine of [
1517
+ 'hooks = true', 'remote_control = true', 'multi_agent = true', 'fast_mode = true',
1518
+ 'fast_mode_ui = true', 'codex_git_commit = true', 'computer_use = true', 'browser_use = true',
1519
+ 'browser_use_external = true', 'image_generation = true', 'in_app_browser = true',
1520
+ 'guardian_approval = true', 'tool_suggest = true', 'apps = true', 'plugins = true'
1521
+ ])
1522
+ next = upsertTomlTableKeyIfAbsent(next, 'features', featureLine);
1523
+ next = upsertTomlTableKeyIfAbsent(next, 'user.fast_mode', 'visible = true');
1524
+ next = upsertTomlTableKeyIfAbsent(next, 'user.fast_mode', 'enabled = true');
1525
+ next = upsertTomlTableKeyIfAbsent(next, 'user.fast_mode', 'default_profile = "sks-fast-high"');
1480
1526
  next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'model = "gpt-5.5"');
1481
1527
  next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'service_tier = "fast"');
1482
1528
  next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'approval_policy = "on-request"');
@@ -1492,9 +1538,17 @@ export function normalizeCodexFastModeUiConfig(text = '') {
1492
1538
  next = upsertTomlTableKey(next, 'profiles.sks-research', 'approval_policy = "never"');
1493
1539
  next = upsertTomlTableKey(next, 'profiles.sks-research', 'sandbox_mode = "workspace-write"');
1494
1540
  next = upsertTomlTableKey(next, 'profiles.sks-research', 'model_reasoning_effort = "xhigh"');
1495
- for (const [name, marketplace] of DEFAULT_CODEX_APP_PLUGINS) {
1496
- const table = `plugins."${name}@${marketplace}"`;
1497
- next = upsertTomlTable(next, table, `[${table}]\nenabled = true`);
1541
+ // Plugin auto-enable is OPT-IN only. Force-writing `[plugins."name@marketplace"] enabled =
1542
+ // true` for marketplace plugins the App may not have installed (different build/channel)
1543
+ // makes the App reference plugins it cannot load -> broken/blocked plugin UI. It also
1544
+ // replaced the user's whole plugin table, reverting any `enabled = false` they set. By
1545
+ // default SKS leaves the user's [plugins] alone; opt in with SKS_MANAGE_CODEX_APP_PLUGINS=1.
1546
+ if (process.env.SKS_MANAGE_CODEX_APP_PLUGINS === '1') {
1547
+ for (const [name, marketplace] of DEFAULT_CODEX_APP_PLUGINS) {
1548
+ const table = `plugins."${name}@${marketplace}"`;
1549
+ if (!hasTomlTable(next, table))
1550
+ next = upsertTomlTable(next, table, `[${table}]\nenabled = true`);
1551
+ }
1498
1552
  }
1499
1553
  return ensureTrailingNewline(next);
1500
1554
  }
@@ -1568,6 +1622,35 @@ function upsertTomlTableKey(text, table, line) {
1568
1622
  lines.splice(end, 0, line);
1569
1623
  return lines.join('\n').replace(/\n{3,}/g, '\n\n');
1570
1624
  }
1625
+ // True if [table] already declares `key` (so we never override a user's explicit value).
1626
+ function hasTomlTableKey(text, table, key) {
1627
+ const lines = String(text || '').split('\n');
1628
+ const header = `[${table}]`;
1629
+ const start = lines.findIndex((x) => x.trim() === header);
1630
+ if (start === -1)
1631
+ return false;
1632
+ const keyRe = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
1633
+ for (let i = start + 1; i < lines.length; i += 1) {
1634
+ const ln = lines[i];
1635
+ if (ln === undefined)
1636
+ continue;
1637
+ if (/^\s*\[.+\]\s*$/.test(ln))
1638
+ break;
1639
+ if (keyRe.test(ln))
1640
+ return true;
1641
+ }
1642
+ return false;
1643
+ }
1644
+ // Set a [table] key only when absent — preserves a Codex App feature the user toggled off
1645
+ // (so SKS never re-enables / re-surfaces UI the user hid). On a fresh config the key/table
1646
+ // is still created, preserving fresh-install enablement.
1647
+ function upsertTomlTableKeyIfAbsent(text, table, line) {
1648
+ const key = String(line).split('=')[0]?.trim() ?? '';
1649
+ return hasTomlTableKey(text, table, key) ? String(text || '') : upsertTomlTableKey(text, table, line);
1650
+ }
1651
+ function upsertTopLevelTomlBooleanIfAbsent(text, key, value) {
1652
+ return hasTopLevelTomlKey(text, key) ? String(text || '') : upsertTopLevelTomlBoolean(text, key, value);
1653
+ }
1571
1654
  function ensureTrailingNewline(text = '') {
1572
1655
  const value = String(text || '').trimEnd();
1573
1656
  return value ? `${value}\n` : '';
@@ -1589,6 +1672,65 @@ function upsertTopLevelTomlString(text, key, value) {
1589
1672
  lines.splice(end, 0, line);
1590
1673
  return lines.join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
1591
1674
  }
1675
+ function hasTopLevelTomlKey(text, key) {
1676
+ const lines = String(text || '').split('\n');
1677
+ const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
1678
+ const end = firstTable === -1 ? lines.length : firstTable;
1679
+ const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
1680
+ for (let i = 0; i < end; i += 1) {
1681
+ if (typeof lines[i] === 'string' && pattern.test(lines[i]))
1682
+ return true;
1683
+ }
1684
+ return false;
1685
+ }
1686
+ // Preserve a user's deliberate top-level scalar (model/service_tier/reasoning); only set
1687
+ // the SKS default when the key is ABSENT. This is what stops `npm i -g` from clobbering
1688
+ // a user's global Codex config on every update.
1689
+ function upsertTopLevelTomlStringIfAbsent(text, key, value) {
1690
+ return hasTopLevelTomlKey(text, key) ? String(text || '') : upsertTopLevelTomlString(text, key, value);
1691
+ }
1692
+ // Lightweight safety gate: detect clearly-broken TOML so we never overwrite (or produce)
1693
+ // an unparseable config that Codex itself would reject. Mirrors the project-config smoke.
1694
+ function codexConfigParseSmoke(text = '') {
1695
+ const str = String(text || '');
1696
+ const tripleTokens = (str.match(/"""|'''/g) || []).length;
1697
+ const unterminatedTriple = tripleTokens % 2 !== 0;
1698
+ const invalidHeader = str.split('\n').find((line) => /^\s*\[/.test(line) && !/^\s*\[\[?[^\]]+\]\]?\s*(?:#.*)?$/.test(line)) || null;
1699
+ return { ok: !unterminatedTriple && !invalidHeader, unterminated_multiline_string: unterminatedTriple, invalid_table_header: invalidHeader };
1700
+ }
1701
+ async function backupCodexConfig(configPath, text, tag) {
1702
+ try {
1703
+ const stamp = `${PACKAGE_VERSION}-${Date.now().toString(36)}`;
1704
+ const backupPath = `${configPath}.sks-${tag}-${stamp}.bak`;
1705
+ await writeTextAtomic(backupPath, text);
1706
+ return backupPath;
1707
+ }
1708
+ catch {
1709
+ return null;
1710
+ }
1711
+ }
1712
+ // Single TOML-safe gate for every codex-lb config write. Mirrors the fast-mode safety so the
1713
+ // codex-lb path can NEVER corrupt ~/.codex/config.toml on install (esp. a fresh/initial one):
1714
+ // - refuse to overwrite an existing config that is already unparseable (back it up, bail),
1715
+ // - refuse to WRITE a result that would not parse (e.g. a regex helper mangled a multiline
1716
+ // string), leaving the existing config untouched,
1717
+ // - otherwise back up the prior config before mutating.
1718
+ export async function safeWriteCodexConfigToml(configPath, current, next, tag = 'codex-lb') {
1719
+ const cur = String(current || '');
1720
+ if (cur.trim() && !codexConfigParseSmoke(cur).ok) {
1721
+ const backupPath = await backupCodexConfig(configPath, cur, `${tag}-unparseable`);
1722
+ return { ok: false, status: 'unparseable_config_preserved', config_path: configPath, backup_path: backupPath };
1723
+ }
1724
+ if (!codexConfigParseSmoke(next).ok) {
1725
+ return { ok: false, status: 'skipped_unsafe_rewrite', config_path: configPath, backup_path: null };
1726
+ }
1727
+ if (next === ensureTrailingNewline(cur)) {
1728
+ return { ok: true, status: 'present', config_path: configPath, backup_path: null, changed: false };
1729
+ }
1730
+ const backupPath = cur.trim() ? await backupCodexConfig(configPath, cur, tag) : null;
1731
+ await writeTextAtomic(configPath, next);
1732
+ return { ok: true, status: 'written', config_path: configPath, backup_path: backupPath, changed: true };
1733
+ }
1592
1734
  function upsertTopLevelTomlBoolean(text, key, value) {
1593
1735
  const line = `${key} = ${value ? 'true' : 'false'}`;
1594
1736
  const lines = String(text || '').split('\n');
@@ -1606,6 +1748,10 @@ function upsertTopLevelTomlBoolean(text, key, value) {
1606
1748
  lines.splice(end, 0, line);
1607
1749
  return lines.join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
1608
1750
  }
1751
+ function hasTomlTable(text, table) {
1752
+ const header = `[${table}]`;
1753
+ return String(text || '').split('\n').some((line) => String(line).trim() === header);
1754
+ }
1609
1755
  function upsertTomlTable(text, table, block) {
1610
1756
  let lines = String(text || '').trimEnd().split('\n');
1611
1757
  if (lines.length === 1 && lines[0] === '')
@@ -1915,7 +2061,7 @@ async function ensureGlobalGetdesignSkillDuringInstall() {
1915
2061
  }
1916
2062
  export async function ensureRelatedCliTools(args = []) {
1917
2063
  const skip = args.includes('--skip-cli-tools') || process.env.SKS_SKIP_CLI_TOOLS === '1';
1918
- const codex = await ensureCodexCliTool({ skip });
2064
+ const codex = await ensureCodexCliTool({ skip, args });
1919
2065
  const zellijRepair = skip ? { status: 'skipped', reason: 'SKS_SKIP_CLI_TOOLS=1 or --skip-cli-tools' } : await ensureZellijCliTool(args);
1920
2066
  const zellij = await checkZellijCapability({ require: false, writeReport: false });
1921
2067
  return {
@@ -1928,11 +2074,42 @@ export async function ensureRelatedCliTools(args = []) {
1928
2074
  current_session: false,
1929
2075
  repair: zellijRepair,
1930
2076
  install_hint: zellij.status === 'ok' ? null : zellijInstallHint(),
1931
- error: zellij.blockers[0] || zellij.warnings[0] || null
2077
+ error: zellijRepair.error || zellij.blockers[0] || zellij.warnings[0] || null
2078
+ }
2079
+ };
2080
+ }
2081
+ export async function ensureMadLaunchDependencies(args = []) {
2082
+ const skip = args.includes('--skip-cli-tools') || process.env.SKS_SKIP_CLI_TOOLS === '1';
2083
+ const zellijRepair = skip ? { target: 'zellij', status: 'skipped', reason: 'SKS_SKIP_CLI_TOOLS=1 or --skip-cli-tools' } : await ensureZellijCliTool(args);
2084
+ const zellij = await checkZellijCapability({ require: false, writeReport: false });
2085
+ const ready = zellij.status === 'ok';
2086
+ return {
2087
+ ready,
2088
+ actions: ready ? [] : [{
2089
+ target: 'zellij',
2090
+ status: zellijRepair.status,
2091
+ command: zellijRepair.command || zellijInstallHint(),
2092
+ error: zellijRepair.error || zellij.blockers[0] || zellij.warnings[0] || null,
2093
+ repair: zellijRepair
2094
+ }],
2095
+ status: {
2096
+ zellij: {
2097
+ ok: ready,
2098
+ status: zellij.status,
2099
+ version: zellij.version,
2100
+ min_version: zellij.min_version,
2101
+ repair: zellijRepair,
2102
+ install_hint: ready ? null : zellijInstallHint()
2103
+ }
1932
2104
  }
1933
2105
  };
1934
2106
  }
1935
- export async function ensureCodexCliTool({ skip = false } = {}) {
2107
+ export function formatMadLaunchDependencyAction(action = {}) {
2108
+ const command = action.command ? ` Run: ${action.command}.` : '';
2109
+ const error = action.error ? ` ${action.error}` : '';
2110
+ return `${action.target || 'dependency'} ${action.status || 'blocked'}.${command}${error}`.trim();
2111
+ }
2112
+ export async function ensureCodexCliTool({ skip = false, args = [] } = {}) {
1936
2113
  if (skip)
1937
2114
  return { status: 'skipped', reason: 'SKS_SKIP_CLI_TOOLS=1 or --skip-cli-tools' };
1938
2115
  const before = await getCodexInfo().catch(() => EMPTY_CODEX_INFO);
@@ -1941,6 +2118,12 @@ export async function ensureCodexCliTool({ skip = false } = {}) {
1941
2118
  const npmBin = await which('npm');
1942
2119
  if (!npmBin)
1943
2120
  return { status: 'failed', error: 'npm not found on PATH; install Codex CLI manually with npm i -g @openai/codex@latest.' };
2121
+ const command = 'npm i -g @openai/codex@latest';
2122
+ if (args.includes('--dry-run'))
2123
+ return { status: 'dry_run', command, error: 'Codex CLI not found on PATH.' };
2124
+ if (!await confirmInstallYesDefault(`Codex CLI is missing. Install latest Codex CLI with ${command}?`, args)) {
2125
+ return { status: 'needs_approval', command, error: 'Codex CLI not found on PATH.' };
2126
+ }
1944
2127
  const install = await runProcess(npmBin, ['i', '-g', '@openai/codex@latest'], {
1945
2128
  timeoutMs: 120000,
1946
2129
  maxOutputBytes: 128 * 1024
@@ -1969,24 +2152,27 @@ export async function ensureZellijCliTool(args = [], opts = {}) {
1969
2152
  const repairCommand = command;
1970
2153
  if (args.includes('--dry-run') || opts.dryRun)
1971
2154
  return { target: 'zellij', status: 'dry_run', command: repairCommand, error: before.blockers[0] || before.warnings[0] || null };
1972
- const question = before.bin
2155
+ const hasInstalledZellij = Boolean(before.version);
2156
+ const question = hasInstalledZellij
1973
2157
  ? `Homebrew Zellij ${before.version || 'unknown'} is not ready. Upgrade to latest Zellij with ${repairCommand}?`
1974
2158
  : `Zellij is missing. Install latest Zellij with ${repairCommand}?`;
1975
2159
  if (!await confirmInstallYesDefault(question, args))
1976
2160
  return { target: 'zellij', status: 'needs_approval', command: repairCommand, error: before.blockers[0] || before.warnings[0] || null };
1977
- const brewArgs = before.bin ? ['upgrade', 'zellij'] : ['install', 'zellij'];
2161
+ const brewArgs = hasInstalledZellij ? ['upgrade', 'zellij'] : ['install', 'zellij'];
1978
2162
  const install = await runProcess(brew, brewArgs, { timeoutMs: 180000, maxOutputBytes: 128 * 1024 }).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
1979
2163
  if (install.code !== 0)
1980
2164
  return { target: 'zellij', status: 'failed', command: repairCommand, error: `${install.stderr || install.stdout || repairCommand + ' failed'}`.trim() };
1981
2165
  const after = await checkZellijCapability({ require: false, writeReport: false });
1982
2166
  if (after.status !== 'ok')
1983
2167
  return { target: 'zellij', status: 'installed_not_ready', command: repairCommand, error: after.blockers[0] || after.warnings[0] || 'zellij installed but not ready' };
1984
- return { target: 'zellij', status: before.version ? 'upgraded' : 'installed', command: repairCommand, bin: after.bin, version: after.version || null };
2168
+ return { target: 'zellij', status: hasInstalledZellij ? 'upgraded' : 'installed', command: repairCommand, bin: after.bin, version: after.version || null };
1985
2169
  }
1986
2170
  function zellijInstallHint() {
1987
2171
  return process.platform === 'darwin' ? 'brew install zellij' : 'Install Zellij from https://zellij.dev/documentation/installation.html';
1988
2172
  }
1989
2173
  async function confirmInstallYesDefault(question, args = []) {
2174
+ if (hasFlag(args, '--from-postinstall') && process.env.SKS_POSTINSTALL_AUTO_INSTALL_CLI_TOOLS !== '1')
2175
+ return false;
1990
2176
  if (shouldAutoApproveInstall(args))
1991
2177
  return true;
1992
2178
  if (!canAskYesNo())
@@ -2023,6 +2209,10 @@ export async function maybePromptCodexUpdateForLaunch(args = [], opts = {}) {
2023
2209
  return installCodexLatest(command, latest.version, current);
2024
2210
  }
2025
2211
  export function shouldAutoApproveInstall(args = [], env = process.env) {
2212
+ if (hasFlag(args, '--from-postinstall') && env.SKS_POSTINSTALL_AUTO_INSTALL_CLI_TOOLS !== '1')
2213
+ return false;
2214
+ if (hasFlag(args, '--from-postinstall') && env.SKS_POSTINSTALL_AUTO_INSTALL_CLI_TOOLS === '1')
2215
+ return true;
2026
2216
  return hasFlag(args, '--yes') || hasFlag(args, '-y') || isAgentRuntime(env);
2027
2217
  }
2028
2218
  function canAskYesNo() {