sneakoscope 0.8.0 → 0.8.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.
@@ -7,7 +7,30 @@ import { getCodexInfo } from './codex-adapter.mjs';
7
7
  export const CODEX_APP_DOCS_URL = 'https://developers.openai.com/codex/app/features';
8
8
  export const CODEX_CHANGELOG_URL = 'https://developers.openai.com/codex/changelog';
9
9
  export const CODEX_REMOTE_CONTROL_MIN_VERSION = '0.130.0';
10
- const REQUIRED_CODEX_APP_FEATURE_FLAGS = ['codex_git_commit', 'hooks', 'fast_mode', 'computer_use', 'apps', 'plugins'];
10
+ const REQUIRED_CODEX_APP_FEATURE_FLAGS = [
11
+ 'codex_git_commit',
12
+ 'hooks',
13
+ 'remote_control',
14
+ 'fast_mode',
15
+ 'computer_use',
16
+ 'browser_use',
17
+ 'browser_use_external',
18
+ 'image_generation',
19
+ 'in_app_browser',
20
+ 'guardian_approval',
21
+ 'tool_suggest',
22
+ 'apps',
23
+ 'plugins'
24
+ ];
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
+ ];
11
34
 
12
35
  export function codexAppCandidatePaths(home = os.homedir(), env = process.env) {
13
36
  const candidates = [];
@@ -104,15 +127,20 @@ export async function codexAppIntegrationStatus(opts = {}) {
104
127
  const featureText = `${featureList.stdout}\n${featureList.stderr}`;
105
128
  const browserUsePath = await findPluginCache('browser-use', opts);
106
129
  const computerUsePath = await findPluginCache('computer-use', opts);
130
+ const defaultPlugins = await codexDefaultPluginStatus(opts);
131
+ const fastModeConfig = await codexFastModeConfigStatus(opts);
107
132
  const computerUseMcpListed = /computer[-_ ]?use/i.test(mcpText);
108
133
  const browserUseMcpListed = /browser[-_ ]?use/i.test(mcpText);
109
134
  const imageGenerationReady = codexFeatureEnabled(featureText, 'image_generation');
135
+ const inAppBrowserReady = codexFeatureEnabled(featureText, 'in_app_browser');
136
+ const browserUseFeatureReady = codexFeatureEnabled(featureText, 'browser_use');
110
137
  const requiredFeatureFlags = Object.fromEntries(REQUIRED_CODEX_APP_FEATURE_FLAGS.map((name) => [name, codexFeatureEnabled(featureText, name)]));
111
138
  const requiredFeatureFlagsOk = Object.values(requiredFeatureFlags).every(Boolean);
112
139
  const computerUseReady = computerUseMcpListed || Boolean(computerUsePath);
113
140
  const browserUseReady = browserUseMcpListed || Boolean(browserUsePath);
141
+ const browserToolReady = inAppBrowserReady || browserUseFeatureReady || browserUseReady;
114
142
  const appInstalled = Boolean(appPath);
115
- const ready = appInstalled && Boolean(codex.bin) && mcpList.ok && featureList.ok && requiredFeatureFlagsOk && imageGenerationReady && computerUseReady && browserUseReady;
143
+ const ready = appInstalled && Boolean(codex.bin) && mcpList.ok && featureList.ok && requiredFeatureFlagsOk && defaultPlugins.ok && fastModeConfig.ok && imageGenerationReady && computerUseReady && browserToolReady;
116
144
  return {
117
145
  ok: ready,
118
146
  app: {
@@ -142,16 +170,30 @@ export async function codexAppIntegrationStatus(opts = {}) {
142
170
  ...requiredFeatureFlags,
143
171
  required_flags: requiredFeatureFlags,
144
172
  required_flags_ok: requiredFeatureFlagsOk,
173
+ fast_mode_config: fastModeConfig,
145
174
  image_generation: imageGenerationReady,
146
175
  image_generation_source: imageGenerationReady ? 'codex_features_list' : 'missing',
176
+ in_app_browser: inAppBrowserReady,
177
+ browser_use: browserUseFeatureReady,
178
+ browser_tool_ready: browserToolReady,
179
+ browser_tool_source: inAppBrowserReady
180
+ ? 'codex_features_list:in_app_browser'
181
+ : browserUseFeatureReady
182
+ ? 'codex_features_list:browser_use'
183
+ : browserUseMcpListed
184
+ ? 'mcp_list:browser_use'
185
+ : browserUsePath
186
+ ? 'plugin_cache:browser-use'
187
+ : 'missing',
147
188
  stdout: featureList.stdout,
148
189
  stderr: featureList.stderr
149
190
  },
150
191
  plugins: {
151
192
  computer_use_cache: computerUsePath,
152
- browser_use_cache: browserUsePath
193
+ browser_use_cache: browserUsePath,
194
+ default_plugins: defaultPlugins
153
195
  },
154
- guidance: codexAppGuidance({ appInstalled, codex, mcpList, featureList, requiredFeatureFlags, requiredFeatureFlagsOk, imageGenerationReady, computerUseReady, browserUseReady, computerUseMcpListed, browserUseMcpListed, remoteControl })
196
+ guidance: codexAppGuidance({ appInstalled, codex, mcpList, featureList, requiredFeatureFlags, requiredFeatureFlagsOk, defaultPlugins, fastModeConfig, imageGenerationReady, inAppBrowserReady, browserUseFeatureReady, computerUseReady, browserUseReady, browserToolReady, computerUseMcpListed, browserUseMcpListed, remoteControl })
155
197
  };
156
198
  }
157
199
 
@@ -206,7 +248,7 @@ export function formatCodexRemoteControlStatus(status) {
206
248
  return lines.filter(Boolean).join('\n');
207
249
  }
208
250
 
209
- export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, requiredFeatureFlags = {}, requiredFeatureFlagsOk = true, imageGenerationReady, computerUseReady, browserUseReady, computerUseMcpListed, browserUseMcpListed, remoteControl }) {
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 }) {
210
252
  const lines = [];
211
253
  if (!appInstalled) {
212
254
  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.');
@@ -229,13 +271,21 @@ export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, re
229
271
  }
230
272
  if (featureList?.checked && featureList.ok && !requiredFeatureFlagsOk) {
231
273
  const missing = missingRequiredFeatureFlags(requiredFeatureFlags);
232
- lines.push(`Codex App feature flag(s) disabled or missing: ${missing.join(', ')}. Commit message generation and app-only tool paths can fail even when CLI chat works.`);
233
- lines.push('Verify with: codex features list | rg "codex_git_commit|hooks|fast_mode|computer_use|apps|plugins"');
274
+ lines.push(`Codex App feature flag(s) disabled or missing: ${missing.join(', ')}. Commit message generation, mobile/remote-control, and app-only tool paths can fail even when CLI chat works.`);
275
+ lines.push('Verify with: codex features list | rg "codex_git_commit|hooks|remote_control|fast_mode|computer_use|browser_use|browser_use_external|image_generation|in_app_browser|guardian_approval|tool_suggest|apps|plugins"');
234
276
  }
235
- if (appInstalled && (!computerUseReady || !browserUseReady)) {
277
+ if (defaultPlugins?.missing_enabled?.length) {
278
+ 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
+ lines.push('Run: sks doctor --fix');
280
+ }
281
+ if (fastModeConfig?.blockers?.length) {
282
+ lines.push(`Codex App speed selector can be hidden or locked by config: ${fastModeConfig.blockers.join(', ')}.`);
283
+ lines.push('Run: sks doctor --fix');
284
+ }
285
+ if (appInstalled && (!computerUseReady || !browserToolReady)) {
236
286
  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.');
237
- lines.push('Required for SKS QA-LOOP UI/browser evidence: Codex Computer Use only. Browser Use can support non-UI browser context, but it does not satisfy UI-level E2E verification.');
238
- lines.push('Verify with: codex mcp list');
287
+ 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.');
288
+ lines.push('Verify with: codex features list; codex mcp list');
239
289
  }
240
290
  if (imageGenerationReady) {
241
291
  lines.push('Image generation is enabled; required raster assets and generated image-review evidence must invoke $imagegen/gpt-image-2 and record real output.');
@@ -245,6 +295,10 @@ export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, re
245
295
  if (computerUseReady && !computerUseMcpListed) {
246
296
  lines.push('Computer Use plugin files are installed, but this check cannot prove the current thread exposes the live Computer Use tools. Start a new Codex App thread and invoke @Computer or @AppName for the actual target app or screen; Codex App readiness itself should stay on `codex features list`, `codex mcp list`, and `sks codex-app check`.');
247
297
  }
298
+ if (browserToolReady) {
299
+ const source = inAppBrowserReady ? 'in-app browser feature' : browserUseFeatureReady ? 'browser_use feature' : 'Browser Use plugin';
300
+ lines.push(`Browser tooling is visible via ${source}; prefer the first-party in-app browser for local web apps, and keep Codex Computer Use as the only accepted UI verification evidence source.`);
301
+ }
248
302
  if (browserUseReady && !browserUseMcpListed) {
249
303
  lines.push('Browser Use plugin files are installed, but `codex mcp list` does not list a browser-use MCP server. Treat Browser Use as plugin-scoped, not as SKS UI verification evidence.');
250
304
  }
@@ -260,8 +314,10 @@ export function formatCodexAppStatus(status, { includeRaw = false } = {}) {
260
314
  `Codex CLI: ${status.codex_cli.ok ? 'ok' : 'missing'}${status.codex_cli.version ? ` ${status.codex_cli.version}` : ''}`,
261
315
  `Remote Ctrl: ${status.remote_control?.ok ? 'ok' : 'missing'}${status.remote_control?.codex_cli?.version_number ? ` min ${status.remote_control.min_version}` : ''}`,
262
316
  `App Flags: ${status.features?.required_flags_ok ? 'ok' : `missing ${missingRequiredFeatureFlags(status.features?.required_flags).join(', ') || 'required flags'}`}`,
317
+ `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?.missing_enabled || []).join(', ') || 'enabled plugin config'}`}`,
263
319
  `Computer Use:${status.mcp.has_computer_use ? status.mcp.computer_use_source === 'plugin_cache' ? ' installed (verify @Computer in thread)' : ' ok' : ' missing'}`,
264
- `Browser Use: ${status.mcp.has_browser_use ? status.mcp.browser_use_source === 'plugin_cache' ? 'installed (plugin scoped)' : 'ok' : 'missing'}`,
320
+ `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'}`,
265
321
  `Image Gen: ${status.features?.image_generation ? 'ok ($imagegen/gpt-image-2)' : status.features?.checked ? 'missing' : 'not checked'}`,
266
322
  `Ready: ${status.ok ? 'yes' : 'no'}`,
267
323
  '',
@@ -296,6 +352,120 @@ function missingRequiredFeatureFlags(flags = {}) {
296
352
  return REQUIRED_CODEX_APP_FEATURE_FLAGS.filter((name) => flags?.[name] !== true);
297
353
  }
298
354
 
355
+ async function codexDefaultPluginStatus(opts = {}) {
356
+ const home = opts.home || os.homedir();
357
+ const cwd = opts.cwd || process.cwd();
358
+ const globalConfigPath = path.join(home || '', '.codex', 'config.toml');
359
+ const projectConfigPath = path.join(cwd || '', '.codex', 'config.toml');
360
+ const globalConfig = await readTextIfExists(globalConfigPath);
361
+ const projectConfig = path.resolve(projectConfigPath) === path.resolve(globalConfigPath)
362
+ ? ''
363
+ : await readTextIfExists(projectConfigPath);
364
+ const configText = `${globalConfig}\n${projectConfig}`;
365
+ const entries = [];
366
+ for (const plugin of DEFAULT_CODEX_APP_PLUGINS) {
367
+ const source = await findDefaultPluginSource(plugin, { home, configText });
368
+ const enabled = codexPluginEnabled(configText, plugin);
369
+ entries.push({
370
+ id: `${plugin.name}@${plugin.marketplace}`,
371
+ name: plugin.name,
372
+ marketplace: plugin.marketplace,
373
+ installed: Boolean(source),
374
+ source,
375
+ enabled
376
+ });
377
+ }
378
+ const installed = entries.filter((entry) => entry.installed);
379
+ const missingEnabled = installed.filter((entry) => !entry.enabled).map((entry) => entry.id);
380
+ return {
381
+ ok: missingEnabled.length === 0,
382
+ checked: true,
383
+ entries,
384
+ missing_enabled: missingEnabled
385
+ };
386
+ }
387
+
388
+ async function codexFastModeConfigStatus(opts = {}) {
389
+ const home = opts.home || os.homedir();
390
+ const cwd = opts.cwd || process.cwd();
391
+ const globalConfigPath = path.join(home || '', '.codex', 'config.toml');
392
+ const projectConfigPath = path.join(cwd || '', '.codex', 'config.toml');
393
+ const configs = [
394
+ { scope: 'global', path: globalConfigPath, text: await readTextIfExists(globalConfigPath) }
395
+ ];
396
+ if (path.resolve(projectConfigPath) !== path.resolve(globalConfigPath)) {
397
+ configs.push({ scope: 'project', path: projectConfigPath, text: await readTextIfExists(projectConfigPath) });
398
+ }
399
+ const blockers = [];
400
+ for (const config of configs) {
401
+ if (!config.text) continue;
402
+ const topLevel = topLevelToml(config.text);
403
+ if (/(^|\n)\s*model_reasoning_effort\s*=/.test(topLevel)) blockers.push(`${config.scope}:top_level_model_reasoning_effort`);
404
+ if (/(^|\n)\s*fast_default_opt_out\s*=\s*true\s*(?:#.*)?(?=\n|$)/.test(tomlTable(config.text, 'notice'))) blockers.push(`${config.scope}:fast_default_opt_out`);
405
+ }
406
+ const merged = configs.map((config) => config.text).join('\n');
407
+ const fastMode = tomlTable(merged, 'user.fast_mode');
408
+ if (!/(^|\n)\s*visible\s*=\s*true\s*(?:#.*)?(?=\n|$)/.test(fastMode)) blockers.push('user.fast_mode.visible_missing');
409
+ if (!/(^|\n)\s*enabled\s*=\s*true\s*(?:#.*)?(?=\n|$)/.test(fastMode)) blockers.push('user.fast_mode.enabled_missing');
410
+ if (!/(^|\n)\s*default_profile\s*=\s*"sks-fast-high"\s*(?:#.*)?(?=\n|$)/.test(fastMode)) blockers.push('user.fast_mode.default_profile_missing');
411
+ return {
412
+ ok: blockers.length === 0,
413
+ checked: true,
414
+ blockers
415
+ };
416
+ }
417
+
418
+ async function readTextIfExists(file) {
419
+ try {
420
+ return await fsp.readFile(file, 'utf8');
421
+ } catch {
422
+ return '';
423
+ }
424
+ }
425
+
426
+ async function findDefaultPluginSource(plugin, { home, configText }) {
427
+ const cached = await findPluginCache(plugin.name, { home });
428
+ if (cached) return cached;
429
+ for (const source of marketplaceSources(configText, plugin.marketplace)) {
430
+ const candidate = path.join(source, 'plugins', plugin.name, '.codex-plugin', 'plugin.json');
431
+ if (await exists(candidate)) return path.dirname(path.dirname(candidate));
432
+ }
433
+ return null;
434
+ }
435
+
436
+ function marketplaceSources(configText = '', marketplaceName = '') {
437
+ const table = `marketplaces.${marketplaceName}`;
438
+ const re = new RegExp(`(?:^|\\n)\\[${escapeRegExp(table)}\\]([\\s\\S]*?)(?=\\n\\[[^\\]]+\\]|\\s*$)`, 'g');
439
+ const sources = [];
440
+ for (const match of String(configText || '').matchAll(re)) {
441
+ const source = match[1].match(/(?:^|\n)\s*source\s*=\s*"([^"]+)"/)?.[1];
442
+ if (source) sources.push(source);
443
+ }
444
+ return Array.from(new Set(sources));
445
+ }
446
+
447
+ function codexPluginEnabled(configText = '', plugin = {}) {
448
+ const table = `plugins."${plugin.name}@${plugin.marketplace}"`;
449
+ const re = new RegExp(`(?:^|\\n)\\[${escapeRegExp(table)}\\]([\\s\\S]*?)(?=\\n\\[[^\\]]+\\]|\\s*$)`);
450
+ const block = String(configText || '').match(re)?.[1] || '';
451
+ return /(?:^|\n)\s*enabled\s*=\s*true\s*(?:#.*)?(?=\n|$)/.test(block);
452
+ }
453
+
454
+ function topLevelToml(text = '') {
455
+ const lines = String(text || '').split('\n');
456
+ const firstTable = lines.findIndex((line) => /^\s*\[.+\]\s*$/.test(line));
457
+ return (firstTable === -1 ? lines : lines.slice(0, firstTable)).join('\n');
458
+ }
459
+
460
+ function tomlTable(text = '', table = '') {
461
+ const re = new RegExp(`(?:^|\\n)\\[${escapeRegExp(table)}\\]([\\s\\S]*?)(?=\\n\\[[^\\]]+\\]|\\s*$)`);
462
+ return String(text || '').match(re)?.[1] || '';
463
+ }
464
+
465
+ function escapeRegExp(text = '') {
466
+ return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
467
+ }
468
+
299
469
  function remoteControlGuidance(status = {}) {
300
470
  if (!status.codex_cli?.ok) return 'Codex remote-control requires Codex CLI 0.130.0+. Install with: npm i -g @openai/codex@latest';
301
471
  if (status.reason === 'codex_cli_version_unknown') return 'Codex remote-control requires Codex CLI 0.130.0+, but the installed CLI version could not be parsed. Check: codex --version';
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.8.0';
8
+ export const PACKAGE_VERSION = '0.8.2';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
@@ -20,7 +20,7 @@ const CODEX_GIT_ACTION_STOP_ARTIFACT = 'codex-git-action-stop-bypass.json';
20
20
  const STOP_REPEAT_GUARD_WINDOW_MS = 10 * 60 * 1000;
21
21
  const STOP_REPEAT_GUARD_MAX_ENTRIES = 25;
22
22
  const DEFAULT_STOP_REPEAT_GUARD_LIMIT = 2;
23
- const CODEX_GIT_ACTION_STOP_TTL_MS = 5 * 60 * 1000;
23
+ const CODEX_GIT_ACTION_STOP_TTL_MS = 15 * 60 * 1000;
24
24
 
25
25
  async function loadHookPayload() {
26
26
  const raw = await readStdin();
@@ -106,12 +106,39 @@ export async function hookMain(name) {
106
106
  function blockForbiddenClientModel(payload = {}) {
107
107
  const model = forbiddenClientModelFromPayload(payload);
108
108
  if (!model || !isForbiddenCodexModel(model)) return null;
109
+ if (looksLikeCodexUiSettingsEvent(payload)) return null;
109
110
  return {
110
111
  decision: 'block',
111
112
  reason: `SKS requires ${REQUIRED_CODEX_MODEL}; client payload requested ${model}. Switch the Codex client/session model to ${REQUIRED_CODEX_MODEL} and retry.`
112
113
  };
113
114
  }
114
115
 
116
+ function looksLikeCodexUiSettingsEvent(payload = {}) {
117
+ const prompt = stripVisibleDecisionAnswerBlocks(extractUserPrompt(payload));
118
+ const haystack = [
119
+ payload.action,
120
+ payload.intent,
121
+ payload.operation,
122
+ payload.permission,
123
+ payload.description,
124
+ payload.kind,
125
+ payload.type,
126
+ payload.feature,
127
+ payload.source,
128
+ payload.event,
129
+ payload.hook,
130
+ payload.hook_name,
131
+ payload.metadata?.action,
132
+ payload.metadata?.intent,
133
+ payload.metadata?.operation,
134
+ payload.metadata?.feature,
135
+ payload.metadata?.source,
136
+ payload.context?.surface,
137
+ payload.session?.surface
138
+ ].filter(Boolean).join(' ');
139
+ return !prompt && /\b(?:settings|preferences|profile|speed|fast[_\s-]*mode|reasoning|model[_\s-]*select|codex[_\s-]*app)\b/i.test(haystack);
140
+ }
141
+
115
142
  function forbiddenClientModelFromPayload(payload = {}) {
116
143
  const candidates = [
117
144
  payload.model,
@@ -148,6 +175,12 @@ async function hookUserPrompt(root, state, payload, noQuestion) {
148
175
  systemMessage: 'SKS: Codex App git action bypassed route gates.'
149
176
  };
150
177
  }
178
+ if (looksLikeCodexUiSettingsEvent(payload)) {
179
+ return {
180
+ continue: true,
181
+ systemMessage: 'SKS: Codex App settings/profile event ignored; route gates unchanged.'
182
+ };
183
+ }
151
184
  if (!noQuestion) {
152
185
  const prompt = stripVisibleDecisionAnswerBlocks(extractUserPrompt(payload));
153
186
  const madSksConfirmation = await handleMadSksUserConfirmation(root, state, prompt);
@@ -359,6 +392,12 @@ async function hookStop(root, state, payload, noQuestion) {
359
392
  systemMessage: 'SKS: Codex App git action accepted without route finalization gates.'
360
393
  };
361
394
  }
395
+ if (looksLikeCodexGitActionStopCompletion(last, payload)) {
396
+ return {
397
+ continue: true,
398
+ systemMessage: 'SKS: Codex App git action completion accepted without route finalization gates.'
399
+ };
400
+ }
362
401
  if (!noQuestion && (hasDfixLightCompletion(last) || await consumeLightRouteStop(root, payload))) {
363
402
  return {
364
403
  continue: true,
@@ -469,12 +508,42 @@ function looksLikeCodexGitAction(payload = {}) {
469
508
  || /커밋\s*메시지\s*생성/i.test(haystack);
470
509
  const promptSignal = /\bgenerate(?:\s+a)?(?:\s+git)?\s+commit\s+message\b/i.test(prompt)
471
510
  || /\bcommit\s+message\b[\s\S]{0,80}\b(?:staged|diff|changes?|git)\b/i.test(prompt)
511
+ || looksLikeStockCodexGitActionPrompt(prompt)
472
512
  || /커밋\s*메시지\s*생성/i.test(prompt);
473
513
  if (!appSignal && !promptSignal) return false;
514
+ if (looksLikeStockCodexGitActionPrompt(prompt)) return true;
474
515
  if (appSignal) return true;
475
516
  return !looksLikeUserImplementationRequest(prompt);
476
517
  }
477
518
 
519
+ function looksLikeStockCodexGitActionPrompt(prompt = '') {
520
+ const text = String(prompt || '').trim();
521
+ if (!text || text.length > 120) return false;
522
+ return /^(?:generate\s+(?:a\s+)?git\s+commit\s+message(?:\s+for\s+(?:the\s+)?(?:staged\s+)?diff)?|commit\s+changes|commit\s+and\s+push\s+changes|push\s+changes|create\s+(?:a\s+)?commit|create\s+(?:a\s+)?pull\s+request)\.?$/i.test(text);
523
+ }
524
+
525
+ function looksLikeCodexGitActionStopCompletion(last = '', payload = {}) {
526
+ const text = String(last || '').trim();
527
+ const haystack = [
528
+ payload.action,
529
+ payload.intent,
530
+ payload.operation,
531
+ payload.kind,
532
+ payload.type,
533
+ payload.feature,
534
+ payload.source,
535
+ payload.event,
536
+ payload.metadata?.action,
537
+ payload.metadata?.intent,
538
+ payload.metadata?.operation,
539
+ payload.metadata?.feature,
540
+ payload.metadata?.source
541
+ ].filter(Boolean).join(' ');
542
+ 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
+ if (!text || text.length > 180) return false;
544
+ 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);
545
+ }
546
+
478
547
  function looksLikeUserImplementationRequest(text = '') {
479
548
  return /(fix|bug|broken|error|issue|implement|change|update|repair|수정|버그|오류|에러|문제|고쳐|고치|해결|변경|수리|패치|안생기|안\s*생기)/i.test(String(text || ''));
480
549
  }
@@ -941,6 +1010,18 @@ export async function selftestCodexCommitHooks() {
941
1010
  const appCommitPushStop = await runHook('stop', { conversation_id: commitPushId, last_assistant_message: 'Commit and push complete.' });
942
1011
  if (appCommitPushStop.code !== 0) throw new Error(`selftest failed: app commit-push stop ${appCommitPushStop.code}: ${appCommitPushStop.stderr}`);
943
1012
  if (JSON.parse(appCommitPushStop.stdout).decision === 'block') throw new Error('selftest failed: app commit-push stop bypass');
1013
+ const metadataLightId = 'metadata-light-commit-push-selftest';
1014
+ const metadataLightHook = await runHook('user-prompt-submit', { conversation_id: metadataLightId, prompt: 'Commit and push changes.' });
1015
+ if (metadataLightHook.code !== 0) throw new Error(`selftest failed: metadata-light commit-push hook ${metadataLightHook.code}: ${metadataLightHook.stderr}`);
1016
+ const metadataLightJson = JSON.parse(metadataLightHook.stdout);
1017
+ if (metadataLightJson.decision === 'block' || metadataLightJson.hookSpecificOutput?.additionalContext || !String(metadataLightJson.systemMessage || '').includes('git action')) throw new Error('selftest failed: metadata-light app commit-push route bypass');
1018
+ const metadataLightStop = await runHook('stop', { conversation_id: metadataLightId, last_assistant_message: 'Commit and push complete.' });
1019
+ if (metadataLightStop.code !== 0) throw new Error(`selftest failed: metadata-light commit-push stop ${metadataLightStop.code}: ${metadataLightStop.stderr}`);
1020
+ if (JSON.parse(metadataLightStop.stdout).decision === 'block') throw new Error('selftest failed: metadata-light commit-push stop bypass');
1021
+ const settingsHook = await runHook('user-prompt-submit', { model: 'gpt-5.0-forbidden', metadata: { source: 'codex_app_settings', feature: 'speed profile' } });
1022
+ if (settingsHook.code !== 0) throw new Error(`selftest failed: settings hook ${settingsHook.code}: ${settingsHook.stderr}`);
1023
+ const settingsJson = JSON.parse(settingsHook.stdout);
1024
+ if (settingsJson.decision === 'block' || settingsJson.hookSpecificOutput?.additionalContext || !String(settingsJson.systemMessage || '').includes('settings/profile event ignored')) throw new Error('selftest failed: settings/profile event should not route or block');
944
1025
  const userHook = await runHook('user-prompt-submit', { prompt: '[커밋 메시지를 생성하지 못했습니다.] 코덱스 앱에서 이 버그 수정해줘' });
945
1026
  if (userHook.code !== 0) throw new Error(`selftest failed: user commit hook ${userHook.code}: ${userHook.stderr}`);
946
1027
  if (!JSON.parse(userHook.stdout).hookSpecificOutput?.additionalContext?.includes('$Team route prepared')) throw new Error('selftest failed: user prompt route');
package/src/core/init.mjs CHANGED
@@ -17,15 +17,32 @@ const GENERATED_PRUNE_POLICY = 'remove_previous_sks_generated_paths_absent_from_
17
17
 
18
18
  export const REQUIRED_GENERATED_CODEX_APP_FEATURE_FLAGS = [
19
19
  'hooks',
20
+ 'remote_control',
20
21
  'multi_agent',
21
22
  'fast_mode',
22
23
  'fast_mode_ui',
23
24
  'codex_git_commit',
24
25
  'computer_use',
26
+ 'browser_use',
27
+ 'browser_use_external',
28
+ 'image_generation',
29
+ 'in_app_browser',
30
+ 'guardian_approval',
31
+ 'tool_suggest',
25
32
  'apps',
26
33
  'plugins'
27
34
  ];
28
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
+
29
46
  export function hasTopLevelCodexModeLock(text = '') {
30
47
  const lines = String(text || '').split('\n');
31
48
  const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
@@ -491,11 +508,18 @@ function mergeManagedCodexConfigToml(existingContent = '') {
491
508
  next = upsertTopLevelTomlString(next, 'service_tier', 'fast');
492
509
  next = upsertTopLevelTomlBoolean(next, 'suppress_unstable_features_warning', true);
493
510
  next = upsertTomlTableKey(next, 'features', 'hooks = true');
511
+ next = upsertTomlTableKey(next, 'features', 'remote_control = true');
494
512
  next = upsertTomlTableKey(next, 'features', 'multi_agent = true');
495
513
  next = upsertTomlTableKey(next, 'features', 'fast_mode = true');
496
514
  next = upsertTomlTableKey(next, 'features', 'fast_mode_ui = true');
497
515
  next = upsertTomlTableKey(next, 'features', 'codex_git_commit = true');
498
516
  next = upsertTomlTableKey(next, 'features', 'computer_use = true');
517
+ next = upsertTomlTableKey(next, 'features', 'browser_use = true');
518
+ next = upsertTomlTableKey(next, 'features', 'browser_use_external = true');
519
+ next = upsertTomlTableKey(next, 'features', 'image_generation = true');
520
+ next = upsertTomlTableKey(next, 'features', 'in_app_browser = true');
521
+ next = upsertTomlTableKey(next, 'features', 'guardian_approval = true');
522
+ next = upsertTomlTableKey(next, 'features', 'tool_suggest = true');
499
523
  next = upsertTomlTableKey(next, 'features', 'apps = true');
500
524
  next = upsertTomlTableKey(next, 'features', 'plugins = true');
501
525
  next = upsertTomlTableKey(next, 'user.fast_mode', 'visible = true');
@@ -506,6 +530,10 @@ function mergeManagedCodexConfigToml(existingContent = '') {
506
530
  for (const block of managedCodexConfigBlocks()) {
507
531
  next = upsertTomlTable(next, block.table, block.text);
508
532
  }
533
+ for (const [name, marketplace] of DEFAULT_CODEX_APP_PLUGINS) {
534
+ const table = `plugins."${name}@${marketplace}"`;
535
+ next = upsertTomlTable(next, table, `[${table}]\nenabled = true`);
536
+ }
509
537
  return `${next.trim()}\n`;
510
538
  }
511
539
 
@@ -517,6 +545,7 @@ async function mergeGlobalCodexConfigIfAvailable(configText = '', configPath = '
517
545
  if (configPath && path.resolve(configPath) === path.resolve(globalConfigPath)) return configText;
518
546
  const globalConfig = await readText(globalConfigPath, '');
519
547
  let next = mergeGlobalMcpServers(configText, globalConfig);
548
+ next = mergeGlobalCodexAppRuntimeTables(next, globalConfig);
520
549
  if (selectedRe.test(next) && /\[model_providers\.codex-lb\]/.test(next)) return `${String(next || '').trim()}\n`;
521
550
  const envPath = path.join(home, '.codex', 'sks-codex-lb.env');
522
551
  if (!(await exists(envPath))) return next;
@@ -562,18 +591,24 @@ function mergeGlobalMcpServers(configText = '', globalConfig = '') {
562
591
  return next;
563
592
  }
564
593
 
594
+ function mergeGlobalCodexAppRuntimeTables(configText = '', globalConfig = '') {
595
+ let next = configText;
596
+ const re = /(?:^|\n)(\[((?:marketplaces|plugins)\.[^\]\r\n]+)\][\s\S]*?)(?=\n\[[^\]]+\]|\s*$)/g;
597
+ for (const match of String(globalConfig || '').matchAll(re)) {
598
+ const block = match[1].trim();
599
+ const table = match[2].trim();
600
+ if (!new RegExp(`(^|\\n)\\[${escapeRegExp(table)}\\]`).test(next)) next = upsertTomlTable(next, table, block);
601
+ }
602
+ return next;
603
+ }
604
+
565
605
  function removeLegacyTopLevelCodexModeLocks(text = '') {
566
- const legacy = {
567
- model_reasoning_effort: new Set(['high'])
568
- };
569
606
  const lines = String(text || '').split('\n');
570
607
  const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
571
608
  const end = firstTable === -1 ? lines.length : firstTable;
572
609
  return lines.filter((line, index) => {
573
610
  if (index >= end) return true;
574
- const match = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=\s*"([^"]*)"\s*(?:#.*)?$/);
575
- if (!match) return true;
576
- return !legacy[match[1]]?.has(match[2]);
611
+ return !/^\s*model_reasoning_effort\s*=/.test(line);
577
612
  }).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
578
613
  }
579
614
 
@@ -874,7 +909,7 @@ export async function installSkills(root) {
874
909
  'computer-use-fast': `---\nname: computer-use-fast\ndescription: Alias for the maximum-speed $Computer-Use/$CU Codex Computer Use lane.\n---\n\nUse the same rules as computer-use: skip Team debate, QA-LOOP clarification, upfront TriWiki refresh, Context7, subagents, and reflection unless explicitly requested. Use Codex Computer Use directly; never substitute Playwright, Chrome MCP, Browser Use, Selenium, Puppeteer, or other browser automation for UI/browser evidence. At the end only, refresh/pack TriWiki, validate it, then provide a concise completion summary plus Honest Mode.\n`,
875
910
  'cu': `---\nname: cu\ndescription: Short alias for the maximum-speed $Computer-Use Codex Computer Use lane.\n---\n\nUse the same rules as computer-use. This is a speed lane for focused UI/browser/visual tasks that require Codex Computer Use evidence, with TriWiki refresh/validate and Honest Mode deferred to final closeout.\n`,
876
911
  'goal': `---\nname: goal\ndescription: Fast $Goal/$goal bridge overlay for Codex native persisted /goal workflows.\n---\n\nUse when the user invokes $Goal/$goal or asks to persist a workflow with Codex native /goal continuation. Prepare with sks goal create or the $Goal route, write only the lightweight bridge artifacts, then use native Codex /goal create, pause, resume, and clear controls where available. Goal does not replace Team, QA, DB, or other SKS execution routes; continue implementation through the selected route and use Context7 only when external API/library docs are involved. Do not recreate the old no-question loop.\n`,
877
- 'research': `---\nname: research\ndescription: Dollar-command route for $Research or $research frontier discovery workflows.\n---\n\nUse when the user invokes $Research/$research or asks for research, hypotheses, new mechanisms, falsification, or testable predictions. Prefer sks research prepare and sks research run. Run the genius-lens scout council with named persona-inspired cognitive roles: Einstein Scout, Feynman Scout, Turing Scout, von Neumann Scout, and Skeptic Scout. These are lenses only; do not impersonate the historical people. Every Research scout ledger row must include display_name, persona, persona_boundary, effort=xhigh, reasoning_effort=xhigh, service_tier when available, one literal "Eureka!" idea, falsifiers, cheap_probes, and challenge_or_response before synthesis. Create research-source-skill.md as a route-local Skill Creator artifact, then maximize layered public web/source search across papers, official/government or leading-institution data, standards/primary docs, current news, public discourse, developer/practitioner sources, and counterevidence before synthesis. Record research-source-skill.md, source-ledger.json, scout-ledger.json, debate-ledger.json, novelty-ledger.json, falsification-ledger.json, research-report.md, research-paper.md, genius-opinion-summary.md, and research-gate.json. Context7 is optional and only needed when the research topic depends on external package/API/framework docs; do not use it as the default research evidence layer. Normal Research may take one or two hours when needed; favor real source collection, cross-layer comparison, falsification, and a concise paper manuscript over speed. Do not use --mock except for selftests or dry harness checks; if live source execution is unavailable, record a blocker and keep the gate unpassed. Do not use for ordinary code edits.\n`,
912
+ 'research': `---\nname: research\ndescription: Dollar-command route for $Research or $research frontier discovery workflows.\n---\n\nUse when the user invokes $Research/$research or asks for research, hypotheses, new mechanisms, falsification, or testable predictions. Prefer sks research prepare and sks research run. Research is not an implementation route: do not edit repository source, docs, package metadata, generated skills, or harness files; write only route-local mission artifacts under .sneakoscope/missions/<mission-id>/. Run the genius-lens scout council with named persona-inspired cognitive roles: Einstein Scout, Feynman Scout, Turing Scout, von Neumann Scout, and Skeptic Scout. These are lenses only; do not impersonate the historical people. Every Research scout ledger row must include display_name, persona, persona_boundary, effort=xhigh, reasoning_effort=xhigh, service_tier when available, one literal "Eureka!" idea, falsifiers, cheap_probes, and challenge_or_response before synthesis. This is not a fixed three-cycle route: repeat source gathering, Eureka ideas, evidence-bound debate, falsification, and synthesis pressure until every scout records final agreement, or until the explicit max-cycle safety cap pauses with an unpassed gate. Create research-source-skill.md as a route-local Skill Creator artifact, then maximize layered public web/source search across latest papers, official/government or leading-institution data, standards/primary docs, current news, public discourse, developer/practitioner sources, traditional background sources, and counterevidence before synthesis. Record research-source-skill.md, source-ledger.json, scout-ledger.json, debate-ledger.json, novelty-ledger.json, falsification-ledger.json, research-report.md, research-paper.md, genius-opinion-summary.md, and research-gate.json. debate-ledger.json must include consensus_iterations, unanimous_consensus, and per-scout agreements; research-gate.json cannot pass until unanimous_consensus=true with every scout agreement recorded. Context7 is optional and only needed when the research topic depends on external package/API/framework docs; do not use it as the default research evidence layer. Normal Research may take one or two hours when needed; favor real source collection, cross-layer comparison, falsification, and a concise paper manuscript over speed. Do not use --mock except for selftests or dry harness checks; if live source execution is unavailable, record a blocker and keep the gate unpassed. Do not use for ordinary code edits.\n`,
878
913
  'autoresearch': `---\nname: autoresearch\ndescription: Dollar-command route for $AutoResearch or $autoresearch iterative experiment loops.\n---\n\nUse for $AutoResearch, iterative improvement, SEO/GEO, ranking, workflow, benchmark, or experiments. Define program, hypothesis, experiment, metric, keep/discard, falsification, next step, and Honest Mode. Load seo-geo-optimizer for README/npm/GitHub/schema/AI-search work.\n`,
879
914
  'db': `---\nname: db\ndescription: Dollar-command route for $DB or $db database and Supabase safety checks.\n---\n\nUse when the user invokes $DB/$db or the task touches SQL, Supabase, Postgres, migrations, Prisma, Drizzle, Knex, MCP database tools, or production data. Run or follow sks db policy, sks db scan, sks db classify, and sks db check. Destructive database operations remain forbidden.\n`,
880
915
  'mad-sks': `---\nname: mad-sks\ndescription: Explicit high-risk authorization modifier for $MAD-SKS scoped Supabase MCP DB permission widening.\n---\n\nUse only when the user explicitly invokes $MAD-SKS or top-level sks --mad. It can be combined with another route, such as $MAD-SKS $Team or $DB ... $MAD-SKS; in that case the other command remains the primary workflow and MAD-SKS is only the temporary permission grant. The widened permission applies only while the active mission gate is open, must be deactivated when the task ends, and opens live server work, Supabase MCP database writes, column/schema cleanup, direct execute SQL, migration application when required, and normal targeted DB writes. Keep only catastrophic safeguards: whole database/schema/table removal, truncate, all-row delete/update, reset, dangerous project/branch management, credential exfiltration, persistent security weakening, and unrequested fallback implementation remain blocked. Do not carry MAD-SKS permission into later prompts or routes. The permission profile is centralized in src/core/permission-gates.mjs so skill/hook/MCP-style gates share one decision function.\n`,
@@ -921,7 +921,7 @@ async function prepareResearch(root, route, task, required) {
921
921
  await writeResearchPlan(dir, task, {});
922
922
  const pipelinePlan = await writePipelinePlan(dir, { missionId: id, route, task, required, ambiguity: { required: false, status: 'direct_route' } });
923
923
  await setCurrent(root, routeState(id, route, 'RESEARCH_PREPARED', required, { prompt: task, pipeline_plan_ready: validatePipelinePlan(pipelinePlan).ok, pipeline_plan_path: PIPELINE_PLAN_ARTIFACT }));
924
- return routeContext(route, id, task, required, 'Run sks research run latest as a real long-running source-gathering pass, never an automatic mock fallback; create research-source-skill.md, maximize layered public source search, require every scout effort=xhigh plus one Eureka! idea, fill source-ledger.json, scout-ledger.json, debate-ledger.json, novelty-ledger.json, falsification-ledger.json, research-report.md, research-paper.md, genius-opinion-summary.md, and pass research-gate.json.');
924
+ return routeContext(route, id, task, required, 'Run sks research run latest as a real long-running source-gathering pass, never an automatic mock fallback; do not modify repository source code; create research-source-skill.md, maximize layered public source search, require every scout effort=xhigh plus one Eureka! idea, repeat scout/debate/falsification cycles until unanimous_consensus=true for every scout or the explicit safety cap pauses the run, fill source-ledger.json, scout-ledger.json, debate-ledger.json, novelty-ledger.json, falsification-ledger.json, research-report.md, research-paper.md, genius-opinion-summary.md, and pass research-gate.json.');
925
925
  }
926
926
 
927
927
  async function prepareAutoResearch(root, route, task, required) {
@@ -1400,6 +1400,8 @@ function normalizeComplianceReason(reason = '') {
1400
1400
  async function passedActiveGate(root, state) {
1401
1401
  const id = state?.mission_id;
1402
1402
  if (!id) return { ok: false, file: null };
1403
+ const hardBlocker = await passedHardBlocker(root, state);
1404
+ if (hardBlocker.ok) return hardBlocker;
1403
1405
  const files = gateFilesForState(state);
1404
1406
  for (const file of files) {
1405
1407
  const p = path.join(missionDir(root, id), file);
@@ -1414,8 +1416,6 @@ async function passedActiveGate(root, state) {
1414
1416
  return { ok: false, file };
1415
1417
  }
1416
1418
  }
1417
- const hardBlocker = await passedHardBlocker(root, state);
1418
- if (hardBlocker.ok) return hardBlocker;
1419
1419
  return { ok: false, file: files[0] || null };
1420
1420
  }
1421
1421