sneakoscope 0.7.38 → 0.7.41

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.
package/README.md CHANGED
@@ -166,10 +166,37 @@ sks tmux check
166
166
  sks tmux status --once
167
167
  ```
168
168
 
169
- Bare `sks` creates or reuses the default named tmux session for Codex CLI and attaches to it in an interactive terminal. Use `sks tmux open` when you need explicit `--workspace` / `--session` flags, `sks tmux check` for readiness without launching, and `sks help` for CLI help. Use `--no-attach` or `SKS_TMUX_NO_AUTO_ATTACH=1` when you only want SKS to create/reuse the session and print the manual attach command.
169
+ Bare `sks` creates or reuses the default named tmux session for Codex CLI and attaches to it in an interactive terminal. By default it launches Codex in the SKS fast-high runtime (`--model gpt-5.5 -c model_reasoning_effort="high"`). Override with `SKS_CODEX_MODEL`, `SKS_CODEX_REASONING`, or disable the default with `SKS_CODEX_FAST_HIGH=0`. Use `sks tmux open` when you need explicit `--workspace` / `--session` flags, `sks tmux check` for readiness without launching, and `sks help` for CLI help. Use `--no-attach` or `SKS_TMUX_NO_AUTO_ATTACH=1` when you only want SKS to create/reuse the session and print the manual attach command.
170
170
 
171
171
  Before opening tmux, SKS checks the installed Codex CLI against npm `@openai/codex@latest`. If a newer version exists, it asks `Y/n`; answering `y` updates automatically with `npm i -g @openai/codex@latest` and then opens tmux with the updated Codex CLI.
172
172
 
173
+ If you use [codex-lb](https://github.com/Soju06/codex-lb), start it first, create an API key in its dashboard, then run:
174
+
175
+ ```sh
176
+ sks codex-lb setup --host https://your-codex-lb.example.com --api-key "sk-clb-..."
177
+ sks
178
+ ```
179
+
180
+ Bare `sks` asks this before opening Codex when codex-lb is not configured:
181
+
182
+ ```text
183
+ Authenticate and route Codex through codex-lb? [y/N]
184
+ ```
185
+
186
+ Answering `y` asks for the hosted domain and API key, writes `~/.codex/config.toml`, stores the key in `~/.codex/sks-codex-lb.env` with mode `0600`, and sources that env file before launching Codex in tmux. When codex-lb is configured from this prompt, SKS opens a fresh tmux session for that launch so the new key is loaded by the Codex process immediately. The generated provider config follows the codex-lb README's Codex CLI API-key setup:
187
+
188
+ ```toml
189
+ model_provider = "codex-lb"
190
+
191
+ [model_providers.codex-lb]
192
+ name = "OpenAI"
193
+ base_url = "http://127.0.0.1:2455/backend-api/codex"
194
+ wire_api = "responses"
195
+ env_key = "CODEX_LB_API_KEY"
196
+ supports_websockets = true
197
+ requires_openai_auth = true
198
+ ```
199
+
173
200
  ### MAD tmux Launch
174
201
 
175
202
  ```sh
@@ -385,12 +412,38 @@ Use these inside Codex App or another agent prompt. They are prompt commands, no
385
412
 
386
413
  ### First Install Checklist
387
414
 
415
+ 1. Install SKS.
416
+
388
417
  ```sh
389
418
  npm i -g sneakoscope
419
+ ```
420
+
421
+ 2. Bootstrap and check dependencies.
422
+
423
+ ```sh
390
424
  sks bootstrap
391
425
  sks deps check
426
+ ```
427
+
428
+ On macOS, missing tmux installs and Homebrew-managed tmux upgrades ask `Y/n` before running `brew install tmux` or `brew upgrade tmux`. If PATH resolves an npm-managed `tmux`, SKS prompts for `npm i -g tmux@latest` instead of using Homebrew. Unknown non-Homebrew `tmux` paths are reported as conflicts so the user can remove, upgrade with the owning package manager, or reorder PATH first.
429
+
430
+ 3. Confirm Codex App command surfaces.
431
+
432
+ ```sh
392
433
  sks codex-app check
393
434
  sks dollar-commands
435
+ ```
436
+
437
+ 4. Optional codex-lb key setup for CLI `sks` runs.
438
+
439
+ ```sh
440
+ sks codex-lb setup --host <domain> --api-key <key>
441
+ sks
442
+ ```
443
+
444
+ 5. Run a local smoke test.
445
+
446
+ ```sh
394
447
  sks selftest --mock
395
448
  ```
396
449
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.7.38",
4
+ "version": "0.7.41",
5
5
  "description": "Sneakoscope Codex: database-safe Codex CLI/App harness with Team, Goal, AutoResearch, TriWiki, and Honest Mode.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
@@ -3,7 +3,7 @@ import os from 'node:os';
3
3
  import fsp from 'node:fs/promises';
4
4
  import readline from 'node:readline/promises';
5
5
  import { stdin as input, stdout as output } from 'node:process';
6
- import { ensureDir, exists, globalSksRoot, packageRoot, runProcess, which, writeTextAtomic } from '../core/fsx.mjs';
6
+ import { ensureDir, exists, globalSksRoot, packageRoot, readText, runProcess, which, writeTextAtomic } from '../core/fsx.mjs';
7
7
  import { getCodexInfo } from '../core/codex-adapter.mjs';
8
8
  import { formatHarnessConflictReport, llmHarnessCleanupPrompt, scanHarnessConflicts } from '../core/harness-conflicts.mjs';
9
9
  import { installSkills } from '../core/init.mjs';
@@ -113,6 +113,132 @@ export async function askPostinstallQuestion(question) {
113
113
  }
114
114
  }
115
115
 
116
+ export function codexLbConfigPath(home = process.env.HOME || os.homedir()) {
117
+ return path.join(home, '.codex', 'config.toml');
118
+ }
119
+
120
+ export function codexLbEnvPath(home = process.env.HOME || os.homedir()) {
121
+ return path.join(home, '.codex', 'sks-codex-lb.env');
122
+ }
123
+
124
+ export function normalizeCodexLbBaseUrl(input = '') {
125
+ let host = String(input || '').trim();
126
+ if (!host) host = 'http://127.0.0.1:2455';
127
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(host)) host = `https://${host}`;
128
+ host = host.replace(/\/+$/, '');
129
+ return /\/backend-api\/codex$/i.test(host) ? host : `${host}/backend-api/codex`;
130
+ }
131
+
132
+ export async function configureCodexLb(opts = {}) {
133
+ const home = opts.home || process.env.HOME || os.homedir();
134
+ const configPath = opts.configPath || codexLbConfigPath(home);
135
+ const envPath = opts.envPath || codexLbEnvPath(home);
136
+ const baseUrl = normalizeCodexLbBaseUrl(opts.host || opts.baseUrl);
137
+ const apiKey = String(opts.apiKey || '').trim();
138
+ if (!apiKey) return { ok: false, status: 'missing_api_key', config_path: configPath, env_path: envPath };
139
+ await ensureDir(path.dirname(configPath));
140
+ const current = await readText(configPath, '');
141
+ const next = upsertCodexLbConfig(current, baseUrl);
142
+ await writeTextAtomic(configPath, next);
143
+ await writeTextAtomic(envPath, `export CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`);
144
+ await fsp.chmod(envPath, 0o600).catch(() => {});
145
+ process.env.CODEX_LB_API_KEY = apiKey;
146
+ return { ok: true, status: 'configured', config_path: configPath, env_path: envPath, base_url: baseUrl, env_key: 'CODEX_LB_API_KEY' };
147
+ }
148
+
149
+ export async function codexLbStatus(opts = {}) {
150
+ const home = opts.home || process.env.HOME || os.homedir();
151
+ const configPath = opts.configPath || codexLbConfigPath(home);
152
+ const envPath = opts.envPath || codexLbEnvPath(home);
153
+ const config = await readText(configPath, '');
154
+ const envExists = await exists(envPath);
155
+ const envText = envExists ? await readText(envPath, '') : '';
156
+ const envKeyConfigured = /^(\s*export\s+)?CODEX_LB_API_KEY\s*=.+$/m.test(envText);
157
+ const providerConfigured = /\[model_providers\.codex-lb\]/.test(config);
158
+ const selected = /model_provider\s*=\s*"codex-lb"/.test(config);
159
+ return {
160
+ ok: selected && providerConfigured && envKeyConfigured,
161
+ config_path: configPath,
162
+ env_path: envPath,
163
+ provider_configured: providerConfigured,
164
+ selected,
165
+ env_file: envExists,
166
+ env_key_configured: envKeyConfigured,
167
+ base_url: config.match(/base_url\s*=\s*"([^"]+)"/)?.[1] || null
168
+ };
169
+ }
170
+
171
+ export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
172
+ if (args.includes('--json') || args.includes('--skip-codex-lb') || process.env.SKS_SKIP_CODEX_LB_PROMPT === '1') return { status: 'skipped' };
173
+ if (!canAskYesNo()) return { status: 'non_interactive' };
174
+ const status = await codexLbStatus(opts);
175
+ if (status.ok) return { status: 'present', ...status };
176
+ const useCodexLb = (await askPostinstallQuestion('\nAuthenticate and route Codex through codex-lb? [y/N] ')).trim();
177
+ if (!/^(y|yes|예|네|응)$/i.test(useCodexLb)) return { status: 'continued_to_codex' };
178
+ const host = (await askPostinstallQuestion('codex-lb host domain [http://127.0.0.1:2455]: ')).trim() || 'http://127.0.0.1:2455';
179
+ const apiKey = (await askPostinstallQuestion('codex-lb API key: ')).trim();
180
+ const configured = await configureCodexLb({ ...opts, host, apiKey });
181
+ if (configured.ok) console.log(`codex-lb configured: ${configured.base_url}`);
182
+ else console.log('codex-lb setup skipped: API key was empty.');
183
+ return configured;
184
+ }
185
+
186
+ function upsertCodexLbConfig(text = '', baseUrl) {
187
+ let next = upsertTopLevelTomlString(text, 'model_provider', 'codex-lb');
188
+ const block = [
189
+ '[model_providers.codex-lb]',
190
+ 'name = "OpenAI"',
191
+ `base_url = "${baseUrl}"`,
192
+ 'wire_api = "responses"',
193
+ 'env_key = "CODEX_LB_API_KEY"',
194
+ 'supports_websockets = true',
195
+ 'requires_openai_auth = true'
196
+ ].join('\n');
197
+ next = upsertTomlTable(next, 'model_providers.codex-lb', block);
198
+ return `${next.trim()}\n`;
199
+ }
200
+
201
+ function upsertTopLevelTomlString(text, key, value) {
202
+ const line = `${key} = "${value}"`;
203
+ const lines = String(text || '').split('\n');
204
+ const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
205
+ const end = firstTable === -1 ? lines.length : firstTable;
206
+ for (let i = 0; i < end; i++) {
207
+ if (new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(lines[i])) {
208
+ lines[i] = line;
209
+ return lines.join('\n').replace(/\n{3,}/g, '\n\n');
210
+ }
211
+ }
212
+ lines.splice(end, 0, line);
213
+ return lines.join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
214
+ }
215
+
216
+ function upsertTomlTable(text, table, block) {
217
+ let lines = String(text || '').trimEnd().split('\n');
218
+ if (lines.length === 1 && lines[0] === '') lines = [];
219
+ const header = `[${table}]`;
220
+ const start = lines.findIndex((x) => x.trim() === header);
221
+ const blockLines = String(block || '').trim().split('\n');
222
+ if (start === -1) return [...lines, ...(lines.length ? [''] : []), ...blockLines].join('\n').replace(/\n{3,}/g, '\n\n');
223
+ let end = lines.length;
224
+ for (let i = start + 1; i < lines.length; i++) {
225
+ if (/^\s*\[.+\]\s*$/.test(lines[i])) {
226
+ end = i;
227
+ break;
228
+ }
229
+ }
230
+ lines.splice(start, end - start, ...blockLines);
231
+ return lines.join('\n').replace(/\n{3,}/g, '\n\n');
232
+ }
233
+
234
+ function shellSingleQuote(value) {
235
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
236
+ }
237
+
238
+ function escapeRegExp(value) {
239
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
240
+ }
241
+
116
242
  export async function ensureSksCommandDuringInstall(opts = {}) {
117
243
  if (process.env.SKS_SKIP_POSTINSTALL_SHIM === '1' && !opts.force) return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_SHIM=1' };
118
244
  const pathEnv = opts.pathEnv ?? process.env.PATH ?? '';
@@ -222,6 +348,7 @@ async function ensureGlobalGetdesignSkillDuringInstall() {
222
348
  export async function ensureRelatedCliTools(args = []) {
223
349
  const skip = args.includes('--skip-cli-tools') || process.env.SKS_SKIP_CLI_TOOLS === '1';
224
350
  const codex = await ensureCodexCliTool({ skip });
351
+ const tmuxRepair = skip ? { status: 'skipped', reason: 'SKS_SKIP_CLI_TOOLS=1 or --skip-cli-tools' } : await ensureTmuxCliTool(args);
225
352
  const tmux = await tmuxReadiness().catch((err) => ({ ok: false, version: null, error: err.message }));
226
353
  return {
227
354
  codex,
@@ -231,6 +358,7 @@ export async function ensureRelatedCliTools(args = []) {
231
358
  version: tmux.version || null,
232
359
  min_version: tmux.min_version || '3.0',
233
360
  current_session: Boolean(tmux.current_session),
361
+ repair: tmuxRepair,
234
362
  install_hint: tmux.ok ? null : platformTmuxInstallHint(),
235
363
  error: tmux.error || null
236
364
  }
@@ -259,6 +387,86 @@ export async function ensureCodexCliTool({ skip = false } = {}) {
259
387
  };
260
388
  }
261
389
 
390
+ export async function ensureTmuxCliTool(args = [], opts = {}) {
391
+ const before = await tmuxReadiness().catch((err) => ({ ok: false, error: err.message }));
392
+ if (before.ok) return { target: 'tmux', status: 'present', bin: before.bin || null, version: before.version || null };
393
+ const command = process.platform === 'darwin' ? 'brew install tmux' : platformTmuxInstallHint();
394
+ if (process.platform !== 'darwin') return { target: 'tmux', status: 'manual_required', command, error: before.error || 'tmux not found' };
395
+ const brew = await which('brew').catch(() => null);
396
+ if (!brew) return { target: 'tmux', status: 'manual_required', command: 'Install Homebrew, then run: brew install tmux', error: before.error || 'tmux not found' };
397
+ const origin = await tmuxInstallOrigin(before.bin, brew);
398
+ if (before.bin && origin.manager === 'npm') {
399
+ const repairCommand = 'npm i -g tmux@latest';
400
+ if (args.includes('--dry-run') || opts.dryRun) return { target: 'tmux', status: 'dry_run', manager: 'npm', command: repairCommand, error: before.error || null };
401
+ const npmBin = await which('npm').catch(() => null);
402
+ if (!npmBin) return { target: 'tmux', status: 'manual_required', manager: 'npm', command: repairCommand, error: 'npm not found on PATH' };
403
+ const question = `npm-managed tmux ${before.version || 'unknown'} is not ready. Upgrade with ${repairCommand}?`;
404
+ if (!await confirmInstallYesDefault(question, args)) return { target: 'tmux', status: 'needs_approval', manager: 'npm', command: repairCommand, error: before.error || null };
405
+ const install = await runProcess(npmBin, ['i', '-g', 'tmux@latest'], { timeoutMs: 180000, maxOutputBytes: 128 * 1024 }).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
406
+ if (install.code !== 0) return { target: 'tmux', status: 'failed', manager: 'npm', command: repairCommand, error: `${install.stderr || install.stdout || repairCommand + ' failed'}`.trim() };
407
+ const after = await tmuxReadiness().catch((err) => ({ ok: false, error: err.message }));
408
+ if (!after.ok) return { target: 'tmux', status: 'installed_not_ready', manager: 'npm', command: repairCommand, error: after.error || 'tmux upgraded with npm but is still not ready' };
409
+ return { target: 'tmux', status: 'upgraded', manager: 'npm', command: repairCommand, bin: after.bin || null, version: after.version || null };
410
+ }
411
+ if (before.bin && origin.manager !== 'homebrew') {
412
+ return {
413
+ target: 'tmux',
414
+ status: 'conflicting_tmux',
415
+ bin: before.bin,
416
+ version: before.version || null,
417
+ manager: origin.manager,
418
+ command,
419
+ error: `${before.error || 'tmux is not ready'}; PATH resolves an unknown non-Homebrew tmux (${origin.reason}). Remove, upgrade with its owning package manager, or reorder PATH first, then run: ${command}`
420
+ };
421
+ }
422
+ const repairCommand = before.bin ? 'brew upgrade tmux' : command;
423
+ if (args.includes('--dry-run') || opts.dryRun) return { target: 'tmux', status: 'dry_run', command: repairCommand, error: before.error || null };
424
+ const question = before.bin
425
+ ? `Homebrew tmux ${before.version || 'unknown'} is too old. Upgrade to latest tmux with ${repairCommand}?`
426
+ : `tmux is missing. Install latest tmux with ${repairCommand}?`;
427
+ if (!await confirmInstallYesDefault(question, args)) return { target: 'tmux', status: 'needs_approval', command: repairCommand, error: before.error || null };
428
+ const brewArgs = before.bin ? ['upgrade', 'tmux'] : ['install', 'tmux'];
429
+ const install = await runProcess(brew, brewArgs, { timeoutMs: 180000, maxOutputBytes: 128 * 1024 }).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
430
+ if (install.code !== 0) return { target: 'tmux', status: 'failed', command: repairCommand, error: `${install.stderr || install.stdout || repairCommand + ' failed'}`.trim() };
431
+ const after = await tmuxReadiness().catch((err) => ({ ok: false, error: err.message }));
432
+ if (!after.ok) return { target: 'tmux', status: 'installed_not_ready', command: repairCommand, error: after.error || 'tmux installed but not ready' };
433
+ return { target: 'tmux', status: before.bin ? 'upgraded' : 'installed', command: repairCommand, bin: after.bin || null, version: after.version || null };
434
+ }
435
+
436
+ async function confirmInstallYesDefault(question, args = []) {
437
+ if (shouldAutoApproveInstall(args)) return true;
438
+ if (!canAskYesNo()) return false;
439
+ const answer = (await askPostinstallQuestion(`${question} [Y/n] `)).trim();
440
+ return answer === '' || /^(y|yes|예|네|응)$/i.test(answer);
441
+ }
442
+
443
+ async function tmuxInstallOrigin(bin, brewBin) {
444
+ if (!bin) return { manager: 'missing', reason: 'tmux not found on PATH' };
445
+ const resolved = await fsp.realpath(bin).catch(() => path.resolve(bin));
446
+ if (brewBin) {
447
+ const brewPrefix = await runProcess(brewBin, ['--prefix'], { timeoutMs: 5000, maxOutputBytes: 4096 }).catch(() => null);
448
+ const prefix = brewPrefix?.code === 0 ? brewPrefix.stdout.trim().split(/\r?\n/).pop() : '';
449
+ const brewTmux = await runProcess(brewBin, ['list', '--versions', 'tmux'], { timeoutMs: 5000, maxOutputBytes: 4096 }).catch(() => null);
450
+ if (prefix && resolved.startsWith(path.resolve(prefix) + path.sep) && brewTmux?.code === 0) {
451
+ return { manager: 'homebrew', reason: `${resolved} under ${prefix}` };
452
+ }
453
+ }
454
+ const npmBin = await which('npm').catch(() => null);
455
+ if (npmBin) {
456
+ const npmPrefix = await runProcess(npmBin, ['prefix', '-g'], { timeoutMs: 5000, maxOutputBytes: 4096 }).catch(() => null);
457
+ const prefix = npmPrefix?.code === 0 ? npmPrefix.stdout.trim().split(/\r?\n/).pop() : '';
458
+ const npmBinDir = prefix ? (process.platform === 'win32' ? prefix : path.join(prefix, 'bin')) : '';
459
+ const npmRoot = prefix ? path.join(prefix, 'lib', 'node_modules') : '';
460
+ if ((npmBinDir && path.resolve(bin).startsWith(path.resolve(npmBinDir) + path.sep)) || (npmRoot && resolved.startsWith(path.resolve(npmRoot) + path.sep))) {
461
+ return { manager: 'npm', reason: `${bin} resolves through npm global prefix ${prefix}` };
462
+ }
463
+ }
464
+ if (/\/node_modules\/(?:\.bin\/)?tmux(?:$|\/)/.test(resolved.split(path.sep).join('/'))) {
465
+ return { manager: 'npm', reason: `${resolved} is inside node_modules` };
466
+ }
467
+ return { manager: 'unknown', reason: `${bin} resolves to ${resolved}` };
468
+ }
469
+
262
470
  export async function maybePromptCodexUpdateForLaunch(args = [], opts = {}) {
263
471
  if (hasFlag(args, '--json') || hasFlag(args, '--skip-cli-tools') || hasFlag(args, '--skip-codex-update') || process.env.SKS_SKIP_CODEX_UPDATE === '1') return { status: 'skipped' };
264
472
  const latest = await npmPackageVersion('@openai/codex');
package/src/cli/main.mjs CHANGED
@@ -18,7 +18,7 @@ import { classifySql, classifyCommand, checkDbOperation, handleMadSksUserConfirm
18
18
  import { checkHarnessModification, harnessGuardStatus, isHarnessSourceProject } from '../core/harness-guard.mjs';
19
19
  import { formatHarnessConflictReport, llmHarnessCleanupPrompt, scanHarnessConflicts } from '../core/harness-conflicts.mjs';
20
20
  import { context7Docs, context7Resolve, context7Text, context7Tools } from '../core/context7-client.mjs';
21
- import { installVersionGitHook, runVersionPreCommit, versioningStatus } from '../core/version-manager.mjs';
21
+ import { bumpProjectVersion, installVersionGitHook, runVersionPreCommit, versioningStatus } from '../core/version-manager.mjs';
22
22
  import { rustInfo } from '../core/rust-accelerator.mjs';
23
23
  import { renderCartridge, validateCartridge, driftCartridge, snapshotCartridge } from '../core/gx-renderer.mjs';
24
24
  import { defaultEvaluationScenario, runEvaluationBenchmark } from '../core/evaluation.mjs';
@@ -58,10 +58,10 @@ import { renderTeamDashboardState, writeTeamDashboardState } from '../core/team-
58
58
  import { GOAL_WORKFLOW_ARTIFACT } from '../core/goal-workflow.mjs';
59
59
  import { CODEX_APP_DOCS_URL, codexAppIntegrationStatus, formatCodexAppStatus } from '../core/codex-app.mjs';
60
60
  import { OPENCLAW_SKILL_NAME, installOpenClawSkill } from '../core/openclaw.mjs';
61
- import { buildTmuxLaunchPlan, buildTmuxOpenArgs, createTmuxSession, isTmuxShellSession, runTmuxLaunchPlanSyntaxCheck, shouldAutoAttachTmux, tmuxReadiness, tmuxStatusKind, defaultTmuxSessionName, formatTmuxBanner, launchTmuxTeamView, launchTmuxUi, platformTmuxInstallHint, runTmuxStatus, sanitizeTmuxSessionName, teamLaneStyle } from '../core/tmux-ui.mjs';
61
+ import { buildTmuxLaunchPlan, buildTmuxOpenArgs, codexLaunchCommand, createTmuxSession, isTmuxShellSession, runTmuxLaunchPlanSyntaxCheck, shouldAutoAttachTmux, tmuxReadiness, tmuxStatusKind, defaultTmuxSessionName, formatTmuxBanner, launchTmuxTeamView, launchTmuxUi, platformTmuxInstallHint, runTmuxStatus, sanitizeTmuxSessionName, teamLaneStyle } from '../core/tmux-ui.mjs';
62
62
  import { autoReviewProfileName, autoReviewStatus, autoReviewSummary, enableAutoReview, disableAutoReview, enableMadHighProfile, madHighProfileName } from '../core/auto-review.mjs';
63
63
  import { context7Command } from './context7-command.mjs';
64
- import { askPostinstallQuestion, checkContext7, checkRequiredSkills, ensureCodexCliTool, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, globalCodexSkillsRoot, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, shouldAutoApproveInstall } from './install-helpers.mjs';
64
+ import { askPostinstallQuestion, checkContext7, checkRequiredSkills, codexLbStatus, configureCodexLb, ensureCodexCliTool, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, globalCodexSkillsRoot, maybePromptCodexLbSetupForLaunch, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, shouldAutoApproveInstall } from './install-helpers.mjs';
65
65
  import { buildTeamPlan, codeStructureCommand, dbCommand, defaultBeta, defaultVGraph, evalCommand, gcCommand, goalCommand, gxCommand, harnessCommand, hproofCommand, memoryCommand, migrateWikiContextPack, parseTeamCreateArgs, perfCommand, profileCommand, projectWikiClaims, proofFieldCommand, qaLoopCommand, quickstartCommand, researchCommand, skillDreamCommand, statsCommand, team, teamWorkflowMarkdown, validateArtifactsCommand, wikiCommand, wikiVoxelRowCount, writeWikiContextPack } from './maintenance-commands.mjs';
66
66
  import { openClawCommand } from './openclaw-command.mjs';
67
67
 
@@ -91,7 +91,7 @@ export async function main(args) {
91
91
  if (cmd === 'dollar-commands' || cmd === 'dollars' || cmd === '$') return dollarCommands(tail);
92
92
  if (String(cmd).toLowerCase() === 'dfix') return dfixHelp();
93
93
  const handlers = {
94
- postinstall: () => postinstall({ bootstrap }), wizard: () => wizard(tail), ui: () => wizard(tail), 'update-check': () => updateCheck(tail), help: () => help(tail), commands: () => commands(tail), usage: () => usage(tail), root: () => rootCommand(tail), quickstart: () => quickstartCommand(), 'codex-app': () => codexAppHelp(tail), openclaw: () => openClawCommand(tail), bootstrap: () => bootstrap(tail), deps: () => deps(sub, rest),
94
+ postinstall: () => postinstall({ bootstrap }), wizard: () => wizard(tail), ui: () => wizard(tail), 'update-check': () => updateCheck(tail), help: () => help(tail), commands: () => commands(tail), usage: () => usage(tail), root: () => rootCommand(tail), quickstart: () => quickstartCommand(), 'codex-app': () => codexAppHelp(tail), 'codex-lb': () => codexLbCommand(sub, rest), openclaw: () => openClawCommand(tail), bootstrap: () => bootstrap(tail), deps: () => deps(sub, rest),
95
95
  'qa-loop': () => qaLoopCommand(sub, rest), ppt: () => pptCommand(sub, rest), context7: () => context7Command(sub, rest), pipeline: () => pipeline(sub, rest), guard: () => guard(sub, rest), conflicts: () => conflicts(sub, rest), versioning: () => versioning(sub, rest), reasoning: () => reasoningCommand(tail), aliases: () => aliases(), setup: () => setup(tail), 'fix-path': () => fixPath(tail), doctor: () => doctor(tail), init: () => init(tail), selftest: () => selftest(tail),
96
96
  goal: () => goalCommand(sub, rest), research: () => researchCommand(sub, rest), hook: () => emitHook(sub), profile: () => profileCommand(sub, rest), hproof: () => hproofCommand(sub, rest), 'validate-artifacts': () => validateArtifactsCommand(tail), perf: () => perfCommand(sub, rest), 'proof-field': () => proofFieldCommand(sub, rest), 'skill-dream': () => skillDreamCommand(sub, rest), 'code-structure': () => codeStructureCommand(sub, rest), memory: () => memoryCommand(sub, rest), gx: () => gxCommand(sub, rest),
97
97
  team: () => team(tail), db: () => dbCommand(sub, rest), eval: () => evalCommand(sub, rest), harness: () => harnessCommand(sub, rest), wiki: () => wikiCommand(sub, rest), gc: () => gcCommand(tail), stats: () => statsCommand(tail)
@@ -118,7 +118,12 @@ async function defaultTmuxCommand(args = []) {
118
118
  process.exitCode = 1;
119
119
  return;
120
120
  }
121
- return launchTmuxUi(args, { conciseBlockers: true });
121
+ const lb = await maybePromptCodexLbSetupForLaunch(args);
122
+ if (lb.status === 'missing_api_key') {
123
+ process.exitCode = 1;
124
+ return;
125
+ }
126
+ return launchTmuxUi(args, codexLbImmediateLaunchOpts(args, lb, { conciseBlockers: true }));
122
127
  }
123
128
 
124
129
  function help(args = []) {
@@ -140,6 +145,7 @@ Usage:
140
145
  sks bootstrap [--install-scope global|project] [--local-only] [--json]
141
146
  sks deps check|install [tmux|codex|context7|all] [--yes] [--json]
142
147
  sks codex-app
148
+ sks codex-lb setup --host <domain> --api-key <key>
143
149
  sks openclaw install|path|print [--dir path] [--force] [--json]
144
150
  sks --mad [--high]
145
151
  sks auto-review status|enable|start [--high]
@@ -819,7 +825,7 @@ async function versioning(sub = 'status', args = []) {
819
825
  return;
820
826
  }
821
827
  if (action === 'bump') {
822
- const res = await runVersionPreCommit(root, { force: true });
828
+ const res = await bumpProjectVersion(root, { force: true });
823
829
  if (flag(args, '--json')) return console.log(JSON.stringify(res, null, 2));
824
830
  if (!res.ok) {
825
831
  console.error(`Version bump failed: ${res.reason || 'unknown'}`);
@@ -839,7 +845,7 @@ async function versioning(sub = 'status', args = []) {
839
845
  return;
840
846
  }
841
847
  if (res.skipped) return;
842
- console.log(res.changed ? `SKS versioning: ${res.previous_version} -> ${res.version}` : `SKS versioning: ${res.version} already unique`);
848
+ console.log(res.changed ? `SKS versioning synced: ${res.version}` : `SKS versioning: ${res.version} verified`);
843
849
  return;
844
850
  }
845
851
  console.error('Usage: sks versioning status|bump|pre-commit [--json]');
@@ -906,7 +912,12 @@ async function tmuxCommand(sub = 'start', args = []) {
906
912
  process.exitCode = 1;
907
913
  return;
908
914
  }
909
- const result = await launchTmuxUi(args);
915
+ const lb = await maybePromptCodexLbSetupForLaunch(args);
916
+ if (lb.status === 'missing_api_key') {
917
+ process.exitCode = 1;
918
+ return;
919
+ }
920
+ const result = await launchTmuxUi(args, codexLbImmediateLaunchOpts(args, lb));
910
921
  if (flag(args, '--json')) console.log(JSON.stringify(result, null, 2));
911
922
  return;
912
923
  }
@@ -914,6 +925,56 @@ async function tmuxCommand(sub = 'start', args = []) {
914
925
  process.exitCode = 1;
915
926
  }
916
927
 
928
+ async function codexLbCommand(action = 'status', args = []) {
929
+ const sub = action || 'status';
930
+ const json = flag(args, '--json');
931
+ if (sub === 'status' || sub === 'check') {
932
+ const status = await codexLbStatus();
933
+ if (json) return console.log(JSON.stringify(status, null, 2));
934
+ console.log('SKS codex-lb\n');
935
+ console.log(`Configured: ${status.ok ? 'yes' : 'no'}`);
936
+ console.log(`Selected: ${status.selected ? 'yes' : 'no'}`);
937
+ console.log(`Provider: ${status.provider_configured ? 'yes' : 'no'}`);
938
+ console.log(`Env file: ${status.env_file ? status.env_path : 'missing'}`);
939
+ if (status.base_url) console.log(`Base URL: ${status.base_url}`);
940
+ if (!status.ok) console.log('\nRun: sks codex-lb setup --host <domain> --api-key <key>');
941
+ return;
942
+ }
943
+ if (sub === 'setup') {
944
+ const host = readOption(args, '--host', readOption(args, '--domain', null));
945
+ const apiKey = readOption(args, '--api-key', readOption(args, '--key', null));
946
+ if (!host || !apiKey) {
947
+ if (json) return console.log(JSON.stringify({ ok: false, reason: 'missing_host_or_api_key' }, null, 2));
948
+ console.error('Usage: sks codex-lb setup --host <domain> --api-key <key>');
949
+ process.exitCode = 1;
950
+ return;
951
+ }
952
+ const result = await configureCodexLb({ host, apiKey });
953
+ if (json) return console.log(JSON.stringify(result, null, 2));
954
+ if (!result.ok) {
955
+ console.error(`codex-lb setup failed: ${result.status}`);
956
+ process.exitCode = 1;
957
+ return;
958
+ }
959
+ console.log(`codex-lb configured: ${result.base_url}`);
960
+ console.log(`Config: ${result.config_path}`);
961
+ console.log(`Key env: ${result.env_path}`);
962
+ return;
963
+ }
964
+ console.error('Usage: sks codex-lb status|setup --host <domain> --api-key <key> [--json]');
965
+ process.exitCode = 1;
966
+ }
967
+
968
+ function codexLbImmediateLaunchOpts(args = [], lb = {}, opts = {}) {
969
+ if (!lb?.ok || lb.status !== 'configured') return opts;
970
+ if (readOption(args, '--session', null) || readOption(args, '--workspace', null)) return opts;
971
+ const root = readOption(args, '--root', process.cwd());
972
+ const session = sanitizeTmuxSessionName(`sks-codex-lb-${Date.now().toString(36)}-${defaultTmuxSessionName(root)}`);
973
+ console.log(`codex-lb key loaded for this launch: ${lb.env_path}`);
974
+ console.log(`Using fresh tmux session: ${session}`);
975
+ return { ...opts, session };
976
+ }
977
+
917
978
  async function madHighCommand(args = []) {
918
979
  const cleanArgs = args.filter((arg) => !['--mad', '--MAD', '--mad-sks', '--high', '--no-auto-install-tmux'].includes(arg));
919
980
  if (flag(args, '--json')) {
@@ -1106,11 +1167,7 @@ async function installContext7Dependency(root) {
1106
1167
  }
1107
1168
 
1108
1169
  async function installTmuxDependency(args = []) {
1109
- const before = await tmuxReadiness().catch(() => ({ ok: false }));
1110
- if (before.ok) return { target: 'tmux', status: 'present', version: before.version || null, app: before.app || null, cli: before.cli || null };
1111
- const command = process.platform === 'darwin' ? 'brew install tmux' : platformTmuxInstallHint();
1112
- if (flag(args, '--dry-run')) return { target: 'tmux', status: 'dry_run', command };
1113
- return { target: 'tmux', status: 'manual_required', command, error: before.error || 'tmux not found' };
1170
+ return ensureTmuxCliTool(args, { dryRun: flag(args, '--dry-run') });
1114
1171
  }
1115
1172
 
1116
1173
  async function confirmInstall(question, args = []) {
@@ -1237,11 +1294,11 @@ function usage(args = []) {
1237
1294
  const topic = String(args[0] || 'overview').toLowerCase();
1238
1295
  const blocks = {
1239
1296
  overview: ['ㅅㅋㅅ Usage', '', 'Discover:', ' sks commands', ' sks quickstart', ' sks root', ' sks bootstrap', ' sks deps check', ' sks codex-app check', ' sks tmux check', ' sks dollar-commands', '', `Topics: ${USAGE_TOPICS}`],
1240
- install: ['Install', '', ' npm i -g sneakoscope', ' sks root', ' sks', '', 'Project bootstrap:', ' sks bootstrap', '', 'Fallback:', ' npx -y -p sneakoscope sks root', '', 'Project:', ' npm i -D sneakoscope', ' npx sks setup --install-scope project'],
1297
+ install: ['Install', '', '1. Global install:', ' npm i -g sneakoscope', '', '2. Bootstrap and check dependencies:', ' sks bootstrap', ' sks deps check', '', '3. Confirm Codex App commands:', ' sks codex-app check', ' sks dollar-commands', '', '4. Optional codex-lb key setup for CLI sks runs:', ' sks codex-lb setup --host <domain> --api-key <key>', ' sks', '', 'Fallback:', ' npx -y -p sneakoscope sks root', '', 'Project:', ' npm i -D sneakoscope', ' npx sks setup --install-scope project'],
1241
1298
  bootstrap: ['Bootstrap', '', ' sks bootstrap', ' sks setup --bootstrap', '', 'Creates project SKS files, Codex App skills/hooks/config, state/guard files, then checks Codex App, Context7, and tmux.'],
1242
1299
  root: ['Root', '', ' sks root [--json]', '', 'Inside a project, SKS uses that project root. Outside any project marker, runtime commands use the per-user global SKS root instead of writing .sneakoscope into the current random folder.'],
1243
- deps: ['Dependencies', '', ' sks deps check [--json]', ' sks deps install [tmux|codex|context7|all] [--yes]', '', 'tmux on macOS uses Homebrew only after approval.'],
1244
- tmux: ['tmux', '', ' sks', ' sks tmux open', ' sks tmux check', ' sks tmux status --once', ' sks deps install tmux', '', 'Running bare `sks` opens or reuses the default tmux Codex CLI session. Before launch, SKS checks npm @openai/codex@latest and prompts Y/n when the installed Codex CLI is missing or outdated. Use `sks tmux open` when you need explicit session/workspace flags, and `sks help` for CLI help.'],
1300
+ deps: ['Dependencies', '', ' sks deps check [--json]', ' sks deps install [tmux|codex|context7|all] [--yes]', '', 'tmux on macOS uses Homebrew after Y/n approval for missing installs or Homebrew-managed upgrades. If PATH resolves an npm-managed tmux, SKS prompts for npm i -g tmux@latest instead. Unknown non-Homebrew tmux paths are reported as conflicts.'],
1301
+ tmux: ['tmux', '', ' sks', ' sks tmux open', ' sks tmux check', ' sks tmux status --once', ' sks deps install tmux', '', 'Running bare `sks` opens or reuses the default tmux Codex CLI session in fast-high mode: --model gpt-5.5 -c model_reasoning_effort="high". Override with SKS_CODEX_MODEL or SKS_CODEX_REASONING. Before launch, SKS checks npm @openai/codex@latest and prompts Y/n when the installed Codex CLI is missing or outdated. Use `sks tmux open` when you need explicit session/workspace flags, and `sks help` for CLI help.'],
1245
1302
  openclaw: ['OpenClaw', '', ' sks openclaw install', ' sks openclaw path', ' sks openclaw print SKILL.md', '', 'Installs an OpenClaw skill package under ~/.openclaw/skills/sneakoscope-codex so OpenClaw agents can attach skills: [sneakoscope-codex] with the shell tool and call local SKS commands from a project root.'],
1246
1303
  team: ['Team', '', ' sks team "task" executor:5 reviewer:2 user:1', ' sks team watch latest', ' sks team lane latest --agent analysis_scout_1 --follow', ' sks team message latest --from analysis_scout_1 --to executor_1 --message "handoff note"', ' sks team cleanup-tmux latest', '', '$Team runs questions -> contract -> scouts -> TriWiki attention -> debate -> runtime graph/inbox -> fresh executors -> review -> cleanup -> reflection -> Honest.'],
1247
1304
  'qa-loop': ['QA-LOOP', '', ' sks qa-loop prepare "QA this app"', ' sks qa-loop answer <MISSION_ID> answers.json', ' sks qa-loop run <MISSION_ID> --max-cycles 8', '', 'Report: YYYY-MM-DD-v<version>-qa-report.md'],
@@ -1937,6 +1994,22 @@ async function selftest() {
1937
1994
  if (!tmuxSyntax.ok || !tmuxSyntax.command.includes('tmux attach-session -t sks-mad-selftest')) throw new Error('selftest failed: MAD tmux attach plan is not stable by session name');
1938
1995
  const tmuxOpenArgs = buildTmuxOpenArgs(workspacePlan);
1939
1996
  if (tmuxOpenArgs.join(' ') !== 'attach-session -t sks-mad-selftest') throw new Error('selftest failed: MAD tmux attach args are not stable by session name');
1997
+ const defaultFastHighPlan = await buildTmuxLaunchPlan({ root: tmp, tmux: { ok: true, bin: 'tmux', version: '3.4' }, codex: { bin: 'codex', version: 'codex-cli 99.0.0' }, app: { ok: true } });
1998
+ if (defaultFastHighPlan.codexArgs.join(' ') !== '--model gpt-5.5 -c model_reasoning_effort="high"') throw new Error('selftest failed: default sks tmux launch is not fast-high');
1999
+ const codexLbHome = path.join(tmp, 'codex-lb-home');
2000
+ const codexLbSetup = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'setup', '--host', 'lb.example.test', '--api-key', 'sk-test', '--json'], {
2001
+ cwd: tmp,
2002
+ env: { HOME: codexLbHome, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-global') },
2003
+ timeoutMs: 15000,
2004
+ maxOutputBytes: 64 * 1024
2005
+ });
2006
+ if (codexLbSetup.code !== 0) throw new Error(`selftest failed: codex-lb setup exited ${codexLbSetup.code}: ${codexLbSetup.stderr}`);
2007
+ const codexLbSetupJson = JSON.parse(codexLbSetup.stdout);
2008
+ const codexLbConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
2009
+ const codexLbEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
2010
+ if (!codexLbSetupJson.ok || codexLbSetupJson.base_url !== 'https://lb.example.test/backend-api/codex' || !codexLbConfig.includes('model_provider = "codex-lb"') || !codexLbConfig.includes('[model_providers.codex-lb]') || !codexLbEnv.includes("CODEX_LB_API_KEY='sk-test'")) throw new Error('selftest failed: codex-lb setup did not write provider config and env key');
2011
+ const codexLbLaunch = codexLaunchCommand(tmp, 'codex', []);
2012
+ if (!codexLbLaunch.includes('sks-codex-lb.env')) throw new Error('selftest failed: tmux launch command does not source codex-lb env file');
1940
2013
  if (!shouldAutoAttachTmux(['--mad'], {}, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest failed: MAD tmux launch does not auto-attach in an interactive terminal');
1941
2014
  if (shouldAutoAttachTmux(['--mad', '--json'], {}, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest failed: MAD tmux json mode should not auto-attach');
1942
2015
  if (shouldAutoAttachTmux(['--mad', '--no-attach'], {}, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest failed: MAD tmux --no-attach should remain print-only');
@@ -2037,9 +2110,12 @@ async function selftest() {
2037
2110
  const versionHookText = await safeReadText(versionStatus.hook_path);
2038
2111
  if (!versionHookText.includes('versioning pre-commit')) throw new Error('selftest failed: versioning hook command missing');
2039
2112
  if (versionHookText.indexOf('versioning pre-commit') > versionHookText.indexOf('exit 0')) throw new Error('selftest failed: versioning hook was appended after an early exit');
2113
+ await writeTextAtomic(path.join(versionTmp, 'CHANGELOG.md'), '# Changelog\n\n## [Unreleased]\n\n## [0.1.0] - 2026-05-08\n\n### Fixed\n\n- Initial version selftest fixture.\n');
2040
2114
  await writeTextAtomic(path.join(versionTmp, 'README.md'), 'version selftest\n');
2041
- await runProcess('git', ['add', 'README.md'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2042
- const firstVersionBump = await runVersionPreCommit(versionTmp);
2115
+ await runProcess('git', ['add', 'README.md', 'CHANGELOG.md'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2116
+ const preCommitVerify = await runVersionPreCommit(versionTmp);
2117
+ if (!preCommitVerify.ok || preCommitVerify.version !== '0.1.0' || preCommitVerify.changed) throw new Error('selftest failed: pre-commit should verify current version without bumping');
2118
+ const firstVersionBump = await bumpProjectVersion(versionTmp);
2043
2119
  if (!firstVersionBump.ok || firstVersionBump.version !== '0.1.1' || !firstVersionBump.changed) throw new Error('selftest failed: first version bump did not advance patch version');
2044
2120
  const bumpedPackage = await readJson(path.join(versionTmp, 'package.json'));
2045
2121
  const bumpedLock = await readJson(path.join(versionTmp, 'package-lock.json'));
@@ -2052,7 +2128,7 @@ async function selftest() {
2052
2128
  await writeJsonAtomic(versionStatus.state_path, { schema_version: 1, last_version: '0.1.5', updated_at: nowIso(), pid: process.pid, changed: true });
2053
2129
  await writeTextAtomic(path.join(versionTmp, 'CHANGELOG.md'), 'collision selftest\n');
2054
2130
  await runProcess('git', ['add', 'CHANGELOG.md'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2055
- const collisionBump = await runVersionPreCommit(versionTmp);
2131
+ const collisionBump = await bumpProjectVersion(versionTmp);
2056
2132
  if (!collisionBump.ok || collisionBump.version !== '0.1.6') throw new Error('selftest failed: version collision state did not bump above last seen version');
2057
2133
  const localOnlyTmp = tmpdir();
2058
2134
  await ensureDir(path.join(localOnlyTmp, '.git'));
@@ -2547,9 +2623,16 @@ async function selftest() {
2547
2623
  const codexConfigText = await safeReadText(path.join(tmp, '.codex', 'config.toml'));
2548
2624
  if (!codexConfigText.includes('multi_agent = true')) throw new Error('selftest failed: multi_agent not enabled');
2549
2625
  if (!hasContext7ConfigText(codexConfigText)) throw new Error('selftest failed: Context7 MCP not configured');
2550
- if (!codexConfigText.includes('[profiles.sks-task-low]') || !codexConfigText.includes('[profiles.sks-task-medium]') || !codexConfigText.includes('[profiles.sks-logic-high]') || !codexConfigText.includes('[profiles.sks-research-xhigh]') || !codexConfigText.includes('[profiles.sks-mad-high]')) throw new Error('selftest failed: GPT-5.5 reasoning profiles not configured');
2626
+ if (!codexConfigText.includes('[profiles.sks-task-low]') || !codexConfigText.includes('[profiles.sks-task-medium]') || !codexConfigText.includes('[profiles.sks-logic-high]') || !codexConfigText.includes('[profiles.sks-fast-high]') || !codexConfigText.includes('[profiles.sks-research-xhigh]') || !codexConfigText.includes('[profiles.sks-mad-high]')) throw new Error('selftest failed: GPT-5.5 reasoning profiles not configured');
2551
2627
  if (!codexConfigText.includes('[agents.analysis_scout]')) throw new Error('selftest failed: analysis_scout agent not configured');
2552
2628
  if (!codexConfigText.includes('[agents.team_consensus]')) throw new Error('selftest failed: team_consensus agent not configured');
2629
+ const preservedConfigTmp = tmpdir();
2630
+ await ensureDir(path.join(preservedConfigTmp, '.codex'));
2631
+ await writeTextAtomic(path.join(preservedConfigTmp, '.codex', 'config.toml'), '[features]\nfast_mode_ui = true\n\n[user.fast_mode]\nvisible = true\n');
2632
+ await initProject(preservedConfigTmp, {});
2633
+ const preservedConfig = await safeReadText(path.join(preservedConfigTmp, '.codex', 'config.toml'));
2634
+ if (!preservedConfig.includes('fast_mode_ui = true') || !preservedConfig.includes('[user.fast_mode]') || !preservedConfig.includes('visible = true')) throw new Error('selftest failed: Codex config merge dropped user Fast mode settings');
2635
+ if (!preservedConfig.includes('codex_hooks = true') || !preservedConfig.includes('[profiles.sks-fast-high]')) throw new Error('selftest failed: Codex config merge did not add SKS managed settings');
2553
2636
  const autoReviewHome = path.join(tmp, 'auto-review-home');
2554
2637
  const autoReviewEnv = { HOME: autoReviewHome };
2555
2638
  const autoReviewEnabled = await enableAutoReview({ env: autoReviewEnv, high: true });
package/src/core/fsx.mjs CHANGED
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import crypto from 'node:crypto';
6
6
  import { spawn } from 'node:child_process';
7
7
 
8
- export const PACKAGE_VERSION = '0.7.38';
8
+ export const PACKAGE_VERSION = '0.7.41';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
package/src/core/init.mjs CHANGED
@@ -415,15 +415,118 @@ export async function initProject(root, opts = {}) {
415
415
  };
416
416
  }
417
417
 
418
- function installPolicy(scope, commandPrefix) {
419
- return {
420
- scope,
421
- default_scope: 'global',
422
- hook_command_prefix: commandPrefix,
423
- global_install: 'npm i -g sneakoscope',
424
- project_install: 'npm i -D sneakoscope && npx sks setup --install-scope project'
425
- };
418
+ function installPolicy(scope, commandPrefix) {
419
+ return {
420
+ scope,
421
+ default_scope: 'global',
422
+ hook_command_prefix: commandPrefix,
423
+ global_install: 'npm i -g sneakoscope',
424
+ project_install: 'npm i -D sneakoscope && npx sks setup --install-scope project'
425
+ };
426
+ }
427
+
428
+ function mergeManagedCodexConfigToml(existingContent = '') {
429
+ let next = String(existingContent || '').trimEnd();
430
+ next = upsertTomlTableKey(next, 'features', 'codex_hooks = true');
431
+ next = upsertTomlTableKey(next, 'features', 'multi_agent = true');
432
+ next = upsertTomlTableKey(next, 'agents', 'max_threads = 6');
433
+ next = upsertTomlTableKey(next, 'agents', 'max_depth = 1');
434
+ for (const block of managedCodexConfigBlocks()) {
435
+ next = upsertTomlTable(next, block.table, block.text);
436
+ }
437
+ return `${next.trim()}\n`;
438
+ }
439
+
440
+ function managedCodexConfigBlocks() {
441
+ return [
442
+ { table: 'mcp_servers.context7', text: context7ConfigToml().trim() },
443
+ { table: 'agents.analysis_scout', text: agentConfigBlock('analysis_scout', 'Read-only SKS scout.', './agents/analysis-scout.toml', ['Scout', 'Mapper']) },
444
+ { table: 'agents.team_consensus', text: agentConfigBlock('team_consensus', 'SKS planning/debate agent.', './agents/team-consensus.toml', ['Consensus', 'Atlas']) },
445
+ { table: 'agents.implementation_worker', text: agentConfigBlock('implementation_worker', 'SKS bounded implementation worker.', './agents/implementation-worker.toml', ['Builder', 'Mason']) },
446
+ { table: 'agents.db_safety_reviewer', text: agentConfigBlock('db_safety_reviewer', 'Read-only DB safety reviewer.', './agents/db-safety-reviewer.toml', ['Sentinel', 'Ledger']) },
447
+ { table: 'agents.qa_reviewer', text: agentConfigBlock('qa_reviewer', 'Read-only QA reviewer.', './agents/qa-reviewer.toml', ['Verifier', 'Scout']) },
448
+ { table: 'profiles.sks-task-low', text: profileConfigBlock('sks-task-low', 'low') },
449
+ { table: 'profiles.sks-task-medium', text: profileConfigBlock('sks-task-medium', 'medium') },
450
+ { table: 'profiles.sks-logic-high', text: profileConfigBlock('sks-logic-high', 'high') },
451
+ { table: 'profiles.sks-fast-high', text: profileConfigBlock('sks-fast-high', 'high') },
452
+ { table: 'profiles.sks-research-xhigh', text: profileConfigBlock('sks-research-xhigh', 'xhigh') },
453
+ { table: 'profiles.sks-research', text: profileConfigBlock('sks-research', 'xhigh', { approval: 'never' }) },
454
+ { table: 'profiles.sks-team', text: profileConfigBlock('sks-team', 'high') },
455
+ { table: 'profiles.sks-mad-high', text: profileConfigBlock('sks-mad-high', 'high', { sandbox: 'danger-full-access', approvalsReviewer: 'auto_review' }) },
456
+ {
457
+ table: 'auto_review',
458
+ text: '[auto_review]\npolicy = "Deny destructive database operations, credential exfiltration, persistent security weakening, broad file deletion, writes outside the workspace, and unrequested fallback implementation code unless explicitly authorized by the user or sealed decision contract."'
459
+ },
460
+ { table: 'profiles.sks-default', text: profileConfigBlock('sks-default', 'high') }
461
+ ];
462
+ }
463
+
464
+ function agentConfigBlock(table, description, configFile, nicknames = []) {
465
+ return [
466
+ `[agents.${table}]`,
467
+ `description = "${description}"`,
468
+ `config_file = "${configFile}"`,
469
+ `nickname_candidates = [${nicknames.map((name) => `"${name}"`).join(', ')}]`
470
+ ].join('\n');
471
+ }
472
+
473
+ function profileConfigBlock(profile, effort, opts = {}) {
474
+ return [
475
+ `[profiles.${profile}]`,
476
+ 'model = "gpt-5.5"',
477
+ `approval_policy = "${opts.approval || 'on-request'}"`,
478
+ ...(opts.approvalsReviewer ? [`approvals_reviewer = "${opts.approvalsReviewer}"`] : []),
479
+ `sandbox_mode = "${opts.sandbox || 'workspace-write'}"`,
480
+ `model_reasoning_effort = "${effort}"`
481
+ ].join('\n');
482
+ }
483
+
484
+ function upsertTomlTableKey(text, table, line) {
485
+ const key = String(line).split('=')[0].trim();
486
+ let lines = String(text || '').split('\n');
487
+ if (lines.length === 1 && lines[0] === '') lines = [];
488
+ const header = `[${table}]`;
489
+ let start = lines.findIndex((x) => x.trim() === header);
490
+ if (start === -1) {
491
+ const prefix = lines.length && lines[lines.length - 1].trim() ? ['', header, line] : [header, line];
492
+ return [...lines, ...prefix].join('\n').replace(/\n{3,}/g, '\n\n');
493
+ }
494
+ let end = lines.length;
495
+ for (let i = start + 1; i < lines.length; i++) {
496
+ if (/^\s*\[.+\]\s*$/.test(lines[i])) {
497
+ end = i;
498
+ break;
499
+ }
500
+ }
501
+ for (let i = start + 1; i < end; i++) {
502
+ if (new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(lines[i])) {
503
+ lines[i] = line;
504
+ return lines.join('\n').replace(/\n{3,}/g, '\n\n');
505
+ }
426
506
  }
507
+ lines.splice(start + 1, 0, line);
508
+ return lines.join('\n').replace(/\n{3,}/g, '\n\n');
509
+ }
510
+
511
+ function upsertTomlTable(text, table, block) {
512
+ let lines = String(text || '').trimEnd().split('\n');
513
+ if (lines.length === 1 && lines[0] === '') lines = [];
514
+ const header = `[${table}]`;
515
+ const start = lines.findIndex((x) => x.trim() === header);
516
+ const blockLines = String(block || '').trim().split('\n');
517
+ if (start === -1) {
518
+ return [...lines, ...(lines.length ? [''] : []), ...blockLines].join('\n').replace(/\n{3,}/g, '\n\n');
519
+ }
520
+ let end = lines.length;
521
+ for (let i = start + 1; i < lines.length; i++) {
522
+ if (/^\s*\[.+\]\s*$/.test(lines[i])) {
523
+ end = i;
524
+ break;
525
+ }
526
+ }
527
+ lines.splice(start, end - start, ...blockLines);
528
+ return lines.join('\n').replace(/\n{3,}/g, '\n\n');
529
+ }
427
530
 
428
531
  const currentState = path.join(sine, 'state', 'current.json');
429
532
  if (!(await exists(currentState)) || opts.force) {
@@ -441,26 +544,9 @@ export async function initProject(root, opts = {}) {
441
544
  created.push('AGENTS.md managed block');
442
545
  }
443
546
 
444
- await writeTextAtomic(path.join(root, '.codex', 'config.toml'), `[features]\ncodex_hooks = true\nmulti_agent = true\n\n[agents]\nmax_threads = 6\nmax_depth = 1\n\n${context7ConfigToml()}\n[agents.analysis_scout]\ndescription = "Read-only SKS scout."\nconfig_file = "./agents/analysis-scout.toml"\nnickname_candidates = ["Scout", "Mapper"]\n\n[agents.team_consensus]\ndescription = "SKS planning/debate agent."\nconfig_file = "./agents/team-consensus.toml"\nnickname_candidates = ["Consensus", "Atlas"]\n\n[agents.implementation_worker]\ndescription = "SKS bounded implementation worker."\nconfig_file = "./agents/implementation-worker.toml"\nnickname_candidates = ["Builder", "Mason"]\n\n[agents.db_safety_reviewer]\ndescription = "Read-only DB safety reviewer."\nconfig_file = "./agents/db-safety-reviewer.toml"\nnickname_candidates = ["Sentinel", "Ledger"]\n\n[agents.qa_reviewer]\ndescription = "Read-only QA reviewer."\nconfig_file = "./agents/qa-reviewer.toml"\nnickname_candidates = ["Verifier", "Scout"]\n\n[profiles.sks-task-medium]\nmodel = "gpt-5.5"\napproval_policy = "on-request"\nsandbox_mode = "workspace-write"\nmodel_reasoning_effort = "medium"\n\n[profiles.sks-logic-high]\nmodel = "gpt-5.5"\napproval_policy = "on-request"\nsandbox_mode = "workspace-write"\nmodel_reasoning_effort = "high"\n\n[profiles.sks-research-xhigh]\nmodel = "gpt-5.5"\napproval_policy = "on-request"\nsandbox_mode = "workspace-write"\nmodel_reasoning_effort = "xhigh"\n\n[profiles.sks-research]\nmodel = "gpt-5.5"\napproval_policy = "never"\nsandbox_mode = "workspace-write"\nmodel_reasoning_effort = "xhigh"\n\n[profiles.sks-team]\nmodel = "gpt-5.5"\napproval_policy = "on-request"\nsandbox_mode = "workspace-write"\nmodel_reasoning_effort = "high"\n\n[profiles.sks-mad-high]
445
- model = "gpt-5.5"
446
- approval_policy = "on-request"
447
- approvals_reviewer = "auto_review"
448
- sandbox_mode = "danger-full-access"
449
- model_reasoning_effort = "high"
450
-
451
- [auto_review]
452
- policy = "Deny destructive database operations, credential exfiltration, persistent security weakening, broad file deletion, writes outside the workspace, and unrequested fallback implementation code unless explicitly authorized by the user or sealed decision contract."
453
-
454
- [profiles.sks-default]\nmodel = "gpt-5.5"\napproval_policy = "on-request"\nsandbox_mode = "workspace-write"\nmodel_reasoning_effort = "medium"\n`);
455
547
  const generatedCodexConfigPath = path.join(root, '.codex', 'config.toml');
456
- let generatedCodexConfig = await readText(generatedCodexConfigPath, '');
457
- if (!generatedCodexConfig.includes('[profiles.sks-task-low]')) {
458
- generatedCodexConfig = generatedCodexConfig.replace(
459
- '[profiles.sks-task-medium]',
460
- '[profiles.sks-task-low]\nmodel = "gpt-5.5"\napproval_policy = "on-request"\nsandbox_mode = "workspace-write"\nmodel_reasoning_effort = "low"\n\n[profiles.sks-task-medium]'
461
- );
462
- await writeTextAtomic(generatedCodexConfigPath, generatedCodexConfig);
463
- }
548
+ const existingCodexConfig = await readText(generatedCodexConfigPath, '');
549
+ await writeTextAtomic(generatedCodexConfigPath, mergeManagedCodexConfigToml(existingCodexConfig));
464
550
  created.push('.codex/config.toml');
465
551
 
466
552
  await writeTextAtomic(path.join(root, '.codex', 'SNEAKOSCOPE.md'), codexAppQuickReference(installScope, hookCommandPrefix));
@@ -456,6 +456,7 @@ export const COMMAND_CATALOG = [
456
456
  { name: 'root', usage: 'sks root [--json]', description: 'Show whether SKS is using a project root or the per-user global SKS runtime root.' },
457
457
  { name: 'deps', usage: 'sks deps check|install [tmux|codex|context7|all] [--yes]', description: 'Check or guided-install Node/npm PATH, Codex CLI/App, Context7, Browser Use, Computer Use, tmux, and Homebrew on macOS.' },
458
458
  { name: 'codex-app', usage: 'sks codex-app [check|open]', description: 'Check Codex App install and first-party MCP/plugin readiness, then show app setup files and examples.' },
459
+ { name: 'codex-lb', usage: 'sks codex-lb status|setup --host <domain> --api-key <key>', description: 'Configure codex-lb as the Codex CLI provider by writing ~/.codex/config.toml and the CODEX_LB_API_KEY env file.' },
459
460
  { name: 'openclaw', usage: 'sks openclaw install|path|print [--dir path] [--force] [--json]', description: 'Generate an OpenClaw skill package so OpenClaw agents can discover and use local SKS workflows.' },
460
461
  { name: 'tmux', usage: 'sks | sks tmux open|check|status [--workspace name]', description: 'Open the default SKS tmux runtime with bare sks, or use tmux subcommands for explicit launch/check/status.' },
461
462
  { name: 'mad', usage: 'sks --mad [--high]', description: 'Open a one-shot tmux Codex CLI workspace with the SKS MAD full-access auto-review profile.' },
@@ -14,6 +14,19 @@ export const SKS_TMUX_LOGO = [
14
14
  'Sneakoscope Codex tmux'
15
15
  ].join('\n');
16
16
 
17
+ export const DEFAULT_SKS_CODEX_MODEL = 'gpt-5.5';
18
+ export const DEFAULT_SKS_CODEX_REASONING = 'high';
19
+
20
+ export function defaultCodexLaunchArgs(env = process.env) {
21
+ if (/^(0|false|off|none)$/i.test(String(env.SKS_CODEX_FAST_HIGH || '').trim())) return [];
22
+ const model = String(env.SKS_CODEX_MODEL || DEFAULT_SKS_CODEX_MODEL).trim();
23
+ const effort = String(env.SKS_CODEX_REASONING || DEFAULT_SKS_CODEX_REASONING).trim();
24
+ const args = [];
25
+ if (model) args.push('--model', model);
26
+ if (effort) args.push('-c', `model_reasoning_effort="${effort}"`);
27
+ return args;
28
+ }
29
+
17
30
  export function sanitizeTmuxSessionName(input) {
18
31
  const base = String(input || 'sks').trim().replace(/[^A-Za-z0-9_.:-]+/g, '-').replace(/^-+|-+$/g, '');
19
32
  return (base || 'sks').slice(0, 80);
@@ -95,6 +108,7 @@ export function codexLaunchCommand(root, codexBin, codexArgs = []) {
95
108
  `printf '\\nProject: %s\\n' ${shellEscape(root)}`,
96
109
  'printf \'Runtime: tmux session for Codex CLI\\n\'',
97
110
  'printf \'Prompt: use canonical $ commands, for example $Team or $QA-LOOP\\n\\n\'',
111
+ '[ -f "$HOME/.codex/sks-codex-lb.env" ] && . "$HOME/.codex/sks-codex-lb.env"',
98
112
  'sleep 1',
99
113
  `exec ${[shellEscape(codexBin), ...extraArgs.map(shellEscape), '--cd', shellEscape(root)].join(' ')}`
100
114
  ].join('; ');
@@ -177,7 +191,7 @@ export async function buildTmuxLaunchPlan(opts = {}) {
177
191
  const codex = opts.codex || await getCodexInfo().catch(() => ({}));
178
192
  const tmux = opts.tmux || await tmuxReadiness(opts);
179
193
  const app = opts.app || await codexAppIntegrationStatus({ codex });
180
- const codexArgs = Array.isArray(opts.codexArgs) ? opts.codexArgs : [];
194
+ const codexArgs = Array.isArray(opts.codexArgs) ? opts.codexArgs : defaultCodexLaunchArgs(opts.env || process.env);
181
195
  return {
182
196
  root,
183
197
  session,
@@ -48,7 +48,11 @@ async function runtimeDriftStatus(root, packageVersion) {
48
48
  if (!packageVersion || process.env.SKS_RUNTIME_DRIFT_CHECK === '0') {
49
49
  return { ok: true, checked: false, reason: packageVersion ? 'disabled' : 'package_json_version_missing' };
50
50
  }
51
- const result = await runProcess('sks', ['--version'], {
51
+ const localBin = path.join(root, 'bin', 'sks.mjs');
52
+ const useLocalBin = await exists(localBin);
53
+ const command = useLocalBin ? process.execPath : 'sks';
54
+ const args = useLocalBin ? [localBin, '--version'] : ['--version'];
55
+ const result = await runProcess(command, args, {
52
56
  cwd: root,
53
57
  timeoutMs: 5000,
54
58
  maxOutputBytes: 16 * 1024,
@@ -68,6 +72,7 @@ async function runtimeDriftStatus(root, packageVersion) {
68
72
  return {
69
73
  ok: comparison >= 0,
70
74
  checked: true,
75
+ command: [command, ...args].join(' '),
71
76
  runtime_version: runtimeVersion,
72
77
  package_version: packageVersion,
73
78
  relation: comparison === 0 ? 'same' : (comparison > 0 ? 'runtime_newer' : 'runtime_older')
@@ -83,7 +88,7 @@ export async function runVersionPreCommit(root, opts = {}) {
83
88
  if (!pkg?.version) return { ok: true, skipped: true, reason: 'package_json_version_missing' };
84
89
  const git = await gitPaths(root);
85
90
  if (!git.ok) return { ok: true, skipped: true, reason: git.reason || 'not_git' };
86
- return withVersionLock(git.common_dir, async () => bumpProjectVersion(root, { ...opts, policy, git }));
91
+ return withVersionLock(git.common_dir, async () => verifyProjectVersion(root, { ...opts, policy, git }));
87
92
  }
88
93
 
89
94
  export async function bumpProjectVersion(root, opts = {}) {
@@ -145,6 +150,43 @@ export async function bumpProjectVersion(root, opts = {}) {
145
150
  };
146
151
  }
147
152
 
153
+ export async function verifyProjectVersion(root, opts = {}) {
154
+ const git = opts.git || await gitPaths(root);
155
+ const pkgPath = path.join(root, 'package.json');
156
+ const pkg = await readJson(pkgPath);
157
+ const current = parseSemver(pkg.version);
158
+ if (!current) return { ok: false, reason: `Unsupported package.json version: ${pkg.version}` };
159
+ const version = formatSemver(current);
160
+ const sourceVersion = await syncSourcePackageVersion(root, version);
161
+ const synced = await syncPackageLockVersions(root, version);
162
+ if (!await changelogHasVersionSection(root, version)) {
163
+ return { ok: false, reason: 'changelog_section_missing', version, expected: `## [${version}]` };
164
+ }
165
+ const staged = await stageVersionFiles(root, [...synced.files, ...sourceVersion.files]);
166
+ if (!staged.ok) return { ok: false, reason: 'git_add_version_files_failed', stderr: staged.stderr };
167
+ const statePath = git.ok ? path.join(git.common_dir, VERSION_STATE_FILE) : null;
168
+ if (statePath) {
169
+ await writeJsonAtomic(statePath, {
170
+ schema_version: 1,
171
+ last_version: version,
172
+ updated_at: nowIso(),
173
+ pid: process.pid,
174
+ mode: 'verify',
175
+ changed: Boolean(synced.files.length || sourceVersion.files.length)
176
+ });
177
+ }
178
+ return {
179
+ ok: true,
180
+ changed: Boolean(synced.files.length || sourceVersion.files.length),
181
+ version,
182
+ previous_version: version,
183
+ synced_files: [...synced.relative_files, ...sourceVersion.relative_files],
184
+ staged_files: staged.relative_files,
185
+ lock_scope: git.common_dir,
186
+ mode: 'verify'
187
+ };
188
+ }
189
+
148
190
  async function versionPolicy(root) {
149
191
  const policy = await readJson(path.join(root, '.sneakoscope', 'policy.json'), {});
150
192
  return {
@@ -252,6 +294,13 @@ async function syncChangelogVersionSection(root, version) {
252
294
  return { files: [file], relative_files: [path.relative(root, file)] };
253
295
  }
254
296
 
297
+ async function changelogHasVersionSection(root, version) {
298
+ const file = path.join(root, 'CHANGELOG.md');
299
+ const text = await readFileMaybe(file);
300
+ const sectionRe = new RegExp(`^##\\s+\\[${escapeRegExp(version)}\\]\\s+-\\s+\\d{4}-\\d{2}-\\d{2}\\s*$`, 'm');
301
+ return sectionRe.test(text);
302
+ }
303
+
255
304
  async function stageVersionFiles(root, files) {
256
305
  const existing = [];
257
306
  for (const file of files) if (await exists(file)) existing.push(path.relative(root, file));