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 +54 -1
- package/package.json +1 -1
- package/src/cli/install-helpers.mjs +209 -1
- package/src/cli/main.mjs +103 -20
- package/src/core/fsx.mjs +1 -1
- package/src/core/init.mjs +113 -27
- package/src/core/routes.mjs +1 -0
- package/src/core/tmux-ui.mjs +15 -1
- package/src/core/version-manager.mjs +51 -2
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
457
|
-
|
|
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));
|
package/src/core/routes.mjs
CHANGED
|
@@ -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.' },
|
package/src/core/tmux-ui.mjs
CHANGED
|
@@ -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
|
|
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 () =>
|
|
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));
|