sneakoscope 0.7.54 → 0.7.56
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/package.json +1 -1
- package/src/cli/codex-app-command.mjs +2 -1
- package/src/cli/install-helpers.mjs +220 -7
- package/src/cli/main.mjs +25 -39
- package/src/core/codex-adapter.mjs +12 -6
- package/src/core/codex-model-guard.mjs +50 -0
- package/src/core/fsx.mjs +1 -1
- package/src/core/hooks-runtime.mjs +43 -1
- package/src/core/team-live.mjs +33 -4
- package/src/core/tmux-ui.mjs +12 -7
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.56",
|
|
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",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { codexRemoteControlStatus, formatCodexRemoteControlStatus } from '../core/codex-app.mjs';
|
|
3
|
+
import { forceGpt55CodexConfigArgs } from '../core/codex-model-guard.mjs';
|
|
3
4
|
|
|
4
5
|
export async function codexAppRemoteControlCommand(args = [], opts = {}) {
|
|
5
6
|
const controlArgs = argsBeforeSeparator(args);
|
|
@@ -27,7 +28,7 @@ export async function codexAppRemoteControlCommand(args = [], opts = {}) {
|
|
|
27
28
|
return;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
const passthrough = stripSeparator(args);
|
|
31
|
+
const passthrough = forceGpt55CodexConfigArgs(stripSeparator(args));
|
|
31
32
|
const spawnFn = opts.spawn || spawn;
|
|
32
33
|
const code = await spawnInherited(spawnFn, status.codex_cli.bin, ['remote-control', ...passthrough], {
|
|
33
34
|
cwd: process.cwd(),
|
|
@@ -8,7 +8,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';
|
|
10
10
|
import { context7ConfigToml, DOLLAR_SKILL_NAMES, GETDESIGN_REFERENCE, hasContext7ConfigText, RECOMMENDED_SKILLS } from '../core/routes.mjs';
|
|
11
|
-
import { platformTmuxInstallHint, tmuxReadiness } from '../core/tmux-ui.mjs';
|
|
11
|
+
import { codexLaunchCommand, platformTmuxInstallHint, tmuxReadiness } from '../core/tmux-ui.mjs';
|
|
12
12
|
|
|
13
13
|
export async function postinstall({ bootstrap }) {
|
|
14
14
|
const installRoot = path.resolve(process.env.INIT_CWD || process.cwd());
|
|
@@ -50,8 +50,10 @@ export async function postinstall({ bootstrap }) {
|
|
|
50
50
|
if (bootstrapDecision.run) {
|
|
51
51
|
console.log(`SKS bootstrap: ${bootstrapDecision.reason}.`);
|
|
52
52
|
await runPostinstallBootstrap(installRoot, bootstrap);
|
|
53
|
+
await reportPostinstallCodexLbAuth();
|
|
53
54
|
return;
|
|
54
55
|
}
|
|
56
|
+
await reportPostinstallCodexLbAuth();
|
|
55
57
|
console.log('\nNext:');
|
|
56
58
|
console.log(' sks bootstrap');
|
|
57
59
|
console.log(`\nSKS bootstrap was not run automatically: ${bootstrapDecision.reason}.`);
|
|
@@ -60,6 +62,15 @@ export async function postinstall({ bootstrap }) {
|
|
|
60
62
|
console.log('Open runtime after readiness is green: sks\n');
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
async function reportPostinstallCodexLbAuth() {
|
|
66
|
+
const codexLbAuth = await ensureCodexLbAuthDuringInstall();
|
|
67
|
+
if (codexLbAuth.status === 'synced' || codexLbAuth.status === 'present') console.log(`codex-lb auth: preserved from ${codexLbAuth.env_path}.`);
|
|
68
|
+
else if (codexLbAuth.status === 'skipped') console.log(`codex-lb auth: skipped (${codexLbAuth.reason}).`);
|
|
69
|
+
else if (codexLbAuth.status === 'missing_env_key') console.log('codex-lb auth: stored key missing. Run `sks codex-lb setup --host <domain> --api-key <key>` to repair.');
|
|
70
|
+
else if (codexLbAuth.status && codexLbAuth.status !== 'not_configured') console.log(`codex-lb auth: repair skipped (${codexLbAuth.status}${codexLbAuth.error ? `: ${codexLbAuth.error}` : ''}).`);
|
|
71
|
+
return codexLbAuth;
|
|
72
|
+
}
|
|
73
|
+
|
|
63
74
|
async function postinstallHarnessConflictNotice(conflictScan) {
|
|
64
75
|
console.log('\nSneakoscope Codex package installed, but SKS setup is blocked.');
|
|
65
76
|
console.log(formatHarnessConflictReport(conflictScan, { includePrompt: false }));
|
|
@@ -159,7 +170,7 @@ export async function codexLbStatus(opts = {}) {
|
|
|
159
170
|
const config = await readText(configPath, '');
|
|
160
171
|
const envExists = await exists(envPath);
|
|
161
172
|
const envText = envExists ? await readText(envPath, '') : '';
|
|
162
|
-
const envKeyConfigured =
|
|
173
|
+
const envKeyConfigured = Boolean(parseCodexLbEnvKey(envText));
|
|
163
174
|
const providerConfigured = /\[model_providers\.codex-lb\]/.test(config);
|
|
164
175
|
const selected = /model_provider\s*=\s*"codex-lb"/.test(config);
|
|
165
176
|
return {
|
|
@@ -197,6 +208,24 @@ export async function repairCodexLbAuth(opts = {}) {
|
|
|
197
208
|
};
|
|
198
209
|
}
|
|
199
210
|
|
|
211
|
+
export async function ensureCodexLbAuthDuringInstall(opts = {}) {
|
|
212
|
+
if (process.env.SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH === '1' && !opts.force) return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH=1' };
|
|
213
|
+
const status = await codexLbStatus(opts);
|
|
214
|
+
if (!status.selected && !status.provider_configured && !status.env_file) return { status: 'not_configured', codex_lb: status };
|
|
215
|
+
if (!status.ok) return { status: status.env_key_configured ? 'not_configured' : 'missing_env_key', codex_lb: status, config_path: status.config_path, env_path: status.env_path };
|
|
216
|
+
const codexLogin = await ensureCodexLbLoginFromEnv(status, { ...opts, force: true });
|
|
217
|
+
return {
|
|
218
|
+
ok: Boolean(codexLogin.ok),
|
|
219
|
+
status: codexLogin.status,
|
|
220
|
+
config_path: status.config_path,
|
|
221
|
+
env_path: status.env_path,
|
|
222
|
+
base_url: status.base_url,
|
|
223
|
+
codex_lb: status,
|
|
224
|
+
codex_login: codexLogin,
|
|
225
|
+
error: codexLogin.error || null
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
200
229
|
export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
|
|
201
230
|
if (args.includes('--json') || args.includes('--skip-codex-lb') || process.env.SKS_SKIP_CODEX_LB_PROMPT === '1') return { status: 'skipped' };
|
|
202
231
|
if (!canAskYesNo()) return { status: 'non_interactive' };
|
|
@@ -237,7 +266,7 @@ async function syncCodexApiKeyLogin(apiKey, opts = {}) {
|
|
|
237
266
|
}
|
|
238
267
|
const login = await runProcess(codexBin, ['login', '--with-api-key'], { input: `${apiKey}\n`, env, timeoutMs: 15000, maxOutputBytes: 8192 });
|
|
239
268
|
if (login.code === 0) return { ok: true, status: 'synced' };
|
|
240
|
-
return { ok: false, status: 'login_failed', error: (login.stderr || login.stdout || 'codex login failed').trim() };
|
|
269
|
+
return { ok: false, status: 'login_failed', error: redactSecretText(login.stderr || login.stdout || 'codex login failed', [apiKey]).trim() };
|
|
241
270
|
}
|
|
242
271
|
|
|
243
272
|
function upsertCodexLbConfig(text = '', baseUrl) {
|
|
@@ -394,11 +423,23 @@ function parseCodexLbEnvKey(text = '') {
|
|
|
394
423
|
const match = String(text || '').match(/^\s*(?:export\s+)?CODEX_LB_API_KEY\s*=\s*(.+?)\s*$/m);
|
|
395
424
|
if (!match) return '';
|
|
396
425
|
const raw = match[1].trim();
|
|
397
|
-
if (raw
|
|
398
|
-
if (raw.startsWith('"
|
|
426
|
+
if (!raw) return '';
|
|
427
|
+
if (raw.startsWith("'")) return raw.endsWith("'") && raw.length > 1 ? raw.slice(1, -1).replace(/'\\''/g, "'") : '';
|
|
428
|
+
if (raw.startsWith('"')) return raw.endsWith('"') && raw.length > 1 ? raw.slice(1, -1).replace(/\\"/g, '"') : '';
|
|
429
|
+
if (raw.includes("'") || raw.includes('"') || /\s/.test(raw)) return '';
|
|
399
430
|
return raw;
|
|
400
431
|
}
|
|
401
432
|
|
|
433
|
+
function redactSecretText(text = '', secrets = []) {
|
|
434
|
+
let out = String(text || '');
|
|
435
|
+
for (const secret of secrets) {
|
|
436
|
+
const value = String(secret || '');
|
|
437
|
+
if (!value) continue;
|
|
438
|
+
out = out.split(value).join('[redacted]');
|
|
439
|
+
}
|
|
440
|
+
return out;
|
|
441
|
+
}
|
|
442
|
+
|
|
402
443
|
function escapeRegExp(value) {
|
|
403
444
|
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
404
445
|
}
|
|
@@ -466,13 +507,20 @@ async function ensureGlobalContext7DuringInstall() {
|
|
|
466
507
|
if (process.env.SKS_SKIP_POSTINSTALL_CONTEXT7 === '1') return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_CONTEXT7=1' };
|
|
467
508
|
const codex = await getCodexInfo().catch(() => ({}));
|
|
468
509
|
if (!codex.bin) return { status: 'codex_missing' };
|
|
469
|
-
const
|
|
510
|
+
const env = withoutSecretEnv(['CODEX_LB_API_KEY']);
|
|
511
|
+
const list = await runProcess(codex.bin, ['mcp', 'list'], { env, timeoutMs: 8000, maxOutputBytes: 32 * 1024 }).catch((err) => ({ code: 1, stderr: err.message, stdout: '' }));
|
|
470
512
|
if (list.code === 0 && /context7/i.test(`${list.stdout}\n${list.stderr}`)) return { status: 'present' };
|
|
471
|
-
const add = await runProcess(codex.bin, ['mcp', 'add', 'context7', '--', 'npx', '-y', '@upstash/context7-mcp@latest'], { timeoutMs: 30000, maxOutputBytes: 64 * 1024 }).catch((err) => ({ code: 1, stderr: err.message
|
|
513
|
+
const add = await runProcess(codex.bin, ['mcp', 'add', 'context7', '--', 'npx', '-y', '@upstash/context7-mcp@latest'], { env, timeoutMs: 30000, maxOutputBytes: 64 * 1024 }).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
|
|
472
514
|
if (add.code === 0) return { status: 'installed' };
|
|
473
515
|
return { status: 'failed', error: `${add.stderr || add.stdout || 'codex mcp add failed'}`.trim() };
|
|
474
516
|
}
|
|
475
517
|
|
|
518
|
+
function withoutSecretEnv(keys = []) {
|
|
519
|
+
const env = { ...process.env };
|
|
520
|
+
for (const key of keys) env[key] = '';
|
|
521
|
+
return env;
|
|
522
|
+
}
|
|
523
|
+
|
|
476
524
|
export async function ensureGlobalCodexSkillsDuringInstall(opts = {}) {
|
|
477
525
|
if (process.env.SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS === '1' && !opts.force) return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS=1' };
|
|
478
526
|
const home = opts.home || process.env.HOME || os.homedir();
|
|
@@ -789,3 +837,168 @@ async function safeReadText(file, fallback = '') {
|
|
|
789
837
|
return fallback;
|
|
790
838
|
}
|
|
791
839
|
}
|
|
840
|
+
|
|
841
|
+
export async function selftestCodexLb(tmp) {
|
|
842
|
+
const codexLbHome = path.join(tmp, 'codex-lb-home');
|
|
843
|
+
await ensureDir(path.join(codexLbHome, '.codex'));
|
|
844
|
+
const codexLbFakeBin = path.join(tmp, 'codex-lb-fake-bin');
|
|
845
|
+
await ensureDir(codexLbFakeBin);
|
|
846
|
+
const codexLbFakeCodex = path.join(codexLbFakeBin, 'codex');
|
|
847
|
+
await writeTextAtomic(codexLbFakeCodex, "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo \"codex-cli 99.0.0\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"status\" ]; then echo \"logged in with browser auth\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"--with-api-key\" ]; then read key; mkdir -p \"$HOME/.codex\"; printf '{\\\"auth_mode\\\":\\\"apikey\\\",\\\"key\\\":\\\"%s\\\"}\\n' \"$key\" > \"$HOME/.codex/auth.json\"; printf '%s\\n' \"$key\" >> \"$HOME/.codex/login-calls.log\"; exit 0; fi\necho \"fake codex unsupported\" >&2\nexit 1\n");
|
|
848
|
+
await fsp.chmod(codexLbFakeCodex, 0o755);
|
|
849
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "high"\nservice_tier = "fast"\n\n[notice]\nfast_default_opt_out = true\n\n[features]\ncodex_hooks = true\n');
|
|
850
|
+
const codexLbEnvForSelftest = { HOME: codexLbHome, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-global'), PATH: `${codexLbFakeBin}${path.delimiter}${process.env.PATH || ''}` };
|
|
851
|
+
const codexLbSetup = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'setup', '--host', 'lb.example.test', '--api-key', 'sk-test', '--json'], {
|
|
852
|
+
cwd: tmp,
|
|
853
|
+
env: codexLbEnvForSelftest,
|
|
854
|
+
timeoutMs: 15000,
|
|
855
|
+
maxOutputBytes: 64 * 1024
|
|
856
|
+
});
|
|
857
|
+
if (codexLbSetup.code !== 0) throw new Error(`selftest failed: codex-lb setup exited ${codexLbSetup.code}: ${codexLbSetup.stderr}`);
|
|
858
|
+
const codexLbSetupJson = JSON.parse(codexLbSetup.stdout);
|
|
859
|
+
const codexLbConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
|
|
860
|
+
const codexLbEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
|
|
861
|
+
const codexLbAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
862
|
+
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'") || !/(\"auth_mode\"\s*:\s*\"apikey\")/.test(codexLbAuth)) throw new Error('selftest failed: codex-lb setup did not write provider config, env key, and Codex API-key auth');
|
|
863
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n');
|
|
864
|
+
const codexLbRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'auth', 'repair', '--json'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
865
|
+
if (codexLbRepair.code !== 0) throw new Error(`selftest failed: codex-lb repair exited ${codexLbRepair.code}: ${codexLbRepair.stderr}`);
|
|
866
|
+
const codexLbRepairJson = JSON.parse(codexLbRepair.stdout);
|
|
867
|
+
const codexLbRepairedAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
868
|
+
if (!codexLbRepairJson.ok || codexLbRepairJson.status !== 'repaired' || !codexLbRepairedAuth.includes('"auth_mode":"apikey"') || !codexLbRepairedAuth.includes('sk-test')) throw new Error('selftest failed: codex-lb repair did not force API-key auth from stored env key');
|
|
869
|
+
const codexLbLoginCallsBeforePostinstall = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
|
|
870
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n');
|
|
871
|
+
const codexLbPostinstall = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
|
|
872
|
+
cwd: tmp,
|
|
873
|
+
env: {
|
|
874
|
+
...codexLbEnvForSelftest,
|
|
875
|
+
SKS_POSTINSTALL_NO_BOOTSTRAP: '1',
|
|
876
|
+
SKS_SKIP_POSTINSTALL_SHIM: '1',
|
|
877
|
+
SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
|
|
878
|
+
SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
|
|
879
|
+
SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
|
|
880
|
+
SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0'
|
|
881
|
+
},
|
|
882
|
+
timeoutMs: 15000,
|
|
883
|
+
maxOutputBytes: 128 * 1024
|
|
884
|
+
});
|
|
885
|
+
if (codexLbPostinstall.code !== 0) throw new Error(`selftest failed: codex-lb postinstall auth preservation exited ${codexLbPostinstall.code}: ${codexLbPostinstall.stderr}`);
|
|
886
|
+
const codexLbPostinstallAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
887
|
+
const codexLbLoginCallsAfterPostinstall = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
|
|
888
|
+
if (!String(codexLbPostinstall.stdout || '').includes('codex-lb auth: preserved') || !codexLbPostinstallAuth.includes('"auth_mode":"apikey"') || !codexLbPostinstallAuth.includes('sk-test') || codexLbLoginCallsAfterPostinstall <= codexLbLoginCallsBeforePostinstall) throw new Error('selftest failed: postinstall did not preserve codex-lb Codex CLI API-key auth from stored env key');
|
|
889
|
+
const postinstallEnvKeys = ['HOME', 'PATH', 'INIT_CWD', 'SKS_GLOBAL_ROOT', 'SKS_POSTINSTALL_BOOTSTRAP', 'SKS_POSTINSTALL_NO_BOOTSTRAP', 'SKS_SKIP_POSTINSTALL_SHIM', 'SKS_SKIP_POSTINSTALL_CONTEXT7', 'SKS_SKIP_POSTINSTALL_GETDESIGN', 'SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS', 'SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH'];
|
|
890
|
+
const postinstallEnvBefore = Object.fromEntries(postinstallEnvKeys.map((key) => [key, process.env[key]]));
|
|
891
|
+
const codexLbLoginCallsBeforeBootstrap = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
|
|
892
|
+
try {
|
|
893
|
+
for (const key of postinstallEnvKeys) delete process.env[key];
|
|
894
|
+
Object.assign(process.env, {
|
|
895
|
+
HOME: codexLbHome,
|
|
896
|
+
PATH: `${codexLbFakeBin}${path.delimiter}${postinstallEnvBefore.PATH || ''}`,
|
|
897
|
+
INIT_CWD: tmp,
|
|
898
|
+
SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-postinstall-global'),
|
|
899
|
+
SKS_POSTINSTALL_BOOTSTRAP: '1',
|
|
900
|
+
SKS_SKIP_POSTINSTALL_SHIM: '1',
|
|
901
|
+
SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
|
|
902
|
+
SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
|
|
903
|
+
SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
|
|
904
|
+
SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0'
|
|
905
|
+
});
|
|
906
|
+
await postinstall({ bootstrap: async () => writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n') });
|
|
907
|
+
} finally {
|
|
908
|
+
for (const key of postinstallEnvKeys) {
|
|
909
|
+
if (postinstallEnvBefore[key] === undefined) delete process.env[key];
|
|
910
|
+
else process.env[key] = postinstallEnvBefore[key];
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
const codexLbPostBootstrapAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
914
|
+
const codexLbLoginCallsAfterBootstrap = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
|
|
915
|
+
if (!codexLbPostBootstrapAuth.includes('"auth_mode":"apikey"') || !codexLbPostBootstrapAuth.includes('sk-test') || codexLbLoginCallsAfterBootstrap <= codexLbLoginCallsBeforeBootstrap) throw new Error('selftest failed: postinstall did not repair codex-lb auth after bootstrap drift');
|
|
916
|
+
const codexLbContext7Bin = path.join(tmp, 'codex-lb-context7-bin');
|
|
917
|
+
await ensureDir(codexLbContext7Bin);
|
|
918
|
+
await writeTextAtomic(path.join(codexLbContext7Bin, 'codex'), '#!/bin/sh\nif [ "$1" = "--version" ]; then echo "codex-cli 99.0.0"; exit 0; fi\nif [ "$CODEX_LB_API_KEY" ]; then echo "context7 leaked CODEX_LB_API_KEY" >&2; exit 77; fi\nif [ "$1" = "mcp" ] && [ "$2" = "list" ]; then echo ""; exit 0; fi\nif [ "$1" = "mcp" ] && [ "$2" = "add" ]; then echo "context7 added"; exit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
|
|
919
|
+
await fsp.chmod(path.join(codexLbContext7Bin, 'codex'), 0o755);
|
|
920
|
+
const codexLbContext7Postinstall = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
|
|
921
|
+
cwd: tmp,
|
|
922
|
+
env: {
|
|
923
|
+
...codexLbEnvForSelftest,
|
|
924
|
+
PATH: `${codexLbContext7Bin}${path.delimiter}${process.env.PATH || ''}`,
|
|
925
|
+
CODEX_LB_API_KEY: 'sk-test',
|
|
926
|
+
SKS_POSTINSTALL_NO_BOOTSTRAP: '1',
|
|
927
|
+
SKS_SKIP_POSTINSTALL_SHIM: '1',
|
|
928
|
+
SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
|
|
929
|
+
SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
|
|
930
|
+
SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '1'
|
|
931
|
+
},
|
|
932
|
+
timeoutMs: 15000,
|
|
933
|
+
maxOutputBytes: 128 * 1024
|
|
934
|
+
});
|
|
935
|
+
if (codexLbContext7Postinstall.code !== 0 || String(`${codexLbContext7Postinstall.stdout}\n${codexLbContext7Postinstall.stderr}`).includes('leaked CODEX_LB_API_KEY')) throw new Error('selftest failed: postinstall Context7 setup leaked CODEX_LB_API_KEY to unrelated Codex subprocesses');
|
|
936
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_API_KEY='unterminated\n");
|
|
937
|
+
const codexLbLoginCallsBeforeMalformed = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
|
|
938
|
+
const codexLbMalformedPostinstall = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
|
|
939
|
+
cwd: tmp,
|
|
940
|
+
env: {
|
|
941
|
+
...codexLbEnvForSelftest,
|
|
942
|
+
SKS_POSTINSTALL_NO_BOOTSTRAP: '1',
|
|
943
|
+
SKS_SKIP_POSTINSTALL_SHIM: '1',
|
|
944
|
+
SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
|
|
945
|
+
SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
|
|
946
|
+
SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
|
|
947
|
+
SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0'
|
|
948
|
+
},
|
|
949
|
+
timeoutMs: 15000,
|
|
950
|
+
maxOutputBytes: 128 * 1024
|
|
951
|
+
});
|
|
952
|
+
const codexLbLoginCallsAfterMalformed = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
|
|
953
|
+
if (codexLbMalformedPostinstall.code !== 0 || !String(codexLbMalformedPostinstall.stdout || '').includes('codex-lb auth: stored key missing') || codexLbLoginCallsAfterMalformed !== codexLbLoginCallsBeforeMalformed) throw new Error('selftest failed: malformed codex-lb env should not be passed to Codex login during postinstall');
|
|
954
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_API_KEY='sk-test'\n");
|
|
955
|
+
const codexLbMissingCli = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
|
|
956
|
+
cwd: tmp,
|
|
957
|
+
env: {
|
|
958
|
+
HOME: codexLbHome,
|
|
959
|
+
SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-missing-cli-global'),
|
|
960
|
+
PATH: '',
|
|
961
|
+
SKS_POSTINSTALL_NO_BOOTSTRAP: '1',
|
|
962
|
+
SKS_SKIP_POSTINSTALL_SHIM: '1',
|
|
963
|
+
SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
|
|
964
|
+
SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
|
|
965
|
+
SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
|
|
966
|
+
SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0'
|
|
967
|
+
},
|
|
968
|
+
timeoutMs: 15000,
|
|
969
|
+
maxOutputBytes: 128 * 1024
|
|
970
|
+
});
|
|
971
|
+
if (codexLbMissingCli.code !== 0 || !String(codexLbMissingCli.stdout || '').includes('codex-lb auth: repair skipped (codex_missing')) throw new Error('selftest failed: postinstall should handle configured codex-lb when Codex CLI is missing');
|
|
972
|
+
const codexLbNotConfiguredHome = path.join(tmp, 'codex-lb-not-configured-home');
|
|
973
|
+
const codexLbNotConfigured = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
|
|
974
|
+
cwd: tmp,
|
|
975
|
+
env: {
|
|
976
|
+
HOME: codexLbNotConfiguredHome,
|
|
977
|
+
SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-not-configured-global'),
|
|
978
|
+
PATH: '',
|
|
979
|
+
SKS_POSTINSTALL_NO_BOOTSTRAP: '1',
|
|
980
|
+
SKS_SKIP_POSTINSTALL_SHIM: '1',
|
|
981
|
+
SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
|
|
982
|
+
SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
|
|
983
|
+
SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
|
|
984
|
+
SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0'
|
|
985
|
+
},
|
|
986
|
+
timeoutMs: 15000,
|
|
987
|
+
maxOutputBytes: 128 * 1024
|
|
988
|
+
});
|
|
989
|
+
if (codexLbNotConfigured.code !== 0 || String(codexLbNotConfigured.stdout || '').includes('codex-lb auth:')) throw new Error('selftest failed: postinstall should stay quiet when codex-lb is not configured');
|
|
990
|
+
const codexLbStatusText = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'status'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
991
|
+
if (!String(codexLbStatusText.stdout || '').includes('Repair auth: sks codex-lb repair')) throw new Error('selftest failed: codex-lb status did not advertise repair command');
|
|
992
|
+
if (!/^model = "gpt-5\.5"/m.test(codexLbConfig) || !codexLbConfig.includes('service_tier = "fast"') || !codexLbConfig.includes('hooks = true') || codexLbConfig.includes('codex_hooks = true') || !codexLbConfig.includes('fast_mode = true') || !codexLbConfig.includes('fast_mode_ui = true') || !codexLbConfig.includes('[user.fast_mode]') || !codexLbConfig.includes('visible = true') || !codexLbConfig.includes('enabled = true') || !codexLbConfig.includes('default_profile = "sks-fast-high"') || !/\[profiles\.sks-fast-high\][\s\S]*?service_tier = "fast"/.test(codexLbConfig) || codexLbConfig.includes('fast_default_opt_out = true') || hasTopLevelCodexModeLock(codexLbConfig)) throw new Error('selftest failed: codex-lb setup did not preserve Codex App Fast mode defaults, force GPT-5.5, or migrate the hooks feature flag');
|
|
993
|
+
const codexLbLaunch = codexLaunchCommand(tmp, 'codex', []);
|
|
994
|
+
if (!codexLbLaunch.includes('sks-codex-lb.env')) throw new Error('selftest failed: tmux launch command does not source codex-lb env file');
|
|
995
|
+
if (!codexLbLaunch.includes("'--model' 'gpt-5.5'")) throw new Error('selftest failed: tmux launch command without args did not force GPT-5.5');
|
|
996
|
+
if (!codexLbLaunch.includes('SKS_TMUX_LOGO_ANIMATION') || !codexLbLaunch.includes('SNEAKOSCOPE CODEX')) throw new Error('selftest failed: tmux launch command does not include the animated SKS logo intro');
|
|
997
|
+
const madLaunchSource = await safeReadText(path.join(packageRoot(), 'src', 'cli', 'main.mjs'));
|
|
998
|
+
if (!madLaunchSource.includes('const lb = await maybePromptCodexLbSetupForLaunch(args)') || !madLaunchSource.includes("const launchLb = lb.status === 'present'") || !madLaunchSource.includes('codexLbImmediateLaunchOpts(cleanArgs, launchLb')) throw new Error('selftest failed: MAD launch does not sync codex-lb auth and fresh-session launch options');
|
|
999
|
+
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function hasTopLevelCodexModeLock(text = '') {
|
|
1003
|
+
return /(^|\n)\s*model\s*=\s*"codex-lb"\s*(\n|$)/.test(text) || /(^|\n)\s*model_provider\s*=\s*"openai"\s*(\n|$)/.test(text);
|
|
1004
|
+
}
|
package/src/cli/main.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import readline from 'node:readline/promises';
|
|
|
5
5
|
import { stdin as input, stdout as output } from 'node:process';
|
|
6
6
|
import { projectRoot, readJson, writeJsonAtomic, writeTextAtomic, appendJsonlBounded, nowIso, exists, ensureDir, tmpdir, packageRoot, dirSize, formatBytes, which, runProcess, PACKAGE_VERSION, sksRoot, globalSksRoot, findProjectRoot, readStdin } from '../core/fsx.mjs';
|
|
7
7
|
import { initProject, installSkills, normalizeInstallScope, sksCommandPrefix } from '../core/init.mjs';
|
|
8
|
-
import { getCodexInfo, runCodexExec } from '../core/codex-adapter.mjs';
|
|
8
|
+
import { buildCodexExecArgs, getCodexInfo, runCodexExec } from '../core/codex-adapter.mjs';
|
|
9
9
|
import { createMission, loadMission, findLatestMission, missionDir, setCurrent, stateFile } from '../core/mission.mjs';
|
|
10
10
|
import { buildQuestionSchema, writeQuestions } from '../core/questions.mjs';
|
|
11
11
|
import { sealContract, validateAnswers } from '../core/decision-contract.mjs';
|
|
@@ -55,7 +55,7 @@ import { rgbaKey, rgbaToWikiCoord, validateWikiCoordinateIndex } from '../core/w
|
|
|
55
55
|
import { ALLOWED_REASONING_EFFORTS, AWESOME_DESIGN_MD_REFERENCE, CODEX_APP_IMAGE_GENERATION_DOC_URL, CODEX_COMPUTER_USE_EVIDENCE_SOURCE, CODEX_COMPUTER_USE_ONLY_POLICY, CODEX_IMAGEGEN_REQUIRED_POLICY, COMMAND_CATALOG, DESIGN_SYSTEM_SSOT, DOLLAR_COMMAND_ALIASES, DOLLAR_COMMANDS, DOLLAR_SKILL_NAMES, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_SOURCE_INVENTORY_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, FROM_CHAT_IMG_VISUAL_MAP_ARTIFACT, FROM_CHAT_IMG_WORK_ORDER_ARTIFACT, GETDESIGN_REFERENCE, PPT_PIPELINE_SKILL_ALLOWLIST, RECOMMENDED_SKILLS, ROUTES, USAGE_TOPICS, context7ConfigToml, hasContext7ConfigText, hasFromChatImgSignal, looksLikeAnswerOnlyRequest, noUnrequestedFallbackCodePolicyText, reflectionRequiredForRoute, reasoningInstruction, routePrompt, routeReasoning, routeRequiresSubagents, speedLanePolicyText, stackCurrentDocsPolicy, triwikiContextTracking } from '../core/routes.mjs';
|
|
56
56
|
import { PIPELINE_PLAN_ARTIFACT, buildPipelinePlan, context7Evidence, evaluateStop, projectGateStatus, recordContext7Evidence, recordSubagentEvidence, validatePipelinePlan, writePipelinePlan } from '../core/pipeline.mjs';
|
|
57
57
|
import { TEAM_DECOMPOSITION_ARTIFACT, TEAM_GRAPH_ARTIFACT, TEAM_INBOX_DIR, TEAM_RUNTIME_TASKS_ARTIFACT, validateTeamRuntimeArtifacts, writeTeamRuntimeArtifacts } from '../core/team-dag.mjs';
|
|
58
|
-
import { appendTeamEvent, initTeamLive, parseTeamSpecText, readTeamDashboard, readTeamLive, readTeamTranscriptTail, renderTeamAgentLane } from '../core/team-live.mjs';
|
|
58
|
+
import { appendTeamEvent, initTeamLive, parseTeamSpecText, readTeamDashboard, readTeamLive, readTeamTranscriptTail, renderTeamAgentLane, renderTeamWatch } from '../core/team-live.mjs';
|
|
59
59
|
import { evaluateTeamReviewPolicyGate } from '../core/team-review-policy.mjs';
|
|
60
60
|
import { ARTIFACT_FILES, validateDogfoodReport, validateEffortDecision, validateFromChatImgVisualMap, validateSkillCandidate, validateSkillInjectionDecision, validateTeamDashboardState, validateWorkOrderLedger } from '../core/artifact-schemas.mjs';
|
|
61
61
|
import { selectEffort, writeEffortDecision } from '../core/effort-orchestrator.mjs';
|
|
@@ -78,7 +78,7 @@ import { OPENCLAW_SKILL_NAME, installOpenClawSkill } from '../core/openclaw.mjs'
|
|
|
78
78
|
import { buildTmuxLaunchPlan, buildTmuxOpenArgs, codexLaunchCommand, createTmuxSession, isTmuxShellSession, runTmuxLaunchPlanSyntaxCheck, shouldAutoAttachTmux, tmuxReadiness, tmuxStatusKind, defaultTmuxSessionName, formatTmuxBanner, launchTmuxTeamView, launchTmuxUi, platformTmuxInstallHint, runTmuxStatus, sanitizeTmuxSessionName, teamLaneStyle } from '../core/tmux-ui.mjs';
|
|
79
79
|
import { autoReviewProfileName, autoReviewStatus, autoReviewSummary, enableAutoReview, disableAutoReview, enableMadHighProfile, madHighProfileName } from '../core/auto-review.mjs';
|
|
80
80
|
import { context7Command } from './context7-command.mjs';
|
|
81
|
-
import { askPostinstallQuestion, checkContext7, checkRequiredSkills, codexLbStatus, configureCodexLb, ensureCodexCliTool, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, globalCodexSkillsRoot, maybePromptCodexLbSetupForLaunch, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, repairCodexLbAuth, shouldAutoApproveInstall } from './install-helpers.mjs';
|
|
81
|
+
import { askPostinstallQuestion, checkContext7, checkRequiredSkills, codexLbStatus, configureCodexLb, ensureCodexCliTool, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, globalCodexSkillsRoot, maybePromptCodexLbSetupForLaunch, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, repairCodexLbAuth, selftestCodexLb, shouldAutoApproveInstall } from './install-helpers.mjs';
|
|
82
82
|
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';
|
|
83
83
|
import { openClawCommand } from './openclaw-command.mjs';
|
|
84
84
|
|
|
@@ -2277,41 +2277,11 @@ async function selftest() {
|
|
|
2277
2277
|
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');
|
|
2278
2278
|
const forcedModelPlan = await buildTmuxLaunchPlan({ root: tmp, env: { SKS_CODEX_MODEL: 'gpt-5.4-mini', SKS_CODEX_FAST_HIGH: '0', SKS_CODEX_REASONING: 'medium' }, tmux: { ok: true, bin: 'tmux', version: '3.4' }, codex: { bin: 'codex', version: 'codex-cli 99.0.0' }, app: { ok: true } });
|
|
2279
2279
|
if (forcedModelPlan.codexArgs.includes('gpt-5.4-mini') || forcedModelPlan.codexArgs.join(' ') !== '--model gpt-5.5 -c model_reasoning_effort="medium"') throw new Error('selftest failed: sks tmux launch allowed a non-GPT-5.5 model override');
|
|
2280
|
-
const
|
|
2281
|
-
|
|
2282
|
-
const
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
await writeTextAtomic(codexLbFakeCodex, "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo \"codex-cli 99.0.0\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"status\" ]; then echo \"logged in with browser auth\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"--with-api-key\" ]; then read key; mkdir -p \"$HOME/.codex\"; printf '{\\\"auth_mode\\\":\\\"apikey\\\",\\\"key\\\":\\\"%s\\\"}\\n' \"$key\" > \"$HOME/.codex/auth.json\"; printf '%s\\n' \"$key\" >> \"$HOME/.codex/login-calls.log\"; exit 0; fi\necho \"fake codex unsupported\" >&2\nexit 1\n");
|
|
2286
|
-
await fsp.chmod(codexLbFakeCodex, 0o755);
|
|
2287
|
-
await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "high"\nservice_tier = "fast"\n\n[notice]\nfast_default_opt_out = true\n\n[features]\ncodex_hooks = true\n');
|
|
2288
|
-
const codexLbEnvForSelftest = { HOME: codexLbHome, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-global'), PATH: `${codexLbFakeBin}${path.delimiter}${process.env.PATH || ''}` };
|
|
2289
|
-
const codexLbSetup = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'setup', '--host', 'lb.example.test', '--api-key', 'sk-test', '--json'], {
|
|
2290
|
-
cwd: tmp,
|
|
2291
|
-
env: codexLbEnvForSelftest,
|
|
2292
|
-
timeoutMs: 15000,
|
|
2293
|
-
maxOutputBytes: 64 * 1024
|
|
2294
|
-
});
|
|
2295
|
-
if (codexLbSetup.code !== 0) throw new Error(`selftest failed: codex-lb setup exited ${codexLbSetup.code}: ${codexLbSetup.stderr}`);
|
|
2296
|
-
const codexLbSetupJson = JSON.parse(codexLbSetup.stdout);
|
|
2297
|
-
const codexLbConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
|
|
2298
|
-
const codexLbEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
|
|
2299
|
-
const codexLbAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
2300
|
-
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'") || !/(\"auth_mode\"\s*:\s*\"apikey\")/.test(codexLbAuth)) throw new Error('selftest failed: codex-lb setup did not write provider config, env key, and Codex API-key auth');
|
|
2301
|
-
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n');
|
|
2302
|
-
const codexLbRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'auth', 'repair', '--json'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2303
|
-
if (codexLbRepair.code !== 0) throw new Error(`selftest failed: codex-lb repair exited ${codexLbRepair.code}: ${codexLbRepair.stderr}`);
|
|
2304
|
-
const codexLbRepairJson = JSON.parse(codexLbRepair.stdout);
|
|
2305
|
-
const codexLbRepairedAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
2306
|
-
if (!codexLbRepairJson.ok || codexLbRepairJson.status !== 'repaired' || !codexLbRepairedAuth.includes('"auth_mode":"apikey"') || !codexLbRepairedAuth.includes('sk-test')) throw new Error('selftest failed: codex-lb repair did not force API-key auth from stored env key');
|
|
2307
|
-
const codexLbStatusText = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'status'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2308
|
-
if (!String(codexLbStatusText.stdout || '').includes('Repair auth: sks codex-lb repair')) throw new Error('selftest failed: codex-lb status did not advertise repair command');
|
|
2309
|
-
if (!/^model = "gpt-5\.5"/m.test(codexLbConfig) || !codexLbConfig.includes('service_tier = "fast"') || !codexLbConfig.includes('hooks = true') || codexLbConfig.includes('codex_hooks = true') || !codexLbConfig.includes('fast_mode = true') || !codexLbConfig.includes('fast_mode_ui = true') || !codexLbConfig.includes('[user.fast_mode]') || !codexLbConfig.includes('visible = true') || !codexLbConfig.includes('enabled = true') || !codexLbConfig.includes('default_profile = "sks-fast-high"') || !/\[profiles\.sks-fast-high\][\s\S]*?service_tier = "fast"/.test(codexLbConfig) || codexLbConfig.includes('fast_default_opt_out = true') || hasTopLevelCodexModeLock(codexLbConfig)) throw new Error('selftest failed: codex-lb setup did not preserve Codex App Fast mode defaults, force GPT-5.5, or migrate the hooks feature flag');
|
|
2310
|
-
const codexLbLaunch = codexLaunchCommand(tmp, 'codex', []);
|
|
2311
|
-
if (!codexLbLaunch.includes('sks-codex-lb.env')) throw new Error('selftest failed: tmux launch command does not source codex-lb env file');
|
|
2312
|
-
if (!codexLbLaunch.includes('SKS_TMUX_LOGO_ANIMATION') || !codexLbLaunch.includes('SNEAKOSCOPE CODEX')) throw new Error('selftest failed: tmux launch command does not include the animated SKS logo intro');
|
|
2313
|
-
const madLaunchSource = await safeReadText(path.join(packageRoot(), 'src', 'cli', 'main.mjs'));
|
|
2314
|
-
if (!madLaunchSource.includes('const lb = await maybePromptCodexLbSetupForLaunch(args)') || !madLaunchSource.includes("const launchLb = lb.status === 'present'") || !madLaunchSource.includes('codexLbImmediateLaunchOpts(cleanArgs, launchLb')) throw new Error('selftest failed: MAD launch does not sync codex-lb auth and fresh-session launch options');
|
|
2280
|
+
const explicitBadModelPlan = await buildTmuxLaunchPlan({ root: tmp, codexArgs: ['--profile', 'legacy-5.4', '--model', 'gpt-5.4-mini', '-c', 'model="gpt-5.4"', '-c', 'model_reasoning_effort="low"'], tmux: { ok: true, bin: 'tmux', version: '3.4' }, codex: { bin: 'codex', version: 'codex-cli 99.0.0' }, app: { ok: true } });
|
|
2281
|
+
if (explicitBadModelPlan.codexArgs.join(' ').includes('gpt-5.4') || explicitBadModelPlan.codexArgs.join(' ') !== '--model gpt-5.5 --profile legacy-5.4 -c model_reasoning_effort="low"') throw new Error('selftest failed: explicit tmux model override was not forced back to GPT-5.5');
|
|
2282
|
+
const codexExecArgs = buildCodexExecArgs({ root: tmp, prompt: 'model guard selftest', profile: 'legacy-5.4', extraArgs: ['--model=gpt-5.4-mini', '--config', 'model = "gpt-5.4"', '-c', 'model_reasoning_effort="medium"'] });
|
|
2283
|
+
if (codexExecArgs.join(' ').includes('gpt-5.4') || !codexExecArgs.includes('gpt-5.5') || codexExecArgs.includes('--model=gpt-5.4-mini')) throw new Error('selftest failed: codex exec args allowed a non-GPT-5.5 model override');
|
|
2284
|
+
await selftestCodexLb(tmp);
|
|
2315
2285
|
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');
|
|
2316
2286
|
if (shouldAutoAttachTmux(['--mad', '--json'], {}, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest failed: MAD tmux json mode should not auto-attach');
|
|
2317
2287
|
if (shouldAutoAttachTmux(['--mad', '--no-attach'], {}, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest failed: MAD tmux --no-attach should remain print-only');
|
|
@@ -2353,7 +2323,7 @@ async function selftest() {
|
|
|
2353
2323
|
if (!String(openClawAutoUpdate.stdout || '').includes('Codex CLI ready: 0.1.0 -> codex-cli 99.0.0')) throw new Error('selftest failed: OpenClaw mode did not auto-approve Codex CLI update before tmux launch');
|
|
2354
2324
|
const remoteControlBin = path.join(tmp, 'remote-control-bin');
|
|
2355
2325
|
await ensureDir(remoteControlBin);
|
|
2356
|
-
await writeTextAtomic(path.join(remoteControlBin, 'codex'), '#!/bin/sh\nif [ "$1" = "--version" ]; then echo "codex-cli 0.130.0"; exit 0; fi\nif [ "$1" = "remote-control" ]; then echo "remote-control $*"; exit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
|
|
2326
|
+
await writeTextAtomic(path.join(remoteControlBin, 'codex'), '#!/bin/sh\nif [ "$1" = "--version" ]; then echo "codex-cli 0.130.0"; exit 0; fi\nif [ "$1" = "remote-control" ]; then shift; for arg in "$@"; do if [ "$arg" = "--model" ]; then echo "remote-control rejects --model" >&2; exit 64; fi; done; echo "remote-control $*"; exit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
|
|
2357
2327
|
await fsp.chmod(path.join(remoteControlBin, 'codex'), 0o755);
|
|
2358
2328
|
const remoteControlStatus = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-app', 'remote-control', '--dry-run', '--json'], {
|
|
2359
2329
|
cwd: globalCwd,
|
|
@@ -2364,6 +2334,14 @@ async function selftest() {
|
|
|
2364
2334
|
if (remoteControlStatus.code !== 0) throw new Error(`selftest failed: Codex remote-control status exited ${remoteControlStatus.code}: ${remoteControlStatus.stderr}`);
|
|
2365
2335
|
const remoteControlJson = JSON.parse(remoteControlStatus.stdout);
|
|
2366
2336
|
if (!remoteControlJson.ok || remoteControlJson.min_version !== '0.130.0' || !String(remoteControlJson.command || '').includes('remote-control')) throw new Error('selftest failed: Codex remote-control status did not report 0.130.0 readiness');
|
|
2337
|
+
const remoteControlLaunch = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-app', 'remote-control', '--', '--model', 'gpt-5.4-mini', '-c', 'model="gpt-5.4"', '--example'], {
|
|
2338
|
+
cwd: globalCwd,
|
|
2339
|
+
env: { SKS_GLOBAL_ROOT: globalRuntimeRoot, PATH: remoteControlBin },
|
|
2340
|
+
timeoutMs: 15000,
|
|
2341
|
+
maxOutputBytes: 64 * 1024
|
|
2342
|
+
});
|
|
2343
|
+
const remoteControlLaunchText = `${remoteControlLaunch.stdout}\n${remoteControlLaunch.stderr}`;
|
|
2344
|
+
if (remoteControlLaunch.code !== 0 || remoteControlLaunchText.includes('gpt-5.4') || remoteControlLaunchText.includes('--model') || !remoteControlLaunchText.includes('-c model="gpt-5.5"')) throw new Error('selftest failed: Codex remote-control passthrough did not force GPT-5.5 with config syntax');
|
|
2367
2345
|
const remoteControlOldBin = path.join(tmp, 'remote-control-old-bin');
|
|
2368
2346
|
await ensureDir(remoteControlOldBin);
|
|
2369
2347
|
await writeTextAtomic(path.join(remoteControlOldBin, 'codex'), '#!/bin/sh\nif [ "$1" = "--version" ]; then echo "codex-cli 0.129.0"; exit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
|
|
@@ -2804,6 +2782,10 @@ async function selftest() {
|
|
|
2804
2782
|
if (hookTeamState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || hookTeamState.implementation_allowed === false || !hookTeamState.team_plan_ready) throw new Error('selftest failed: $Team hook did not prepare direct Team mission');
|
|
2805
2783
|
if (!hookTeamState.pipeline_plan_ready || !(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), PIPELINE_PLAN_ARTIFACT)))) throw new Error('selftest failed: $Team hook did not write a pipeline plan');
|
|
2806
2784
|
if (!(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'team-plan.json')))) throw new Error('selftest failed: Team plan was not created directly');
|
|
2785
|
+
const hookForbiddenModelResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookTeamTmp, input: JSON.stringify({ cwd: hookTeamTmp, prompt: '$Team should be blocked before route work', model: 'gpt-5.5', metadata: { client: { modelId: 'gpt-5.4-mini' } } }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
|
|
2786
|
+
if (hookForbiddenModelResult.code !== 0) throw new Error(`selftest failed: forbidden model hook exited ${hookForbiddenModelResult.code}: ${hookForbiddenModelResult.stderr}`);
|
|
2787
|
+
const hookForbiddenModelJson = JSON.parse(hookForbiddenModelResult.stdout);
|
|
2788
|
+
if (hookForbiddenModelJson.decision !== 'block' || !String(hookForbiddenModelJson.reason || '').includes('gpt-5.5') || !String(hookForbiddenModelJson.reason || '').includes('gpt-5.4-mini')) throw new Error('selftest failed: hook did not block GPT-5.4 client model metadata');
|
|
2807
2789
|
const hookTeamPendingResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookTeamTmp, input: JSON.stringify({ cwd: hookTeamTmp, prompt: '$Team 새 작업으로 넘어가' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
|
|
2808
2790
|
if (hookTeamPendingResult.code !== 0) throw new Error(`selftest failed: pending clarification hook exited ${hookTeamPendingResult.code}: ${hookTeamPendingResult.stderr}`);
|
|
2809
2791
|
const hookTeamPendingJson = JSON.parse(hookTeamPendingResult.stdout);
|
|
@@ -3262,12 +3244,16 @@ async function selftest() {
|
|
|
3262
3244
|
if (!roleTeamPlan.roster.debate_team.some((agent) => /inconvenience/.test(agent.persona))) throw new Error('selftest failed: user friction persona missing from debate team');
|
|
3263
3245
|
const tmuxTeam = await launchTmuxTeamView({ root: tmp, missionId: teamId, plan: roleTeamPlan, json: true });
|
|
3264
3246
|
if (!tmuxTeam.agents?.length || !tmuxTeam.agents.some((entry) => entry.agent === 'analysis_scout_1') || !tmuxTeam.agents.every((entry) => String(entry.command || '').includes('team lane') && String(entry.command || '').includes('--agent'))) throw new Error('selftest failed: Team tmux view did not expose agent live lanes');
|
|
3247
|
+
if (!roleTeamPlan.roster.analysis_team.every((agent) => tmuxTeam.agents.some((entry) => entry.agent === agent.id))) throw new Error('selftest failed: Team tmux view collapsed numbered analysis scout lanes');
|
|
3265
3248
|
if (!tmuxTeam.overview?.command?.includes('team watch') || !tmuxTeam.lanes?.some((entry) => entry.role === 'overview') || !tmuxTeam.lanes?.some((entry) => entry.agent === 'analysis_scout_1')) throw new Error('selftest failed: Team tmux view did not expose orchestration overview plus agent lanes');
|
|
3266
3249
|
if (tmuxTeam.split_ui?.mode !== 'single_window_split_panes' || tmuxTeam.split_ui?.layout !== 'tiled' || tmuxTeam.split_ui?.live_updates !== true) throw new Error('selftest failed: Team tmux view did not expose single-window split UI metadata');
|
|
3267
3250
|
if (String(tmuxTeam.overview?.command || '').includes('SNEAKOSCOPE CODEX') || !String(tmuxTeam.overview?.command || '').includes('Follow: team watch')) throw new Error('selftest failed: Team tmux pane banner is too noisy or missing compact follow hint');
|
|
3268
3251
|
if (teamLaneStyle('analysis_scout_1').role !== 'scout' || teamLaneStyle('executor_1').role !== 'execution' || teamLaneStyle('reviewer_1').role !== 'review') throw new Error('selftest failed: Team tmux role palette did not classify lane roles');
|
|
3269
3252
|
if (!String(tmuxTeam.cleanup_policy || '').includes('mark-complete') || !tmuxTeam.lanes.every((entry) => entry.style?.color && entry.title)) throw new Error('selftest failed: Team tmux view did not expose color/title metadata and cleanup policy');
|
|
3270
3253
|
if (tmuxTeam.session !== `sks-team-${teamId}` || !tmuxTeam.attach_command?.includes(`sks-team-${teamId}`)) throw new Error('selftest failed: Team tmux session is not named for visibility');
|
|
3254
|
+
await initTeamLive(teamId, teamDir, '역할 팀 테스트', { agentSessions: roleTeamPlan.agent_session_count, roleCounts: roleTeamPlan.role_counts, roster: roleTeamPlan.roster });
|
|
3255
|
+
const teamWatch = await renderTeamWatch(teamDir, { missionId: teamId });
|
|
3256
|
+
if (!roleTeamPlan.roster.analysis_team.every((agent) => teamWatch.includes(`- ${agent.id}:`))) throw new Error('selftest failed: Team watch overview collapsed numbered analysis scout lanes');
|
|
3271
3257
|
if (routeReasoning(routePrompt('$Research frontier idea'), '$Research frontier idea').effort !== 'xhigh') throw new Error('selftest failed: research reasoning not xhigh');
|
|
3272
3258
|
if (routeReasoning(routePrompt('$From-Chat-IMG 채팅 이미지 작업'), '$From-Chat-IMG 채팅 이미지 작업').effort !== 'xhigh') throw new Error('selftest failed: From-Chat-IMG reasoning not xhigh');
|
|
3273
3259
|
if (routeReasoning(routePrompt('$Computer-Use localhost UI smoke'), '$Computer-Use localhost UI smoke').effort !== 'low') throw new Error('selftest failed: Computer Use fast lane reasoning not low');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { exists, packageRoot, runProcess, which } from './fsx.mjs';
|
|
3
|
+
import { forceGpt55CodexArgs } from './codex-model-guard.mjs';
|
|
3
4
|
|
|
4
5
|
export async function findCodexBinary() {
|
|
5
6
|
const env = process.env.SKS_CODEX_BIN || process.env.DCODEX_CODEX_BIN || process.env.CODEX_BIN;
|
|
@@ -25,17 +26,22 @@ export async function getCodexInfo() {
|
|
|
25
26
|
return { bin, version, available: Boolean(bin) };
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
export
|
|
29
|
-
const bin = await findCodexBinary();
|
|
30
|
-
if (!bin) {
|
|
31
|
-
return { code: 127, stdout: '', stderr: 'Codex CLI not found. Install @openai/codex or set SKS_CODEX_BIN.' };
|
|
32
|
-
}
|
|
29
|
+
export function buildCodexExecArgs({ root, prompt, outputFile, json = true, profile = null, extraArgs = [] }) {
|
|
33
30
|
const args = ['exec', '--cd', root];
|
|
34
31
|
if (profile) args.push('--profile', profile);
|
|
35
32
|
if (json) args.push('--json');
|
|
36
33
|
if (outputFile) args.push('--output-last-message', outputFile);
|
|
37
|
-
args.push(...extraArgs);
|
|
34
|
+
args.push(...forceGpt55CodexArgs(extraArgs));
|
|
38
35
|
args.push(prompt);
|
|
36
|
+
return args;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function runCodexExec({ root, prompt, outputFile, json = true, profile = null, extraArgs = [], onStdout, onStderr, logDir = null, stdoutFile = null, stderrFile = null, maxBufferBytes = 256 * 1024, timeoutMs = null }) {
|
|
40
|
+
const bin = await findCodexBinary();
|
|
41
|
+
if (!bin) {
|
|
42
|
+
return { code: 127, stdout: '', stderr: 'Codex CLI not found. Install @openai/codex or set SKS_CODEX_BIN.' };
|
|
43
|
+
}
|
|
44
|
+
const args = buildCodexExecArgs({ root, prompt, outputFile, json, profile, extraArgs });
|
|
39
45
|
const effectiveTimeoutMs = Number(timeoutMs || process.env.SKS_CODEX_TIMEOUT_MS || process.env.DCODEX_CODEX_TIMEOUT_MS || 30 * 60 * 1000);
|
|
40
46
|
return runProcess(bin, args, {
|
|
41
47
|
cwd: root,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const REQUIRED_CODEX_MODEL = 'gpt-5.5';
|
|
2
|
+
|
|
3
|
+
const MODEL_VALUE_FLAGS = new Set(['--model', '-m']);
|
|
4
|
+
const CONFIG_VALUE_FLAGS = new Set(['-c', '--config']);
|
|
5
|
+
|
|
6
|
+
function isModelConfigOverride(value = '') {
|
|
7
|
+
return /^model\s*=/.test(String(value || '').trim());
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function stripCodexModelOverrides(args = []) {
|
|
11
|
+
const out = [];
|
|
12
|
+
const input = Array.isArray(args) ? args : [];
|
|
13
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
14
|
+
const arg = String(input[i]);
|
|
15
|
+
if (MODEL_VALUE_FLAGS.has(arg)) {
|
|
16
|
+
i += 1;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (arg.startsWith('--model=') || arg.startsWith('-m=')) continue;
|
|
20
|
+
if (CONFIG_VALUE_FLAGS.has(arg)) {
|
|
21
|
+
const value = i + 1 < input.length ? String(input[i + 1]) : '';
|
|
22
|
+
if (isModelConfigOverride(value)) {
|
|
23
|
+
i += 1;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
out.push(arg);
|
|
27
|
+
if (i + 1 < input.length) out.push(String(input[++i]));
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (arg.startsWith('-c=') || arg.startsWith('--config=')) {
|
|
31
|
+
const value = arg.slice(arg.indexOf('=') + 1);
|
|
32
|
+
if (isModelConfigOverride(value)) continue;
|
|
33
|
+
}
|
|
34
|
+
out.push(arg);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function forceGpt55CodexArgs(args = []) {
|
|
40
|
+
return ['--model', REQUIRED_CODEX_MODEL, ...stripCodexModelOverrides(args)];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function forceGpt55CodexConfigArgs(args = []) {
|
|
44
|
+
return ['-c', `model="${REQUIRED_CODEX_MODEL}"`, ...stripCodexModelOverrides(args)];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isForbiddenCodexModel(value = '') {
|
|
48
|
+
const model = String(value || '').trim().toLowerCase();
|
|
49
|
+
return /^gpt-5\./.test(model) && model !== REQUIRED_CODEX_MODEL;
|
|
50
|
+
}
|
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.56';
|
|
9
9
|
export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
|
|
10
10
|
export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
|
|
11
11
|
|
|
@@ -6,6 +6,7 @@ import { checkDbOperation, dbBlockReason, handleMadSksUserConfirmation } from '.
|
|
|
6
6
|
import { checkHarnessModification, harnessGuardBlockReason } from './harness-guard.mjs';
|
|
7
7
|
import { activeRouteContext, evaluateStop, prepareRoute, promptPipelineContext as routePipelineContext, recordContext7Evidence, recordSubagentEvidence, routePrompt } from './pipeline.mjs';
|
|
8
8
|
import { classifyToolError } from './evaluation.mjs';
|
|
9
|
+
import { REQUIRED_CODEX_MODEL, isForbiddenCodexModel } from './codex-model-guard.mjs';
|
|
9
10
|
|
|
10
11
|
const TEAM_DIGEST_MAX_EVENTS = 4;
|
|
11
12
|
const TEAM_DIGEST_MESSAGE_CHARS = 180;
|
|
@@ -91,7 +92,11 @@ export async function hookMain(name) {
|
|
|
91
92
|
const root = await projectRoot(payload.cwd || process.cwd());
|
|
92
93
|
const state = await loadState(root);
|
|
93
94
|
const noQuestion = isNoQuestionRunning(state);
|
|
94
|
-
if (name === 'user-prompt-submit')
|
|
95
|
+
if (name === 'user-prompt-submit') {
|
|
96
|
+
const modelBlock = blockForbiddenClientModel(payload);
|
|
97
|
+
if (modelBlock) return modelBlock;
|
|
98
|
+
return hookUserPrompt(root, state, payload, noQuestion);
|
|
99
|
+
}
|
|
95
100
|
if (name === 'pre-tool') return hookPreTool(root, state, payload, noQuestion);
|
|
96
101
|
if (name === 'post-tool') return hookPostTool(root, state, payload, noQuestion);
|
|
97
102
|
if (name === 'permission-request') return hookPermission(root, state, payload, noQuestion);
|
|
@@ -99,6 +104,43 @@ export async function hookMain(name) {
|
|
|
99
104
|
return { continue: true };
|
|
100
105
|
}
|
|
101
106
|
|
|
107
|
+
function blockForbiddenClientModel(payload = {}) {
|
|
108
|
+
const model = forbiddenClientModelFromPayload(payload);
|
|
109
|
+
if (!model || !isForbiddenCodexModel(model)) return null;
|
|
110
|
+
return {
|
|
111
|
+
decision: 'block',
|
|
112
|
+
reason: `SKS requires ${REQUIRED_CODEX_MODEL}; client payload requested ${model}. Switch the Codex client/session model to ${REQUIRED_CODEX_MODEL} and retry.`
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function forbiddenClientModelFromPayload(payload = {}) {
|
|
117
|
+
const candidates = [
|
|
118
|
+
payload.model,
|
|
119
|
+
payload.model_id,
|
|
120
|
+
payload.modelId,
|
|
121
|
+
payload.client_model,
|
|
122
|
+
payload.clientModel,
|
|
123
|
+
...clientModelCandidates(payload.client),
|
|
124
|
+
...clientModelCandidates(payload.metadata),
|
|
125
|
+
...clientModelCandidates(payload.context),
|
|
126
|
+
...clientModelCandidates(payload.thread),
|
|
127
|
+
...clientModelCandidates(payload.session)
|
|
128
|
+
];
|
|
129
|
+
return candidates.find((value) => typeof value === 'string' && isForbiddenCodexModel(value)) || '';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function clientModelCandidates(value, depth = 0) {
|
|
133
|
+
if (!value || typeof value !== 'object' || depth > 4) return [];
|
|
134
|
+
const out = [];
|
|
135
|
+
for (const key of ['model', 'model_id', 'modelId', 'client_model', 'clientModel']) {
|
|
136
|
+
if (typeof value[key] === 'string') out.push(value[key]);
|
|
137
|
+
}
|
|
138
|
+
for (const key of ['client', 'metadata', 'context', 'thread', 'session']) {
|
|
139
|
+
out.push(...clientModelCandidates(value[key], depth + 1));
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
102
144
|
async function hookUserPrompt(root, state, payload, noQuestion) {
|
|
103
145
|
if (!noQuestion) {
|
|
104
146
|
const prompt = extractUserPrompt(payload);
|
package/src/core/team-live.mjs
CHANGED
|
@@ -457,10 +457,7 @@ export async function renderTeamWatch(dir, opts = {}) {
|
|
|
457
457
|
const control = await readTeamControl(dir);
|
|
458
458
|
const runtime = await readJson(path.join(dir, TEAM_RUNTIME_TASKS_ARTIFACT), null);
|
|
459
459
|
const missionId = opts.missionId || dashboard?.mission_id || runtime?.mission_id || path.basename(dir);
|
|
460
|
-
const
|
|
461
|
-
const visibleAgents = agents
|
|
462
|
-
.filter(([name]) => name !== 'parent_orchestrator')
|
|
463
|
-
.slice(0, Math.max(3, Number(dashboard?.agent_session_count) || 3));
|
|
460
|
+
const visibleAgents = visibleDashboardAgentEntries(dashboard);
|
|
464
461
|
const events = (await readTeamTranscriptTail(dir, lines)).map(parseTranscriptLine).filter(Boolean);
|
|
465
462
|
const runtimeTasks = Array.isArray(runtime?.tasks) ? runtime.tasks : Array.isArray(runtime) ? runtime : [];
|
|
466
463
|
return [
|
|
@@ -497,6 +494,26 @@ export async function renderTeamWatch(dir, opts = {}) {
|
|
|
497
494
|
].filter((line) => line !== null).join('\n');
|
|
498
495
|
}
|
|
499
496
|
|
|
497
|
+
function visibleDashboardAgentEntries(dashboard = {}) {
|
|
498
|
+
const agents = dashboard?.agents || {};
|
|
499
|
+
const roster = dashboard?.roster || {};
|
|
500
|
+
const analysis = uniqueAgentIds(roster.analysis_team || []);
|
|
501
|
+
const debate = uniqueAgentIds(roster.debate_team || []);
|
|
502
|
+
const development = uniqueAgentIds(roster.development_team || []);
|
|
503
|
+
const validation = uniqueAgentIds(roster.validation_team || []);
|
|
504
|
+
const reviewers = validation.filter((id) => /review|qa|validation/i.test(id));
|
|
505
|
+
const reviewerTarget = Math.max(MIN_TEAM_REVIEWER_LANES, Number(dashboard?.role_counts?.reviewer) || 0);
|
|
506
|
+
const reviewLanes = reviewers.slice(0, reviewerTarget);
|
|
507
|
+
const phaseRepresentatives = [development[0], debate[0]].filter(Boolean);
|
|
508
|
+
const requiredVisible = [...analysis, ...reviewLanes, ...phaseRepresentatives];
|
|
509
|
+
const concreteAgentIds = Object.keys(agents).filter((name) => name !== 'parent_orchestrator' && !DEFAULT_AGENTS.includes(name));
|
|
510
|
+
const fallbackAgentIds = Object.keys(agents).filter((name) => name !== 'parent_orchestrator');
|
|
511
|
+
const limit = Math.max(3, Number(dashboard?.agent_session_count) || 3, requiredVisible.length);
|
|
512
|
+
return uniqueAgentIds([...requiredVisible, ...concreteAgentIds, ...debate, ...development, ...validation, ...fallbackAgentIds])
|
|
513
|
+
.slice(0, limit)
|
|
514
|
+
.map((id) => [id, agents[id] || { status: 'pending', phase: null, last_seen: null }]);
|
|
515
|
+
}
|
|
516
|
+
|
|
500
517
|
function normalizeEvent(event = {}) {
|
|
501
518
|
return {
|
|
502
519
|
ts: event.ts || nowIso(),
|
|
@@ -509,6 +526,18 @@ function normalizeEvent(event = {}) {
|
|
|
509
526
|
};
|
|
510
527
|
}
|
|
511
528
|
|
|
529
|
+
function uniqueAgentIds(agents = []) {
|
|
530
|
+
const ids = [];
|
|
531
|
+
const seen = new Set();
|
|
532
|
+
for (const agent of agents) {
|
|
533
|
+
const id = agent?.id || String(agent || '');
|
|
534
|
+
if (!id || seen.has(id)) continue;
|
|
535
|
+
seen.add(id);
|
|
536
|
+
ids.push(id);
|
|
537
|
+
}
|
|
538
|
+
return ids;
|
|
539
|
+
}
|
|
540
|
+
|
|
512
541
|
function parseTranscriptLine(line) {
|
|
513
542
|
try {
|
|
514
543
|
return JSON.parse(line);
|
package/src/core/tmux-ui.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
4
4
|
import { exists, nowIso, packageRoot, readJson, runProcess, sha256, sksRoot, which, writeJsonAtomic } from './fsx.mjs';
|
|
5
5
|
import { getCodexInfo } from './codex-adapter.mjs';
|
|
6
6
|
import { codexAppIntegrationStatus, formatCodexAppStatus } from './codex-app.mjs';
|
|
7
|
+
import { REQUIRED_CODEX_MODEL, forceGpt55CodexArgs } from './codex-model-guard.mjs';
|
|
7
8
|
import { MIN_TEAM_REVIEWER_LANES } from './team-review-policy.mjs';
|
|
8
9
|
|
|
9
10
|
export const SKS_TMUX_LOGO = [
|
|
@@ -114,7 +115,7 @@ const SKS_TMUX_LOGO_ANIMATION_STEPS = Object.freeze([
|
|
|
114
115
|
{ frame: 9, color: '51', bold: true, delay: '0.16' }
|
|
115
116
|
]);
|
|
116
117
|
|
|
117
|
-
export const DEFAULT_SKS_CODEX_MODEL =
|
|
118
|
+
export const DEFAULT_SKS_CODEX_MODEL = REQUIRED_CODEX_MODEL;
|
|
118
119
|
export const DEFAULT_SKS_CODEX_REASONING = 'high';
|
|
119
120
|
|
|
120
121
|
export function defaultCodexLaunchArgs(env = process.env) {
|
|
@@ -200,7 +201,7 @@ export function tmuxStatusKind(tmux = {}) {
|
|
|
200
201
|
}
|
|
201
202
|
|
|
202
203
|
export function codexLaunchCommand(root, codexBin, codexArgs = []) {
|
|
203
|
-
const extraArgs =
|
|
204
|
+
const extraArgs = forceGpt55CodexArgs(codexArgs);
|
|
204
205
|
return [
|
|
205
206
|
sksLogoIntroCommand(codexBin),
|
|
206
207
|
`printf '\\nProject: %s\\n' ${shellEscape(root)}`,
|
|
@@ -322,7 +323,7 @@ export async function buildTmuxLaunchPlan(opts = {}) {
|
|
|
322
323
|
const codex = opts.codex || await getCodexInfo().catch(() => ({}));
|
|
323
324
|
const tmux = opts.tmux || await tmuxReadiness(opts);
|
|
324
325
|
const app = opts.app || await codexAppIntegrationStatus({ codex });
|
|
325
|
-
const codexArgs = Array.isArray(opts.codexArgs) ? opts.codexArgs : defaultCodexLaunchArgs(opts.env || process.env);
|
|
326
|
+
const codexArgs = forceGpt55CodexArgs(Array.isArray(opts.codexArgs) ? opts.codexArgs : defaultCodexLaunchArgs(opts.env || process.env));
|
|
326
327
|
return {
|
|
327
328
|
root,
|
|
328
329
|
session,
|
|
@@ -426,13 +427,16 @@ export async function createTmuxSession(plan = {}, panes = [], opts = {}) {
|
|
|
426
427
|
return { ok: true, reused: true, session, panes: [], attach_command: `tmux attach-session -t ${session}` };
|
|
427
428
|
}
|
|
428
429
|
const first = normalizedPanes[0] || { cwd: root, command: 'pwd' };
|
|
429
|
-
const
|
|
430
|
+
const detachedWidth = String(Math.max(120, Number(opts.width || opts.detachedWidth) || 180));
|
|
431
|
+
const detachedHeight = String(Math.max(36, Number(opts.height || opts.detachedHeight) || 48));
|
|
432
|
+
const create = await tmuxRun(tmuxBin, ['new-session', '-d', '-x', detachedWidth, '-y', detachedHeight, '-s', session, '-c', path.resolve(first.cwd || root), '-n', 'sks', '-P', '-F', '#{pane_id}', first.command || 'pwd']);
|
|
430
433
|
if (create.code !== 0) return { ok: false, session, panes: [], stderr: create.stderr || create.stdout || 'tmux new-session failed' };
|
|
431
434
|
const created = [{ pane_id: paneId(create.stdout), role: first.role || 'overview', title: first.title || 'overview' }];
|
|
432
435
|
for (const pane of normalizedPanes.slice(1)) {
|
|
433
436
|
const split = await tmuxRun(tmuxBin, ['split-window', '-t', session, pane.vertical ? '-v' : '-h', '-d', '-P', '-F', '#{pane_id}', '-c', path.resolve(pane.cwd || root), pane.command || 'pwd']);
|
|
434
437
|
if (split.code !== 0) return { ok: false, session, panes: created, stderr: split.stderr || split.stdout || 'tmux split-window failed' };
|
|
435
438
|
created.push({ pane_id: paneId(split.stdout), role: pane.role || 'lane', title: pane.title || null });
|
|
439
|
+
await tmuxRun(tmuxBin, ['select-layout', '-t', session, opts.layout || 'tiled']).catch(() => null);
|
|
436
440
|
}
|
|
437
441
|
await tmuxRun(tmuxBin, ['select-layout', '-t', session, opts.layout || 'tiled']).catch(() => null);
|
|
438
442
|
return { ok: true, reused: false, session, panes: created, attach_command: `tmux attach-session -t ${session}` };
|
|
@@ -504,9 +508,10 @@ function teamViewAgentIds(plan = {}) {
|
|
|
504
508
|
const reviewers = validation.filter((id) => teamLaneStyle(id).role === 'review');
|
|
505
509
|
const reviewerTarget = Math.max(MIN_TEAM_REVIEWER_LANES, Number(plan.review_policy?.minimum_reviewer_lanes) || 0, Number(plan.role_counts?.reviewer) || 0);
|
|
506
510
|
const reviewLanes = reviewers.slice(0, reviewerTarget);
|
|
507
|
-
const
|
|
508
|
-
const
|
|
509
|
-
const
|
|
511
|
+
const phaseRepresentatives = [development[0], debate[0]].filter(Boolean);
|
|
512
|
+
const requiredVisible = [...analysis, ...reviewLanes, ...phaseRepresentatives];
|
|
513
|
+
const ordered = [...requiredVisible, ...debate, ...development, ...validation];
|
|
514
|
+
const limit = Math.max(Number(plan.agent_session_count) || MIN_TEAM_REVIEWER_LANES, requiredVisible.length);
|
|
510
515
|
return uniqueAgentIds(ordered).slice(0, Math.max(1, limit));
|
|
511
516
|
}
|
|
512
517
|
|