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.
package/README.md CHANGED
@@ -212,7 +212,7 @@ sks qa-loop prepare "http://localhost:3000"
212
212
  sks qa-loop run latest --max-cycles 2
213
213
  sks goal create "persist this migration workflow"
214
214
  sks research prepare "evaluate this approach"
215
- sks research run latest --max-cycles 3 --cycle-timeout-minutes 120
215
+ sks research run latest --max-cycles 12 --cycle-timeout-minutes 120
216
216
  sks research status latest
217
217
  sks recallpulse run latest
218
218
  sks recallpulse status latest --json
@@ -235,7 +235,7 @@ sks skill-dream run --json
235
235
  sks code-structure scan --json
236
236
  ```
237
237
 
238
- `sks research` prepares a named genius-lens scout council, requires every scout to run at `xhigh`, records one literal `Eureka!` idea per scout, runs an evidence-bound debate, and now creates `research-source-skill.md` as a route-local source collection skill before synthesis. The required Research persona lenses are Einstein Scout, Feynman Scout, Turing Scout, von Neumann Scout, and Skeptic Scout; they are cognitive roles, not impersonations, and `scout-ledger.json` must include `display_name`, `persona`, `persona_boundary`, `reasoning_effort`, falsifiers, cheap probes, and `challenge_or_response`. Normal Research is intentionally allowed to take one or two hours when the problem needs it; `--mock` is only for selftests or dry harness checks, and a real run blocks with `research-blocker.json` instead of silently substituting mock output when the Codex execution path is unavailable. The source layer contract separates latest papers, official/government or leading-institution sources, standards/primary docs, current news such as BBC/CNN/GDELT-style sources, public discourse such as X/Reddit, developer/practitioner knowledge such as Stack Overflow/GitHub, and counterevidence/fact-checking; `source-ledger.json` must record layer coverage, source quality, blockers, citations, and cross-layer triangulation. Context7 is optional for `$Research` and only becomes relevant when the research topic specifically depends on package, API, framework, or SDK documentation. Research runs require `research-report.md`, `research-paper.md`, `genius-opinion-summary.md`, `research-source-skill.md`, `source-ledger.json`, `scout-ledger.json`, `debate-ledger.json`, `novelty-ledger.json`, `falsification-ledger.json`, and `research-gate.json` so they stay source-backed, adversarially checked, falsifiable, paper-ready, and clear about every scout lens opinion. `research status` reports source entries, source-layer coverage, triangulation checks, counterevidence, xhigh scout count, Eureka moments, debate exchanges, paper presence/sections, genius-opinion summary coverage, scout findings, and falsification cases alongside the gate.
238
+ `sks research` prepares a named genius-lens scout council, requires every scout to run at `xhigh`, records one literal `Eureka!` idea per scout, runs an evidence-bound debate, and creates `research-source-skill.md` as a route-local source collection skill before synthesis. Research is not a code-change route: real runs may write only their own mission artifacts under `.sneakoscope/missions/<id>/`, and source/package/docs/config mutations block the run with `research-code-mutation-blocker.json`. The required Research persona lenses are Einstein Scout, Feynman Scout, Turing Scout, von Neumann Scout, and Skeptic Scout; they are cognitive roles, not impersonations, and `scout-ledger.json` must include `display_name`, `persona`, `persona_boundary`, `reasoning_effort`, falsifiers, cheap probes, and `challenge_or_response`. Normal Research is not a fixed three-cycle flow: it repeats source gathering, Eureka ideas, debate, falsification, and synthesis pressure until every scout records final agreement, or pauses at the explicit max-cycle safety cap with an unpassed gate. `debate-ledger.json` must include `consensus_iterations`, `unanimous_consensus`, and per-scout agreements; `research-gate.json` cannot pass until unanimous consensus is true for all scouts. Normal Research is intentionally allowed to take one or two hours when the problem needs it; `--mock` is only for selftests or dry harness checks, and a real run blocks with `research-blocker.json` instead of silently substituting mock output when the Codex execution path is unavailable. The source layer contract separates latest papers, official/government or leading-institution sources, standards/primary docs, current news such as BBC/CNN/GDELT-style sources, public discourse such as X/Reddit, developer/practitioner knowledge such as Stack Overflow/GitHub, traditional background sources, and counterevidence/fact-checking; `source-ledger.json` must record layer coverage, source quality, blockers, citations, and cross-layer triangulation. Context7 is optional for `$Research` and only becomes relevant when the research topic specifically depends on package, API, framework, or SDK documentation. Research runs require `research-report.md`, `research-paper.md`, `genius-opinion-summary.md`, `research-source-skill.md`, `source-ledger.json`, `scout-ledger.json`, `debate-ledger.json`, `novelty-ledger.json`, `falsification-ledger.json`, and `research-gate.json` so they stay source-backed, adversarially checked, falsifiable, paper-ready, and clear about every scout lens opinion. `research status` reports source entries, source-layer coverage, triangulation checks, counterevidence, xhigh scout count, Eureka moments, debate exchanges, consensus iterations, unanimous consensus, paper presence/sections, genius-opinion summary coverage, scout findings, and falsification cases alongside the gate.
239
239
 
240
240
  `sks recallpulse` is the 0.8.0 report-only RecallPulse utility. It writes `recallpulse-decision.json`, `mission-status-ledger.json`, `route-proof-capsule.json`, `evidence-envelope.json`, `recallpulse-governance-report.json`, `recallpulse-task-goal-ledger.json`, and `recallpulse-eval-report.json` for the current mission. RecallPulse does not replace route gates, Honest Mode, DB safety, imagegen evidence, or TriWiki validation; it records cache hits, hydration needs, duplicate suppression, route-governance risks, and final-summary-ready durable status so later releases can promote only measured improvements. Checklist updates are sequential: every `Txxx` row is treated as a child `$Goal` checkpoint, and `sks recallpulse checklist ... --task T001 --apply` refuses out-of-order checks unless explicitly overridden.
241
241
 
@@ -275,7 +275,7 @@ For headless remotely controllable Codex App/server sessions on Codex CLI 0.130.
275
275
  sks codex-app remote-control -- --help
276
276
  ```
277
277
 
278
- `sks codex-app check` reports whether the installed Codex CLI is new enough. 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.
278
+ `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. 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.
279
279
 
280
280
  Then open Codex App and use prompt commands directly in the chat. Examples:
281
281
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.8.0",
4
+ "version": "0.8.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",
@@ -10,6 +10,16 @@ import { initProject, installSkills } from '../core/init.mjs';
10
10
  import { context7ConfigToml, DOLLAR_SKILL_NAMES, GETDESIGN_REFERENCE, hasContext7ConfigText, RECOMMENDED_SKILLS } from '../core/routes.mjs';
11
11
  import { codexLaunchCommand, platformTmuxInstallHint, tmuxReadiness } from '../core/tmux-ui.mjs';
12
12
 
13
+ const DEFAULT_CODEX_APP_PLUGINS = [
14
+ ['browser', 'openai-bundled'],
15
+ ['chrome', 'openai-bundled'],
16
+ ['computer-use', 'openai-bundled'],
17
+ ['latex', 'openai-bundled'],
18
+ ['documents', 'openai-primary-runtime'],
19
+ ['presentations', 'openai-primary-runtime'],
20
+ ['spreadsheets', 'openai-primary-runtime']
21
+ ];
22
+
13
23
  export async function postinstall({ bootstrap }) {
14
24
  const installRoot = path.resolve(process.env.INIT_CWD || process.cwd());
15
25
  const conflictScan = await scanHarnessConflicts(installRoot);
@@ -488,11 +498,18 @@ export function normalizeCodexFastModeUiConfig(text = '') {
488
498
  next = upsertTopLevelTomlString(next, 'service_tier', 'fast');
489
499
  next = upsertTopLevelTomlBoolean(next, 'suppress_unstable_features_warning', true);
490
500
  next = upsertTomlTableKey(next, 'features', 'hooks = true');
501
+ next = upsertTomlTableKey(next, 'features', 'remote_control = true');
491
502
  next = upsertTomlTableKey(next, 'features', 'multi_agent = true');
492
503
  next = upsertTomlTableKey(next, 'features', 'fast_mode = true');
493
504
  next = upsertTomlTableKey(next, 'features', 'fast_mode_ui = true');
494
505
  next = upsertTomlTableKey(next, 'features', 'codex_git_commit = true');
495
506
  next = upsertTomlTableKey(next, 'features', 'computer_use = true');
507
+ next = upsertTomlTableKey(next, 'features', 'browser_use = true');
508
+ next = upsertTomlTableKey(next, 'features', 'browser_use_external = true');
509
+ next = upsertTomlTableKey(next, 'features', 'image_generation = true');
510
+ next = upsertTomlTableKey(next, 'features', 'in_app_browser = true');
511
+ next = upsertTomlTableKey(next, 'features', 'guardian_approval = true');
512
+ next = upsertTomlTableKey(next, 'features', 'tool_suggest = true');
496
513
  next = upsertTomlTableKey(next, 'features', 'apps = true');
497
514
  next = upsertTomlTableKey(next, 'features', 'plugins = true');
498
515
  next = upsertTomlTableKey(next, 'user.fast_mode', 'visible = true');
@@ -503,21 +520,30 @@ export function normalizeCodexFastModeUiConfig(text = '') {
503
520
  next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'approval_policy = "on-request"');
504
521
  next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'sandbox_mode = "workspace-write"');
505
522
  next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'model_reasoning_effort = "high"');
523
+ next = upsertTomlTableKey(next, 'profiles.sks-research-xhigh', 'model = "gpt-5.5"');
524
+ next = upsertTomlTableKey(next, 'profiles.sks-research-xhigh', 'service_tier = "fast"');
525
+ next = upsertTomlTableKey(next, 'profiles.sks-research-xhigh', 'approval_policy = "on-request"');
526
+ next = upsertTomlTableKey(next, 'profiles.sks-research-xhigh', 'sandbox_mode = "workspace-write"');
527
+ next = upsertTomlTableKey(next, 'profiles.sks-research-xhigh', 'model_reasoning_effort = "xhigh"');
528
+ next = upsertTomlTableKey(next, 'profiles.sks-research', 'model = "gpt-5.5"');
529
+ next = upsertTomlTableKey(next, 'profiles.sks-research', 'service_tier = "fast"');
530
+ next = upsertTomlTableKey(next, 'profiles.sks-research', 'approval_policy = "never"');
531
+ next = upsertTomlTableKey(next, 'profiles.sks-research', 'sandbox_mode = "workspace-write"');
532
+ next = upsertTomlTableKey(next, 'profiles.sks-research', 'model_reasoning_effort = "xhigh"');
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
+ }
506
537
  return ensureTrailingNewline(next);
507
538
  }
508
539
 
509
540
  function removeLegacyTopLevelCodexModeLocks(text = '') {
510
- const legacy = {
511
- model_reasoning_effort: new Set(['high'])
512
- };
513
541
  const lines = String(text || '').split('\n');
514
542
  const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
515
543
  const end = firstTable === -1 ? lines.length : firstTable;
516
544
  return lines.filter((line, index) => {
517
545
  if (index >= end) return true;
518
- const match = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=\s*"([^"]*)"\s*(?:#.*)?$/);
519
- if (!match) return true;
520
- return !legacy[match[1]]?.has(match[2]);
546
+ return !/^\s*model_reasoning_effort\s*=/.test(line);
521
547
  }).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
522
548
  }
523
549
 
@@ -1056,7 +1082,7 @@ export async function selftestCodexLb(tmp) {
1056
1082
  const codexLbFakeCodex = path.join(codexLbFakeBin, 'codex');
1057
1083
  await writeTextAtomic(codexLbFakeCodex, "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo \"codex-cli 99.0.0\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"status\" ]; then echo \"logged in with browser auth\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"--with-api-key\" ]; then read key; mkdir -p \"$HOME/.codex\"; printf '{\\\"auth_mode\\\":\\\"apikey\\\",\\\"key\\\":\\\"%s\\\"}\\n' \"$key\" > \"$HOME/.codex/auth.json\"; printf '%s\\n' \"$key\" >> \"$HOME/.codex/login-calls.log\"; exit 0; fi\necho \"fake codex unsupported\" >&2\nexit 1\n");
1058
1084
  await fsp.chmod(codexLbFakeCodex, 0o755);
1059
- await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "high"\nservice_tier = "fast"\n\n[notice]\nfast_default_opt_out = true\n\n[features]\ncodex_hooks = true\n');
1085
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "low"\nservice_tier = "fast"\n\n[profiles.custom]\nmodel_reasoning_effort = "low"\n\n[notice]\nfast_default_opt_out = true\n\n[features]\ncodex_hooks = true\n');
1060
1086
  const codexLbEnvForSelftest = { HOME: codexLbHome, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-global'), PATH: `${codexLbFakeBin}${path.delimiter}${process.env.PATH || ''}` };
1061
1087
  const codexLbSetup = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'setup', '--host', 'lb.example.test', '--api-key', 'sk-test', '--json'], {
1062
1088
  cwd: tmp,
@@ -1285,7 +1311,7 @@ export async function selftestCodexLb(tmp) {
1285
1311
  }
1286
1312
  );
1287
1313
  if (brokenChain.ok || brokenChain.status !== 'previous_response_not_found' || brokenChain.chain_unhealthy !== true) throw new Error('selftest: codex-lb response chain health check did not detect previous_response_not_found');
1288
- if (!/^model = "gpt-5\.5"/m.test(codexLbConfig) || !codexLbConfig.includes('service_tier = "fast"') || !codexLbConfig.includes('hooks = true') || hasDeprecatedCodexHooksFeatureFlag(codexLbConfig) || !codexLbConfig.includes('multi_agent = true') || !codexLbConfig.includes('fast_mode = true') || !codexLbConfig.includes('fast_mode_ui = true') || !codexLbConfig.includes('codex_git_commit = true') || !codexLbConfig.includes('computer_use = true') || !codexLbConfig.includes('apps = true') || !codexLbConfig.includes('plugins = true') || !codexLbConfig.includes('[user.fast_mode]') || !codexLbConfig.includes('visible = true') || !codexLbConfig.includes('enabled = true') || !codexLbConfig.includes('default_profile = "sks-fast-high"') || !/\[profiles\.sks-fast-high\][\s\S]*?service_tier = "fast"/.test(codexLbConfig) || codexLbConfig.includes('fast_default_opt_out = true') || hasTopLevelCodexModeLock(codexLbConfig)) throw new Error('selftest: codex-lb setup did not preserve Codex App feature flags, Fast mode defaults, Codex Git commit generation, force GPT-5.5, or migrate the hooks feature flag');
1314
+ if (!/^model = "gpt-5\.5"/m.test(codexLbConfig) || !codexLbConfig.includes('service_tier = "fast"') || !codexLbConfig.includes('hooks = true') || hasDeprecatedCodexHooksFeatureFlag(codexLbConfig) || !codexLbConfig.includes('remote_control = true') || !codexLbConfig.includes('multi_agent = true') || !codexLbConfig.includes('fast_mode = true') || !codexLbConfig.includes('fast_mode_ui = true') || !codexLbConfig.includes('codex_git_commit = true') || !codexLbConfig.includes('computer_use = true') || !codexLbConfig.includes('browser_use = true') || !codexLbConfig.includes('browser_use_external = true') || !codexLbConfig.includes('guardian_approval = true') || !codexLbConfig.includes('tool_suggest = true') || !codexLbConfig.includes('apps = true') || !codexLbConfig.includes('plugins = true') || !codexLbConfig.includes('[plugins."latex@openai-bundled"]') || !codexLbConfig.includes('[plugins."documents@openai-primary-runtime"]') || !codexLbConfig.includes('[user.fast_mode]') || !codexLbConfig.includes('visible = true') || !codexLbConfig.includes('enabled = true') || !codexLbConfig.includes('default_profile = "sks-fast-high"') || !/\[profiles\.custom\][\s\S]*?model_reasoning_effort = "low"/.test(codexLbConfig) || !/\[profiles\.sks-fast-high\][\s\S]*?service_tier = "fast"/.test(codexLbConfig) || codexLbConfig.includes('fast_default_opt_out = true') || hasTopLevelCodexModeLock(codexLbConfig)) throw new Error('selftest: codex-lb setup did not preserve Codex App feature flags, default plugins, profile-scoped reasoning effort, Fast mode defaults, Codex Git commit generation, force GPT-5.5, or migrate the hooks feature flag');
1289
1315
  if (!hasCodexUnstableFeatureWarningSuppression(codexLbConfig)) throw new Error('selftest: codex-lb setup did not suppress Codex unstable feature warning');
1290
1316
  const codexLbLaunch = codexLaunchCommand(tmp, 'codex', []);
1291
1317
  if (!codexLbLaunch.includes('sks-codex-lb.env')) throw new Error('selftest: tmux launch command does not source codex-lb env file');
@@ -1297,7 +1323,10 @@ export async function selftestCodexLb(tmp) {
1297
1323
  }
1298
1324
 
1299
1325
  function hasTopLevelCodexModeLock(text = '') {
1300
- return /(^|\n)\s*model\s*=\s*"codex-lb"\s*(\n|$)/.test(text) || /(^|\n)\s*model_provider\s*=\s*"openai"\s*(\n|$)/.test(text);
1326
+ const lines = String(text || '').split('\n');
1327
+ const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
1328
+ const top = (firstTable === -1 ? lines : lines.slice(0, firstTable)).join('\n');
1329
+ return /(^|\n)\s*model\s*=\s*"codex-lb"\s*(\n|$)/.test(top) || /(^|\n)\s*model_provider\s*=\s*"openai"\s*(\n|$)/.test(top) || /(^|\n)\s*model_reasoning_effort\s*=/.test(top);
1301
1330
  }
1302
1331
 
1303
1332
  function hasDeprecatedCodexHooksFeatureFlag(text = '') {
package/src/cli/main.mjs CHANGED
@@ -22,7 +22,7 @@ import { bumpProjectVersion, disableVersionGitHook, runVersionPreCommit, version
22
22
  import { rustInfo } from '../core/rust-accelerator.mjs';
23
23
  import { renderCartridge, validateCartridge, driftCartridge, snapshotCartridge } from '../core/gx-renderer.mjs';
24
24
  import { defaultEvaluationScenario, runEvaluationBenchmark } from '../core/evaluation.mjs';
25
- import { evaluateResearchGate, writeMockResearchResult, writeResearchPlan } from '../core/research.mjs';
25
+ import { buildResearchPrompt, evaluateResearchGate, writeMockResearchResult, writeResearchPlan } from '../core/research.mjs';
26
26
  import { evaluateRecallPulseFixtures, readMissionStatusLedger, writeRecallPulseArtifacts } from '../core/recallpulse.mjs';
27
27
  import {
28
28
  PPT_AUDIENCE_STRATEGY_ARTIFACT,
@@ -1277,7 +1277,7 @@ async function depsStatus(root = null, opts = {}) {
1277
1277
  codex_cli: { ok: Boolean(codex.bin), bin: codex.bin || null, version: codex.version || null },
1278
1278
  codex_app: app,
1279
1279
  context7,
1280
- browser_use: { ok: app.mcp.has_browser_use, cache: app.plugins.browser_use_cache },
1280
+ browser_use: { ok: Boolean(app.features?.browser_tool_ready || app.mcp.has_browser_use), cache: app.plugins.browser_use_cache, source: app.features?.browser_tool_source || app.mcp.browser_use_source || null },
1281
1281
  computer_use: { ok: app.mcp.has_computer_use, cache: app.plugins.computer_use_cache },
1282
1282
  tmux: { ok: Boolean(tmux.ok), bin: tmux.bin || null, version: tmux.version || null, min_version: tmux.min_version || '3.0', current_session: Boolean(tmux.current_session), install_hint: tmux.ok ? null : platformTmuxInstallHint(), error: tmux.error || null },
1283
1283
  homebrew: process.platform === 'darwin' ? { ok: Boolean(brew), bin: brew, required_for_tmux_install: homebrewNeeded } : { ok: null, bin: null, required_for_tmux_install: false },
@@ -1307,7 +1307,7 @@ function printDepsStatus(status) {
1307
1307
  console.log(`Codex App: ${status.codex_app.app.installed ? 'ok' : 'missing'}`);
1308
1308
  console.log(`Image Gen: ${status.codex_app.features?.image_generation ? 'ok' : 'missing'}`);
1309
1309
  console.log(`Context7: ${status.context7.ok ? 'ok' : 'missing'}`);
1310
- console.log(`Browser Use: ${status.browser_use.ok ? 'ok' : 'missing'}`);
1310
+ console.log(`Browser: ${status.browser_use.ok ? `ok${status.browser_use.source ? ` (${status.browser_use.source})` : ''}` : 'missing'}`);
1311
1311
  console.log(`Computer Use:${status.computer_use.ok ? ' ok' : ' missing'}`);
1312
1312
  console.log(`tmux: ${tmuxStatusKind(status.tmux)} ${status.tmux.version || status.tmux.error || ''}`.trimEnd());
1313
1313
  if (process.platform === 'darwin') console.log(`Homebrew: ${status.homebrew.ok ? 'ok' : 'missing'} ${status.homebrew.bin || ''}`.trimEnd());
@@ -1637,7 +1637,7 @@ async function setup(args) {
1637
1637
  else console.log('Git: .gitignore ignores SKS generated files');
1638
1638
  console.log(`Codex App: .codex/config.toml, .codex/hooks.json, .agents/skills, .codex/agents, .codex/SNEAKOSCOPE.md`);
1639
1639
  console.log(`Global $: ${globalSkills.status === 'installed' ? 'ok' : globalSkills.status} ${globalSkills.root || ''}`.trimEnd());
1640
- console.log(`App tools: ${appRuntime.ok ? 'ok' : 'needs setup'} Codex App=${appRuntime.app.installed ? 'ok' : 'missing'} Browser Use=${appRuntime.mcp.has_browser_use ? 'ok' : 'missing'} Computer Use=${appRuntime.mcp.has_computer_use ? 'ok' : 'missing'} Image Gen=${appRuntime.features?.image_generation ? 'ok' : 'missing'}`);
1640
+ 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'}`);
1641
1641
  console.log(`Prompt: intent-first routing, $Answer fact-check route, $DFix ultralight Direct Fix route, $PPT HTML/PDF presentation route, Context7 gate`);
1642
1642
  console.log(`Skills: .agents/skills`);
1643
1643
  console.log(`Next: sks context7 check; sks selftest --mock; sks commands; sks dollar-commands`);
@@ -1777,7 +1777,7 @@ async function doctor(args) {
1777
1777
  console.log(`Rust acc.: ${rust.available ? rust.version : 'optional-missing'}`);
1778
1778
  console.log(`State: ${result.sneakoscope.ok ? 'ok' : 'missing .sneakoscope'}`);
1779
1779
  console.log(`Context7: ${result.context7.ok ? 'ok' : 'missing MCP config'} project=${result.context7.project.ok ? 'ok' : 'missing'} global=${result.context7.global.ok ? 'ok' : 'missing'}`);
1780
- console.log(`App tools: ${appRuntime.ok ? 'ok' : 'needs setup'} Codex App=${appRuntime.app.installed ? 'ok' : 'missing'} Browser Use=${appRuntime.mcp.has_browser_use ? 'ok' : 'missing'} Computer Use=${appRuntime.mcp.has_computer_use ? 'ok' : 'missing'} Image Gen=${appRuntime.features?.image_generation ? 'ok' : 'missing'}`);
1780
+ 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'}`);
1781
1781
  console.log(`tmux: ${tmuxStatusKind(result.runtime.tmux)} ${result.runtime.tmux.version || result.runtime.tmux.error || ''}`.trimEnd());
1782
1782
  console.log(`Guard: ${result.harness_guard.ok ? 'ok' : 'blocked'}${result.harness_guard.source_exception ? ' source-exception' : ''}`);
1783
1783
  console.log(`Version: ${result.versioning.ok ? 'ok' : 'missing'}${result.versioning.enabled ? ` ${result.versioning.package_version || ''}` : ` ${result.versioning.reason || 'disabled'}`}`);
@@ -1963,6 +1963,12 @@ async function safeReadText(file, fallback = '') {
1963
1963
  }
1964
1964
 
1965
1965
  async function resolveMissionId(root, arg) { return (!arg || arg === 'latest') ? findLatestMission(root) : arg; }
1966
+
1967
+ function hasResearchProfileConfig(text = '') {
1968
+ return /\[profiles\.sks-research-xhigh\][\s\S]*?model = "gpt-5\.5"[\s\S]*?model_reasoning_effort = "xhigh"/.test(text)
1969
+ && /\[profiles\.sks-research\][\s\S]*?model = "gpt-5\.5"[\s\S]*?approval_policy = "never"[\s\S]*?model_reasoning_effort = "xhigh"/.test(text);
1970
+ }
1971
+
1966
1972
  function readMaxCycles(args, fallback) {
1967
1973
  const i = args.indexOf('--max-cycles');
1968
1974
  const raw = i >= 0 && args[i + 1] ? Number(args[i + 1]) : Number(fallback);
@@ -2012,6 +2018,8 @@ async function selftest() {
2012
2018
  if (trippedStop) throw new Error('selftest: compliance loop guard did not terminally trip');
2013
2019
  const loopBlocker = await readJson(path.join(loopMission.dir, 'hard-blocker.json'), null);
2014
2020
  if (loopBlocker?.reason !== 'compliance_loop_guard_tripped') throw new Error('selftest: compliance loop guard did not write hard blocker');
2021
+ const hardBlockerUnblocked = await evaluateStop(tmp, loopState, { last_assistant_message: 'done' });
2022
+ if (hardBlockerUnblocked?.decision === 'block' && !String(hardBlockerUnblocked.reason || '').includes('reflection')) throw new Error('selftest: hard blocker did not unblock incomplete active gate');
2015
2023
  const clarificationMission = await createMission(tmp, { mode: 'team', prompt: 'visible question gate selftest' });
2016
2024
  await writeTextAtomic(path.join(clarificationMission.dir, 'questions.md'), '# Questions\n\n1. GOAL_PRECISE: What should be changed?\n');
2017
2025
  await writeJsonAtomic(path.join(clarificationMission.dir, 'required-answers.schema.json'), { slots: [{ id: 'GOAL_PRECISE', question: 'What should be changed?' }] });
@@ -2167,6 +2175,7 @@ async function selftest() {
2167
2175
  const doctorGlobalCodexConfig = await safeReadText(path.join(doctorGlobalHome, '.codex', 'config.toml'));
2168
2176
  if (!doctorGlobalRepairJson.repair?.global_codex_config) throw new Error('selftest: doctor global config repair missing');
2169
2177
  assertCodexWarn(doctorGlobalCodexConfig, 'doctor global config');
2178
+ 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');
2170
2179
  for (const name of stalePluginSkillNames) {
2171
2180
  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`);
2172
2181
  }
@@ -2237,6 +2246,7 @@ async function selftest() {
2237
2246
  const postinstallNoMarkerConfig = await safeReadText(path.join(postinstallNoMarkerGlobalRoot, '.codex', 'config.toml'));
2238
2247
  if (missingGeneratedCodexAppFeatureFlags(postinstallNoMarkerConfig).length || hasDeprecatedCodexHooksFeatureFlag(postinstallNoMarkerConfig)) throw new Error('selftest: no-marker flags');
2239
2248
  assertCodexWarn(postinstallNoMarkerConfig, 'postinstall global runtime config');
2249
+ if (!hasResearchProfileConfig(postinstallNoMarkerConfig)) throw new Error('selftest: postinstall global runtime config did not restore Research xhigh profiles');
2240
2250
  if (await exists(path.join(postinstallNoMarkerCwd, '.sneakoscope'))) throw new Error('selftest: no-marker postinstall polluted install cwd');
2241
2251
  if (await exists(path.join(postinstallNoMarkerGlobalRoot, '.gitignore'))) throw new Error('selftest: global runtime bootstrap without project git wrote shared .gitignore');
2242
2252
  const bootstrapJsonTmp = tmpdir();
@@ -2271,12 +2281,15 @@ async function selftest() {
2271
2281
  if (tmuxOpenArgs.join(' ') !== 'attach-session -t sks-mad-selftest') throw new Error('selftest: MAD tmux attach args are not stable by session name');
2272
2282
  const defaultFastHighPlan = await buildTmuxLaunchPlan({ root: tmp, tmux: { ok: true, bin: 'tmux', version: '3.4' }, codex: { bin: 'codex', version: 'codex-cli 99.0.0' }, app: { ok: true } });
2273
2283
  if (defaultFastHighPlan.codexArgs.join(' ') !== '--model gpt-5.5 -c service_tier="fast" -c model_reasoning_effort="high"') throw new Error('selftest: default sks tmux launch is not fast-high');
2274
- const forcedModelPlan = await buildTmuxLaunchPlan({ root: tmp, env: { SKS_CODEX_MODEL: 'gpt-5.4-mini', SKS_CODEX_FAST_HIGH: '0', SKS_CODEX_REASONING: 'medium' }, tmux: { ok: true, bin: 'tmux', version: '3.4' }, codex: { bin: 'codex', version: 'codex-cli 99.0.0' }, app: { ok: true } });
2275
- if (forcedModelPlan.codexArgs.includes('gpt-5.4-mini') || forcedModelPlan.codexArgs.join(' ') !== '--model gpt-5.5 -c service_tier="fast" -c model_reasoning_effort="medium"') throw new Error('selftest: sks tmux launch allowed a non-GPT-5.5 model override');
2276
- const explicitBadModelPlan = await buildTmuxLaunchPlan({ root: tmp, codexArgs: ['--profile', 'legacy-5.4', '--model', 'gpt-5.4-mini', '-c', 'model="gpt-5.4"', '-c', 'model_reasoning_effort="low"'], tmux: { ok: true, bin: 'tmux', version: '3.4' }, codex: { bin: 'codex', version: 'codex-cli 99.0.0' }, app: { ok: true } });
2277
- if (explicitBadModelPlan.codexArgs.join(' ').includes('gpt-5.4') || explicitBadModelPlan.codexArgs.join(' ') !== '--model gpt-5.5 -c service_tier="fast" --profile legacy-5.4 -c model_reasoning_effort="low"') throw new Error('selftest: explicit tmux model override was not forced back to GPT-5.5');
2278
- const codexExecArgs = buildCodexExecArgs({ root: tmp, prompt: 'model guard selftest', profile: 'legacy-5.4', extraArgs: ['--model=gpt-5.4-mini', '--config', 'model = "gpt-5.4"', '-c', 'model_reasoning_effort="medium"'] });
2279
- if (codexExecArgs.join(' ').includes('gpt-5.4') || !codexExecArgs.includes('gpt-5.5') || codexExecArgs.includes('--model=gpt-5.4-mini')) throw new Error('selftest: codex exec args allowed a non-GPT-5.5 model override');
2284
+ const forcedModelPlan = await buildTmuxLaunchPlan({ root: tmp, env: { SKS_CODEX_MODEL: 'gpt-5.0-forbidden', SKS_CODEX_FAST_HIGH: '0', SKS_CODEX_REASONING: 'medium' }, tmux: { ok: true, bin: 'tmux', version: '3.4' }, codex: { bin: 'codex', version: 'codex-cli 99.0.0' }, app: { ok: true } });
2285
+ if (forcedModelPlan.codexArgs.includes('gpt-5.0-forbidden') || forcedModelPlan.codexArgs.join(' ') !== '--model gpt-5.5 -c service_tier="fast" -c model_reasoning_effort="medium"') throw new Error('selftest: sks tmux launch allowed a non-GPT-5.5 model override');
2286
+ const explicitBadModelPlan = await buildTmuxLaunchPlan({ root: tmp, codexArgs: ['--profile', 'legacy-forbidden-model', '--model', 'gpt-5.0-forbidden', '-c', 'model="gpt-5.0-forbidden"', '-c', 'model_reasoning_effort="low"'], tmux: { ok: true, bin: 'tmux', version: '3.4' }, codex: { bin: 'codex', version: 'codex-cli 99.0.0' }, app: { ok: true } });
2287
+ if (explicitBadModelPlan.codexArgs.join(' ').includes('gpt-5.0-forbidden') || explicitBadModelPlan.codexArgs.join(' ') !== '--model gpt-5.5 -c service_tier="fast" --profile legacy-forbidden-model -c model_reasoning_effort="low"') throw new Error('selftest: explicit tmux model override was not forced back to GPT-5.5');
2288
+ const codexExecArgs = buildCodexExecArgs({ root: tmp, prompt: 'model guard selftest', profile: 'legacy-forbidden-model', extraArgs: ['--model=gpt-5.0-forbidden', '--config', 'model = "gpt-5.0-forbidden"', '-c', 'model_reasoning_effort="medium"'] });
2289
+ if (codexExecArgs.join(' ').includes('gpt-5.0-forbidden') || !codexExecArgs.includes('gpt-5.5') || codexExecArgs.includes('--model=gpt-5.0-forbidden')) throw new Error('selftest: codex exec args allowed a non-GPT-5.5 model override');
2290
+ const researchExecArgs = buildCodexExecArgs({ root: tmp, prompt: 'research exec selftest', profile: 'sks-research', extraArgs: ['-c', 'service_tier="fast"', '-c', 'model_reasoning_effort="xhigh"'] });
2291
+ const researchExecJoined = researchExecArgs.join(' ');
2292
+ if (!researchExecJoined.includes('--profile sks-research') || !researchExecJoined.includes('--model gpt-5.5') || !researchExecJoined.includes('service_tier="fast"') || !researchExecJoined.includes('model_reasoning_effort="xhigh"')) throw new Error('selftest: research exec args did not force GPT-5.5 fast xhigh execution');
2280
2293
  await selftestCodexLb(tmp);
2281
2294
  if (!shouldAutoAttachTmux(['--mad'], {}, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest: MAD tmux launch does not auto-attach in an interactive terminal');
2282
2295
  if (shouldAutoAttachTmux(['--mad', '--json'], {}, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest: MAD tmux json mode should not auto-attach');
@@ -2330,14 +2343,14 @@ async function selftest() {
2330
2343
  if (remoteControlStatus.code !== 0) throw new Error(`selftest: Codex remote-control status exited ${remoteControlStatus.code}: ${remoteControlStatus.stderr}`);
2331
2344
  const remoteControlJson = JSON.parse(remoteControlStatus.stdout);
2332
2345
  if (!remoteControlJson.ok || remoteControlJson.min_version !== '0.130.0' || !String(remoteControlJson.command || '').includes('remote-control')) throw new Error('selftest: Codex remote-control status did not report 0.130.0 readiness');
2333
- const remoteControlLaunch = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-app', 'remote-control', '--', '--model', 'gpt-5.4-mini', '-c', 'model="gpt-5.4"', '--example'], {
2346
+ const remoteControlLaunch = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-app', 'remote-control', '--', '--model', 'gpt-5.0-forbidden', '-c', 'model="gpt-5.0-forbidden"', '--example'], {
2334
2347
  cwd: globalCwd,
2335
2348
  env: { SKS_GLOBAL_ROOT: globalRuntimeRoot, PATH: remoteControlBin },
2336
2349
  timeoutMs: 15000,
2337
2350
  maxOutputBytes: 64 * 1024
2338
2351
  });
2339
2352
  const remoteControlLaunchText = `${remoteControlLaunch.stdout}\n${remoteControlLaunch.stderr}`;
2340
- if (remoteControlLaunch.code !== 0 || remoteControlLaunchText.includes('gpt-5.4') || remoteControlLaunchText.includes('--model') || !remoteControlLaunchText.includes('-c model="gpt-5.5"')) throw new Error('selftest: Codex remote-control passthrough did not force GPT-5.5 with config syntax');
2353
+ if (remoteControlLaunch.code !== 0 || remoteControlLaunchText.includes('gpt-5.0-forbidden') || remoteControlLaunchText.includes('--model') || !remoteControlLaunchText.includes('-c model="gpt-5.5"')) throw new Error('selftest: Codex remote-control passthrough did not force GPT-5.5 with config syntax');
2341
2354
  const remoteControlOldBin = path.join(tmp, 'remote-control-old-bin');
2342
2355
  await ensureDir(remoteControlOldBin);
2343
2356
  await writeTextAtomic(path.join(remoteControlOldBin, 'codex'), '#!/bin/sh\nif [ "$1" = "--version" ]; then echo "codex-cli 0.129.0"; exit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
@@ -2848,10 +2861,10 @@ async function selftest() {
2848
2861
  if (hookTeamState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || hookTeamState.implementation_allowed === false || !hookTeamState.team_plan_ready) throw new Error('selftest: $Team hook did not prepare direct Team mission');
2849
2862
  if (!hookTeamState.pipeline_plan_ready || !(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), PIPELINE_PLAN_ARTIFACT)))) throw new Error('selftest: $Team hook did not write a pipeline plan');
2850
2863
  if (!(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'team-plan.json')))) throw new Error('selftest: Team plan was not created directly');
2851
- const hookForbiddenModelResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookTeamTmp, input: JSON.stringify({ cwd: hookTeamTmp, prompt: '$Team should be blocked before route work', model: 'gpt-5.5', metadata: { client: { modelId: 'gpt-5.4-mini' } } }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
2864
+ const hookForbiddenModelResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookTeamTmp, input: JSON.stringify({ cwd: hookTeamTmp, prompt: '$Team should be blocked before route work', model: 'gpt-5.5', metadata: { client: { modelId: 'gpt-5.0-forbidden' } } }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
2852
2865
  if (hookForbiddenModelResult.code !== 0) throw new Error(`selftest: forbidden model hook exited ${hookForbiddenModelResult.code}: ${hookForbiddenModelResult.stderr}`);
2853
2866
  const hookForbiddenModelJson = JSON.parse(hookForbiddenModelResult.stdout);
2854
- if (hookForbiddenModelJson.decision !== 'block' || !String(hookForbiddenModelJson.reason || '').includes('gpt-5.5') || !String(hookForbiddenModelJson.reason || '').includes('gpt-5.4-mini')) throw new Error('selftest: hook did not block GPT-5.4 client model metadata');
2867
+ if (hookForbiddenModelJson.decision !== 'block' || !String(hookForbiddenModelJson.reason || '').includes('gpt-5.5') || !String(hookForbiddenModelJson.reason || '').includes('gpt-5.0-forbidden')) throw new Error('selftest: hook did not block forbidden client model metadata');
2855
2868
  const hookTeamPendingResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookTeamTmp, input: JSON.stringify({ cwd: hookTeamTmp, prompt: '$Team 새 작업으로 넘어가' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
2856
2869
  if (hookTeamPendingResult.code !== 0) throw new Error(`selftest: pending clarification hook exited ${hookTeamPendingResult.code}: ${hookTeamPendingResult.stderr}`);
2857
2870
  const hookTeamPendingJson = JSON.parse(hookTeamPendingResult.stdout);
@@ -3024,7 +3037,8 @@ async function selftest() {
3024
3037
  if (missingCodexConfigFlags.length || hasDeprecatedCodexHooksFeatureFlag(codexConfigText)) throw new Error(`selftest: generated Codex App feature flags missing or deprecated: ${missingCodexConfigFlags.join(', ')}`);
3025
3038
  assertCodexWarn(codexConfigText, 'generated Codex App config');
3026
3039
  if (!hasContext7ConfigText(codexConfigText)) throw new Error('selftest: Context7 MCP not configured');
3027
- if (!codexConfigText.includes('[profiles.sks-task-low]') || !codexConfigText.includes('[profiles.sks-task-medium]') || !codexConfigText.includes('[profiles.sks-logic-high]') || !codexConfigText.includes('[profiles.sks-fast-high]') || !codexConfigText.includes('[profiles.sks-research-xhigh]') || !codexConfigText.includes('[profiles.sks-mad-high]')) throw new Error('selftest: GPT-5.5 reasoning profiles not configured');
3040
+ if (!codexConfigText.includes('[profiles.sks-task-low]') || !codexConfigText.includes('[profiles.sks-task-medium]') || !codexConfigText.includes('[profiles.sks-logic-high]') || !codexConfigText.includes('[profiles.sks-fast-high]') || !codexConfigText.includes('[profiles.sks-research-xhigh]') || !codexConfigText.includes('[profiles.sks-research]') || !codexConfigText.includes('[profiles.sks-mad-high]')) throw new Error('selftest: GPT-5.5 reasoning profiles not configured');
3041
+ if (!hasResearchProfileConfig(codexConfigText)) throw new Error('selftest: generated Research xhigh profiles not configured');
3028
3042
  if (!/\[profiles\.sks-mad-high\][\s\S]*?approval_policy = "never"[\s\S]*?sandbox_mode = "danger-full-access"/.test(codexConfigText)) throw new Error('selftest: generated sks-mad-high profile is not full access');
3029
3043
  if (!codexConfigText.includes('[agents.analysis_scout]')) throw new Error('selftest: analysis_scout agent not configured');
3030
3044
  if (!codexConfigText.includes('[agents.team_consensus]')) throw new Error('selftest: team_consensus agent not configured');
@@ -3037,20 +3051,22 @@ async function selftest() {
3037
3051
  assertCodexWarn(preservedConfig, 'merged Codex config');
3038
3052
  if (preservedConfig.includes('fast_default_opt_out = true') || !preservedConfig.includes('keep = true')) throw new Error('selftest: Codex config merge did not remove stale Fast opt-out notice while preserving other notice keys');
3039
3053
  const missingPreservedFlags = missingGeneratedCodexAppFeatureFlags(preservedConfig);
3040
- if (missingPreservedFlags.length || hasDeprecatedCodexHooksFeatureFlag(preservedConfig) || !preservedConfig.includes('custom_preview = true') || !preservedConfig.includes('[profiles.sks-fast-high]')) throw new Error(`selftest: Codex config merge did not add required app feature flags, preserve existing feature flags, or remove deprecated codex_hooks: ${missingPreservedFlags.join(', ')}`);
3054
+ if (missingPreservedFlags.length || hasDeprecatedCodexHooksFeatureFlag(preservedConfig) || !preservedConfig.includes('custom_preview = true') || !preservedConfig.includes('[profiles.sks-fast-high]') || !hasResearchProfileConfig(preservedConfig)) throw new Error(`selftest: Codex config merge did not add required app feature flags, Research profiles, preserve existing feature flags, or remove deprecated codex_hooks: ${missingPreservedFlags.join(', ')}`);
3041
3055
  if (hasTopLevelCodexModeLock(preservedConfig)) throw new Error('selftest: Codex config merge left top-level legacy model/reasoning locks that hide Fast mode UI');
3042
3056
  const appFeatureTmp = tmpdir();
3043
3057
  const fakeCodexApp = path.join(appFeatureTmp, 'Codex.app');
3044
3058
  const fakeCodexBinDir = path.join(appFeatureTmp, 'bin');
3045
3059
  await ensureDir(fakeCodexApp);
3046
3060
  await ensureDir(fakeCodexBinDir);
3061
+ await ensureDir(path.join(appFeatureTmp, '.codex'));
3062
+ await writeTextAtomic(path.join(appFeatureTmp, '.codex', 'config.toml'), codexConfigText);
3047
3063
  const fakeCodex = path.join(fakeCodexBinDir, 'codex');
3048
- 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\ncodex_git_commit under development true\ncomputer_use stable true\nfast_mode stable true\nhooks stable true\nimage_generation stable true\nplugins stable true\nEOF\nexit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
3064
+ 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');
3049
3065
  await fsp.chmod(fakeCodex, 0o755);
3050
3066
  const codexAppFeatureStatus = await codexAppIntegrationStatus({ codex: { bin: fakeCodex, version: 'codex-cli 99.0.0' }, home: appFeatureTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } });
3051
- if (!codexAppFeatureStatus.ok || !codexAppFeatureStatus.features?.required_flags_ok || !codexAppFeatureStatus.features?.codex_git_commit) throw new Error('selftest: codex-app check did not accept required app feature flags including under-development codex_git_commit');
3067
+ if (!codexAppFeatureStatus.ok || !codexAppFeatureStatus.features?.required_flags_ok || !codexAppFeatureStatus.features?.codex_git_commit || !codexAppFeatureStatus.features?.remote_control || !codexAppFeatureStatus.features?.fast_mode_config?.ok) throw new Error('selftest: codex-app check did not accept required app feature flags, remote_control, and unlocked Fast UI config');
3052
3068
  const fakeCodexMissing = path.join(fakeCodexBinDir, 'codex-missing-git-commit');
3053
- 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\ncodex_git_commit under development false\ncomputer_use stable true\nfast_mode stable true\nhooks stable true\nimage_generation stable true\nplugins stable true\nEOF\nexit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
3069
+ 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');
3054
3070
  await fsp.chmod(fakeCodexMissing, 0o755);
3055
3071
  const codexAppMissingFeatureStatus = await codexAppIntegrationStatus({ codex: { bin: fakeCodexMissing, version: 'codex-cli 99.0.0' }, home: appFeatureTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } });
3056
3072
  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');
@@ -3929,17 +3945,20 @@ async function selftest() {
3929
3945
  const { dir: researchDir, mission: researchMission } = await createMission(tmp, { mode: 'research', prompt: '새로운 코드 리뷰 방법론 연구' });
3930
3946
  const researchPlan = await writeResearchPlan(researchDir, researchMission.prompt, {});
3931
3947
  if (researchPlan.methodology !== 'genius-scout-council-frontier-discovery-loop' || researchPlan.web_research_policy?.mode !== 'layered_source_retrieval_and_triangulation') throw new Error('selftest: research plan contract');
3948
+ if (researchPlan.execution_policy?.default_max_cycles !== 12 || researchPlan.mutation_policy?.implementation_allowed !== false || !String(researchPlan.research_council?.debate_policy?.rule || '').includes('every scout records final agreement')) throw new Error('selftest: research consensus/no-code contract');
3932
3949
  if (!researchPlan.research_council?.scouts?.every((scout) => scout.display_name && scout.persona && scout.persona_boundary && scout.reasoning_effort === 'xhigh')) throw new Error('selftest: research scout persona contract missing from plan');
3950
+ const researchPrompt = buildResearchPrompt({ id: researchMission.id, mission: researchMission, plan: researchPlan, cycle: 1, previous: '' });
3951
+ if (!researchPrompt.includes('NO-CODE-MUTATION POLICY') || !researchPrompt.includes('not a fixed three-cycle run') || !researchPrompt.includes('unanimous_consensus=true')) throw new Error('selftest: research prompt missing no-code unanimous consensus policy');
3933
3952
  const rArts = researchPlan.required_artifacts || [];
3934
3953
  for (const a of [rss, 'source-ledger.json', 'scout-ledger.json', 'debate-ledger.json', 'falsification-ledger.json']) if (!rArts.includes(a) || !(await exists(path.join(researchDir, a)))) throw new Error('selftest: research artifact');
3935
3954
  if (!rArts.includes('research-paper.md') || !rArts.includes(gos)) throw new Error('selftest: research paper');
3936
3955
  const initialResearchGate = await evaluateResearchGate(researchDir);
3937
- if (initialResearchGate.passed || ['web_search_pass_missing', 'eureka_missing', 'debate_exchanges_missing', 'research_paper_missing'].some((r) => !initialResearchGate.reasons.includes(r))) throw new Error('selftest: research gate');
3956
+ if (initialResearchGate.passed || ['web_search_pass_missing', 'eureka_missing', 'debate_exchanges_missing', 'research_paper_missing', 'consensus_iteration_missing', 'unanimous_consensus_missing'].some((r) => !initialResearchGate.reasons.includes(r))) throw new Error('selftest: research gate');
3938
3957
  const researchGate = await writeMockResearchResult(researchDir, researchPlan);
3939
3958
  if (!researchGate.passed) throw new Error('selftest: mock research gate did not pass');
3940
3959
  const rm = researchGate.metrics || {};
3941
3960
  if (rm.scout_persona_contract_ok !== true || (rm.scout_persona_issues || []).length) throw new Error('selftest: research scout persona contract did not pass');
3942
- if (['independent_scouts', 'xhigh_scouts', 'eureka_moments', 'debate_participants', 'genius_opinion_summaries'].some((m) => rm[m] < 5) || ['counterevidence_sources', 'falsification_cases', 'triangulation_checks'].some((m) => rm[m] < 1) || rm.paper_sections < 8 || rm.citation_coverage !== true || rm.source_layers_covered < 7) throw new Error('selftest: research metrics');
3961
+ if (['independent_scouts', 'xhigh_scouts', 'eureka_moments', 'debate_participants', 'genius_opinion_summaries'].some((m) => rm[m] < 5) || ['counterevidence_sources', 'falsification_cases', 'triangulation_checks'].some((m) => rm[m] < 1) || rm.paper_sections < 8 || rm.citation_coverage !== true || rm.source_layers_covered < 7 || rm.consensus_iterations < 1 || rm.unanimous_consensus !== true || rm.consensus_agreed_scouts < 5) throw new Error('selftest: research metrics');
3943
3962
  await writeJsonAtomic(path.join(dir, 'done-gate.json'), { passed: true, unsupported_critical_claims: 0, database_safety_violation: false, database_safety_reviewed: true, visual_drift: 'low', wiki_drift: 'low', tests_required: false });
3944
3963
  const gate = await evaluateDoneGate(tmp, id);
3945
3964
  if (!gate.passed) throw new Error('selftest: done gate');
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import fsp from 'node:fs/promises';
3
- import { readJson, readText, writeJsonAtomic, writeTextAtomic, appendJsonlBounded, nowIso, exists, ensureDir, packageRoot, dirSize, formatBytes, PACKAGE_VERSION, sksRoot, readStdin } from '../core/fsx.mjs';
3
+ import { createHash } from 'node:crypto';
4
+ import { readJson, readText, writeJsonAtomic, writeTextAtomic, appendJsonlBounded, nowIso, exists, ensureDir, packageRoot, dirSize, formatBytes, PACKAGE_VERSION, sksRoot, readStdin, runProcess } from '../core/fsx.mjs';
4
5
  import { initProject } from '../core/init.mjs';
5
6
  import { getCodexInfo, runCodexExec } from '../core/codex-adapter.mjs';
6
7
  import { createMission, loadMission, findLatestMission, missionDir, setCurrent, stateFile } from '../core/mission.mjs';
@@ -42,7 +43,7 @@ const flag = (args, name) => args.includes(name);
42
43
  const promptOf = (args) => args.filter((x) => !String(x).startsWith('--')).join(' ').trim();
43
44
  const TEAM_SESSION_CLEANUP_ARTIFACT = 'team-session-cleanup.json';
44
45
  const REPOSITORY_URL = 'https://github.com/mandarange/Sneakoscope-Codex.git';
45
- const RESEARCH_DEFAULT_MAX_CYCLES = 3;
46
+ const RESEARCH_DEFAULT_MAX_CYCLES = 12;
46
47
  const RESEARCH_DEFAULT_CYCLE_TIMEOUT_MINUTES = 120;
47
48
  const RESEARCH_MIN_CYCLE_TIMEOUT_MINUTES = 15;
48
49
  const RESEARCH_MAX_CYCLE_TIMEOUT_MINUTES = 240;
@@ -495,6 +496,7 @@ async function researchPrepare(args) {
495
496
  console.log(`Source skill: ${RESEARCH_SOURCE_SKILL_ARTIFACT}`);
496
497
  console.log('Ledgers: source-ledger.json, scout-ledger.json, debate-ledger.json, novelty-ledger.json, falsification-ledger.json');
497
498
  console.log(`Run: sks research run ${id} --max-cycles ${RESEARCH_DEFAULT_MAX_CYCLES} --cycle-timeout-minutes ${RESEARCH_DEFAULT_CYCLE_TIMEOUT_MINUTES}`);
499
+ console.log('Loop: Research runs until the gate records unanimous scout consensus, or pauses at the explicit safety cap.');
498
500
  }
499
501
 
500
502
  async function researchRun(args) {
@@ -546,13 +548,36 @@ async function researchRun(args) {
546
548
  return;
547
549
  }
548
550
  let last = '';
551
+ const researchCodexArgs = ['-c', 'service_tier="fast"', '-c', 'model_reasoning_effort="xhigh"'];
552
+ const sourceMutationBaseline = await researchCodeMutationSnapshot(root, id);
549
553
  for (let cycle = 1; cycle <= maxCycles; cycle++) {
550
554
  const cycleDir = path.join(dir, 'research', `cycle-${cycle}`);
551
555
  const outputFile = path.join(cycleDir, 'final.md');
552
- await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.cycle.start', cycle, timeoutMinutes: cycleTimeoutMinutes });
556
+ await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.cycle.start', cycle, timeoutMinutes: cycleTimeoutMinutes, profile: 'sks-research', enforced_reasoning_effort: 'xhigh' });
553
557
  const prompt = buildResearchPrompt({ id, mission, plan, cycle, previous: last });
554
- const result = await runCodexExec({ root, prompt, outputFile, json: true, profile: 'sks-research', logDir: cycleDir, timeoutMs: cycleTimeoutMs });
558
+ const result = await runCodexExec({ root, prompt, outputFile, json: true, profile: 'sks-research', extraArgs: researchCodexArgs, logDir: cycleDir, timeoutMs: cycleTimeoutMs });
555
559
  await writeJsonAtomic(path.join(cycleDir, 'process.json'), { code: result.code, stdout_tail: result.stdout, stderr_tail: result.stderr, stdout_bytes: result.stdoutBytes, stderr_bytes: result.stderrBytes, truncated: result.truncated, timed_out: result.timedOut });
560
+ const mutation = await researchCodeMutationDelta(root, sourceMutationBaseline, id);
561
+ if (mutation.blocked) {
562
+ const blocker = {
563
+ schema_version: 1,
564
+ mission_id: id,
565
+ ts: nowIso(),
566
+ phase: 'RESEARCH_BLOCKED_CODE_MUTATION',
567
+ reason: 'Research mode must not modify repository source files. Only route-local mission artifacts are allowed.',
568
+ changed_paths: mutation.changed_paths,
569
+ allowed_prefixes: mutation.allowed_prefixes,
570
+ required_action: 'Review the changed paths, keep or revert them manually as appropriate, then rerun Research after the worktree is clean for source files.',
571
+ implementation_allowed: false
572
+ };
573
+ await writeJsonAtomic(path.join(dir, 'research-code-mutation-blocker.json'), blocker);
574
+ await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: blocker.ts, type: 'research.blocked.code_mutation', changed_paths: mutation.changed_paths });
575
+ await setCurrent(root, { mission_id: id, mode: 'RESEARCH', phase: 'RESEARCH_BLOCKED_CODE_MUTATION', questions_allowed: true, implementation_allowed: false, blocker: 'research-code-mutation-blocker.json' });
576
+ console.error('Research cannot continue: source-code mutation detected outside the route-local mission artifacts.');
577
+ console.error(JSON.stringify(mutation.changed_paths, null, 2));
578
+ process.exitCode = 2;
579
+ return;
580
+ }
556
581
  last = await safeReadText(outputFile, result.stdout || result.stderr || '');
557
582
  if (containsUserQuestion(last)) {
558
583
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.guard.question_blocked', cycle });
@@ -570,7 +595,7 @@ async function researchRun(args) {
570
595
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.cycle.continue', cycle, reasons: gate.reasons });
571
596
  }
572
597
  await setCurrent(root, { mission_id: id, mode: 'RESEARCH', phase: 'RESEARCH_PAUSED_MAX_CYCLES', questions_allowed: true, implementation_allowed: false });
573
- console.log(`Research paused after max cycles: ${id}`);
598
+ console.log(`Research paused after max cycles without unanimous scout consensus: ${id}`);
574
599
  }
575
600
 
576
601
  async function researchStatus(args) {
@@ -606,6 +631,8 @@ async function researchStatus(args) {
606
631
  eureka_moments: scoutRows.length ? scoutRows.filter((scout) => scout.eureka?.exclamation === 'Eureka!' && String(scout.eureka?.idea || '').trim()).length : null,
607
632
  scout_findings: scoutRows.length ? scoutRows.reduce((sum, scout) => sum + (Array.isArray(scout.findings) ? scout.findings.length : 0), 0) : null,
608
633
  debate_exchanges: debateLedger?.exchanges?.length ?? null,
634
+ consensus_iterations: gate?.metrics?.consensus_iterations ?? gate?.consensus_iterations ?? debateLedger?.consensus_iterations ?? null,
635
+ unanimous_consensus: gate?.metrics?.unanimous_consensus ?? gate?.unanimous_consensus ?? debateLedger?.unanimous_consensus ?? false,
609
636
  research_source_skill_present: Boolean(sourceSkillText.trim()),
610
637
  genius_opinion_summary_present: Boolean(geniusSummaryText.trim()),
611
638
  paper_present: Boolean(paperText.trim()),
@@ -662,6 +689,71 @@ async function safeReadText(file, fallback = '') {
662
689
  try { return await fsp.readFile(file, 'utf8'); } catch { return fallback; }
663
690
  }
664
691
 
692
+ async function researchCodeMutationSnapshot(root, missionId = null) {
693
+ const tracked = await runProcess('git', ['ls-files'], { cwd: root, timeoutMs: 15000, maxOutputBytes: 2 * 1024 * 1024 }).catch((err) => ({ code: 1, stderr: err.message, stdout: '' }));
694
+ const status = await runProcess('git', ['status', '--porcelain=v1', '--untracked-files=all'], { cwd: root, timeoutMs: 15000, maxOutputBytes: 2 * 1024 * 1024 }).catch((err) => ({ code: 1, stderr: err.message, stdout: '' }));
695
+ if (tracked.code !== 0 || status.code !== 0) return { ok: false, reason: 'git_unavailable', hashes: {}, status_rows: [], error: tracked.stderr || status.stderr };
696
+ const allowedPrefixes = researchAllowedMutationPrefixes(missionId);
697
+ const hashes = {};
698
+ for (const rel of tracked.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)) {
699
+ if (researchMutationAllowedPath(rel, allowedPrefixes)) continue;
700
+ const file = path.join(root, rel);
701
+ try {
702
+ const bytes = await fsp.readFile(file);
703
+ hashes[rel] = createHash('sha256').update(bytes).digest('hex');
704
+ } catch {
705
+ hashes[rel] = null;
706
+ }
707
+ }
708
+ return {
709
+ ok: true,
710
+ hashes,
711
+ status_rows: status.stdout.split(/\r?\n/).filter(Boolean),
712
+ allowed_prefixes: allowedPrefixes
713
+ };
714
+ }
715
+
716
+ async function researchCodeMutationDelta(root, baseline, missionId) {
717
+ if (!baseline?.ok) return { blocked: false, changed_paths: [], reason: baseline?.reason || 'baseline_unavailable' };
718
+ const current = await researchCodeMutationSnapshot(root, missionId);
719
+ if (!current.ok) return { blocked: false, changed_paths: [], reason: current.reason || 'current_snapshot_unavailable' };
720
+ const changed = new Set();
721
+ for (const [rel, hash] of Object.entries(current.hashes)) {
722
+ if (baseline.hashes[rel] !== hash) changed.add(rel);
723
+ }
724
+ for (const rel of Object.keys(baseline.hashes)) {
725
+ if (!(rel in current.hashes)) changed.add(rel);
726
+ }
727
+ const baselineRows = new Set(baseline.status_rows || []);
728
+ for (const row of current.status_rows || []) {
729
+ if (baselineRows.has(row)) continue;
730
+ const rel = porcelainStatusPath(row);
731
+ if (rel && !researchMutationAllowedPath(rel, current.allowed_prefixes)) changed.add(rel);
732
+ }
733
+ const changedPaths = [...changed].sort();
734
+ return {
735
+ blocked: changedPaths.length > 0,
736
+ changed_paths: changedPaths,
737
+ allowed_prefixes: current.allowed_prefixes
738
+ };
739
+ }
740
+
741
+ function researchAllowedMutationPrefixes(missionId = null) {
742
+ return missionId ? [`.sneakoscope/missions/${missionId}/`] : ['.sneakoscope/missions/'];
743
+ }
744
+
745
+ function researchMutationAllowedPath(rel = '', prefixes = []) {
746
+ const normalized = String(rel || '').replace(/\\/g, '/').replace(/^\.\//, '');
747
+ return prefixes.some((prefix) => normalized.startsWith(prefix));
748
+ }
749
+
750
+ function porcelainStatusPath(row = '') {
751
+ const payload = String(row || '').slice(3).trim();
752
+ if (!payload) return '';
753
+ const renamed = payload.split(' -> ').pop();
754
+ return String(renamed || '').replace(/^"|"$/g, '');
755
+ }
756
+
665
757
  function readBoundedIntegerFlag(args, name, fallback, min, max) {
666
758
  const i = args.indexOf(name);
667
759
  const raw = i >= 0 && args[i + 1] ? Number(args[i + 1]) : Number(fallback);
@@ -1909,7 +2001,7 @@ export function buildTeamPlan(id, prompt, opts = {}) {
1909
2001
  reasoning: teamReasoningPolicy(prompt, roster),
1910
2002
  codex_config_required: {
1911
2003
  service_tier: 'fast',
1912
- features: { multi_agent: true, hooks: true, fast_mode: true, fast_mode_ui: true, codex_git_commit: true, computer_use: true, apps: true, plugins: true },
2004
+ features: { multi_agent: true, hooks: true, remote_control: true, fast_mode: true, fast_mode_ui: true, codex_git_commit: true, computer_use: true, browser_use: true, browser_use_external: true, image_generation: true, in_app_browser: true, guardian_approval: true, tool_suggest: true, apps: true, plugins: true },
1913
2005
  agents: { max_threads: 6, max_depth: 1 },
1914
2006
  custom_agents_dir: '.codex/agents'
1915
2007
  },