sneakoscope 0.7.69 → 0.7.74

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
@@ -192,7 +192,7 @@ sks qa-loop prepare "http://localhost:3000"
192
192
  sks qa-loop run latest --max-cycles 2
193
193
  sks goal create "persist this migration workflow"
194
194
  sks research prepare "evaluate this approach"
195
- sks research run latest --max-cycles 3
195
+ sks research run latest --max-cycles 3 --cycle-timeout-minutes 120
196
196
  sks research status latest
197
197
  sks db scan --json
198
198
  sks wiki refresh
@@ -211,7 +211,7 @@ sks skill-dream run --json
211
211
  sks code-structure scan --json
212
212
  ```
213
213
 
214
- `sks research` prepares a genius-lens scout council, requires every scout to run at `xhigh`, records one literal `Eureka!` idea per scout, runs an evidence-bound debate, maximizes available web/source retrieval before synthesis, and requires `research-report.md`, `research-paper.md`, `source-ledger.json`, `scout-ledger.json`, `debate-ledger.json`, `novelty-ledger.json`, `falsification-ledger.json`, and `research-gate.json` so research runs stay source-backed, adversarially checked, falsifiable, and paper-ready. `research status` reports source entries, counterevidence, xhigh scout count, Eureka moments, debate exchanges, paper presence/sections, scout findings, and falsification cases alongside the gate.
214
+ `sks research` prepares a 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. 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.
215
215
 
216
216
  `sks pipeline plan` shows the active route lane, kept/skipped stages, verification commands, and no-unrequested-fallback invariant. `sks proof-field scan` is the lightweight rubric for small changes; risky or broad signals return to the full Team/Honest path.
217
217
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.7.69",
4
+ "version": "0.7.74",
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",
@@ -361,6 +361,7 @@ export function normalizeCodexFastModeUiConfig(text = '') {
361
361
  next = removeTomlTableKey(next, 'features', 'codex_hooks');
362
362
  next = upsertTopLevelTomlString(next, 'model', 'gpt-5.5');
363
363
  next = upsertTopLevelTomlString(next, 'service_tier', 'fast');
364
+ next = upsertTopLevelTomlBoolean(next, 'suppress_unstable_features_warning', true);
364
365
  next = upsertTomlTableKey(next, 'features', 'hooks = true');
365
366
  next = upsertTomlTableKey(next, 'features', 'multi_agent = true');
366
367
  next = upsertTomlTableKey(next, 'features', 'fast_mode = true');
@@ -457,6 +458,21 @@ function upsertTopLevelTomlString(text, key, value) {
457
458
  return lines.join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
458
459
  }
459
460
 
461
+ function upsertTopLevelTomlBoolean(text, key, value) {
462
+ const line = `${key} = ${value ? 'true' : 'false'}`;
463
+ const lines = String(text || '').split('\n');
464
+ const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
465
+ const end = firstTable === -1 ? lines.length : firstTable;
466
+ for (let i = 0; i < end; i += 1) {
467
+ if (new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(lines[i])) {
468
+ lines[i] = line;
469
+ return lines.join('\n').replace(/\n{3,}/g, '\n\n');
470
+ }
471
+ }
472
+ lines.splice(end, 0, line);
473
+ return lines.join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
474
+ }
475
+
460
476
  function upsertTomlTable(text, table, block) {
461
477
  let lines = String(text || '').trimEnd().split('\n');
462
478
  if (lines.length === 1 && lines[0] === '') lines = [];
@@ -929,15 +945,18 @@ export async function selftestCodexLb(tmp) {
929
945
  const codexLbEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
930
946
  const codexLbAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
931
947
  if (!codexLbSetupJson.ok || codexLbSetupJson.base_url !== 'https://lb.example.test/backend-api/codex' || !codexLbConfig.includes('model_provider = "codex-lb"') || !codexLbConfig.includes('[model_providers.codex-lb]') || !codexLbEnv.includes("CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'") || !codexLbEnv.includes("CODEX_LB_API_KEY='sk-test'") || !/(\"auth_mode\"\s*:\s*\"apikey\")/.test(codexLbAuth)) throw new Error('selftest: codex-lb setup');
948
+ if (!hasCodexUnstableFeatureWarningSuppression(codexLbConfig)) throw new Error('selftest: codex-lb setup did not suppress Codex unstable feature warning');
932
949
  await initProject(codexLbHome, { installScope: 'global', force: true, repair: true });
933
950
  const codexLbRepairSetupConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
934
951
  if (!codexLbRepairSetupConfig.includes('model_provider = "codex-lb"') || !codexLbRepairSetupConfig.includes('[model_providers.codex-lb]') || !codexLbRepairSetupConfig.includes('https://lb.example.test/backend-api/codex') || codexLbRepairSetupConfig.includes('sk-test')) throw new Error('selftest: init codex-lb');
952
+ if (!hasCodexUnstableFeatureWarningSuppression(codexLbRepairSetupConfig)) throw new Error('selftest: init codex-lb did not suppress Codex unstable feature warning');
935
953
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), `${codexLbConfig}\n[mcp_servers.supabase]\nurl = "https://mcp.supabase.com/mcp?project_ref=ref&read_only=true&features=database,docs"\n`);
936
954
  const ptmp = path.join(tmp, 'codex-lb-project-config'), prevHome = process.env.HOME;
937
955
  try { process.env.HOME = codexLbHome; await initProject(ptmp, { installScope: 'global' }); }
938
956
  finally { if (prevHome === undefined) delete process.env.HOME; else process.env.HOME = prevHome; }
939
957
  const pcfg = await safeReadText(path.join(ptmp, '.codex', 'config.toml'));
940
958
  if (!pcfg.includes('model_provider = "codex-lb"') || !pcfg.includes('[model_providers.codex-lb]') || !pcfg.includes('[mcp_servers.supabase]') || !pcfg.includes('read_only=true')) throw new Error('selftest: project codex-lb');
959
+ if (!hasCodexUnstableFeatureWarningSuppression(pcfg)) throw new Error('selftest: project codex-lb config did not suppress Codex unstable feature warning');
941
960
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n');
942
961
  const codexLbRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'auth', 'repair', '--json'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
943
962
  if (codexLbRepair.code !== 0) throw new Error(`selftest: codex-lb repair exited ${codexLbRepair.code}: ${codexLbRepair.stderr}`);
@@ -1014,7 +1033,7 @@ export async function selftestCodexLb(tmp) {
1014
1033
  const codexLbDoctorJson = JSON.parse(codexLbDoctorRepair.stdout);
1015
1034
  const codexLbDoctorAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1016
1035
  const codexLbDoctorConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1017
- if (!codexLbDoctorJson.repair?.codex_lb?.ok || !codexLbDoctorJson.repair.codex_lb.config_repaired || !codexLbDoctorJson.codex_lb?.ok || !codexLbDoctorAuth.includes('"auth_mode":"apikey"') || !codexLbDoctorAuth.includes('sk-test') || !codexLbDoctorConfig.includes('model_provider = "codex-lb"') || !codexLbDoctorConfig.includes('https://lb.example.test/backend-api/codex')) throw new Error('selftest: doctor codex-lb');
1036
+ if (!codexLbDoctorJson.repair?.codex_lb?.ok || !codexLbDoctorJson.repair.codex_lb.config_repaired || !codexLbDoctorJson.codex_lb?.ok || !codexLbDoctorAuth.includes('"auth_mode":"apikey"') || !codexLbDoctorAuth.includes('sk-test') || !codexLbDoctorConfig.includes('model_provider = "codex-lb"') || !codexLbDoctorConfig.includes('https://lb.example.test/backend-api/codex') || !hasCodexUnstableFeatureWarningSuppression(codexLbDoctorConfig)) throw new Error('selftest: doctor codex-lb');
1018
1037
  const codexLbContext7Bin = path.join(tmp, 'codex-lb-context7-bin');
1019
1038
  await ensureDir(codexLbContext7Bin);
1020
1039
  await writeTextAtomic(path.join(codexLbContext7Bin, 'codex'), '#!/bin/sh\nif [ "$1" = "--version" ]; then echo "codex-cli 99.0.0"; exit 0; fi\nif [ "$CODEX_LB_API_KEY" ]; then echo "context7 leaked CODEX_LB_API_KEY" >&2; exit 77; fi\nif [ "$1" = "mcp" ] && [ "$2" = "list" ]; then echo ""; exit 0; fi\nif [ "$1" = "mcp" ] && [ "$2" = "add" ]; then echo "context7 added"; exit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
@@ -1092,6 +1111,7 @@ export async function selftestCodexLb(tmp) {
1092
1111
  const codexLbStatusText = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'status'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
1093
1112
  if (!String(codexLbStatusText.stdout || '').includes('Repair auth: sks codex-lb repair')) throw new Error('selftest: codex-lb status did not advertise repair command');
1094
1113
  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');
1114
+ if (!hasCodexUnstableFeatureWarningSuppression(codexLbConfig)) throw new Error('selftest: codex-lb setup did not suppress Codex unstable feature warning');
1095
1115
  const codexLbLaunch = codexLaunchCommand(tmp, 'codex', []);
1096
1116
  if (!codexLbLaunch.includes('sks-codex-lb.env')) throw new Error('selftest: tmux launch command does not source codex-lb env file');
1097
1117
  if (!codexLbLaunch.includes("'--model' 'gpt-5.5'")) throw new Error('selftest: tmux launch command without args did not force GPT-5.5');
@@ -1118,3 +1138,7 @@ function hasDeprecatedCodexHooksFeatureFlag(text = '') {
1118
1138
  }
1119
1139
  return lines.slice(start + 1, end).some((line) => /^\s*codex_hooks\s*=/.test(line));
1120
1140
  }
1141
+
1142
+ function hasCodexUnstableFeatureWarningSuppression(text = '') {
1143
+ return /(^|\n)\s*suppress_unstable_features_warning\s*=\s*true\s*(?:#.*)?(?=\n|$)/.test(String(text || ''));
1144
+ }
package/src/cli/main.mjs CHANGED
@@ -4,7 +4,7 @@ import fsp from 'node:fs/promises';
4
4
  import readline from 'node:readline/promises';
5
5
  import { stdin as input, stdout as output } from 'node:process';
6
6
  import { projectRoot, readJson, writeJsonAtomic, writeTextAtomic, appendJsonlBounded, nowIso, exists, ensureDir, tmpdir, packageRoot, dirSize, formatBytes, which, runProcess, PACKAGE_VERSION, sksRoot, globalSksRoot, findProjectRoot, readStdin } from '../core/fsx.mjs';
7
- import { initProject, installSkills, normalizeInstallScope, sksCommandPrefix } from '../core/init.mjs';
7
+ import { assertCodexWarningSuppressed as assertCodexWarn, hasDeprecatedCodexHooksFeatureFlag, hasTopLevelCodexModeLock, initProject, installSkills, missingGeneratedCodexAppFeatureFlags, normalizeInstallScope, sksCommandPrefix } from '../core/init.mjs';
8
8
  import { buildCodexExecArgs, getCodexInfo, runCodexExec } from '../core/codex-adapter.mjs';
9
9
  import { createMission, loadMission, findLatestMission, missionDir, setCurrent, stateFile } from '../core/mission.mjs';
10
10
  import { buildQuestionSchema, writeQuestions } from '../core/questions.mjs';
@@ -22,7 +22,7 @@ import { bumpProjectVersion, installVersionGitHook, 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 { buildResearchPrompt, evaluateResearchGate, writeMockResearchResult, writeResearchPlan } from '../core/research.mjs';
25
+ import { evaluateResearchGate, writeMockResearchResult, writeResearchPlan } from '../core/research.mjs';
26
26
  import {
27
27
  PPT_AUDIENCE_STRATEGY_ARTIFACT,
28
28
  PPT_CLEANUP_REPORT_ARTIFACT,
@@ -78,7 +78,7 @@ import { OPENCLAW_SKILL_NAME, installOpenClawSkill } from '../core/openclaw.mjs'
78
78
  import { buildTmuxLaunchPlan, buildTmuxOpenArgs, codexLaunchCommand, createTmuxSession, defaultCodexLaunchArgs, isTmuxShellSession, runTmuxLaunchPlanSyntaxCheck, shouldAutoAttachTmux, sksAsciiLogo, tmuxReadiness, tmuxStatusKind, defaultTmuxSessionName, formatTmuxBanner, launchMadTmuxUi, launchTmuxTeamView, launchTmuxUi, platformTmuxInstallHint, reconcileTmuxTeamCockpit, runTmuxStatus, sanitizeTmuxSessionName, teamLaneStyle } from '../core/tmux-ui.mjs';
79
79
  import { autoReviewProfileName, autoReviewStatus, autoReviewSummary, enableAutoReview, disableAutoReview, enableMadHighProfile, madHighProfileName } from '../core/auto-review.mjs';
80
80
  import { context7Command } from './context7-command.mjs';
81
- import { askPostinstallQuestion, checkContext7, checkRequiredSkills, codexLbStatus, configureCodexLb, ensureCodexCliTool, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, globalCodexSkillsRoot, maybePromptCodexLbSetupForLaunch, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, repairCodexLbAuth, selftestCodexLb, shouldAutoApproveInstall } from './install-helpers.mjs';
81
+ import { askPostinstallQuestion, checkContext7, checkRequiredSkills, codexLbStatus, configureCodexLb, ensureCodexCliTool, ensureGlobalCodexFastModeDuringInstall, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, globalCodexSkillsRoot, maybePromptCodexLbSetupForLaunch, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, repairCodexLbAuth, selftestCodexLb, shouldAutoApproveInstall } from './install-helpers.mjs';
82
82
  import { buildTeamPlan, codeStructureCommand, dbCommand, defaultBeta, defaultVGraph, evalCommand, gcCommand, goalCommand, gxCommand, harnessCommand, hproofCommand, madHighCommand as runMadHighCommand, memoryCommand, migrateWikiContextPack, parseTeamCreateArgs, perfCommand, profileCommand, projectWikiClaims, proofFieldCommand, qaLoopCommand, quickstartCommand, researchCommand, skillDreamCommand, statsCommand, team, teamWorkflowMarkdown, validateArtifactsCommand, wikiCommand, wikiVoxelRowCount, writeWikiContextPack } from './maintenance-commands.mjs';
83
83
  import { openClawCommand } from './openclaw-command.mjs';
84
84
 
@@ -211,7 +211,7 @@ Usage:
211
211
  sks team attach-tmux [mission-id|latest]
212
212
  sks team cleanup-tmux [mission-id|latest]
213
213
  sks research prepare "topic" [--depth frontier]
214
- sks research run <mission-id|latest> [--mock] [--max-cycles N]
214
+ sks research run <mission-id|latest> [--mock] [--max-cycles N] [--cycle-timeout-minutes N]
215
215
  sks research status <mission-id|latest>
216
216
  sks db policy
217
217
  sks db scan [--migrations] [--json]
@@ -1659,6 +1659,7 @@ async function doctor(args) {
1659
1659
  let conflictScan = await scanHarnessConflicts(root);
1660
1660
  let repairApplied = false;
1661
1661
  let globalSkillsRepair = null;
1662
+ let globalCodexConfigRepair = null;
1662
1663
  let projectRepair = null;
1663
1664
  let codexLbRepair = null;
1664
1665
  const globalCommand = await globalSksCommand();
@@ -1666,6 +1667,7 @@ async function doctor(args) {
1666
1667
  const existingManifest = await readJson(path.join(root, '.sneakoscope', 'manifest.json'), null);
1667
1668
  const fixScope = requestedScope || normalizeInstallScope(existingManifest?.installation?.scope || 'global');
1668
1669
  projectRepair = await initProject(root, { installScope: fixScope, globalCommand, localOnly: flag(args, '--local-only') || Boolean(existingManifest?.git?.local_only), force: true, repair: true });
1670
+ if (!flag(args, '--local-only')) globalCodexConfigRepair = await ensureGlobalCodexFastModeDuringInstall();
1669
1671
  if (!flag(args, '--local-only')) globalSkillsRepair = await ensureGlobalCodexSkillsDuringInstall({ force: true });
1670
1672
  codexLbRepair = await repairCodexLbAuth();
1671
1673
  repairApplied = true;
@@ -1695,7 +1697,7 @@ async function doctor(args) {
1695
1697
  const result = {
1696
1698
  node: { ok: nodeOk, version: process.version }, root, codex, rust,
1697
1699
  install,
1698
- repair: { applied: repairApplied, project: projectRepair, global_skills: globalSkillsRepair, codex_lb: codexLbRepair, blocked_by_other_harness: flag(args, '--fix') && conflictScan.hard_block },
1700
+ repair: { applied: repairApplied, project: projectRepair, global_codex_config: globalCodexConfigRepair, global_skills: globalSkillsRepair, codex_lb: codexLbRepair, blocked_by_other_harness: flag(args, '--fix') && conflictScan.hard_block },
1699
1701
  harness_conflicts: {
1700
1702
  ok: conflictScan.ok,
1701
1703
  hard_block: conflictScan.hard_block,
@@ -1729,6 +1731,7 @@ async function doctor(args) {
1729
1731
  console.log(`Install: ${install.ok ? 'ok' : 'missing'} ${install.scope} (${install.command_prefix})`);
1730
1732
  console.log(`Conflicts: ${result.harness_conflicts.hard_block ? 'blocked' : 'ok'} ${result.harness_conflicts.conflicts.length} finding(s)`);
1731
1733
  if (repairApplied) console.log('Repair: regenerated SKS managed files from the installed package template');
1734
+ if (globalCodexConfigRepair) console.log(`Global Codex config: ${globalCodexConfigRepair.status} ${globalCodexConfigRepair.config_path || ''}`.trimEnd());
1732
1735
  if (globalSkillsRepair) {
1733
1736
  const removed = globalSkillsRepair.removed_stale_generated_skills || [];
1734
1737
  const cleanup = removed.length ? ` removed stale generated skill shadow(s): ${removed.join(', ')}` : '';
@@ -1925,34 +1928,6 @@ async function safeReadText(file, fallback = '') {
1925
1928
  try { return await fsp.readFile(file, 'utf8'); } catch { return fallback; }
1926
1929
  }
1927
1930
 
1928
- function hasTopLevelCodexModeLock(text = '') {
1929
- const lines = String(text || '').split('\n');
1930
- const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
1931
- const top = (firstTable === -1 ? lines : lines.slice(0, firstTable)).join('\n');
1932
- const model = top.match(/^model\s*=\s*"([^"]+)"/m)?.[1];
1933
- return (Boolean(model) && model !== 'gpt-5.5') || /^model_reasoning_effort\s*=/m.test(top);
1934
- }
1935
-
1936
- function hasDeprecatedCodexHooksFeatureFlag(text = '') {
1937
- const lines = String(text || '').split('\n');
1938
- const start = lines.findIndex((line) => line.trim() === '[features]');
1939
- if (start === -1) return false;
1940
- let end = lines.length;
1941
- for (let i = start + 1; i < lines.length; i += 1) {
1942
- if (/^\s*\[.+\]\s*$/.test(lines[i])) {
1943
- end = i;
1944
- break;
1945
- }
1946
- }
1947
- return lines.slice(start + 1, end).some((line) => /^\s*codex_hooks\s*=/.test(line));
1948
- }
1949
-
1950
- const REQUIRED_GENERATED_CODEX_APP_FEATURE_FLAGS = ['hooks', 'multi_agent', 'fast_mode', 'fast_mode_ui', 'codex_git_commit', 'computer_use', 'apps', 'plugins'];
1951
-
1952
- function missingGeneratedCodexAppFeatureFlags(text = '') {
1953
- return REQUIRED_GENERATED_CODEX_APP_FEATURE_FLAGS.filter((name) => !String(text || '').includes(`${name} = true`));
1954
- }
1955
-
1956
1931
  async function resolveMissionId(root, arg) { return (!arg || arg === 'latest') ? findLatestMission(root) : arg; }
1957
1932
  function readMaxCycles(args, fallback) {
1958
1933
  const i = args.indexOf('--max-cycles');
@@ -1963,7 +1938,7 @@ function readMaxCycles(args, fallback) {
1963
1938
 
1964
1939
  function positionalArgs(args = []) {
1965
1940
  const out = [];
1966
- const valueFlags = new Set(['--format', '--iterations', '--out', '--baseline', '--candidate', '--install-scope', '--max-cycles', '--depth', '--scope', '--transport', '--query', '--topic', '--tokens', '--timeout-ms', '--sql', '--command', '--project-ref', '--agent', '--phase', '--message', '--role', '--max-anchors', '--lines', '--dir']);
1941
+ const valueFlags = new Set(['--format', '--iterations', '--out', '--baseline', '--candidate', '--install-scope', '--max-cycles', '--cycle-timeout-minutes', '--depth', '--scope', '--transport', '--query', '--topic', '--tokens', '--timeout-ms', '--sql', '--command', '--project-ref', '--agent', '--phase', '--message', '--role', '--max-anchors', '--lines', '--dir']);
1967
1942
  for (let i = 0; i < args.length; i++) {
1968
1943
  const arg = String(args[i]);
1969
1944
  if (valueFlags.has(arg)) {
@@ -2118,6 +2093,8 @@ async function selftest() {
2118
2093
  if (!doctorRepairJson.repair?.applied || doctorRepairJson.install?.scope !== 'project' || !doctorRepairJson.install?.ok || !doctorRepairJson.install?.source_project) throw new Error('selftest: doctor scope');
2119
2094
  const repairedManifest = await readJson(path.join(repairTmp, '.sneakoscope', 'manifest.json'));
2120
2095
  if (repairedManifest.installation?.scope !== 'project' || repairedManifest.installation?.hook_command_prefix !== 'node ./bin/sks.mjs') throw new Error('selftest: manifest scope');
2096
+ const repairedCodexConfig = await safeReadText(path.join(repairTmp, '.codex', 'config.toml'));
2097
+ assertCodexWarn(repairedCodexConfig, 'doctor project config');
2121
2098
  const repairedTeamSkill = await safeReadText(path.join(repairTmp, '.agents', 'skills', 'team', 'SKILL.md'));
2122
2099
  if (!repairedTeamSkill.includes('SKS Team orchestration') || repairedTeamSkill.includes('tampered')) throw new Error('selftest: doctor repair did not regenerate team skill');
2123
2100
  if (await exists(path.join(repairTmp, '.agents', 'skills', 'agent-team', 'SKILL.md'))) throw new Error('selftest: doctor repair did not remove deprecated agent-team alias skill');
@@ -2153,6 +2130,9 @@ async function selftest() {
2153
2130
  });
2154
2131
  if (doctorGlobalRepair.code !== 0) throw new Error(`selftest: doctor --fix global skill repair exited ${doctorGlobalRepair.code}: ${doctorGlobalRepair.stderr}`);
2155
2132
  const doctorGlobalRepairJson = JSON.parse(doctorGlobalRepair.stdout || '{}');
2133
+ const doctorGlobalCodexConfig = await safeReadText(path.join(doctorGlobalHome, '.codex', 'config.toml'));
2134
+ if (!doctorGlobalRepairJson.repair?.global_codex_config) throw new Error('selftest: doctor global config repair missing');
2135
+ assertCodexWarn(doctorGlobalCodexConfig, 'doctor global config');
2156
2136
  for (const name of stalePluginSkillNames) {
2157
2137
  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`);
2158
2138
  }
@@ -2185,6 +2165,7 @@ async function selftest() {
2185
2165
  if (!(await exists(path.join(postinstallSetupTmp, '.codex', 'hooks.json')))) throw new Error('selftest: postinstall did not create project hooks during automatic bootstrap');
2186
2166
  const postinstallSetupConfig = await safeReadText(path.join(postinstallSetupTmp, '.codex', 'config.toml'));
2187
2167
  if (missingGeneratedCodexAppFeatureFlags(postinstallSetupConfig).length || hasDeprecatedCodexHooksFeatureFlag(postinstallSetupConfig)) throw new Error('selftest: postinstall flags');
2168
+ assertCodexWarn(postinstallSetupConfig, 'postinstall project config');
2188
2169
  if (!String(postinstallSetup.stdout || '').includes('Codex App global $ skills: installed')) throw new Error('selftest: postinstall did not report automatic global Codex App skills');
2189
2170
  if (!String(postinstallSetup.stdout || '').includes('Removed stale generated skill shadow(s):')) throw new Error('selftest: postinstall did not report stale first-party plugin shadow cleanup');
2190
2171
  const postinstallSetupManifest = await readJson(path.join(postinstallSetupTmp, '.sneakoscope', 'manifest.json'));
@@ -2221,6 +2202,7 @@ async function selftest() {
2221
2202
  if (!(await exists(path.join(postinstallNoMarkerGlobalRoot, '.sneakoscope', 'manifest.json')))) throw new Error('selftest: no-marker postinstall did not bootstrap global runtime root');
2222
2203
  const postinstallNoMarkerConfig = await safeReadText(path.join(postinstallNoMarkerGlobalRoot, '.codex', 'config.toml'));
2223
2204
  if (missingGeneratedCodexAppFeatureFlags(postinstallNoMarkerConfig).length || hasDeprecatedCodexHooksFeatureFlag(postinstallNoMarkerConfig)) throw new Error('selftest: no-marker flags');
2205
+ assertCodexWarn(postinstallNoMarkerConfig, 'postinstall global runtime config');
2224
2206
  if (await exists(path.join(postinstallNoMarkerCwd, '.sneakoscope'))) throw new Error('selftest: no-marker postinstall polluted install cwd');
2225
2207
  if (await exists(path.join(postinstallNoMarkerGlobalRoot, '.gitignore'))) throw new Error('selftest: global runtime bootstrap without project git wrote shared .gitignore');
2226
2208
  const bootstrapJsonTmp = tmpdir();
@@ -2647,19 +2629,21 @@ async function selftest() {
2647
2629
  const hookResearchTeamResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookResearchMarkdownTmp, input: hookResearchTeamPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
2648
2630
  if (hookResearchTeamResult.code !== 0) throw new Error(`selftest: active Team setup before markdown $Research hook exited ${hookResearchTeamResult.code}: ${hookResearchTeamResult.stderr}`);
2649
2631
  const hookResearchTeamState = await readJson(stateFile(hookResearchMarkdownTmp), {});
2650
- const hookResearchMarkdownPayload = JSON.stringify({ cwd: hookResearchMarkdownTmp, prompt: '[$research](/tmp/research/SKILL.md) Codex Computer Use 도구 노출 문제를 QA루프 관점에서 연구' });
2632
+ const hookResearchMarkdownPayload = JSON.stringify({ cwd: hookResearchMarkdownTmp, prompt: '논문 [$research](x) 커밋 푸쉬 연구' });
2651
2633
  const hookResearchMarkdownResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookResearchMarkdownTmp, input: hookResearchMarkdownPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
2652
2634
  if (hookResearchMarkdownResult.code !== 0) throw new Error(`selftest: markdown $Research hook exited ${hookResearchMarkdownResult.code}: ${hookResearchMarkdownResult.stderr}`);
2653
2635
  const hookResearchMarkdownJson = JSON.parse(hookResearchMarkdownResult.stdout);
2654
2636
  const hookResearchMarkdownContext = hookResearchMarkdownJson.hookSpecificOutput?.additionalContext || '';
2655
- if (!hookResearchMarkdownContext.includes('$Research route prepared')) throw new Error('selftest: markdown $Research hook did not prepare Research route');
2656
- if (hookResearchMarkdownContext.includes(`Active Team mission ${hookResearchTeamState.mission_id}`)) throw new Error('selftest: markdown $Research hook retained stale active Team context');
2657
- if (!String(hookResearchMarkdownJson.systemMessage || '').includes('Research route') || String(hookResearchMarkdownJson.systemMessage || '').includes('QA-LOOP route')) throw new Error('selftest: markdown $Research visible hook message was hijacked by QA-LOOP policy text');
2637
+ if (!hookResearchMarkdownContext.includes('$Research route prepared')) throw new Error('selftest: markdown research hook');
2638
+ if (hookResearchMarkdownContext.includes(`Active Team mission ${hookResearchTeamState.mission_id}`)) throw new Error('selftest: stale Team context');
2639
+ if (!String(hookResearchMarkdownJson.systemMessage || '').includes('Research route') || String(hookResearchMarkdownJson.systemMessage || '').includes('QA-LOOP route')) throw new Error('selftest: research hook message');
2658
2640
  const hookResearchMarkdownState = await readJson(stateFile(hookResearchMarkdownTmp), {});
2659
- if (hookResearchMarkdownState.mode !== 'RESEARCH' || hookResearchMarkdownState.route !== 'Research' || hookResearchMarkdownState.mission_id === hookResearchTeamState.mission_id || hookResearchMarkdownState.stop_gate !== 'research-gate.json' || !hookResearchMarkdownState.pipeline_plan_ready) throw new Error('selftest: markdown $Research hook did not replace active Team with prepared Research mission state');
2641
+ if (hookResearchMarkdownState.mode !== 'RESEARCH' || hookResearchMarkdownState.route !== 'Research' || hookResearchMarkdownState.mission_id === hookResearchTeamState.mission_id || hookResearchMarkdownState.stop_gate !== 'research-gate.json' || !hookResearchMarkdownState.pipeline_plan_ready) throw new Error('selftest: research hook state');
2660
2642
  const hookResearchMissionDir = missionDir(hookResearchMarkdownTmp, hookResearchMarkdownState.mission_id);
2661
- if (!(await exists(path.join(hookResearchMissionDir, PIPELINE_PLAN_ARTIFACT)))) throw new Error('selftest: markdown $Research hook did not write pipeline plan');
2662
- for (const artifact of ['source-ledger.json', 'scout-ledger.json', 'debate-ledger.json', 'falsification-ledger.json']) {
2643
+ if (!(await exists(path.join(hookResearchMissionDir, PIPELINE_PLAN_ARTIFACT)))) throw new Error('selftest: research hook plan');
2644
+ const rss = 'research-source-skill.md';
2645
+ const gos = 'genius-opinion-summary.md';
2646
+ for (const artifact of [rss, 'source-ledger.json', 'scout-ledger.json', 'debate-ledger.json', 'falsification-ledger.json']) {
2663
2647
  if (!(await exists(path.join(hookResearchMissionDir, artifact)))) throw new Error(`selftest: hook research ${artifact}`);
2664
2648
  }
2665
2649
  const hookPayload = JSON.stringify({ cwd: hookGoalTmp, prompt: '$Goal 로그인 세션 만료 UX 개선' });
@@ -2999,6 +2983,7 @@ async function selftest() {
2999
2983
  const codexConfigText = await safeReadText(path.join(tmp, '.codex', 'config.toml'));
3000
2984
  const missingCodexConfigFlags = missingGeneratedCodexAppFeatureFlags(codexConfigText);
3001
2985
  if (missingCodexConfigFlags.length || hasDeprecatedCodexHooksFeatureFlag(codexConfigText)) throw new Error(`selftest: generated Codex App feature flags missing or deprecated: ${missingCodexConfigFlags.join(', ')}`);
2986
+ assertCodexWarn(codexConfigText, 'generated Codex App config');
3002
2987
  if (!hasContext7ConfigText(codexConfigText)) throw new Error('selftest: Context7 MCP not configured');
3003
2988
  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');
3004
2989
  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');
@@ -3010,6 +2995,7 @@ async function selftest() {
3010
2995
  await initProject(preservedConfigTmp, {});
3011
2996
  const preservedConfig = await safeReadText(path.join(preservedConfigTmp, '.codex', 'config.toml'));
3012
2997
  if (!/^model = "gpt-5\.5"/m.test(preservedConfig) || !preservedConfig.includes('service_tier = "fast"') || !preservedConfig.includes('fast_mode = true') || !preservedConfig.includes('fast_mode_ui = true') || !preservedConfig.includes('[user.fast_mode]') || !preservedConfig.includes('visible = true') || !preservedConfig.includes('enabled = true') || !preservedConfig.includes('default_profile = "sks-fast-high"') || !/\[profiles\.sks-fast-high\][\s\S]*?service_tier = "fast"/.test(preservedConfig)) throw new Error('selftest: Codex config merge dropped or failed to enable Fast mode defaults and GPT-5.5');
2998
+ assertCodexWarn(preservedConfig, 'merged Codex config');
3013
2999
  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');
3014
3000
  const missingPreservedFlags = missingGeneratedCodexAppFeatureFlags(preservedConfig);
3015
3001
  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(', ')}`);
@@ -3245,7 +3231,7 @@ async function selftest() {
3245
3231
  if (teamPlan.agent_session_count !== 5) throw new Error('selftest: team default sessions not 5');
3246
3232
  if (teamPlan.role_counts.executor !== 3 || teamPlan.role_counts.user !== 1 || teamPlan.role_counts.reviewer !== 5) throw new Error('selftest: team default role counts invalid');
3247
3233
  const teamPlanFeatureFlags = teamPlan.codex_config_required?.features || {};
3248
- const missingTeamPlanFeatureFlags = REQUIRED_GENERATED_CODEX_APP_FEATURE_FLAGS.filter((name) => teamPlanFeatureFlags[name] !== true);
3234
+ const missingTeamPlanFeatureFlags = missingGeneratedCodexAppFeatureFlags(teamPlanFeatureFlags);
3249
3235
  if (missingTeamPlanFeatureFlags.length || teamPlanFeatureFlags.codex_hooks === true) throw new Error(`selftest: team plan Codex config missing required app flags or still uses deprecated codex_hooks: ${missingTeamPlanFeatureFlags.join(', ')}`);
3250
3236
  if (!teamPlan.review_gate?.passed || teamPlan.review_gate.required_reviewer_lanes !== 5) throw new Error('selftest: team review policy gate did not pass default plan');
3251
3237
  if (teamPlan.codex_config_required?.service_tier !== 'fast' || teamPlan.reasoning?.service_tier !== 'fast') throw new Error('selftest: team plan did not require Fast service tier');
@@ -3476,7 +3462,7 @@ async function selftest() {
3476
3462
  if (!(await readTeamTranscriptTail(teamDir, 1)).join('\n').includes('selftest mapped options')) throw new Error('selftest: team transcript tail missing event');
3477
3463
  const teamLane = await renderTeamAgentLane(teamDir, { missionId: teamId, agent: 'analysis_scout_1', lines: 4 });
3478
3464
  if (!teamLane.includes('selftest mapped repo slice')) throw new Error('selftest: team agent lane missing event context');
3479
- if (!teamLane.includes('## Live Chat') || !teamLane.includes('selftest mapped repo slice') || teamLane.includes('## Global Tail')) throw new Error('selftest:cht');
3465
+ if (!teamLane.includes('## Codex Chat') || !teamLane.includes('+-- me [status]') || !teamLane.includes('selftest mapped repo slice') || teamLane.includes('## Global Tail')) throw new Error('selftest: chat lane');
3480
3466
  const teamLaneCli = await runProcess(process.execPath, [hookBin, 'team', 'lane', teamId, '--agent', 'analysis_scout_1', '--lines', '4'], { cwd: tmp, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
3481
3467
  if (teamLaneCli.code !== 0 || !String(teamLaneCli.stdout || '').includes('SKS Team Agent Lane') || !String(teamLaneCli.stdout || '').includes('analysis_scout_1')) throw new Error('selftest: sks team lane CLI did not render an agent lane');
3482
3468
  await writeTextAtomic(path.join(teamDir, 'team-analysis.md'), '- claim: analysis scout mapped route registry | source: src/core/routes.mjs | risk: high | confidence: supported\n');
@@ -3782,15 +3768,16 @@ async function selftest() {
3782
3768
  if (wikiPruneDryRun.candidates < 1 || !wikiPruneDryRun.actions.some((action) => action.reason === 'low_wiki_trust')) throw new Error('selftest: wiki prune did not flag low-trust artifact');
3783
3769
  const { dir: researchDir, mission: researchMission } = await createMission(tmp, { mode: 'research', prompt: '새로운 코드 리뷰 방법론 연구' });
3784
3770
  const researchPlan = await writeResearchPlan(researchDir, researchMission.prompt, {});
3785
- if (researchPlan.methodology !== 'genius-scout-council-frontier-discovery-loop' || researchPlan.web_research_policy?.mode !== 'maximum_source_retrieval') throw new Error('selftest: research plan contract');
3771
+ 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');
3786
3772
  const rArts = researchPlan.required_artifacts || [];
3787
- for (const a of ['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');
3788
- if (!rArts.includes('research-paper.md')) throw new Error('selftest: research paper');
3773
+ 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');
3774
+ if (!rArts.includes('research-paper.md') || !rArts.includes(gos)) throw new Error('selftest: research paper');
3789
3775
  const initialResearchGate = await evaluateResearchGate(researchDir);
3790
3776
  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');
3791
3777
  const researchGate = await writeMockResearchResult(researchDir, researchPlan);
3792
3778
  if (!researchGate.passed) throw new Error('selftest: mock research gate did not pass');
3793
- if (['independent_scouts', 'xhigh_scouts', 'eureka_moments', 'debate_participants'].some((m) => researchGate.metrics?.[m] < 5) || researchGate.metrics?.counterevidence_sources < 1 || researchGate.metrics?.paper_sections < 8 || researchGate.metrics?.citation_coverage !== true || researchGate.metrics?.falsification_cases < 1) throw new Error('selftest: research metrics');
3779
+ const rm = researchGate.metrics || {};
3780
+ 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');
3794
3781
  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 });
3795
3782
  const gate = await evaluateDoneGate(tmp, id);
3796
3783
  if (!gate.passed) throw new Error('selftest: done gate');
@@ -8,14 +8,14 @@ import { buildQuestionSchema, writeQuestions } from '../core/questions.mjs';
8
8
  import { sealContract } from '../core/decision-contract.mjs';
9
9
  import { buildQaLoopQuestionSchema, buildQaLoopPrompt, evaluateQaGate, qaStatus, writeMockQaResult, writeQaLoopArtifacts } from '../core/qa-loop.mjs';
10
10
  import { containsUserQuestion, noQuestionContinuationReason } from '../core/no-question-guard.mjs';
11
- import { RESEARCH_PAPER_ARTIFACT, countResearchPaperSections, buildResearchPrompt, evaluateResearchGate, writeMockResearchResult, writeResearchPlan } from '../core/research.mjs';
11
+ import { RESEARCH_GENIUS_SUMMARY_ARTIFACT, RESEARCH_PAPER_ARTIFACT, RESEARCH_SOURCE_SKILL_ARTIFACT, countGeniusOpinionSummaries, countResearchPaperSections, buildResearchPrompt, evaluateResearchGate, writeMockResearchResult, writeResearchPlan } from '../core/research.mjs';
12
12
  import { storageReport, enforceRetention, pruneWikiArtifacts } from '../core/retention.mjs';
13
13
  import { evaluateDoneGate } from '../core/hproof.mjs';
14
14
  import { renderCartridge, validateCartridge, driftCartridge, snapshotCartridge } from '../core/gx-renderer.mjs';
15
15
  import { DEFAULT_EVAL_THRESHOLDS, compareEvaluationReports, runEvaluationBenchmark } from '../core/evaluation.mjs';
16
16
  import { contextCapsule } from '../core/triwiki-attention.mjs';
17
17
  import { rgbaKey, rgbaToWikiCoord, validateWikiCoordinateIndex } from '../core/wiki-coordinate.mjs';
18
- import { ALLOWED_REASONING_EFFORTS, CODEX_COMPUTER_USE_ONLY_POLICY, DOLLAR_SKILL_NAMES, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_SOURCE_INVENTORY_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, FROM_CHAT_IMG_VISUAL_MAP_ARTIFACT, FROM_CHAT_IMG_WORK_ORDER_ARTIFACT, RECOMMENDED_SKILLS, ROUTES, hasFromChatImgSignal, reflectionRequiredForRoute, routePrompt, routeReasoning, routeRequiresSubagents, stackCurrentDocsPolicy, stripVisibleDecisionAnswerBlocks, triwikiContextTracking } from '../core/routes.mjs';
18
+ import { ALLOWED_REASONING_EFFORTS, CODEX_COMPUTER_USE_ONLY_POLICY, DOLLAR_SKILL_NAMES, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_SOURCE_INVENTORY_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, FROM_CHAT_IMG_VISUAL_MAP_ARTIFACT, FROM_CHAT_IMG_WORK_ORDER_ARTIFACT, RECOMMENDED_SKILLS, ROUTES, hasFromChatImgSignal, reflectionRequiredForRoute, routeNeedsContext7, routePrompt, routeReasoning, routeRequiresSubagents, stackCurrentDocsPolicy, stripVisibleDecisionAnswerBlocks, triwikiContextTracking } from '../core/routes.mjs';
19
19
  import { TEAM_DECOMPOSITION_ARTIFACT, TEAM_GRAPH_ARTIFACT, TEAM_INBOX_DIR, TEAM_RUNTIME_TASKS_ARTIFACT, teamRuntimePlanMetadata, teamRuntimeRequiredArtifacts, writeTeamRuntimeArtifacts } from '../core/team-dag.mjs';
20
20
  import { appendTeamEvent, formatAgentReasoning, formatRoleCounts, initTeamLive, normalizeTeamSpec, parseTeamSpecArgs, readTeamControl, readTeamDashboard, readTeamLive, readTeamTranscriptTail, renderTeamAgentLane, renderTeamCleanupSummary, renderTeamWatch, requestTeamSessionCleanup, teamCleanupRequested, teamReasoningPolicy } from '../core/team-live.mjs';
21
21
  import { evaluateTeamReviewPolicyGate, MIN_TEAM_REVIEWER_LANES, MIN_TEAM_REVIEW_POLICY_TEXT, teamReviewPolicy } from '../core/team-review-policy.mjs';
@@ -42,6 +42,10 @@ const flag = (args, name) => args.includes(name);
42
42
  const promptOf = (args) => args.filter((x) => !String(x).startsWith('--')).join(' ').trim();
43
43
  const TEAM_SESSION_CLEANUP_ARTIFACT = 'team-session-cleanup.json';
44
44
  const REPOSITORY_URL = 'https://github.com/mandarange/Sneakoscope-Codex.git';
45
+ const RESEARCH_DEFAULT_MAX_CYCLES = 3;
46
+ const RESEARCH_DEFAULT_CYCLE_TIMEOUT_MINUTES = 120;
47
+ const RESEARCH_MIN_CYCLE_TIMEOUT_MINUTES = 15;
48
+ const RESEARCH_MAX_CYCLE_TIMEOUT_MINUTES = 240;
45
49
 
46
50
  async function resolveMissionId(root, arg) { return (!arg || arg === 'latest') ? findLatestMission(root) : arg; }
47
51
 
@@ -429,7 +433,7 @@ async function researchPrepare(args) {
429
433
  if (!prompt) throw new Error('Missing research topic.');
430
434
  const { id, dir } = await createMission(root, { mode: 'research', prompt });
431
435
  const route = ROUTES.find((entry) => entry.id === 'Research') || routePrompt('$Research');
432
- const context7Required = true;
436
+ const context7Required = routeNeedsContext7(route, prompt);
433
437
  const reasoning = routeReasoning(route, prompt);
434
438
  const plan = await writeResearchPlan(dir, prompt, { depth: readFlagValue(args, '--depth', 'frontier') });
435
439
  const pipelinePlan = await writePipelinePlan(dir, { missionId: id, route, task: prompt, required: context7Required, ambiguity: { required: false, status: 'direct_research_cli' } });
@@ -457,7 +461,7 @@ async function researchPrepare(args) {
457
461
  mode: route.mode,
458
462
  phase: 'RESEARCH_PREPARED',
459
463
  questions_allowed: false,
460
- implementation_allowed: true,
464
+ implementation_allowed: false,
461
465
  context7_required: context7Required,
462
466
  context7_verified: false,
463
467
  subagents_required: routeRequiresSubagents(route, prompt),
@@ -480,14 +484,16 @@ async function researchPrepare(args) {
480
484
  console.log(`Plan: ${path.relative(root, path.join(dir, 'research-plan.md'))}`);
481
485
  console.log(`Pipeline: ${path.relative(root, path.join(dir, PIPELINE_PLAN_ARTIFACT))}`);
482
486
  console.log(`Paper: ${RESEARCH_PAPER_ARTIFACT}`);
487
+ console.log(`Genius summary: ${RESEARCH_GENIUS_SUMMARY_ARTIFACT}`);
488
+ console.log(`Source skill: ${RESEARCH_SOURCE_SKILL_ARTIFACT}`);
483
489
  console.log('Ledgers: source-ledger.json, scout-ledger.json, debate-ledger.json, novelty-ledger.json, falsification-ledger.json');
484
- console.log(`Run: sks research run ${id} --max-cycles 3`);
490
+ console.log(`Run: sks research run ${id} --max-cycles ${RESEARCH_DEFAULT_MAX_CYCLES} --cycle-timeout-minutes ${RESEARCH_DEFAULT_CYCLE_TIMEOUT_MINUTES}`);
485
491
  }
486
492
 
487
493
  async function researchRun(args) {
488
494
  const root = await sksRoot();
489
495
  const id = await resolveMissionId(root, args[0]);
490
- if (!id) throw new Error('Usage: sks research run <mission-id|latest> [--mock] [--max-cycles N]');
496
+ if (!id) throw new Error('Usage: sks research run <mission-id|latest> [--mock] [--max-cycles N] [--cycle-timeout-minutes N]');
491
497
  const { dir, mission } = await loadMission(root, id);
492
498
  const planPath = path.join(dir, 'research-plan.json');
493
499
  if (!(await exists(planPath))) await writeResearchPlan(dir, mission.prompt || '', {});
@@ -499,32 +505,46 @@ async function researchRun(args) {
499
505
  process.exitCode = 2;
500
506
  return;
501
507
  }
502
- const maxCycles = readMaxCycles(args, 3);
508
+ const maxCycles = readMaxCycles(args, RESEARCH_DEFAULT_MAX_CYCLES);
509
+ const cycleTimeoutMinutes = readResearchCycleTimeoutMinutes(args);
510
+ const cycleTimeoutMs = cycleTimeoutMinutes * 60 * 1000;
503
511
  const mock = flag(args, '--mock');
504
- await setCurrent(root, { mission_id: id, mode: 'RESEARCH', phase: 'RESEARCH_RUNNING_NO_QUESTIONS', questions_allowed: false });
505
- await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.run.started', maxCycles, mock });
512
+ await setCurrent(root, { mission_id: id, mode: 'RESEARCH', phase: 'RESEARCH_RUNNING_NO_QUESTIONS', questions_allowed: false, implementation_allowed: false, research_real_run_required: !mock, research_cycle_timeout_minutes: cycleTimeoutMinutes });
513
+ await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.run.started', maxCycles, mock, cycleTimeoutMinutes, real_run_required: !mock });
506
514
  if (mock) {
507
515
  const gate = await writeMockResearchResult(dir, plan);
508
- await setCurrent(root, { mission_id: id, mode: 'RESEARCH', phase: gate.passed ? 'RESEARCH_DONE' : 'RESEARCH_PAUSED', questions_allowed: true });
516
+ await setCurrent(root, { mission_id: id, mode: 'RESEARCH', phase: gate.passed ? 'RESEARCH_DONE' : 'RESEARCH_PAUSED', questions_allowed: true, implementation_allowed: false });
509
517
  console.log(`Mock research done: ${id}`);
510
518
  console.log(`Gate: ${gate.passed ? 'passed' : 'blocked'}`);
511
519
  return;
512
520
  }
513
521
  const codex = await getCodexInfo();
514
522
  if (!codex.bin) {
515
- console.error('Codex CLI not found. Running mock research instead.');
516
- const gate = await writeMockResearchResult(dir, plan);
517
- await setCurrent(root, { mission_id: id, mode: 'RESEARCH', phase: gate.passed ? 'RESEARCH_DONE' : 'RESEARCH_PAUSED', questions_allowed: true });
518
- console.log(`Mock research done: ${id}`);
523
+ const blocker = {
524
+ schema_version: 1,
525
+ mission_id: id,
526
+ ts: nowIso(),
527
+ phase: 'RESEARCH_BLOCKED_REAL_RUN_REQUIRED',
528
+ reason: 'Codex CLI not found; normal Research cannot fall back to mock output.',
529
+ required_action: 'Install/configure the Codex CLI or set SKS_CODEX_BIN to a valid executable, then rerun sks research run without --mock.',
530
+ mock_policy: '--mock is allowed only for selftests and dry harness checks.',
531
+ implementation_allowed: false
532
+ };
533
+ await writeJsonAtomic(path.join(dir, 'research-blocker.json'), blocker);
534
+ await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: blocker.ts, type: 'research.blocked.real_run_required', reason: blocker.reason, blocker: 'research-blocker.json' });
535
+ await setCurrent(root, { mission_id: id, mode: 'RESEARCH', phase: 'RESEARCH_BLOCKED_REAL_RUN_REQUIRED', questions_allowed: true, implementation_allowed: false, research_real_run_required: true, blocker: 'research-blocker.json' });
536
+ console.error('Research cannot run real sources: Codex CLI not found.');
537
+ console.error('Mock fallback is disabled for normal Research. Use --mock only for selftests, or install/configure Codex CLI/SKS_CODEX_BIN.');
538
+ process.exitCode = 2;
519
539
  return;
520
540
  }
521
541
  let last = '';
522
542
  for (let cycle = 1; cycle <= maxCycles; cycle++) {
523
543
  const cycleDir = path.join(dir, 'research', `cycle-${cycle}`);
524
544
  const outputFile = path.join(cycleDir, 'final.md');
525
- await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.cycle.start', cycle });
545
+ await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.cycle.start', cycle, timeoutMinutes: cycleTimeoutMinutes });
526
546
  const prompt = buildResearchPrompt({ id, mission, plan, cycle, previous: last });
527
- const result = await runCodexExec({ root, prompt, outputFile, json: true, profile: 'sks-research', logDir: cycleDir, timeoutMs: 45 * 60 * 1000 });
547
+ const result = await runCodexExec({ root, prompt, outputFile, json: true, profile: 'sks-research', logDir: cycleDir, timeoutMs: cycleTimeoutMs });
528
548
  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 });
529
549
  last = await safeReadText(outputFile, result.stdout || result.stderr || '');
530
550
  if (containsUserQuestion(last)) {
@@ -534,7 +554,7 @@ async function researchRun(args) {
534
554
  }
535
555
  const gate = await evaluateResearchGate(dir);
536
556
  if (gate.passed) {
537
- await setCurrent(root, { mission_id: id, mode: 'RESEARCH', phase: 'RESEARCH_DONE', questions_allowed: true });
557
+ await setCurrent(root, { mission_id: id, mode: 'RESEARCH', phase: 'RESEARCH_DONE', questions_allowed: true, implementation_allowed: false });
538
558
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.done', cycle });
539
559
  await enforceRetention(root).catch(() => {});
540
560
  console.log(`Research done: ${id}`);
@@ -542,7 +562,7 @@ async function researchRun(args) {
542
562
  }
543
563
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'research.cycle.continue', cycle, reasons: gate.reasons });
544
564
  }
545
- await setCurrent(root, { mission_id: id, mode: 'RESEARCH', phase: 'RESEARCH_PAUSED_MAX_CYCLES', questions_allowed: true });
565
+ await setCurrent(root, { mission_id: id, mode: 'RESEARCH', phase: 'RESEARCH_PAUSED_MAX_CYCLES', questions_allowed: true, implementation_allowed: false });
546
566
  console.log(`Research paused after max cycles: ${id}`);
547
567
  }
548
568
 
@@ -558,19 +578,29 @@ async function researchStatus(args) {
558
578
  const scoutLedger = await readJson(path.join(dir, 'scout-ledger.json'), null);
559
579
  const debateLedger = await readJson(path.join(dir, 'debate-ledger.json'), null);
560
580
  const falsificationLedger = await readJson(path.join(dir, 'falsification-ledger.json'), null);
581
+ const sourceSkillText = await readText(path.join(dir, RESEARCH_SOURCE_SKILL_ARTIFACT), '');
582
+ const geniusSummaryText = await readText(path.join(dir, RESEARCH_GENIUS_SUMMARY_ARTIFACT), '');
561
583
  const paperText = await readText(path.join(dir, RESEARCH_PAPER_ARTIFACT), '');
562
584
  const scoutRows = Array.isArray(scoutLedger?.scouts) ? scoutLedger.scouts : [];
585
+ const sourceLayerRows = Array.isArray(sourceLedger?.source_layers) ? sourceLedger.source_layers : [];
586
+ const sourceLayersCovered = sourceLayerRows.filter((layer) => layer.status === 'covered' && ((Array.isArray(layer.source_ids) && layer.source_ids.length) || (Array.isArray(layer.counterevidence_ids) && layer.counterevidence_ids.length))).length;
563
587
  console.log(JSON.stringify({
564
588
  mission,
565
589
  state,
566
590
  gate,
567
591
  novelty_entries: ledger?.entries?.length ?? null,
568
592
  source_entries: sourceLedger?.sources?.length ?? null,
593
+ source_layers_required: sourceLayerRows.length || gate?.metrics?.source_layers_required || gate?.source_layers_required || null,
594
+ source_layers_covered: gate?.metrics?.source_layers_covered ?? gate?.source_layers_covered ?? (sourceLayerRows.length ? sourceLayersCovered : null),
595
+ triangulation_checks: sourceLedger?.triangulation?.cross_layer_checks?.length ?? gate?.metrics?.triangulation_checks ?? gate?.triangulation_checks ?? null,
596
+ genius_opinion_summaries: gate?.metrics?.genius_opinion_summaries ?? gate?.genius_opinion_summaries ?? (geniusSummaryText.trim() ? countGeniusOpinionSummaries(geniusSummaryText) : null),
569
597
  counterevidence_sources: sourceLedger?.counterevidence_sources?.length ?? null,
570
598
  xhigh_scouts: scoutRows.length ? scoutRows.filter((scout) => scout.effort === 'xhigh').length : null,
571
599
  eureka_moments: scoutRows.length ? scoutRows.filter((scout) => scout.eureka?.exclamation === 'Eureka!' && String(scout.eureka?.idea || '').trim()).length : null,
572
600
  scout_findings: scoutRows.length ? scoutRows.reduce((sum, scout) => sum + (Array.isArray(scout.findings) ? scout.findings.length : 0), 0) : null,
573
601
  debate_exchanges: debateLedger?.exchanges?.length ?? null,
602
+ research_source_skill_present: Boolean(sourceSkillText.trim()),
603
+ genius_opinion_summary_present: Boolean(geniusSummaryText.trim()),
574
604
  paper_present: Boolean(paperText.trim()),
575
605
  paper_sections: countResearchPaperSections(paperText),
576
606
  falsification_cases: falsificationLedger?.cases?.length ?? null
@@ -625,11 +655,19 @@ async function safeReadText(file, fallback = '') {
625
655
  try { return await fsp.readFile(file, 'utf8'); } catch { return fallback; }
626
656
  }
627
657
 
628
- function readMaxCycles(args, fallback) {
629
- const i = args.indexOf('--max-cycles');
658
+ function readBoundedIntegerFlag(args, name, fallback, min, max) {
659
+ const i = args.indexOf(name);
630
660
  const raw = i >= 0 && args[i + 1] ? Number(args[i + 1]) : Number(fallback);
631
- if (!Number.isFinite(raw)) return Math.max(1, Number.parseInt(fallback, 10) || 1);
632
- return Math.max(1, Math.min(50, Math.floor(raw)));
661
+ if (!Number.isFinite(raw)) return Math.max(min, Number.parseInt(fallback, 10) || min);
662
+ return Math.max(min, Math.min(max, Math.floor(raw)));
663
+ }
664
+
665
+ function readMaxCycles(args, fallback) {
666
+ return readBoundedIntegerFlag(args, '--max-cycles', fallback, 1, 50);
667
+ }
668
+
669
+ function readResearchCycleTimeoutMinutes(args) {
670
+ return readBoundedIntegerFlag(args, '--cycle-timeout-minutes', RESEARCH_DEFAULT_CYCLE_TIMEOUT_MINUTES, RESEARCH_MIN_CYCLE_TIMEOUT_MINUTES, RESEARCH_MAX_CYCLE_TIMEOUT_MINUTES);
633
671
  }
634
672
 
635
673
  export async function goalCommand(sub, args) {
@@ -1580,7 +1618,7 @@ export async function statsCommand(args) {
1580
1618
 
1581
1619
  function positionalArgs(args = []) {
1582
1620
  const out = [];
1583
- const valueFlags = new Set(['--format', '--iterations', '--out', '--baseline', '--candidate', '--install-scope', '--max-cycles', '--depth', '--scope', '--transport', '--query', '--topic', '--tokens', '--timeout-ms', '--sql', '--command', '--project-ref', '--agent', '--phase', '--message', '--role', '--max-anchors', '--lines', '--intent', '--changed', '--route', '--skills', '--prompt-signature']);
1621
+ const valueFlags = new Set(['--format', '--iterations', '--out', '--baseline', '--candidate', '--install-scope', '--max-cycles', '--cycle-timeout-minutes', '--depth', '--scope', '--transport', '--query', '--topic', '--tokens', '--timeout-ms', '--sql', '--command', '--project-ref', '--agent', '--phase', '--message', '--role', '--max-anchors', '--lines', '--intent', '--changed', '--route', '--skills', '--prompt-signature']);
1584
1622
  for (let i = 0; i < args.length; i++) {
1585
1623
  const arg = String(args[i]);
1586
1624
  if (valueFlags.has(arg)) {
package/src/core/fsx.mjs CHANGED
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import crypto from 'node:crypto';
6
6
  import { spawn } from 'node:child_process';
7
7
 
8
- export const PACKAGE_VERSION = '0.7.69';
8
+ export const PACKAGE_VERSION = '0.7.74';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
@@ -7,7 +7,7 @@ import { checkHarnessModification, harnessGuardBlockReason } from './harness-gua
7
7
  import { activeRouteContext, evaluateStop, prepareRoute, promptPipelineContext as routePipelineContext, recordContext7Evidence, recordSubagentEvidence, routePrompt } from './pipeline.mjs';
8
8
  import { classifyToolError } from './evaluation.mjs';
9
9
  import { REQUIRED_CODEX_MODEL, isForbiddenCodexModel } from './codex-model-guard.mjs';
10
- import { stripVisibleDecisionAnswerBlocks } from './routes.mjs';
10
+ import { dollarCommand, stripVisibleDecisionAnswerBlocks } from './routes.mjs';
11
11
 
12
12
  const TEAM_DIGEST_MAX_EVENTS = 4;
13
13
  const TEAM_DIGEST_MESSAGE_CHARS = 180;
@@ -77,13 +77,6 @@ function toolFailed(payload = {}) {
77
77
  return false;
78
78
  }
79
79
 
80
- function dollarCommand(prompt) {
81
- const text = String(prompt || '').trim();
82
- const match = text.match(/^\$([A-Za-z][A-Za-z0-9_-]*)(?:\s|:|$)/)
83
- || text.match(/^\[\$([A-Za-z][A-Za-z0-9_-]*)\]\([^)]+\)(?:\s|:|$)/);
84
- return match ? match[1].toUpperCase() : null;
85
- }
86
-
87
80
  function looksLikeUpdateDecline(prompt) {
88
81
  return /^(no|nope|skip|later|not now|don't|dont|아니|아니요|안해|안 함|나중에|건너뛰|스킵)/i.test(String(prompt || '').trim());
89
82
  }