sneakoscope 0.9.0 → 0.9.2
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 +14 -2
- package/package.json +1 -1
- package/src/cli/install-helpers.mjs +49 -5
- package/src/cli/main.mjs +48 -12
- package/src/core/codex-app.mjs +117 -15
- package/src/core/fsx.mjs +1 -1
- package/src/core/hooks-runtime.mjs +95 -2
- package/src/core/init.mjs +1 -11
- package/src/core/routes.mjs +15 -5
package/README.md
CHANGED
|
@@ -187,7 +187,7 @@ sks codex-lb repair
|
|
|
187
187
|
sks
|
|
188
188
|
```
|
|
189
189
|
|
|
190
|
-
Bare `sks` can also prompt for codex-lb auth; SKS stores the base URL/key in `~/.codex/sks-codex-lb.env`, writes
|
|
190
|
+
Bare `sks` can also prompt for codex-lb auth; SKS stores the base URL/key in `~/.codex/sks-codex-lb.env`, writes the upstream codex-lb Codex CLI / IDE Extension provider block into `~/.codex/config.toml` for Codex App routing, loads the provider env key for tmux launches, and syncs the macOS user launch environment so the Codex App can see `CODEX_LB_API_KEY` after restart. If the provider block disappears but the stored env file is still recoverable, bare `sks`, npm postinstall upgrades, `sks doctor --fix`, and `sks codex-lb repair` restore it with `env_key = "CODEX_LB_API_KEY"`, `supports_websockets = true`, and `requires_openai_auth = true` as documented by codex-lb. If an older SKS release left the codex-lb dashboard key only in the shared Codex `auth.json` login cache, SKS migrates that key back into `~/.codex/sks-codex-lb.env` when a codex-lb provider or env base URL is already recoverable. It does not rewrite the shared Codex `auth.json` login cache by default; set `SKS_CODEX_LB_SYNC_CODEX_LOGIN=1` only if you intentionally want the old API-key login-cache behavior. When codex-lb is active, SKS opens a fresh `sks-codex-lb-*` tmux session and sweeps older detached codex-lb sessions for the same repo before launch so stale Responses API chains are not reused. Configured launch paths, including non-interactive runs, verify that codex-lb can continue a Responses API chain with `previous_response_id`; if that check fails, SKS bypasses codex-lb for that launch with `model_provider="openai"` instead of letting the Codex session fail mid-work.
|
|
191
191
|
|
|
192
192
|
If codex-lb provider auth drifts after launch/reinstall, run `sks doctor --fix` or `sks codex-lb repair`; to replace it, run `sks codex-lb reconfigure --host <domain> --api-key <key>`.
|
|
193
193
|
|
|
@@ -288,7 +288,9 @@ For headless remotely controllable Codex App/server sessions on Codex CLI 0.130.
|
|
|
288
288
|
sks codex-app remote-control -- --help
|
|
289
289
|
```
|
|
290
290
|
|
|
291
|
-
`sks codex-app check` reports whether the installed Codex CLI is new enough, whether the required app flags are visible, whether Fast/speed-selector config is unlocked, and whether installed OpenAI default plugins such as Browser, Chrome, Computer Use, Documents, Presentations, Spreadsheets, and LaTeX are enabled. When codex-lb is configured, SKS keeps it selected as the top-level Codex App provider while still preserving required app flags and plugin settings. Codex CLI 0.130.0+ app-server/remote-control threads can pick up config changes live; older CLI/TUI sessions should still be restarted after `.codex/config.toml` or MCP/plugin changes.
|
|
291
|
+
`sks codex-app check` reports whether the installed Codex CLI is new enough, whether the required app flags are visible, whether Fast/speed-selector config is unlocked, whether Codex App Git Actions can use Commit, Push, Commit and Push, and PR flows, and whether installed OpenAI default plugins such as Browser, Chrome, Computer Use, Documents, Presentations, Spreadsheets, and LaTeX are enabled. When codex-lb is configured, SKS keeps it selected as the top-level Codex App provider while still preserving required app flags and plugin settings. Codex CLI 0.130.0+ app-server/remote-control threads can pick up config changes live; older CLI/TUI sessions should still be restarted after `.codex/config.toml` or MCP/plugin changes.
|
|
292
|
+
|
|
293
|
+
Image-review routes are intentionally strict. `$Image-UX-Review`, `$UX-Review`, `$Visual-Review`, and `$UI-UX-Review` require real Codex App `$imagegen`/`gpt-image-2` generated annotated review images before `image-ux-review-gate.json` can pass; disabled or missing `image_generation` remains a blocker that `sks codex-app check` and selftest cover.
|
|
292
294
|
|
|
293
295
|
Then open Codex App and use prompt commands directly in the chat. Examples:
|
|
294
296
|
|
|
@@ -297,6 +299,7 @@ $Team implement the checkout fix and verify it
|
|
|
297
299
|
$DFix change this label and spacing only
|
|
298
300
|
$QA-LOOP dogfood localhost:3000 and fix safe issues
|
|
299
301
|
$PPT create an investor deck as HTML/PDF
|
|
302
|
+
$UX-Review this screenshot with gpt-image-2 callouts, then fix the issues
|
|
300
303
|
$Goal persist this migration workflow with native /goal continuation
|
|
301
304
|
$Research investigate this mechanism with source-backed scout lenses
|
|
302
305
|
$Wiki refresh and validate the context pack
|
|
@@ -420,6 +423,15 @@ codex mcp list
|
|
|
420
423
|
|
|
421
424
|
Codex App workflows need the app installed. UI/browser evidence requires first-party Codex Computer Use, and generated raster/image-review evidence requires real `$imagegen`/`gpt-image-2` output. After setup/upgrade, start a fresh thread so Codex reloads plugin tools.
|
|
422
425
|
|
|
426
|
+
### Codex App commit/push is blocked
|
|
427
|
+
|
|
428
|
+
```sh
|
|
429
|
+
sks doctor --fix
|
|
430
|
+
sks codex-app check
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
`sks codex-app check` now prints `Git Actions`. It should be `ok` for Codex App Commit, Push, Commit and Push, and PR buttons to bypass SKS route gates. If it is blocked, repair config with `sks doctor --fix`; if the blocker mentions remote-control, update Codex CLI to `0.130.0` or newer and restart older app-server/TUI sessions.
|
|
434
|
+
|
|
423
435
|
### Codex App UI looks stale after codex-lb changes
|
|
424
436
|
|
|
425
437
|
If Codex App UI panels or auth-dependent controls still look wrong after codex-lb setup, repair, or upgrade, restart the app first. If the UI still does not recover, sign out of Codex App, sign back in, then run `sks codex-app check` or `sks codex-lb repair` as needed.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.2",
|
|
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",
|
|
@@ -230,11 +230,13 @@ export async function codexLbStatus(opts = {}) {
|
|
|
230
230
|
const providerConfigured = /\[model_providers\.codex-lb\]/.test(config);
|
|
231
231
|
const selected = hasTopLevelCodexLbSelected(config);
|
|
232
232
|
const baseUrl = codexLbProviderBaseUrl(config) || parseCodexLbEnvBaseUrl(envText) || null;
|
|
233
|
+
const providerRequiresOpenAiAuth = codexLbProviderRequiresOpenAiAuth(config);
|
|
233
234
|
return {
|
|
234
|
-
ok: providerConfigured && envKeyConfigured && Boolean(baseUrl),
|
|
235
|
+
ok: providerConfigured && envKeyConfigured && Boolean(baseUrl) && providerRequiresOpenAiAuth,
|
|
235
236
|
config_path: configPath,
|
|
236
237
|
env_path: envPath,
|
|
237
238
|
provider_configured: providerConfigured,
|
|
239
|
+
provider_requires_openai_auth: providerRequiresOpenAiAuth,
|
|
238
240
|
selected,
|
|
239
241
|
env_file: envExists,
|
|
240
242
|
env_key_configured: envKeyConfigured,
|
|
@@ -373,6 +375,11 @@ function codexLbProviderBaseUrl(text = '') {
|
|
|
373
375
|
return block.match(/(^|\n)\s*base_url\s*=\s*"([^"]+)"/)?.[2] || '';
|
|
374
376
|
}
|
|
375
377
|
|
|
378
|
+
function codexLbProviderRequiresOpenAiAuth(text = '') {
|
|
379
|
+
const block = String(text || '').match(/(^|\n)\[model_providers\.codex-lb\]([\s\S]*?)(?=\n\[[^\]]+\]|\s*$)/)?.[2] || '';
|
|
380
|
+
return /(^|\n)\s*requires_openai_auth\s*=\s*true\s*(?:#.*)?(?=\n|$)/.test(block);
|
|
381
|
+
}
|
|
382
|
+
|
|
376
383
|
export async function repairCodexLbAuth(opts = {}) {
|
|
377
384
|
let status = await codexLbStatus(opts);
|
|
378
385
|
let configRepaired = false;
|
|
@@ -387,7 +394,7 @@ export async function repairCodexLbAuth(opts = {}) {
|
|
|
387
394
|
status = await codexLbStatus(opts);
|
|
388
395
|
}
|
|
389
396
|
}
|
|
390
|
-
if (status.env_key_configured && status.base_url && (!status.ok || !status.selected || legacyAuthMigrated || hasTopLevelCodexModeLock(currentConfig))) {
|
|
397
|
+
if (status.env_key_configured && status.base_url && (!status.ok || !status.selected || !status.provider_requires_openai_auth || legacyAuthMigrated || hasTopLevelCodexModeLock(currentConfig))) {
|
|
391
398
|
await ensureDir(path.dirname(status.config_path));
|
|
392
399
|
const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(currentConfig, status.base_url));
|
|
393
400
|
await writeTextAtomic(status.config_path, next);
|
|
@@ -426,7 +433,7 @@ export async function ensureCodexLbAuthDuringInstall(opts = {}) {
|
|
|
426
433
|
if (process.env.SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH === '1' && !opts.force) return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH=1' };
|
|
427
434
|
const status = await codexLbStatus(opts);
|
|
428
435
|
if (!status.selected && !status.provider_configured && !status.env_file) return { status: 'not_configured', codex_lb: status };
|
|
429
|
-
if (status.ok && !status.selected) return repairCodexLbAuth(opts);
|
|
436
|
+
if (status.ok && (!status.selected || !status.provider_requires_openai_auth)) return repairCodexLbAuth(opts);
|
|
430
437
|
if (!status.ok) {
|
|
431
438
|
if (status.base_url && (status.env_key_configured || status.provider_configured || status.selected || status.env_base_url_configured)) return repairCodexLbAuth(opts);
|
|
432
439
|
return { status: status.env_key_configured ? 'missing_base_url' : 'missing_env_key', codex_lb: status, config_path: status.config_path, env_path: status.env_path };
|
|
@@ -465,7 +472,21 @@ async function restoreCodexLbEnvFromSharedLogin(status = {}, opts = {}) {
|
|
|
465
472
|
|
|
466
473
|
export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
|
|
467
474
|
if (args.includes('--json') || args.includes('--skip-codex-lb') || process.env.SKS_SKIP_CODEX_LB_PROMPT === '1') return { status: 'skipped' };
|
|
468
|
-
|
|
475
|
+
let status = await codexLbStatus(opts);
|
|
476
|
+
if (status.env_key_configured && status.base_url && (!status.provider_configured || !status.selected || !status.provider_requires_openai_auth)) {
|
|
477
|
+
let promptedRestore = false;
|
|
478
|
+
if (!status.provider_configured && canAskYesNo()) {
|
|
479
|
+
promptedRestore = true;
|
|
480
|
+
const restore = (await askPostinstallQuestion('\ncodex-lb provider section is missing, but stored auth exists. Restore and route Codex through codex-lb? [Y/n] ')).trim();
|
|
481
|
+
if (/^(n|no|아니|아니요|ㄴ)$/i.test(restore)) return { status: 'continued_to_codex', codex_lb: status };
|
|
482
|
+
}
|
|
483
|
+
const repaired = await repairCodexLbAuth(opts);
|
|
484
|
+
status = await codexLbStatus(opts);
|
|
485
|
+
if (!status.ok) return { status: 'repair_failed', ok: false, repair: repaired, codex_lb: status };
|
|
486
|
+
if (!repaired.ok && repaired.error && promptedRestore) console.log(`codex-lb provider restored, but launch environment sync reported: ${repaired.error}`);
|
|
487
|
+
else if (!repaired.ok && promptedRestore) console.log(`codex-lb provider restored, but provider auth sync reported: ${repaired.status}`);
|
|
488
|
+
else if (repaired.config_repaired && promptedRestore) console.log(`codex-lb provider restored: ${status.base_url}`);
|
|
489
|
+
}
|
|
469
490
|
if (status.ok) {
|
|
470
491
|
const codexEnvironment = await syncCodexLbProviderEnvironment(status, opts);
|
|
471
492
|
if (codexEnvironment.status === 'synced') console.log('codex-lb provider auth synced for this user session.');
|
|
@@ -480,7 +501,7 @@ export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
|
|
|
480
501
|
return { status: 'present', ...status, codex_environment: codexEnvironment, codex_login: codexLogin, chain_health: chainHealth };
|
|
481
502
|
}
|
|
482
503
|
if (!canAskYesNo()) return { status: 'non_interactive', codex_lb: status };
|
|
483
|
-
const useCodexLb = (await askPostinstallQuestion('\
|
|
504
|
+
const useCodexLb = (await askPostinstallQuestion('\ncodex-lb is not configured for this Codex App profile. Configure and route Codex through codex-lb now? [y/N] ')).trim();
|
|
484
505
|
if (!/^(y|yes|예|네|응)$/i.test(useCodexLb)) return { status: 'continued_to_codex' };
|
|
485
506
|
const host = (await askPostinstallQuestion('codex-lb host domain [http://127.0.0.1:2455]: ')).trim() || 'http://127.0.0.1:2455';
|
|
486
507
|
const apiKey = (await askPostinstallQuestion('codex-lb API key: ')).trim();
|
|
@@ -1235,6 +1256,7 @@ export async function selftestCodexLb(tmp) {
|
|
|
1235
1256
|
const codexLbEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
|
|
1236
1257
|
const codexLbAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
1237
1258
|
if (!codexLbSetupJson.ok || codexLbSetupJson.base_url !== 'https://lb.example.test/backend-api/codex' || !hasTopLevelCodexLbSelected(codexLbConfig) || !codexLbConfig.includes('[model_providers.codex-lb]') || !codexLbEnv.includes("CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'") || !codexLbEnv.includes("CODEX_LB_API_KEY='sk-test'") || codexLbSetupJson.codex_environment?.ok !== true || codexLbSetupJson.codex_login?.status !== 'skipped' || codexLbAuth.trim()) throw new Error('selftest: codex-lb setup');
|
|
1259
|
+
if (!codexLbConfig.includes('requires_openai_auth = true')) throw new Error('selftest: codex-lb setup did not write upstream-required requires_openai_auth');
|
|
1238
1260
|
const codexLbFailLaunchctl = path.join(codexLbFakeBin, 'launchctl-fail');
|
|
1239
1261
|
await writeTextAtomic(codexLbFailLaunchctl, '#!/bin/sh\necho "launchctl denied" >&2\nexit 7\n');
|
|
1240
1262
|
await fsp.chmod(codexLbFailLaunchctl, 0o755);
|
|
@@ -1244,6 +1266,7 @@ export async function selftestCodexLb(tmp) {
|
|
|
1244
1266
|
await initProject(codexLbHome, { installScope: 'global', force: true, repair: true });
|
|
1245
1267
|
const codexLbRepairSetupConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
|
|
1246
1268
|
if (!hasTopLevelCodexLbSelected(codexLbRepairSetupConfig) || !codexLbRepairSetupConfig.includes('[model_providers.codex-lb]') || !codexLbRepairSetupConfig.includes('https://lb.example.test/backend-api/codex') || codexLbRepairSetupConfig.includes('sk-test')) throw new Error('selftest: init codex-lb');
|
|
1269
|
+
if (!codexLbRepairSetupConfig.includes('requires_openai_auth = true')) throw new Error('selftest: init codex-lb did not preserve upstream-required requires_openai_auth');
|
|
1247
1270
|
if (!hasCodexUnstableFeatureWarningSuppression(codexLbRepairSetupConfig)) throw new Error('selftest: init codex-lb did not suppress Codex unstable feature warning');
|
|
1248
1271
|
await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), `${codexLbConfig}\n[mcp_servers.supabase]\nurl = "https://mcp.supabase.com/mcp?project_ref=ref&read_only=true&features=database,docs"\n`);
|
|
1249
1272
|
const ptmp = path.join(tmp, 'codex-lb-project-config'), prevHome = process.env.HOME;
|
|
@@ -1251,6 +1274,7 @@ export async function selftestCodexLb(tmp) {
|
|
|
1251
1274
|
finally { if (prevHome === undefined) delete process.env.HOME; else process.env.HOME = prevHome; }
|
|
1252
1275
|
const pcfg = await safeReadText(path.join(ptmp, '.codex', 'config.toml'));
|
|
1253
1276
|
if (!hasTopLevelCodexLbSelected(pcfg) || !pcfg.includes('[model_providers.codex-lb]') || !pcfg.includes('[mcp_servers.supabase]') || !pcfg.includes('read_only=true')) throw new Error('selftest: project codex-lb');
|
|
1277
|
+
if (!pcfg.includes('requires_openai_auth = true')) throw new Error('selftest: project codex-lb did not copy upstream-required requires_openai_auth');
|
|
1254
1278
|
if (!hasCodexUnstableFeatureWarningSuppression(pcfg)) throw new Error('selftest: project codex-lb config did not suppress Codex unstable feature warning');
|
|
1255
1279
|
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n');
|
|
1256
1280
|
const codexLbRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'auth', 'repair', '--json'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
@@ -1310,6 +1334,7 @@ export async function selftestCodexLb(tmp) {
|
|
|
1310
1334
|
const codexLbLoginCallsAfterBootstrap = await codexLbLoginCallCount(codexLbHome);
|
|
1311
1335
|
if (!codexLbPostBootstrapAuth.includes('"auth_mode":"browser"') || codexLbPostBootstrapAuth.includes('sk-test') || codexLbLoginCallsAfterBootstrap !== codexLbLoginCallsBeforeBootstrap) throw new Error('selftest: postinstall drift auth');
|
|
1312
1336
|
if (!hasTopLevelCodexLbSelected(codexLbPostBootstrapConfig) || !codexLbPostBootstrapConfig.includes('[model_providers.codex-lb]') || !codexLbPostBootstrapConfig.includes('https://lb.example.test/backend-api/codex') || codexLbPostBootstrapConfig.includes('sk-test')) throw new Error('selftest: postinstall drift config');
|
|
1337
|
+
if (!codexLbPostBootstrapConfig.includes('requires_openai_auth = true')) throw new Error('selftest: postinstall drift config did not restore upstream-required requires_openai_auth');
|
|
1313
1338
|
const doctorProject = tmpdir();
|
|
1314
1339
|
await ensureDir(path.join(doctorProject, '.git'));
|
|
1315
1340
|
await writeTextAtomic(path.join(doctorProject, 'package.json'), '{"name":"codex-lb-doctor-project","version":"0.0.0"}\n');
|
|
@@ -1327,6 +1352,7 @@ export async function selftestCodexLb(tmp) {
|
|
|
1327
1352
|
const codexLbDoctorAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
1328
1353
|
const codexLbDoctorConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
|
|
1329
1354
|
if (!codexLbDoctorJson.repair?.codex_lb?.ok || !codexLbDoctorJson.repair.codex_lb.config_repaired || !codexLbDoctorJson.codex_lb?.ok || !codexLbDoctorAuth.includes('"auth_mode":"browser"') || codexLbDoctorAuth.includes('sk-test') || !hasTopLevelCodexLbSelected(codexLbDoctorConfig) || !codexLbDoctorConfig.includes('https://lb.example.test/backend-api/codex') || !hasCodexUnstableFeatureWarningSuppression(codexLbDoctorConfig)) throw new Error('selftest: doctor codex-lb');
|
|
1355
|
+
if (!codexLbDoctorConfig.includes('requires_openai_auth = true')) throw new Error('selftest: doctor codex-lb did not restore upstream-required requires_openai_auth');
|
|
1330
1356
|
const codexLbContext7Bin = path.join(tmp, 'codex-lb-context7-bin');
|
|
1331
1357
|
await ensureDir(codexLbContext7Bin);
|
|
1332
1358
|
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');
|
|
@@ -1468,6 +1494,7 @@ export async function selftestCodexLb(tmp) {
|
|
|
1468
1494
|
home: codexLbHome,
|
|
1469
1495
|
apiKey: 'sk-test',
|
|
1470
1496
|
codexBin: path.join(codexLbFakeBin, 'codex'),
|
|
1497
|
+
syncLaunchEnv: false,
|
|
1471
1498
|
timeoutMs: 1000,
|
|
1472
1499
|
fetch: async (url, init) => {
|
|
1473
1500
|
nonInteractiveLaunchChainCalls.push({ url, body: JSON.parse(init.body) });
|
|
@@ -1479,6 +1506,7 @@ export async function selftestCodexLb(tmp) {
|
|
|
1479
1506
|
home: codexLbHome,
|
|
1480
1507
|
apiKey: 'sk-test',
|
|
1481
1508
|
codexBin: path.join(codexLbFakeBin, 'codex'),
|
|
1509
|
+
syncLaunchEnv: false,
|
|
1482
1510
|
timeoutMs: 1000,
|
|
1483
1511
|
fetch: async (_url, init) => {
|
|
1484
1512
|
const body = JSON.parse(init.body);
|
|
@@ -1487,6 +1515,22 @@ export async function selftestCodexLb(tmp) {
|
|
|
1487
1515
|
}
|
|
1488
1516
|
});
|
|
1489
1517
|
if (nonInteractiveBrokenLaunch.status !== 'chain_unhealthy' || nonInteractiveBrokenLaunch.bypass_codex_lb !== true || nonInteractiveBrokenLaunch.chain_health?.status !== 'previous_response_not_found') throw new Error('selftest: non-interactive codex-lb launch path did not bypass on previous_response_not_found');
|
|
1518
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nservice_tier = "fast"\n');
|
|
1519
|
+
await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'\nexport CODEX_LB_API_KEY='sk-test'\n");
|
|
1520
|
+
const missingProviderLaunchCalls = [];
|
|
1521
|
+
const missingProviderLaunch = await maybePromptCodexLbSetupForLaunch([], {
|
|
1522
|
+
home: codexLbHome,
|
|
1523
|
+
apiKey: 'sk-test',
|
|
1524
|
+
codexBin: path.join(codexLbFakeBin, 'codex'),
|
|
1525
|
+
syncLaunchEnv: false,
|
|
1526
|
+
timeoutMs: 1000,
|
|
1527
|
+
fetch: async (url, init) => {
|
|
1528
|
+
missingProviderLaunchCalls.push({ url, body: JSON.parse(init.body) });
|
|
1529
|
+
return new Response(JSON.stringify({ id: missingProviderLaunchCalls.length === 1 ? 'resp_missing_provider_1' : 'resp_missing_provider_2' }), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
1530
|
+
}
|
|
1531
|
+
});
|
|
1532
|
+
const missingProviderRepairedConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
|
|
1533
|
+
if (!missingProviderLaunch.ok || missingProviderLaunch.status !== 'present' || missingProviderLaunch.chain_health?.status !== 'chain_ok' || missingProviderLaunchCalls.length !== 2 || !hasTopLevelCodexLbSelected(missingProviderRepairedConfig) || !missingProviderRepairedConfig.includes('[model_providers.codex-lb]') || !missingProviderRepairedConfig.includes('env_key = "CODEX_LB_API_KEY"') || !missingProviderRepairedConfig.includes('supports_websockets = true') || !missingProviderRepairedConfig.includes('requires_openai_auth = true')) throw new Error('selftest: bare sks launch did not restore missing upstream codex-lb provider block from stored env');
|
|
1490
1534
|
const chainCalls = [];
|
|
1491
1535
|
const okChain = await checkCodexLbResponseChain(
|
|
1492
1536
|
{ base_url: 'https://lb.example.test/backend-api/codex', env_path: path.join(codexLbHome, '.codex', 'sks-codex-lb.env') },
|
package/src/cli/main.mjs
CHANGED
|
@@ -1146,9 +1146,11 @@ async function codexLbCommand(action = 'status', args = []) {
|
|
|
1146
1146
|
console.log(`Configured: ${status.ok ? 'yes' : 'no'}`);
|
|
1147
1147
|
console.log(`Selected: ${status.selected ? 'yes' : 'no'}`);
|
|
1148
1148
|
console.log(`Provider: ${status.provider_configured ? 'yes' : 'no'}`);
|
|
1149
|
+
console.log(`Codex App auth: ${status.provider_requires_openai_auth ? 'yes' : 'missing'}`);
|
|
1149
1150
|
console.log(`Env file: ${status.env_file ? status.env_path : 'missing'}`);
|
|
1150
1151
|
if (status.base_url) console.log(`Base URL: ${status.base_url}`);
|
|
1151
1152
|
if (status.ok && !status.selected) console.log('\nRun: sks codex-lb repair to activate codex-lb for Codex App.');
|
|
1153
|
+
else if (!status.ok && status.base_url && status.env_key_configured) console.log('\nRun: sks codex-lb repair to restore the upstream codex-lb provider block.');
|
|
1152
1154
|
else if (!status.ok) console.log('\nRun: sks codex-lb setup --host <domain> --api-key <key>');
|
|
1153
1155
|
else console.log('\nRepair provider auth: sks codex-lb repair');
|
|
1154
1156
|
return;
|
|
@@ -1646,7 +1648,7 @@ async function setup(args) {
|
|
|
1646
1648
|
else console.log('Git: .gitignore ignores SKS generated files');
|
|
1647
1649
|
console.log(`Codex App: .codex/config.toml, .codex/hooks.json, .agents/skills, .codex/agents, .codex/SNEAKOSCOPE.md`);
|
|
1648
1650
|
console.log(`Global $: ${globalSkills.status === 'installed' ? 'ok' : globalSkills.status} ${globalSkills.root || ''}`.trimEnd());
|
|
1649
|
-
console.log(`App tools: ${appRuntime.ok ? 'ok' : 'needs setup'} Codex App=${appRuntime.app.installed ? 'ok' : 'missing'} Browser=${appRuntime.features?.browser_tool_ready ? 'ok' : 'missing'} Computer Use=${appRuntime.mcp.has_computer_use ? 'ok' : 'missing'} Image Gen=${appRuntime.features?.image_generation ? 'ok' : 'missing'}`);
|
|
1651
|
+
console.log(`App tools: ${appRuntime.ok ? 'ok' : 'needs setup'} Codex App=${appRuntime.app.installed ? 'ok' : 'missing'} Browser=${appRuntime.features?.browser_tool_ready ? 'ok' : 'missing'} Computer Use=${appRuntime.mcp.has_computer_use ? 'ok' : 'missing'} Image Gen=${appRuntime.features?.image_generation ? 'ok' : 'missing'} Git Actions=${appRuntime.features?.git_actions?.ok ? 'ok' : 'missing'}`);
|
|
1650
1652
|
console.log(`Prompt: intent-first routing, $Answer fact-check route, $DFix ultralight Direct Fix route, $PPT HTML/PDF presentation route, Context7 gate`);
|
|
1651
1653
|
console.log(`Skills: .agents/skills`);
|
|
1652
1654
|
console.log(`Next: sks context7 check; sks selftest --mock; sks commands; sks dollar-commands`);
|
|
@@ -2103,14 +2105,18 @@ async function selftest() {
|
|
|
2103
2105
|
await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', 'agent-team', 'SKILL.md'), '---\nname: agent-team\ndescription: Fallback Codex App picker alias for $Team.\n---\n');
|
|
2104
2106
|
await ensureDir(path.join(repairTmp, '.agents', 'skills', 'stale-sks-generated'));
|
|
2105
2107
|
await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', 'stale-sks-generated', 'SKILL.md'), '---\nname: stale-sks-generated\ndescription: Old SKS generated skill that should disappear on update.\n---\n');
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
+
const stalePluginSkillNames = ['browser', 'browser-use', 'computer-use', 'chrome', 'documents', 'presentations', 'spreadsheets', 'latex'];
|
|
2109
|
+
const stalePluginSkillContent = (name) => `---\nname: ${name}\ndescription: Sneakoscope generated stale plugin collision for selftest.\n---\n\nCodex App pipeline activation:\n- stale selftest marker\n`;
|
|
2110
|
+
for (const name of stalePluginSkillNames) {
|
|
2111
|
+
await ensureDir(path.join(repairTmp, '.agents', 'skills', name));
|
|
2112
|
+
await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', name, 'SKILL.md'), stalePluginSkillContent(name));
|
|
2113
|
+
}
|
|
2108
2114
|
await writeJsonAtomic(path.join(repairTmp, '.agents', 'skills', '.sks-generated.json'), {
|
|
2109
2115
|
schema_version: 1,
|
|
2110
2116
|
generated_by: 'sneakoscope',
|
|
2111
2117
|
version: '0.0.1',
|
|
2112
|
-
skills: ['team', 'stale-sks-generated',
|
|
2113
|
-
files: ['.agents/skills/team/SKILL.md', '.agents/skills/stale-sks-generated/SKILL.md',
|
|
2118
|
+
skills: ['team', 'stale-sks-generated', ...stalePluginSkillNames],
|
|
2119
|
+
files: ['.agents/skills/team/SKILL.md', '.agents/skills/stale-sks-generated/SKILL.md', ...stalePluginSkillNames.map((name) => `.agents/skills/${name}/SKILL.md`)]
|
|
2114
2120
|
});
|
|
2115
2121
|
const staleCodexAgentRel = '.codex/agents/stale-generated.toml';
|
|
2116
2122
|
await writeTextAtomic(path.join(repairTmp, staleCodexAgentRel), 'name = "stale_generated"\n');
|
|
@@ -2131,8 +2137,6 @@ async function selftest() {
|
|
|
2131
2137
|
await writeJsonAtomic(path.join(repairTmp, '.sneakoscope', 'policy.json'), { broken: true });
|
|
2132
2138
|
const existingAgentsMd = await safeReadText(path.join(repairTmp, 'AGENTS.md'));
|
|
2133
2139
|
await writeTextAtomic(path.join(repairTmp, 'AGENTS.md'), existingAgentsMd.replace(/<!-- BEGIN Sneakoscope Codex GX MANAGED BLOCK -->[\s\S]*?<!-- END Sneakoscope Codex GX MANAGED BLOCK -->\n?/, '<!-- BEGIN Sneakoscope Codex GX MANAGED BLOCK -->\ntampered managed block\n<!-- END Sneakoscope Codex GX MANAGED BLOCK -->\n'));
|
|
2134
|
-
const stalePluginSkillNames = ['computer-use', 'browser-use', 'browser'];
|
|
2135
|
-
const stalePluginSkillContent = (name) => `---\nname: ${name}\ndescription: Sneakoscope generated stale plugin collision for selftest.\n---\n\nCodex App pipeline activation:\n- stale selftest marker\n`;
|
|
2136
2140
|
const doctorRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'doctor', '--fix', '--local-only', '--json'], {
|
|
2137
2141
|
cwd: repairTmp,
|
|
2138
2142
|
env: { HOME: path.join(repairTmp, 'home'), SKS_DISABLE_UPDATE_CHECK: '1' },
|
|
@@ -2150,7 +2154,9 @@ async function selftest() {
|
|
|
2150
2154
|
if (!repairedTeamSkill.includes('SKS Team orchestration') || repairedTeamSkill.includes('tampered')) throw new Error('selftest: doctor repair did not regenerate team skill');
|
|
2151
2155
|
if (await exists(path.join(repairTmp, '.agents', 'skills', 'agent-team', 'SKILL.md'))) throw new Error('selftest: doctor repair did not remove deprecated agent-team alias skill');
|
|
2152
2156
|
if (await exists(path.join(repairTmp, '.agents', 'skills', 'stale-sks-generated', 'SKILL.md'))) throw new Error('selftest: doctor repair did not prune stale generated skill from previous SKS manifest');
|
|
2153
|
-
|
|
2157
|
+
for (const name of stalePluginSkillNames) {
|
|
2158
|
+
if (await exists(path.join(repairTmp, '.agents', 'skills', name, 'SKILL.md'))) throw new Error(`selftest: doctor repair left stale generated ${name} plugin shadow skill`);
|
|
2159
|
+
}
|
|
2154
2160
|
if (await exists(path.join(repairTmp, staleCodexAgentRel))) throw new Error('selftest: doctor repair did not prune stale generated agent file from previous SKS manifest');
|
|
2155
2161
|
if (!doctorRepairJson.repair?.project?.skill_install?.removed_stale_generated_skills?.includes('.agents/skills/stale-sks-generated')) throw new Error('selftest: stale skill report');
|
|
2156
2162
|
const generatedCleanupReport = doctorRepairJson.repair?.project?.generated_cleanup || {};
|
|
@@ -2169,6 +2175,8 @@ async function selftest() {
|
|
|
2169
2175
|
await writeJsonAtomic(path.join(doctorGlobalTmp, 'package.json'), { name: 'doctor-global-skill-repair-smoke', version: '0.0.0' });
|
|
2170
2176
|
await initProject(doctorGlobalTmp, { installScope: 'global' });
|
|
2171
2177
|
const doctorGlobalHome = path.join(doctorGlobalTmp, 'home');
|
|
2178
|
+
await ensureDir(path.join(doctorGlobalHome, '.codex'));
|
|
2179
|
+
await writeTextAtomic(path.join(doctorGlobalHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "high"\nservice_tier = "fast"\n\n[features]\nplugins = false\napps = false\n');
|
|
2172
2180
|
for (const name of stalePluginSkillNames) {
|
|
2173
2181
|
await ensureDir(path.join(doctorGlobalHome, '.agents', 'skills', name));
|
|
2174
2182
|
await writeTextAtomic(path.join(doctorGlobalHome, '.agents', 'skills', name, 'SKILL.md'), stalePluginSkillContent(name));
|
|
@@ -2184,6 +2192,7 @@ async function selftest() {
|
|
|
2184
2192
|
const doctorGlobalCodexConfig = await safeReadText(path.join(doctorGlobalHome, '.codex', 'config.toml'));
|
|
2185
2193
|
if (!doctorGlobalRepairJson.repair?.global_codex_config) throw new Error('selftest: doctor global config repair missing');
|
|
2186
2194
|
assertCodexWarn(doctorGlobalCodexConfig, 'doctor global config');
|
|
2195
|
+
if (hasTopLevelCodexModeLock(doctorGlobalCodexConfig)) throw new Error('selftest: doctor global config repair left top-level model_reasoning_effort lock that can hide Codex App plugin UI');
|
|
2187
2196
|
if (missingGeneratedCodexAppFeatureFlags(doctorGlobalCodexConfig).length || hasDeprecatedCodexHooksFeatureFlag(doctorGlobalCodexConfig) || !hasResearchProfileConfig(doctorGlobalCodexConfig)) throw new Error('selftest: doctor global config repair did not restore Codex App feature flags and Research xhigh profiles');
|
|
2188
2197
|
for (const name of stalePluginSkillNames) {
|
|
2189
2198
|
if (await exists(path.join(doctorGlobalHome, '.agents', 'skills', name, 'SKILL.md'))) throw new Error(`selftest: doctor --fix did not remove global generated ${name} plugin shadow skill`);
|
|
@@ -3068,17 +3077,44 @@ async function selftest() {
|
|
|
3068
3077
|
await ensureDir(fakeCodexApp);
|
|
3069
3078
|
await ensureDir(fakeCodexBinDir);
|
|
3070
3079
|
await ensureDir(path.join(appFeatureTmp, '.codex'));
|
|
3071
|
-
|
|
3080
|
+
const codexAppFixtureConfigText = codexConfigText.replace(/(?:^|\n)\[marketplaces\.[^\]\r\n]+\][\s\S]*?(?=\n\[[^\]]+\]|\s*$)/g, '\n').replace(/\n{3,}/g, '\n\n');
|
|
3081
|
+
await writeTextAtomic(path.join(appFeatureTmp, '.codex', 'config.toml'), codexAppFixtureConfigText);
|
|
3082
|
+
const fakeDefaultPluginCacheNames = ['browser', 'chrome', 'computer-use', 'latex', 'documents', 'presentations', 'spreadsheets'];
|
|
3083
|
+
for (const name of fakeDefaultPluginCacheNames) await ensureDir(path.join(appFeatureTmp, '.codex', 'plugins', 'cache', name));
|
|
3072
3084
|
const fakeCodex = path.join(fakeCodexBinDir, 'codex');
|
|
3073
3085
|
await writeTextAtomic(fakeCodex, '#!/bin/sh\nif [ "$1" = "mcp" ] && [ "$2" = "list" ]; then printf "%s\\n" "computer-use enabled" "browser-use enabled"; exit 0; fi\nif [ "$1" = "features" ] && [ "$2" = "list" ]; then cat <<EOF\napps stable true\nbrowser_use stable true\nbrowser_use_external stable true\ncodex_git_commit under development true\ncomputer_use stable true\nfast_mode stable true\nguardian_approval stable true\nhooks stable true\nimage_generation stable true\nin_app_browser stable true\nplugins stable true\nremote_control under development true\ntool_suggest stable true\nEOF\nexit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
|
|
3074
3086
|
await fsp.chmod(fakeCodex, 0o755);
|
|
3075
|
-
const
|
|
3076
|
-
|
|
3087
|
+
const codexAppFixtureOpts = { codex: { bin: fakeCodex, version: 'codex-cli 99.0.0' }, home: appFeatureTmp, cwd: appFeatureTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } };
|
|
3088
|
+
const codexAppFeatureStatus = await codexAppIntegrationStatus(codexAppFixtureOpts);
|
|
3089
|
+
if (!codexAppFeatureStatus.ok || !codexAppFeatureStatus.features?.required_flags_ok || !codexAppFeatureStatus.features?.codex_git_commit || !codexAppFeatureStatus.features?.remote_control || !codexAppFeatureStatus.features?.git_actions?.ok || !codexAppFeatureStatus.features?.fast_mode_config?.ok) throw new Error('selftest: codex-app check did not accept required app feature flags, git actions, remote_control, and unlocked Fast UI config');
|
|
3090
|
+
const codexAppOldCliStatus = await codexAppIntegrationStatus({ codex: { bin: fakeCodex, version: 'codex-cli 0.129.0' }, home: appFeatureTmp, cwd: appFeatureTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } });
|
|
3091
|
+
if (codexAppOldCliStatus.ok || codexAppOldCliStatus.features?.git_actions?.ok || !codexAppOldCliStatus.guidance.some((line) => line.includes('git commit/push actions are blocked'))) throw new Error('selftest: codex-app check did not block commit/push actions on old Codex CLI remote-control');
|
|
3092
|
+
const missingDefaultPluginTmp = tmpdir();
|
|
3093
|
+
await ensureDir(path.join(missingDefaultPluginTmp, '.codex'));
|
|
3094
|
+
const codexConfigWithoutMarketplaceSources = codexConfigText.replace(/(?:^|\n)\[marketplaces\.[^\]\r\n]+\][\s\S]*?(?=\n\[[^\]]+\]|\s*$)/g, '').trim();
|
|
3095
|
+
await writeTextAtomic(path.join(missingDefaultPluginTmp, '.codex', 'config.toml'), `${codexConfigWithoutMarketplaceSources}\n`);
|
|
3096
|
+
const codexAppMissingDefaultPluginStatus = await codexAppIntegrationStatus({ codex: { bin: fakeCodex, version: 'codex-cli 99.0.0' }, home: missingDefaultPluginTmp, cwd: missingDefaultPluginTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } });
|
|
3097
|
+
if (codexAppMissingDefaultPluginStatus.ok || codexAppMissingDefaultPluginStatus.plugins?.default_plugins?.ok || codexAppMissingDefaultPluginStatus.plugins?.picker?.ok || !codexAppMissingDefaultPluginStatus.plugins?.default_plugins?.missing_installed?.includes('browser@openai-bundled') || !codexAppMissingDefaultPluginStatus.guidance.some((line) => line.includes('default plugin source'))) throw new Error('selftest: codex-app check did not block missing default plugin source');
|
|
3098
|
+
await ensureDir(path.join(appFeatureTmp, '.agents', 'skills', 'browser'));
|
|
3099
|
+
await writeTextAtomic(path.join(appFeatureTmp, '.agents', 'skills', 'browser', 'SKILL.md'), stalePluginSkillContent('browser'));
|
|
3100
|
+
const codexAppShadowStatus = await codexAppIntegrationStatus(codexAppFixtureOpts);
|
|
3101
|
+
if (codexAppShadowStatus.ok || codexAppShadowStatus.plugins?.picker?.ok || codexAppShadowStatus.plugins?.skill_shadows?.blocking?.[0]?.name !== 'browser' || codexAppShadowStatus.plugins?.skill_shadows?.generated?.[0]?.name !== 'browser' || !codexAppShadowStatus.guidance.some((line) => line.includes('plugin picker generated skill shadow'))) throw new Error('selftest: codex-app check did not block generated skill shadow that can hide @ plugin picker entries');
|
|
3102
|
+
await fsp.rm(path.join(appFeatureTmp, '.agents', 'skills', 'browser'), { recursive: true, force: true });
|
|
3103
|
+
await ensureDir(path.join(appFeatureTmp, '.agents', 'skills', 'browser'));
|
|
3104
|
+
await writeTextAtomic(path.join(appFeatureTmp, '.agents', 'skills', 'browser', 'SKILL.md'), '---\nname: browser\ndescription: User custom skill, not generated by SKS.\n---\n');
|
|
3105
|
+
const codexAppCustomShadowStatus = await codexAppIntegrationStatus(codexAppFixtureOpts);
|
|
3106
|
+
if (codexAppCustomShadowStatus.ok || codexAppCustomShadowStatus.plugins?.picker?.ok || codexAppCustomShadowStatus.plugins?.skill_shadows?.custom?.[0]?.name !== 'browser' || codexAppCustomShadowStatus.plugins?.skill_shadows?.generated?.length || !codexAppCustomShadowStatus.guidance.some((line) => line.includes('user-owned reserved skill name')) || codexAppCustomShadowStatus.guidance.some((line) => line.includes('plugin picker generated skill shadow'))) throw new Error('selftest: codex-app check did not distinguish user-owned reserved plugin skill names from generated shadows');
|
|
3107
|
+
await fsp.rm(path.join(appFeatureTmp, '.agents', 'skills', 'browser'), { recursive: true, force: true });
|
|
3077
3108
|
const fakeCodexMissing = path.join(fakeCodexBinDir, 'codex-missing-git-commit');
|
|
3078
3109
|
await writeTextAtomic(fakeCodexMissing, '#!/bin/sh\nif [ "$1" = "mcp" ] && [ "$2" = "list" ]; then printf "%s\\n" "computer-use enabled" "browser-use enabled"; exit 0; fi\nif [ "$1" = "features" ] && [ "$2" = "list" ]; then cat <<EOF\napps stable true\nbrowser_use stable true\nbrowser_use_external stable true\ncodex_git_commit under development false\ncomputer_use stable true\nfast_mode stable true\nguardian_approval stable true\nhooks stable true\nimage_generation stable true\nin_app_browser stable true\nplugins stable true\nremote_control under development true\ntool_suggest stable true\nEOF\nexit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
|
|
3079
3110
|
await fsp.chmod(fakeCodexMissing, 0o755);
|
|
3080
3111
|
const codexAppMissingFeatureStatus = await codexAppIntegrationStatus({ codex: { bin: fakeCodexMissing, version: 'codex-cli 99.0.0' }, home: appFeatureTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } });
|
|
3081
|
-
if (codexAppMissingFeatureStatus.ok || codexAppMissingFeatureStatus.features?.required_flags_ok || codexAppMissingFeatureStatus.features?.codex_git_commit) throw new Error('selftest: codex-app check did not block disabled codex_git_commit feature flag');
|
|
3112
|
+
if (codexAppMissingFeatureStatus.ok || codexAppMissingFeatureStatus.features?.required_flags_ok || codexAppMissingFeatureStatus.features?.codex_git_commit || codexAppMissingFeatureStatus.features?.git_actions?.ok) throw new Error('selftest: codex-app check did not block disabled codex_git_commit feature flag');
|
|
3113
|
+
const fakeCodexMissingImageGen = path.join(fakeCodexBinDir, 'codex-missing-imagegen');
|
|
3114
|
+
await writeTextAtomic(fakeCodexMissingImageGen, '#!/bin/sh\nif [ "$1" = "mcp" ] && [ "$2" = "list" ]; then printf "%s\\n" "computer-use enabled" "browser-use enabled"; exit 0; fi\nif [ "$1" = "features" ] && [ "$2" = "list" ]; then cat <<EOF\napps stable true\nbrowser_use stable true\nbrowser_use_external stable true\ncodex_git_commit under development true\ncomputer_use stable true\nfast_mode stable true\nguardian_approval stable true\nhooks stable true\nimage_generation stable false\nin_app_browser stable true\nplugins stable true\nremote_control under development true\ntool_suggest stable true\nEOF\nexit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
|
|
3115
|
+
await fsp.chmod(fakeCodexMissingImageGen, 0o755);
|
|
3116
|
+
const codexAppMissingImageGenStatus = await codexAppIntegrationStatus({ codex: { bin: fakeCodexMissingImageGen, version: 'codex-cli 99.0.0' }, home: appFeatureTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } });
|
|
3117
|
+
if (codexAppMissingImageGenStatus.ok || codexAppMissingImageGenStatus.features?.required_flags_ok || codexAppMissingImageGenStatus.features?.image_generation || !codexAppMissingImageGenStatus.guidance.some((line) => line.includes('image_generation'))) throw new Error('selftest: codex-app check did not block disabled image_generation for imagegen pipelines');
|
|
3082
3118
|
const autoReviewHome = path.join(tmp, 'auto-review-home');
|
|
3083
3119
|
const autoReviewEnv = { HOME: autoReviewHome };
|
|
3084
3120
|
const autoReviewEnabled = await enableAutoReview({ env: autoReviewEnv, high: true });
|
package/src/core/codex-app.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import os from 'node:os';
|
|
|
3
3
|
import fsp from 'node:fs/promises';
|
|
4
4
|
import { exists, runProcess } from './fsx.mjs';
|
|
5
5
|
import { getCodexInfo } from './codex-adapter.mjs';
|
|
6
|
+
import { DEFAULT_CODEX_APP_PLUGINS as DEFAULT_CODEX_APP_PLUGIN_TUPLES, RESERVED_CODEX_PLUGIN_SKILL_NAMES } from './routes.mjs';
|
|
6
7
|
|
|
7
8
|
export const CODEX_APP_DOCS_URL = 'https://developers.openai.com/codex/app/features';
|
|
8
9
|
export const CODEX_CHANGELOG_URL = 'https://developers.openai.com/codex/changelog';
|
|
@@ -22,15 +23,7 @@ const REQUIRED_CODEX_APP_FEATURE_FLAGS = [
|
|
|
22
23
|
'apps',
|
|
23
24
|
'plugins'
|
|
24
25
|
];
|
|
25
|
-
const DEFAULT_CODEX_APP_PLUGINS = [
|
|
26
|
-
{ name: 'browser', marketplace: 'openai-bundled' },
|
|
27
|
-
{ name: 'chrome', marketplace: 'openai-bundled' },
|
|
28
|
-
{ name: 'computer-use', marketplace: 'openai-bundled' },
|
|
29
|
-
{ name: 'latex', marketplace: 'openai-bundled' },
|
|
30
|
-
{ name: 'documents', marketplace: 'openai-primary-runtime' },
|
|
31
|
-
{ name: 'presentations', marketplace: 'openai-primary-runtime' },
|
|
32
|
-
{ name: 'spreadsheets', marketplace: 'openai-primary-runtime' }
|
|
33
|
-
];
|
|
26
|
+
const DEFAULT_CODEX_APP_PLUGINS = DEFAULT_CODEX_APP_PLUGIN_TUPLES.map(([name, marketplace]) => ({ name, marketplace }));
|
|
34
27
|
|
|
35
28
|
export function codexAppCandidatePaths(home = os.homedir(), env = process.env) {
|
|
36
29
|
const candidates = [];
|
|
@@ -128,6 +121,7 @@ export async function codexAppIntegrationStatus(opts = {}) {
|
|
|
128
121
|
const browserUsePath = await findPluginCache('browser-use', opts);
|
|
129
122
|
const computerUsePath = await findPluginCache('computer-use', opts);
|
|
130
123
|
const defaultPlugins = await codexDefaultPluginStatus(opts);
|
|
124
|
+
const pluginSkillShadows = await codexPluginSkillShadowStatus(opts);
|
|
131
125
|
const fastModeConfig = await codexFastModeConfigStatus(opts);
|
|
132
126
|
const computerUseMcpListed = /computer[-_ ]?use/i.test(mcpText);
|
|
133
127
|
const browserUseMcpListed = /browser[-_ ]?use/i.test(mcpText);
|
|
@@ -140,7 +134,9 @@ export async function codexAppIntegrationStatus(opts = {}) {
|
|
|
140
134
|
const browserUseReady = browserUseMcpListed || Boolean(browserUsePath);
|
|
141
135
|
const browserToolReady = inAppBrowserReady || browserUseFeatureReady || browserUseReady;
|
|
142
136
|
const appInstalled = Boolean(appPath);
|
|
143
|
-
const
|
|
137
|
+
const pluginPickerReady = requiredFeatureFlags.tool_suggest && requiredFeatureFlags.plugins && requiredFeatureFlags.apps && defaultPlugins.ok && pluginSkillShadows.ok && fastModeConfig.ok;
|
|
138
|
+
const gitActions = codexGitActionReadiness({ requiredFeatureFlags, remoteControl });
|
|
139
|
+
const ready = appInstalled && Boolean(codex.bin) && mcpList.ok && featureList.ok && requiredFeatureFlagsOk && pluginPickerReady && fastModeConfig.ok && imageGenerationReady && gitActions.ok && computerUseReady && browserToolReady;
|
|
144
140
|
return {
|
|
145
141
|
ok: ready,
|
|
146
142
|
app: {
|
|
@@ -171,6 +167,7 @@ export async function codexAppIntegrationStatus(opts = {}) {
|
|
|
171
167
|
required_flags: requiredFeatureFlags,
|
|
172
168
|
required_flags_ok: requiredFeatureFlagsOk,
|
|
173
169
|
fast_mode_config: fastModeConfig,
|
|
170
|
+
git_actions: gitActions,
|
|
174
171
|
image_generation: imageGenerationReady,
|
|
175
172
|
image_generation_source: imageGenerationReady ? 'codex_features_list' : 'missing',
|
|
176
173
|
in_app_browser: inAppBrowserReady,
|
|
@@ -191,9 +188,17 @@ export async function codexAppIntegrationStatus(opts = {}) {
|
|
|
191
188
|
plugins: {
|
|
192
189
|
computer_use_cache: computerUsePath,
|
|
193
190
|
browser_use_cache: browserUsePath,
|
|
194
|
-
default_plugins: defaultPlugins
|
|
191
|
+
default_plugins: defaultPlugins,
|
|
192
|
+
skill_shadows: pluginSkillShadows,
|
|
193
|
+
picker: {
|
|
194
|
+
ok: pluginPickerReady,
|
|
195
|
+
required_flags_ok: Boolean(requiredFeatureFlags.tool_suggest && requiredFeatureFlags.plugins && requiredFeatureFlags.apps),
|
|
196
|
+
default_plugins_ok: defaultPlugins.ok,
|
|
197
|
+
skill_shadows_ok: pluginSkillShadows.ok,
|
|
198
|
+
fast_mode_config_ok: fastModeConfig.ok
|
|
199
|
+
}
|
|
195
200
|
},
|
|
196
|
-
guidance: codexAppGuidance({ appInstalled, codex, mcpList, featureList, requiredFeatureFlags, requiredFeatureFlagsOk, defaultPlugins, fastModeConfig, imageGenerationReady, inAppBrowserReady, browserUseFeatureReady, computerUseReady, browserUseReady, browserToolReady, computerUseMcpListed, browserUseMcpListed, remoteControl })
|
|
201
|
+
guidance: codexAppGuidance({ appInstalled, codex, mcpList, featureList, requiredFeatureFlags, requiredFeatureFlagsOk, defaultPlugins, pluginSkillShadows, fastModeConfig, gitActions, imageGenerationReady, inAppBrowserReady, browserUseFeatureReady, computerUseReady, browserUseReady, browserToolReady, computerUseMcpListed, browserUseMcpListed, remoteControl })
|
|
197
202
|
};
|
|
198
203
|
}
|
|
199
204
|
|
|
@@ -248,7 +253,7 @@ export function formatCodexRemoteControlStatus(status) {
|
|
|
248
253
|
return lines.filter(Boolean).join('\n');
|
|
249
254
|
}
|
|
250
255
|
|
|
251
|
-
export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, requiredFeatureFlags = {}, requiredFeatureFlagsOk = true, defaultPlugins = { ok: true, missing_enabled: [] }, fastModeConfig = { ok: true, blockers: [] }, imageGenerationReady, inAppBrowserReady, browserUseFeatureReady, computerUseReady, browserUseReady, browserToolReady, computerUseMcpListed, browserUseMcpListed, remoteControl }) {
|
|
256
|
+
export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, requiredFeatureFlags = {}, requiredFeatureFlagsOk = true, defaultPlugins = { ok: true, missing_enabled: [] }, pluginSkillShadows = { ok: true, blocking: [] }, fastModeConfig = { ok: true, blockers: [] }, gitActions = { ok: true, blockers: [] }, imageGenerationReady, inAppBrowserReady, browserUseFeatureReady, computerUseReady, browserUseReady, browserToolReady, computerUseMcpListed, browserUseMcpListed, remoteControl }) {
|
|
252
257
|
const lines = [];
|
|
253
258
|
if (!appInstalled) {
|
|
254
259
|
lines.push('Install and open Codex App for first-party MCP/plugin tools. SKS tmux launch can still run with Codex CLI alone, but Codex Computer Use and imagegen/gpt-image-2 evidence will be unavailable until Codex App is ready.');
|
|
@@ -278,10 +283,29 @@ export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, re
|
|
|
278
283
|
lines.push(`Codex default plugin(s) installed but not enabled: ${defaultPlugins.missing_enabled.join(', ')}. Composer/tool UI can hide built-in surfaces even while feature flags look green.`);
|
|
279
284
|
lines.push('Run: sks doctor --fix');
|
|
280
285
|
}
|
|
286
|
+
if (defaultPlugins?.missing_installed?.length) {
|
|
287
|
+
lines.push(`Codex default plugin source(s) missing: ${defaultPlugins.missing_installed.join(', ')}. The @ plugin picker can hide built-in surfaces when plugin files are absent even if config says enabled.`);
|
|
288
|
+
lines.push('Run: sks doctor --fix, then restart Codex App if the plugin cache was just restored.');
|
|
289
|
+
}
|
|
290
|
+
if (pluginSkillShadows?.generated?.length) {
|
|
291
|
+
const names = pluginSkillShadows.generated.map((entry) => `${entry.name}:${entry.scope}`).join(', ');
|
|
292
|
+
lines.push(`Codex plugin picker generated skill shadow(s) detected: ${names}. Generated SKS skills with first-party plugin names can hide @ plugin entries after upgrades.`);
|
|
293
|
+
lines.push('Run: sks doctor --fix');
|
|
294
|
+
}
|
|
295
|
+
if (pluginSkillShadows?.custom?.length) {
|
|
296
|
+
const names = pluginSkillShadows.custom.map((entry) => `${entry.name}:${entry.scope}`).join(', ');
|
|
297
|
+
lines.push(`Codex plugin picker user-owned reserved skill name(s) detected: ${names}. Rename or remove these custom skills to avoid hiding first-party @ plugin entries; SKS doctor will not delete user-owned skills.`);
|
|
298
|
+
}
|
|
281
299
|
if (fastModeConfig?.blockers?.length) {
|
|
282
300
|
lines.push(`Codex App speed selector can be hidden or locked by config: ${fastModeConfig.blockers.join(', ')}.`);
|
|
283
301
|
lines.push('Run: sks doctor --fix');
|
|
284
302
|
}
|
|
303
|
+
if (!gitActions?.ok) {
|
|
304
|
+
lines.push(`Codex App git commit/push actions are blocked: ${gitActions?.blockers?.join(', ') || 'git action readiness'}. The app Commit, Push, Commit and Push, and PR flows need codex_git_commit, hooks, remote_control, and Codex CLI remote-control support.`);
|
|
305
|
+
lines.push(`Run: sks doctor --fix; if remote-control is still blocked, update Codex CLI to ${CODEX_REMOTE_CONTROL_MIN_VERSION}+ and restart older app-server/TUI sessions.`);
|
|
306
|
+
} else {
|
|
307
|
+
lines.push('Codex App git actions are enabled for Commit, Push, Commit and Push, and PR flows; SKS hooks treat those app metadata actions as lightweight git UI actions.');
|
|
308
|
+
}
|
|
285
309
|
if (appInstalled && (!computerUseReady || !browserToolReady)) {
|
|
286
310
|
lines.push('Open Codex App settings and enable recommended MCP/plugin tools. Codex CLI 0.130.0+ remote-control/app-server sessions can pick up config changes live; restart older CLI/TUI sessions.');
|
|
287
311
|
lines.push('Required for SKS QA-LOOP UI/browser evidence: Codex Computer Use only. Browser tools can support browsing context, but they do not satisfy UI-level E2E verification.');
|
|
@@ -315,7 +339,9 @@ export function formatCodexAppStatus(status, { includeRaw = false } = {}) {
|
|
|
315
339
|
`Remote Ctrl: ${status.remote_control?.ok ? 'ok' : 'missing'}${status.remote_control?.codex_cli?.version_number ? ` min ${status.remote_control.min_version}` : ''}`,
|
|
316
340
|
`App Flags: ${status.features?.required_flags_ok ? 'ok' : `missing ${missingRequiredFeatureFlags(status.features?.required_flags).join(', ') || 'required flags'}`}`,
|
|
317
341
|
`Fast UI: ${status.features?.fast_mode_config?.ok ? 'ok' : `locked ${(status.features?.fast_mode_config?.blockers || []).join(', ') || 'config'}`}`,
|
|
318
|
-
`Default Plugins:${status.plugins?.default_plugins?.ok ? ' ok' : ` missing ${(status.plugins?.default_plugins
|
|
342
|
+
`Default Plugins:${status.plugins?.default_plugins?.ok ? ' ok' : ` missing ${defaultPluginMissingSummary(status.plugins?.default_plugins) || 'plugin install/config'}`}`,
|
|
343
|
+
`Plugin Picker:${status.plugins?.picker?.ok ? ' ok' : ` blocked ${pluginPickerBlockers(status).join(', ') || 'config'}`}`,
|
|
344
|
+
`Git Actions:${status.features?.git_actions?.ok ? ' ok' : ` blocked ${(status.features?.git_actions?.blockers || []).join(', ') || 'config'}`}`,
|
|
319
345
|
`Computer Use:${status.mcp.has_computer_use ? status.mcp.computer_use_source === 'plugin_cache' ? ' installed (verify @Computer in thread)' : ' ok' : ' missing'}`,
|
|
320
346
|
`Browser: ${status.features?.browser_tool_ready ? `ok (${status.features.browser_tool_source})` : status.mcp.has_browser_use ? status.mcp.browser_use_source === 'plugin_cache' ? 'installed (plugin scoped)' : 'ok' : 'missing'}`,
|
|
321
347
|
`Image Gen: ${status.features?.image_generation ? 'ok ($imagegen/gpt-image-2)' : status.features?.checked ? 'missing' : 'not checked'}`,
|
|
@@ -352,6 +378,25 @@ function missingRequiredFeatureFlags(flags = {}) {
|
|
|
352
378
|
return REQUIRED_CODEX_APP_FEATURE_FLAGS.filter((name) => flags?.[name] !== true);
|
|
353
379
|
}
|
|
354
380
|
|
|
381
|
+
function codexGitActionReadiness({ requiredFeatureFlags = {}, remoteControl = {} } = {}) {
|
|
382
|
+
const blockers = [];
|
|
383
|
+
if (requiredFeatureFlags.codex_git_commit !== true) blockers.push('codex_git_commit');
|
|
384
|
+
if (requiredFeatureFlags.hooks !== true) blockers.push('hooks');
|
|
385
|
+
if (requiredFeatureFlags.remote_control !== true) blockers.push('remote_control_feature');
|
|
386
|
+
if (!remoteControl?.ok) blockers.push(remoteControl?.reason || 'codex_cli_remote_control');
|
|
387
|
+
const ok = blockers.length === 0;
|
|
388
|
+
return {
|
|
389
|
+
ok,
|
|
390
|
+
blockers,
|
|
391
|
+
commit: ok,
|
|
392
|
+
push: ok,
|
|
393
|
+
commit_push: ok,
|
|
394
|
+
pull_request: ok,
|
|
395
|
+
required_flags: ['codex_git_commit', 'hooks', 'remote_control'],
|
|
396
|
+
remote_control_min_version: CODEX_REMOTE_CONTROL_MIN_VERSION
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
355
400
|
async function codexDefaultPluginStatus(opts = {}) {
|
|
356
401
|
const home = opts.home || os.homedir();
|
|
357
402
|
const cwd = opts.cwd || process.cwd();
|
|
@@ -376,15 +421,56 @@ async function codexDefaultPluginStatus(opts = {}) {
|
|
|
376
421
|
});
|
|
377
422
|
}
|
|
378
423
|
const installed = entries.filter((entry) => entry.installed);
|
|
424
|
+
const missingInstalled = entries.filter((entry) => !entry.installed).map((entry) => entry.id);
|
|
379
425
|
const missingEnabled = installed.filter((entry) => !entry.enabled).map((entry) => entry.id);
|
|
380
426
|
return {
|
|
381
|
-
ok: missingEnabled.length === 0,
|
|
427
|
+
ok: missingInstalled.length === 0 && missingEnabled.length === 0,
|
|
382
428
|
checked: true,
|
|
383
429
|
entries,
|
|
430
|
+
missing_installed: missingInstalled,
|
|
384
431
|
missing_enabled: missingEnabled
|
|
385
432
|
};
|
|
386
433
|
}
|
|
387
434
|
|
|
435
|
+
async function codexPluginSkillShadowStatus(opts = {}) {
|
|
436
|
+
const home = opts.home || os.homedir();
|
|
437
|
+
const cwd = opts.cwd || process.cwd();
|
|
438
|
+
const roots = [
|
|
439
|
+
{ scope: 'global', root: path.join(home || '', '.agents', 'skills') }
|
|
440
|
+
];
|
|
441
|
+
const projectRoot = path.join(cwd || '', '.agents', 'skills');
|
|
442
|
+
if (path.resolve(projectRoot) !== path.resolve(roots[0].root)) roots.push({ scope: 'project', root: projectRoot });
|
|
443
|
+
const entries = [];
|
|
444
|
+
for (const root of roots) {
|
|
445
|
+
for (const name of RESERVED_CODEX_PLUGIN_SKILL_NAMES) {
|
|
446
|
+
const skillPath = path.join(root.root, name, 'SKILL.md');
|
|
447
|
+
if (!(await exists(skillPath))) continue;
|
|
448
|
+
const text = await readTextIfExists(skillPath);
|
|
449
|
+
entries.push({
|
|
450
|
+
name,
|
|
451
|
+
scope: root.scope,
|
|
452
|
+
path: skillPath,
|
|
453
|
+
generated: isGeneratedSksPluginShadow(text, name)
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
ok: entries.length === 0,
|
|
459
|
+
checked: true,
|
|
460
|
+
reserved_names: RESERVED_CODEX_PLUGIN_SKILL_NAMES,
|
|
461
|
+
blocking: entries,
|
|
462
|
+
generated: entries.filter((entry) => entry.generated),
|
|
463
|
+
custom: entries.filter((entry) => !entry.generated)
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function isGeneratedSksPluginShadow(text = '', name = '') {
|
|
468
|
+
const s = String(text || '');
|
|
469
|
+
if (!new RegExp(`^name:\\s*${escapeRegExp(name)}\\s*$`, 'm').test(s)) return false;
|
|
470
|
+
if (/\bnot generated by SKS\b/i.test(s)) return false;
|
|
471
|
+
return /Sneakoscope generated|Codex App pipeline activation:|Dollar-command route generated by SKS|stale plugin collision/i.test(s);
|
|
472
|
+
}
|
|
473
|
+
|
|
388
474
|
async function codexFastModeConfigStatus(opts = {}) {
|
|
389
475
|
const home = opts.home || os.homedir();
|
|
390
476
|
const cwd = opts.cwd || process.cwd();
|
|
@@ -451,6 +537,22 @@ function codexPluginEnabled(configText = '', plugin = {}) {
|
|
|
451
537
|
return /(?:^|\n)\s*enabled\s*=\s*true\s*(?:#.*)?(?=\n|$)/.test(block);
|
|
452
538
|
}
|
|
453
539
|
|
|
540
|
+
function pluginPickerBlockers(status = {}) {
|
|
541
|
+
const out = [];
|
|
542
|
+
if (!status.plugins?.picker?.required_flags_ok) out.push('tool_suggest/plugins/apps');
|
|
543
|
+
if (!status.plugins?.picker?.default_plugins_ok) out.push('default_plugins');
|
|
544
|
+
if (!status.plugins?.picker?.skill_shadows_ok) out.push('skill_shadows');
|
|
545
|
+
if (!status.plugins?.picker?.fast_mode_config_ok) out.push('fast_mode_config');
|
|
546
|
+
return out;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function defaultPluginMissingSummary(defaultPlugins = {}) {
|
|
550
|
+
return [
|
|
551
|
+
...(defaultPlugins?.missing_installed || []),
|
|
552
|
+
...(defaultPlugins?.missing_enabled || [])
|
|
553
|
+
].join(', ');
|
|
554
|
+
}
|
|
555
|
+
|
|
454
556
|
function topLevelToml(text = '') {
|
|
455
557
|
const lines = String(text || '').split('\n');
|
|
456
558
|
const firstTable = lines.findIndex((line) => /^\s*\[.+\]\s*$/.test(line));
|
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.9.
|
|
8
|
+
export const PACKAGE_VERSION = '0.9.2';
|
|
9
9
|
export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
|
|
10
10
|
export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
|
|
11
11
|
|
|
@@ -59,6 +59,83 @@ function extractCommand(payload) {
|
|
|
59
59
|
return payload.command || payload.tool_input?.command || payload.toolInput?.command || payload.input?.command || payload.tool?.input?.command || '';
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
function codexGitActionMetadataText(payload = {}) {
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
const out = [];
|
|
65
|
+
const interesting = new Set([
|
|
66
|
+
'action',
|
|
67
|
+
'intent',
|
|
68
|
+
'operation',
|
|
69
|
+
'permission',
|
|
70
|
+
'description',
|
|
71
|
+
'kind',
|
|
72
|
+
'type',
|
|
73
|
+
'feature',
|
|
74
|
+
'tool_name',
|
|
75
|
+
'toolName',
|
|
76
|
+
'name',
|
|
77
|
+
'label',
|
|
78
|
+
'title',
|
|
79
|
+
'source',
|
|
80
|
+
'event',
|
|
81
|
+
'hook',
|
|
82
|
+
'hook_name',
|
|
83
|
+
'hookName',
|
|
84
|
+
'hook_event_name',
|
|
85
|
+
'hookEventName',
|
|
86
|
+
'id',
|
|
87
|
+
'command'
|
|
88
|
+
]);
|
|
89
|
+
const noisy = new Set([
|
|
90
|
+
'prompt',
|
|
91
|
+
'user_prompt',
|
|
92
|
+
'userPrompt',
|
|
93
|
+
'message',
|
|
94
|
+
'assistant_message',
|
|
95
|
+
'last_assistant_message',
|
|
96
|
+
'response',
|
|
97
|
+
'raw',
|
|
98
|
+
'stdout',
|
|
99
|
+
'stderr'
|
|
100
|
+
]);
|
|
101
|
+
function walk(value, depth = 0, parentKey = '') {
|
|
102
|
+
if (!value || typeof value !== 'object' || depth > 5 || seen.has(value)) return;
|
|
103
|
+
seen.add(value);
|
|
104
|
+
for (const [key, candidate] of Object.entries(value)) {
|
|
105
|
+
if (noisy.has(key)) continue;
|
|
106
|
+
if (typeof candidate === 'string') {
|
|
107
|
+
if (interesting.has(key) || /\b(?:codex[_\s-]*app|git[_\s-]*actions?|codex_git_|gitCommit|gitPush|pull\s+request)\b/i.test(candidate)) {
|
|
108
|
+
out.push(`${key}:${candidate}`);
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (candidate && typeof candidate === 'object') {
|
|
113
|
+
const allowedContainer = interesting.has(key)
|
|
114
|
+
|| /^(?:input|metadata|context|client|thread|session|request|payload|tool|tool_input|toolInput|permission_request|permissionRequest)$/i.test(key)
|
|
115
|
+
|| parentKey;
|
|
116
|
+
if (allowedContainer) walk(candidate, depth + 1, key);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
walk(payload);
|
|
121
|
+
return out.join(' ');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function codexGitActionMetadataSignal(text = '') {
|
|
125
|
+
const s = String(text || '');
|
|
126
|
+
if (!s) return false;
|
|
127
|
+
const action = String(s)
|
|
128
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
129
|
+
.replace(/[_-]+/g, ' ');
|
|
130
|
+
if (/\bcodex\s*app\b[\s\S]{0,120}\bgit\b[\s\S]{0,120}\b(?:action|actions|commit|push|pr|pull request)\b/i.test(action)) return true;
|
|
131
|
+
if (/\bgit\s*actions?\b[\s\S]{0,120}\b(?:commit|push|pr|pull request|commit\s*(?:and|&)\s*push)\b/i.test(action)) return true;
|
|
132
|
+
if (/\bcodex\s*git\s*(?:commit|push|pr|pull request|commit\s*(?:and|&)\s*push)\b/i.test(action)) return true;
|
|
133
|
+
if (/\b(?:git\s*)?(?:commit|push|commit\s*(?:and|&)\s*push|create\s+(?:a\s+)?pull\s+request|pull\s+request|pr)\b/i.test(action)) {
|
|
134
|
+
return /\b(?:action|intent|operation|permission|feature|tool\s*name|source|event|hook|name|label|title|type|kind|id)\s*:/i.test(action);
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
62
139
|
function toolFailed(payload = {}) {
|
|
63
140
|
const candidates = [
|
|
64
141
|
payload.exit_code,
|
|
@@ -335,6 +412,7 @@ function looksLikeUserGitAction(payload = {}) {
|
|
|
335
412
|
const command = extractCommand(payload);
|
|
336
413
|
const haystack = [
|
|
337
414
|
command,
|
|
415
|
+
codexGitActionMetadataText(payload),
|
|
338
416
|
payload.action,
|
|
339
417
|
payload.intent,
|
|
340
418
|
payload.operation,
|
|
@@ -345,6 +423,7 @@ function looksLikeUserGitAction(payload = {}) {
|
|
|
345
423
|
payload.toolName
|
|
346
424
|
].filter(Boolean).join(' ');
|
|
347
425
|
if (/\b(?:reset\s+--hard|clean\s+-[^\s]*f|checkout\s+--|restore\s+|rm\s+|push\s+--force|push\s+-[^\s]*f)\b/i.test(command)) return false;
|
|
426
|
+
if (codexGitActionMetadataSignal(haystack)) return true;
|
|
348
427
|
if (/\bcodex\b[\s_-]*(?:app\s*)?(?:git\s*)?(?:action|commit|push|pr)\b/i.test(haystack)) return true;
|
|
349
428
|
if (!/^\s*git\s+/i.test(command)) return false;
|
|
350
429
|
return /\bgit\s+(?:status|diff|add|commit|push|branch|remote|rev-parse|log)\b/i.test(command);
|
|
@@ -474,7 +553,9 @@ function explicitConversationId(payload = {}) {
|
|
|
474
553
|
|
|
475
554
|
function looksLikeCodexGitAction(payload = {}) {
|
|
476
555
|
const prompt = stripVisibleDecisionAnswerBlocks(extractUserPrompt(payload));
|
|
556
|
+
const metadataText = codexGitActionMetadataText(payload);
|
|
477
557
|
const haystack = [
|
|
558
|
+
metadataText,
|
|
478
559
|
payload.action,
|
|
479
560
|
payload.intent,
|
|
480
561
|
payload.operation,
|
|
@@ -502,9 +583,10 @@ function looksLikeCodexGitAction(payload = {}) {
|
|
|
502
583
|
].filter(Boolean).join(' ');
|
|
503
584
|
const codexAppGitSignal = /\bcodex[_\s-]*app\b[\s\S]{0,80}\bgit\b[\s\S]{0,80}\b(?:action|actions|commit|push|pr)\b/i.test(haystack);
|
|
504
585
|
const gitActionSignal = /\bgit[_\s-]*actions?\b[\s\S]{0,80}\b(?:commit|push|commit[\s_-]*(?:and|&)?[\s_-]*push)\b/i.test(haystack);
|
|
505
|
-
const appSignal =
|
|
586
|
+
const appSignal = codexGitActionMetadataSignal(metadataText)
|
|
587
|
+
|| codexAppGitSignal
|
|
506
588
|
|| gitActionSignal
|
|
507
|
-
|| /\b(?:codex[_\s-]*(?:app[_\s-]*)?)?(?:git[_\s-]*)?(?:commit[_\s-]*message|git[_\s-]*commit|codex_git_commit)\b/i.test(haystack)
|
|
589
|
+
|| /\b(?:codex[_\s-]*(?:app[_\s-]*)?)?(?:git[_\s-]*)?(?:commit[_\s-]*message|git[_\s-]*commit|git[_\s-]*push|git[_\s-]*pr|codex_git_commit|codex_git_push|codex_git_pr)\b/i.test(haystack)
|
|
508
590
|
|| /커밋\s*메시지\s*생성/i.test(haystack);
|
|
509
591
|
const promptSignal = /\bgenerate(?:\s+a)?(?:\s+git)?\s+commit\s+message\b/i.test(prompt)
|
|
510
592
|
|| /\bcommit\s+message\b[\s\S]{0,80}\b(?:staged|diff|changes?|git)\b/i.test(prompt)
|
|
@@ -524,7 +606,9 @@ function looksLikeStockCodexGitActionPrompt(prompt = '') {
|
|
|
524
606
|
|
|
525
607
|
function looksLikeCodexGitActionStopCompletion(last = '', payload = {}) {
|
|
526
608
|
const text = String(last || '').trim();
|
|
609
|
+
const metadataText = codexGitActionMetadataText(payload);
|
|
527
610
|
const haystack = [
|
|
611
|
+
metadataText,
|
|
528
612
|
payload.action,
|
|
529
613
|
payload.intent,
|
|
530
614
|
payload.operation,
|
|
@@ -539,6 +623,7 @@ function looksLikeCodexGitActionStopCompletion(last = '', payload = {}) {
|
|
|
539
623
|
payload.metadata?.feature,
|
|
540
624
|
payload.metadata?.source
|
|
541
625
|
].filter(Boolean).join(' ');
|
|
626
|
+
if (codexGitActionMetadataSignal(metadataText)) return true;
|
|
542
627
|
if (/\bcodex[_\s-]*app\b[\s\S]{0,80}\bgit\b[\s\S]{0,80}\b(?:action|commit|push|pr)\b/i.test(haystack)) return true;
|
|
543
628
|
if (!text || text.length > 180) return false;
|
|
544
629
|
return /^(?:commit(?:ted)?(?:\s+and\s+pushed)?(?:\s+changes)?(?:\s+complete[.!]?)?|push(?:ed)?(?:\s+changes)?(?:\s+complete[.!]?)?|created\s+(?:a\s+)?pull\s+request[.!]?)$/i.test(text);
|
|
@@ -1010,6 +1095,14 @@ export async function selftestCodexCommitHooks() {
|
|
|
1010
1095
|
const appCommitPushStop = await runHook('stop', { conversation_id: commitPushId, last_assistant_message: 'Commit and push complete.' });
|
|
1011
1096
|
if (appCommitPushStop.code !== 0) throw new Error(`selftest failed: app commit-push stop ${appCommitPushStop.code}: ${appCommitPushStop.stderr}`);
|
|
1012
1097
|
if (JSON.parse(appCommitPushStop.stdout).decision === 'block') throw new Error('selftest failed: app commit-push stop bypass');
|
|
1098
|
+
const appPushId = 'app-push-selftest';
|
|
1099
|
+
const appPushHook = await runHook('user-prompt-submit', { conversation_id: appPushId, metadata: { source: 'codex_app', action: 'Git Actions Push' }, prompt: 'Push changes.' });
|
|
1100
|
+
if (appPushHook.code !== 0) throw new Error(`selftest failed: app push hook ${appPushHook.code}: ${appPushHook.stderr}`);
|
|
1101
|
+
const appPushJson = JSON.parse(appPushHook.stdout);
|
|
1102
|
+
if (appPushJson.decision === 'block' || appPushJson.hookSpecificOutput?.additionalContext || !String(appPushJson.systemMessage || '').includes('git action')) throw new Error('selftest failed: app push metadata route bypass');
|
|
1103
|
+
const appPushStop = await runHook('stop', { conversation_id: appPushId, metadata: { source: 'codex_app', action: 'Git Actions Push' }, last_assistant_message: 'Done.' });
|
|
1104
|
+
if (appPushStop.code !== 0) throw new Error(`selftest failed: app push stop ${appPushStop.code}: ${appPushStop.stderr}`);
|
|
1105
|
+
if (JSON.parse(appPushStop.stdout).decision === 'block') throw new Error('selftest failed: app push metadata stop bypass');
|
|
1013
1106
|
const metadataLightId = 'metadata-light-commit-push-selftest';
|
|
1014
1107
|
const metadataLightHook = await runHook('user-prompt-submit', { conversation_id: metadataLightId, prompt: 'Commit and push changes.' });
|
|
1015
1108
|
if (metadataLightHook.code !== 0) throw new Error(`selftest failed: metadata-light commit-push hook ${metadataLightHook.code}: ${metadataLightHook.stderr}`);
|
package/src/core/init.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { isHarnessSourceProject, writeHarnessGuardPolicy } from './harness-guard
|
|
|
7
7
|
import { repairSksGeneratedArtifacts } from './harness-conflicts.mjs';
|
|
8
8
|
import { disableVersionGitHook } from './version-manager.mjs';
|
|
9
9
|
import { MIN_TEAM_REVIEWER_LANES, MIN_TEAM_REVIEW_POLICY_TEXT } from './team-review-policy.mjs';
|
|
10
|
-
import { AWESOME_DESIGN_MD_REFERENCE, CODEX_APP_IMAGE_GENERATION_DOC_URL, CODEX_COMPUTER_USE_ONLY_POLICY, CODEX_IMAGEGEN_REQUIRED_POLICY, DESIGN_SYSTEM_SSOT, DOLLAR_COMMANDS, DOLLAR_COMMAND_ALIASES, DOLLAR_SKILL_NAMES, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, GETDESIGN_REFERENCE, PPT_CONDITIONAL_SKILL_ALLOWLIST, PPT_PIPELINE_MCP_ALLOWLIST, PPT_PIPELINE_SKILL_ALLOWLIST, RECOMMENDED_DESIGN_REFERENCES, RECOMMENDED_MCP_SERVERS, RECOMMENDED_SKILLS, RESERVED_CODEX_PLUGIN_SKILL_NAMES, SOLUTION_SCOUT_SKILL_NAME, chatCaptureIntakeText, context7ConfigToml, getdesignReferencePolicyText, imageUxReviewPipelinePolicyText, outcomeRubricPolicyText, pptPipelineAllowlistPolicyText, solutionScoutPolicyText, speedLanePolicyText, stackCurrentDocsPolicyText, triwikiContextTracking, triwikiContextTrackingText, triwikiStagePolicyText } from './routes.mjs';
|
|
10
|
+
import { AWESOME_DESIGN_MD_REFERENCE, CODEX_APP_IMAGE_GENERATION_DOC_URL, CODEX_COMPUTER_USE_ONLY_POLICY, CODEX_IMAGEGEN_REQUIRED_POLICY, DEFAULT_CODEX_APP_PLUGINS, DESIGN_SYSTEM_SSOT, DOLLAR_COMMANDS, DOLLAR_COMMAND_ALIASES, DOLLAR_SKILL_NAMES, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, GETDESIGN_REFERENCE, PPT_CONDITIONAL_SKILL_ALLOWLIST, PPT_PIPELINE_MCP_ALLOWLIST, PPT_PIPELINE_SKILL_ALLOWLIST, RECOMMENDED_DESIGN_REFERENCES, RECOMMENDED_MCP_SERVERS, RECOMMENDED_SKILLS, RESERVED_CODEX_PLUGIN_SKILL_NAMES, SOLUTION_SCOUT_SKILL_NAME, chatCaptureIntakeText, context7ConfigToml, getdesignReferencePolicyText, imageUxReviewPipelinePolicyText, outcomeRubricPolicyText, pptPipelineAllowlistPolicyText, solutionScoutPolicyText, speedLanePolicyText, stackCurrentDocsPolicyText, triwikiContextTracking, triwikiContextTrackingText, triwikiStagePolicyText } from './routes.mjs';
|
|
11
11
|
import { SKILL_DREAM_POLICY, skillDreamPolicyText } from './skill-forge.mjs';
|
|
12
12
|
|
|
13
13
|
const REFLECTION_MEMORY_PATH = '.sneakoscope/memory/q2_facts/post-route-reflection.md';
|
|
@@ -33,16 +33,6 @@ export const REQUIRED_GENERATED_CODEX_APP_FEATURE_FLAGS = [
|
|
|
33
33
|
'plugins'
|
|
34
34
|
];
|
|
35
35
|
|
|
36
|
-
const DEFAULT_CODEX_APP_PLUGINS = [
|
|
37
|
-
['browser', 'openai-bundled'],
|
|
38
|
-
['chrome', 'openai-bundled'],
|
|
39
|
-
['computer-use', 'openai-bundled'],
|
|
40
|
-
['latex', 'openai-bundled'],
|
|
41
|
-
['documents', 'openai-primary-runtime'],
|
|
42
|
-
['presentations', 'openai-primary-runtime'],
|
|
43
|
-
['spreadsheets', 'openai-primary-runtime']
|
|
44
|
-
];
|
|
45
|
-
|
|
46
36
|
export function hasTopLevelCodexModeLock(text = '') {
|
|
47
37
|
const lines = String(text || '').split('\n');
|
|
48
38
|
const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
|
package/src/core/routes.mjs
CHANGED
|
@@ -35,7 +35,19 @@ export const CODEX_APP_IMAGE_GENERATION_DOC_URL = 'https://developers.openai.com
|
|
|
35
35
|
export const OPENAI_IMAGE_GENERATION_DOC_URL = 'https://developers.openai.com/api/docs/guides/image-generation';
|
|
36
36
|
export const CODEX_COMPUTER_USE_ONLY_POLICY = 'Pipeline UI/browser verification and visual inspection must use Codex Computer Use only. Do not use or install Playwright packages, Chrome MCP, Browser Use, Selenium, Puppeteer, or any other browser automation substitute; if Codex Computer Use is unavailable for the target UI, mark the UI/browser evidence unverified instead of substituting another tool. Codex App readiness/config verification is not target-UI evidence: use the Codex-provided control surfaces `codex features list`, `codex mcp list`, `sks codex-app check`, remote-control status, and plugin/tool exposure, not direct OS Accessibility control of the Codex App bundle. In Codex App prompts, invoke @Computer or @AppName in a new thread when live Computer Use tools are needed for the actual target app or screen; SKS hooks and skills can require the policy but cannot attach missing host tools to an already-started turn.';
|
|
37
37
|
export const CODEX_IMAGEGEN_REQUIRED_POLICY = 'Pipeline image generation, raster asset creation/editing, and generated image-review evidence must use real Codex App imagegen/$imagegen with gpt-image-2 when that evidence is required. Do not substitute placeholder SVG/HTML/CSS, prose-only critique, stock-like stand-ins, manually fabricated files, or missing-output ledgers for requested/generated raster assets or required generated review images. If imagegen/gpt-image-2 is unavailable, record the blocker and mark the image asset or review evidence unverified instead of passing the gate. In Codex App prompts, invoke $imagegen when live image generation is needed; SKS hooks and skills can require the policy but cannot attach missing host image-generation tools to an already-started turn.';
|
|
38
|
-
export const
|
|
38
|
+
export const DEFAULT_CODEX_APP_PLUGINS = Object.freeze([
|
|
39
|
+
['browser', 'openai-bundled'],
|
|
40
|
+
['chrome', 'openai-bundled'],
|
|
41
|
+
['computer-use', 'openai-bundled'],
|
|
42
|
+
['latex', 'openai-bundled'],
|
|
43
|
+
['documents', 'openai-primary-runtime'],
|
|
44
|
+
['presentations', 'openai-primary-runtime'],
|
|
45
|
+
['spreadsheets', 'openai-primary-runtime']
|
|
46
|
+
]);
|
|
47
|
+
export const RESERVED_CODEX_PLUGIN_SKILL_NAMES = Object.freeze([
|
|
48
|
+
'browser-use',
|
|
49
|
+
...DEFAULT_CODEX_APP_PLUGINS.map(([name]) => name)
|
|
50
|
+
].sort());
|
|
39
51
|
export const FORBIDDEN_BROWSER_AUTOMATION_RE = /\b(playwright|chrome\s+mcp|browser\s+use|selenium|puppeteer)\b/i;
|
|
40
52
|
|
|
41
53
|
export function evidenceMentionsForbiddenBrowserAutomation(value, seen = new Set()) {
|
|
@@ -499,10 +511,8 @@ export const DOLLAR_COMMANDS = ROUTES.flatMap(({ command, route, description, do
|
|
|
499
511
|
]);
|
|
500
512
|
export function routeAppSkillNames(route) {
|
|
501
513
|
const canonical = dollarSkillName(route.command);
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
...(route.appSkillAliases || [])
|
|
505
|
-
];
|
|
514
|
+
const reserved = new Set(RESERVED_CODEX_PLUGIN_SKILL_NAMES);
|
|
515
|
+
return [canonical, ...(route.appSkillAliases || [])].filter((name) => !reserved.has(name));
|
|
506
516
|
}
|
|
507
517
|
|
|
508
518
|
export const DOLLAR_SKILL_NAMES = ROUTES.flatMap((route) => routeAppSkillNames(route));
|