sneakoscope 0.7.78 → 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.
@@ -8,6 +8,7 @@ import { activeRouteContext, evaluateStop, prepareRoute, promptPipelineContext a
8
8
  import { classifyToolError } from './evaluation.mjs';
9
9
  import { REQUIRED_CODEX_MODEL, isForbiddenCodexModel } from './codex-model-guard.mjs';
10
10
  import { dollarCommand, stripVisibleDecisionAnswerBlocks } from './routes.mjs';
11
+ import { appendMissionStatus } from './recallpulse.mjs';
11
12
 
12
13
  const TEAM_DIGEST_MAX_EVENTS = 4;
13
14
  const TEAM_DIGEST_MESSAGE_CHARS = 180;
@@ -19,7 +20,7 @@ const CODEX_GIT_ACTION_STOP_ARTIFACT = 'codex-git-action-stop-bypass.json';
19
20
  const STOP_REPEAT_GUARD_WINDOW_MS = 10 * 60 * 1000;
20
21
  const STOP_REPEAT_GUARD_MAX_ENTRIES = 25;
21
22
  const DEFAULT_STOP_REPEAT_GUARD_LIMIT = 2;
22
- const CODEX_GIT_ACTION_STOP_TTL_MS = 5 * 60 * 1000;
23
+ const CODEX_GIT_ACTION_STOP_TTL_MS = 15 * 60 * 1000;
23
24
 
24
25
  async function loadHookPayload() {
25
26
  const raw = await readStdin();
@@ -105,12 +106,39 @@ export async function hookMain(name) {
105
106
  function blockForbiddenClientModel(payload = {}) {
106
107
  const model = forbiddenClientModelFromPayload(payload);
107
108
  if (!model || !isForbiddenCodexModel(model)) return null;
109
+ if (looksLikeCodexUiSettingsEvent(payload)) return null;
108
110
  return {
109
111
  decision: 'block',
110
112
  reason: `SKS requires ${REQUIRED_CODEX_MODEL}; client payload requested ${model}. Switch the Codex client/session model to ${REQUIRED_CODEX_MODEL} and retry.`
111
113
  };
112
114
  }
113
115
 
116
+ function looksLikeCodexUiSettingsEvent(payload = {}) {
117
+ const prompt = stripVisibleDecisionAnswerBlocks(extractUserPrompt(payload));
118
+ const haystack = [
119
+ payload.action,
120
+ payload.intent,
121
+ payload.operation,
122
+ payload.permission,
123
+ payload.description,
124
+ payload.kind,
125
+ payload.type,
126
+ payload.feature,
127
+ payload.source,
128
+ payload.event,
129
+ payload.hook,
130
+ payload.hook_name,
131
+ payload.metadata?.action,
132
+ payload.metadata?.intent,
133
+ payload.metadata?.operation,
134
+ payload.metadata?.feature,
135
+ payload.metadata?.source,
136
+ payload.context?.surface,
137
+ payload.session?.surface
138
+ ].filter(Boolean).join(' ');
139
+ return !prompt && /\b(?:settings|preferences|profile|speed|fast[_\s-]*mode|reasoning|model[_\s-]*select|codex[_\s-]*app)\b/i.test(haystack);
140
+ }
141
+
114
142
  function forbiddenClientModelFromPayload(payload = {}) {
115
143
  const candidates = [
116
144
  payload.model,
@@ -147,6 +175,12 @@ async function hookUserPrompt(root, state, payload, noQuestion) {
147
175
  systemMessage: 'SKS: Codex App git action bypassed route gates.'
148
176
  };
149
177
  }
178
+ if (looksLikeCodexUiSettingsEvent(payload)) {
179
+ return {
180
+ continue: true,
181
+ systemMessage: 'SKS: Codex App settings/profile event ignored; route gates unchanged.'
182
+ };
183
+ }
150
184
  if (!noQuestion) {
151
185
  const prompt = stripVisibleDecisionAnswerBlocks(extractUserPrompt(payload));
152
186
  const madSksConfirmation = await handleMadSksUserConfirmation(root, state, prompt);
@@ -358,6 +392,12 @@ async function hookStop(root, state, payload, noQuestion) {
358
392
  systemMessage: 'SKS: Codex App git action accepted without route finalization gates.'
359
393
  };
360
394
  }
395
+ if (looksLikeCodexGitActionStopCompletion(last, payload)) {
396
+ return {
397
+ continue: true,
398
+ systemMessage: 'SKS: Codex App git action completion accepted without route finalization gates.'
399
+ };
400
+ }
361
401
  if (!noQuestion && (hasDfixLightCompletion(last) || await consumeLightRouteStop(root, payload))) {
362
402
  return {
363
403
  continue: true,
@@ -468,12 +508,42 @@ function looksLikeCodexGitAction(payload = {}) {
468
508
  || /커밋\s*메시지\s*생성/i.test(haystack);
469
509
  const promptSignal = /\bgenerate(?:\s+a)?(?:\s+git)?\s+commit\s+message\b/i.test(prompt)
470
510
  || /\bcommit\s+message\b[\s\S]{0,80}\b(?:staged|diff|changes?|git)\b/i.test(prompt)
511
+ || looksLikeStockCodexGitActionPrompt(prompt)
471
512
  || /커밋\s*메시지\s*생성/i.test(prompt);
472
513
  if (!appSignal && !promptSignal) return false;
514
+ if (looksLikeStockCodexGitActionPrompt(prompt)) return true;
473
515
  if (appSignal) return true;
474
516
  return !looksLikeUserImplementationRequest(prompt);
475
517
  }
476
518
 
519
+ function looksLikeStockCodexGitActionPrompt(prompt = '') {
520
+ const text = String(prompt || '').trim();
521
+ if (!text || text.length > 120) return false;
522
+ return /^(?:generate\s+(?:a\s+)?git\s+commit\s+message(?:\s+for\s+(?:the\s+)?(?:staged\s+)?diff)?|commit\s+changes|commit\s+and\s+push\s+changes|push\s+changes|create\s+(?:a\s+)?commit|create\s+(?:a\s+)?pull\s+request)\.?$/i.test(text);
523
+ }
524
+
525
+ function looksLikeCodexGitActionStopCompletion(last = '', payload = {}) {
526
+ const text = String(last || '').trim();
527
+ const haystack = [
528
+ payload.action,
529
+ payload.intent,
530
+ payload.operation,
531
+ payload.kind,
532
+ payload.type,
533
+ payload.feature,
534
+ payload.source,
535
+ payload.event,
536
+ payload.metadata?.action,
537
+ payload.metadata?.intent,
538
+ payload.metadata?.operation,
539
+ payload.metadata?.feature,
540
+ payload.metadata?.source
541
+ ].filter(Boolean).join(' ');
542
+ if (/\bcodex[_\s-]*app\b[\s\S]{0,80}\bgit\b[\s\S]{0,80}\b(?:action|commit|push|pr)\b/i.test(haystack)) return true;
543
+ if (!text || text.length > 180) return false;
544
+ return /^(?:commit(?:ted)?(?:\s+and\s+pushed)?(?:\s+changes)?(?:\s+complete[.!]?)?|push(?:ed)?(?:\s+changes)?(?:\s+complete[.!]?)?|created\s+(?:a\s+)?pull\s+request[.!]?)$/i.test(text);
545
+ }
546
+
477
547
  function looksLikeUserImplementationRequest(text = '') {
478
548
  return /(fix|bug|broken|error|issue|implement|change|update|repair|수정|버그|오류|에러|문제|고쳐|고치|해결|변경|수리|패치|안생기|안\s*생기)/i.test(String(text || ''));
479
549
  }
@@ -541,6 +611,18 @@ async function finalizationRepeatDecision(root, state = {}, payload = {}, reason
541
611
  }
542
612
  };
543
613
  await writeJsonAtomic(guardPath, record).catch(() => null);
614
+ if (state.mission_id) {
615
+ await appendMissionStatus(root, state.mission_id, {
616
+ category: repeatCount >= limit ? 'warning' : 'blocker',
617
+ audience: ['user', 'route', 'final-summary'],
618
+ stage_id: 'before_final',
619
+ message: repeatCount >= limit
620
+ ? `Repeated ${kind} stop prompt was suppressed; route completion is still unclaimed until evidence passes.`
621
+ : reason,
622
+ dedupe_key: key,
623
+ evidence: [STOP_REPEAT_GUARD_ARTIFACT]
624
+ }).catch(() => null);
625
+ }
544
626
  if (repeatCount < limit) return null;
545
627
  return {
546
628
  continue: true,
@@ -928,6 +1010,18 @@ export async function selftestCodexCommitHooks() {
928
1010
  const appCommitPushStop = await runHook('stop', { conversation_id: commitPushId, last_assistant_message: 'Commit and push complete.' });
929
1011
  if (appCommitPushStop.code !== 0) throw new Error(`selftest failed: app commit-push stop ${appCommitPushStop.code}: ${appCommitPushStop.stderr}`);
930
1012
  if (JSON.parse(appCommitPushStop.stdout).decision === 'block') throw new Error('selftest failed: app commit-push stop bypass');
1013
+ const metadataLightId = 'metadata-light-commit-push-selftest';
1014
+ const metadataLightHook = await runHook('user-prompt-submit', { conversation_id: metadataLightId, prompt: 'Commit and push changes.' });
1015
+ if (metadataLightHook.code !== 0) throw new Error(`selftest failed: metadata-light commit-push hook ${metadataLightHook.code}: ${metadataLightHook.stderr}`);
1016
+ const metadataLightJson = JSON.parse(metadataLightHook.stdout);
1017
+ if (metadataLightJson.decision === 'block' || metadataLightJson.hookSpecificOutput?.additionalContext || !String(metadataLightJson.systemMessage || '').includes('git action')) throw new Error('selftest failed: metadata-light app commit-push route bypass');
1018
+ const metadataLightStop = await runHook('stop', { conversation_id: metadataLightId, last_assistant_message: 'Commit and push complete.' });
1019
+ if (metadataLightStop.code !== 0) throw new Error(`selftest failed: metadata-light commit-push stop ${metadataLightStop.code}: ${metadataLightStop.stderr}`);
1020
+ if (JSON.parse(metadataLightStop.stdout).decision === 'block') throw new Error('selftest failed: metadata-light commit-push stop bypass');
1021
+ const settingsHook = await runHook('user-prompt-submit', { model: 'gpt-5.0-forbidden', metadata: { source: 'codex_app_settings', feature: 'speed profile' } });
1022
+ if (settingsHook.code !== 0) throw new Error(`selftest failed: settings hook ${settingsHook.code}: ${settingsHook.stderr}`);
1023
+ const settingsJson = JSON.parse(settingsHook.stdout);
1024
+ if (settingsJson.decision === 'block' || settingsJson.hookSpecificOutput?.additionalContext || !String(settingsJson.systemMessage || '').includes('settings/profile event ignored')) throw new Error('selftest failed: settings/profile event should not route or block');
931
1025
  const userHook = await runHook('user-prompt-submit', { prompt: '[커밋 메시지를 생성하지 못했습니다.] 코덱스 앱에서 이 버그 수정해줘' });
932
1026
  if (userHook.code !== 0) throw new Error(`selftest failed: user commit hook ${userHook.code}: ${userHook.stderr}`);
933
1027
  if (!JSON.parse(userHook.stdout).hookSpecificOutput?.additionalContext?.includes('$Team route prepared')) throw new Error('selftest failed: user prompt route');
package/src/core/init.mjs CHANGED
@@ -17,15 +17,32 @@ const GENERATED_PRUNE_POLICY = 'remove_previous_sks_generated_paths_absent_from_
17
17
 
18
18
  export const REQUIRED_GENERATED_CODEX_APP_FEATURE_FLAGS = [
19
19
  'hooks',
20
+ 'remote_control',
20
21
  'multi_agent',
21
22
  'fast_mode',
22
23
  'fast_mode_ui',
23
24
  'codex_git_commit',
24
25
  'computer_use',
26
+ 'browser_use',
27
+ 'browser_use_external',
28
+ 'image_generation',
29
+ 'in_app_browser',
30
+ 'guardian_approval',
31
+ 'tool_suggest',
25
32
  'apps',
26
33
  'plugins'
27
34
  ];
28
35
 
36
+ const DEFAULT_CODEX_APP_PLUGINS = [
37
+ ['browser', 'openai-bundled'],
38
+ ['chrome', 'openai-bundled'],
39
+ ['computer-use', 'openai-bundled'],
40
+ ['latex', 'openai-bundled'],
41
+ ['documents', 'openai-primary-runtime'],
42
+ ['presentations', 'openai-primary-runtime'],
43
+ ['spreadsheets', 'openai-primary-runtime']
44
+ ];
45
+
29
46
  export function hasTopLevelCodexModeLock(text = '') {
30
47
  const lines = String(text || '').split('\n');
31
48
  const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
@@ -491,11 +508,18 @@ function mergeManagedCodexConfigToml(existingContent = '') {
491
508
  next = upsertTopLevelTomlString(next, 'service_tier', 'fast');
492
509
  next = upsertTopLevelTomlBoolean(next, 'suppress_unstable_features_warning', true);
493
510
  next = upsertTomlTableKey(next, 'features', 'hooks = true');
511
+ next = upsertTomlTableKey(next, 'features', 'remote_control = true');
494
512
  next = upsertTomlTableKey(next, 'features', 'multi_agent = true');
495
513
  next = upsertTomlTableKey(next, 'features', 'fast_mode = true');
496
514
  next = upsertTomlTableKey(next, 'features', 'fast_mode_ui = true');
497
515
  next = upsertTomlTableKey(next, 'features', 'codex_git_commit = true');
498
516
  next = upsertTomlTableKey(next, 'features', 'computer_use = true');
517
+ next = upsertTomlTableKey(next, 'features', 'browser_use = true');
518
+ next = upsertTomlTableKey(next, 'features', 'browser_use_external = true');
519
+ next = upsertTomlTableKey(next, 'features', 'image_generation = true');
520
+ next = upsertTomlTableKey(next, 'features', 'in_app_browser = true');
521
+ next = upsertTomlTableKey(next, 'features', 'guardian_approval = true');
522
+ next = upsertTomlTableKey(next, 'features', 'tool_suggest = true');
499
523
  next = upsertTomlTableKey(next, 'features', 'apps = true');
500
524
  next = upsertTomlTableKey(next, 'features', 'plugins = true');
501
525
  next = upsertTomlTableKey(next, 'user.fast_mode', 'visible = true');
@@ -506,6 +530,10 @@ function mergeManagedCodexConfigToml(existingContent = '') {
506
530
  for (const block of managedCodexConfigBlocks()) {
507
531
  next = upsertTomlTable(next, block.table, block.text);
508
532
  }
533
+ for (const [name, marketplace] of DEFAULT_CODEX_APP_PLUGINS) {
534
+ const table = `plugins."${name}@${marketplace}"`;
535
+ next = upsertTomlTable(next, table, `[${table}]\nenabled = true`);
536
+ }
509
537
  return `${next.trim()}\n`;
510
538
  }
511
539
 
@@ -517,6 +545,7 @@ async function mergeGlobalCodexConfigIfAvailable(configText = '', configPath = '
517
545
  if (configPath && path.resolve(configPath) === path.resolve(globalConfigPath)) return configText;
518
546
  const globalConfig = await readText(globalConfigPath, '');
519
547
  let next = mergeGlobalMcpServers(configText, globalConfig);
548
+ next = mergeGlobalCodexAppRuntimeTables(next, globalConfig);
520
549
  if (selectedRe.test(next) && /\[model_providers\.codex-lb\]/.test(next)) return `${String(next || '').trim()}\n`;
521
550
  const envPath = path.join(home, '.codex', 'sks-codex-lb.env');
522
551
  if (!(await exists(envPath))) return next;
@@ -562,18 +591,24 @@ function mergeGlobalMcpServers(configText = '', globalConfig = '') {
562
591
  return next;
563
592
  }
564
593
 
594
+ function mergeGlobalCodexAppRuntimeTables(configText = '', globalConfig = '') {
595
+ let next = configText;
596
+ const re = /(?:^|\n)(\[((?:marketplaces|plugins)\.[^\]\r\n]+)\][\s\S]*?)(?=\n\[[^\]]+\]|\s*$)/g;
597
+ for (const match of String(globalConfig || '').matchAll(re)) {
598
+ const block = match[1].trim();
599
+ const table = match[2].trim();
600
+ if (!new RegExp(`(^|\\n)\\[${escapeRegExp(table)}\\]`).test(next)) next = upsertTomlTable(next, table, block);
601
+ }
602
+ return next;
603
+ }
604
+
565
605
  function removeLegacyTopLevelCodexModeLocks(text = '') {
566
- const legacy = {
567
- model_reasoning_effort: new Set(['high'])
568
- };
569
606
  const lines = String(text || '').split('\n');
570
607
  const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
571
608
  const end = firstTable === -1 ? lines.length : firstTable;
572
609
  return lines.filter((line, index) => {
573
610
  if (index >= end) return true;
574
- const match = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=\s*"([^"]*)"\s*(?:#.*)?$/);
575
- if (!match) return true;
576
- return !legacy[match[1]]?.has(match[2]);
611
+ return !/^\s*model_reasoning_effort\s*=/.test(line);
577
612
  }).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
578
613
  }
579
614
 
@@ -874,7 +909,7 @@ export async function installSkills(root) {
874
909
  'computer-use-fast': `---\nname: computer-use-fast\ndescription: Alias for the maximum-speed $Computer-Use/$CU Codex Computer Use lane.\n---\n\nUse the same rules as computer-use: skip Team debate, QA-LOOP clarification, upfront TriWiki refresh, Context7, subagents, and reflection unless explicitly requested. Use Codex Computer Use directly; never substitute Playwright, Chrome MCP, Browser Use, Selenium, Puppeteer, or other browser automation for UI/browser evidence. At the end only, refresh/pack TriWiki, validate it, then provide a concise completion summary plus Honest Mode.\n`,
875
910
  'cu': `---\nname: cu\ndescription: Short alias for the maximum-speed $Computer-Use Codex Computer Use lane.\n---\n\nUse the same rules as computer-use. This is a speed lane for focused UI/browser/visual tasks that require Codex Computer Use evidence, with TriWiki refresh/validate and Honest Mode deferred to final closeout.\n`,
876
911
  'goal': `---\nname: goal\ndescription: Fast $Goal/$goal bridge overlay for Codex native persisted /goal workflows.\n---\n\nUse when the user invokes $Goal/$goal or asks to persist a workflow with Codex native /goal continuation. Prepare with sks goal create or the $Goal route, write only the lightweight bridge artifacts, then use native Codex /goal create, pause, resume, and clear controls where available. Goal does not replace Team, QA, DB, or other SKS execution routes; continue implementation through the selected route and use Context7 only when external API/library docs are involved. Do not recreate the old no-question loop.\n`,
877
- 'research': `---\nname: research\ndescription: Dollar-command route for $Research or $research frontier discovery workflows.\n---\n\nUse when the user invokes $Research/$research or asks for research, hypotheses, new mechanisms, falsification, or testable predictions. Prefer sks research prepare and sks research run. Run the genius-lens scout council with Einstein/Feynman/Turing/von Neumann-inspired cognitive roles plus a skeptic lens; do not impersonate the historical people. Every Research scout must run with effort=xhigh, record one literal "Eureka!" idea, and participate in a vigorous evidence-bound debate before synthesis. Create research-source-skill.md as a route-local Skill Creator artifact, then maximize layered public web/source search across papers, official/government or leading-institution data, standards/primary docs, current news, public discourse, developer/practitioner sources, and counterevidence before synthesis. Record research-source-skill.md, source-ledger.json, scout-ledger.json, debate-ledger.json, novelty-ledger.json, falsification-ledger.json, research-report.md, research-paper.md, genius-opinion-summary.md, and research-gate.json. Context7 is optional and only needed when the research topic depends on external package/API/framework docs; do not use it as the default research evidence layer. Normal Research may take one or two hours when needed; favor real source collection, cross-layer comparison, falsification, and a concise paper manuscript over speed. Do not use --mock except for selftests or dry harness checks; if live source execution is unavailable, record a blocker and keep the gate unpassed. Do not use for ordinary code edits.\n`,
912
+ 'research': `---\nname: research\ndescription: Dollar-command route for $Research or $research frontier discovery workflows.\n---\n\nUse when the user invokes $Research/$research or asks for research, hypotheses, new mechanisms, falsification, or testable predictions. Prefer sks research prepare and sks research run. Research is not an implementation route: do not edit repository source, docs, package metadata, generated skills, or harness files; write only route-local mission artifacts under .sneakoscope/missions/<mission-id>/. Run the genius-lens scout council with named persona-inspired cognitive roles: Einstein Scout, Feynman Scout, Turing Scout, von Neumann Scout, and Skeptic Scout. These are lenses only; do not impersonate the historical people. Every Research scout ledger row must include display_name, persona, persona_boundary, effort=xhigh, reasoning_effort=xhigh, service_tier when available, one literal "Eureka!" idea, falsifiers, cheap_probes, and challenge_or_response before synthesis. This is not a fixed three-cycle route: repeat source gathering, Eureka ideas, evidence-bound debate, falsification, and synthesis pressure until every scout records final agreement, or until the explicit max-cycle safety cap pauses with an unpassed gate. Create research-source-skill.md as a route-local Skill Creator artifact, then maximize layered public web/source search across latest papers, official/government or leading-institution data, standards/primary docs, current news, public discourse, developer/practitioner sources, traditional background sources, and counterevidence before synthesis. Record research-source-skill.md, source-ledger.json, scout-ledger.json, debate-ledger.json, novelty-ledger.json, falsification-ledger.json, research-report.md, research-paper.md, genius-opinion-summary.md, and research-gate.json. debate-ledger.json must include consensus_iterations, unanimous_consensus, and per-scout agreements; research-gate.json cannot pass until unanimous_consensus=true with every scout agreement recorded. Context7 is optional and only needed when the research topic depends on external package/API/framework docs; do not use it as the default research evidence layer. Normal Research may take one or two hours when needed; favor real source collection, cross-layer comparison, falsification, and a concise paper manuscript over speed. Do not use --mock except for selftests or dry harness checks; if live source execution is unavailable, record a blocker and keep the gate unpassed. Do not use for ordinary code edits.\n`,
878
913
  'autoresearch': `---\nname: autoresearch\ndescription: Dollar-command route for $AutoResearch or $autoresearch iterative experiment loops.\n---\n\nUse for $AutoResearch, iterative improvement, SEO/GEO, ranking, workflow, benchmark, or experiments. Define program, hypothesis, experiment, metric, keep/discard, falsification, next step, and Honest Mode. Load seo-geo-optimizer for README/npm/GitHub/schema/AI-search work.\n`,
879
914
  'db': `---\nname: db\ndescription: Dollar-command route for $DB or $db database and Supabase safety checks.\n---\n\nUse when the user invokes $DB/$db or the task touches SQL, Supabase, Postgres, migrations, Prisma, Drizzle, Knex, MCP database tools, or production data. Run or follow sks db policy, sks db scan, sks db classify, and sks db check. Destructive database operations remain forbidden.\n`,
880
915
  'mad-sks': `---\nname: mad-sks\ndescription: Explicit high-risk authorization modifier for $MAD-SKS scoped Supabase MCP DB permission widening.\n---\n\nUse only when the user explicitly invokes $MAD-SKS or top-level sks --mad. It can be combined with another route, such as $MAD-SKS $Team or $DB ... $MAD-SKS; in that case the other command remains the primary workflow and MAD-SKS is only the temporary permission grant. The widened permission applies only while the active mission gate is open, must be deactivated when the task ends, and opens live server work, Supabase MCP database writes, column/schema cleanup, direct execute SQL, migration application when required, and normal targeted DB writes. Keep only catastrophic safeguards: whole database/schema/table removal, truncate, all-row delete/update, reset, dangerous project/branch management, credential exfiltration, persistent security weakening, and unrequested fallback implementation remain blocked. Do not carry MAD-SKS permission into later prompts or routes. The permission profile is centralized in src/core/permission-gates.mjs so skill/hook/MCP-style gates share one decision function.\n`,
@@ -896,7 +931,7 @@ export async function installSkills(root) {
896
931
  'gx-visual-read': `---\nname: gx-visual-read\ndescription: Read a Sneakoscope Codex deterministic visual sheet and produce context notes.\n---\n\nExtract nodes, edges, invariants, tests, risks, uncertainties, and RGBA anchors from source/render/snapshot. Do not infer hidden nodes.\n`,
897
932
  'gx-visual-validate': `---\nname: gx-visual-validate\ndescription: Validate render metadata against vgraph.json and beta.json.\n---\n\nRun sks gx validate and drift; fail stale or incomplete hashes, nodes, edges, invariants, or anchors.\n`,
898
933
  'turbo-context-pack': `---\nname: turbo-context-pack\ndescription: Build ultra-low-token context packet with Q4 bits, Q3 tags, top-K claims, and minimal evidence.\n---\n\nDefault to Q4/Q3 plus TriWiki RGBA anchors and attention.use_first. Add Q2/Q1 only when needed or when attention.hydrate_first says source hydration is required. Keep id, hash, path, and coordinate tuple for hydration.\n`,
899
- 'research-discovery': `---\nname: research-discovery\ndescription: Run SKS Research Mode for frontier-style research, hypotheses, novelty ledgers, falsification, and experiments.\n---\n\nFrame criteria, map assumptions, run maximum available web/source search, generate xhigh scout findings through Einstein/Feynman/Turing/von Neumann-inspired lenses plus a skeptic lens, require each scout to record a literal "Eureka!" idea, run evidence-bound debate, falsify, keep surviving insights, and record source ids, novelty/confidence/falsifiers/next experiments. Do not overclaim.\n`,
934
+ 'research-discovery': `---\nname: research-discovery\ndescription: Run SKS Research Mode for frontier-style research, hypotheses, novelty ledgers, falsification, and experiments.\n---\n\nFrame criteria, map assumptions, run maximum available web/source search, generate xhigh scout findings through Einstein Scout, Feynman Scout, Turing Scout, von Neumann Scout, and Skeptic Scout persona-inspired lenses, require each scout to record display_name/persona/persona_boundary plus a literal "Eureka!" idea, run evidence-bound debate, falsify, keep surviving insights, and record source ids, novelty/confidence/falsifiers/next experiments. Do not impersonate historical people and do not overclaim.\n`,
900
935
  'performance-evaluator': `---\nname: performance-evaluator\ndescription: Evaluate SKS performance, token-saving, accuracy-proxy, context-compression, or workflow improvements.\n---\n\nUse sks eval run/compare before claims. Report token_savings_pct, accuracy_delta/proxy, required_recall, support, and meaningful_improvement.\n`,
901
936
  'image-ux-review': imageUxReviewSkill('image-ux-review'),
902
937
  'ux-review': imageUxReviewSkill('ux-review'),
@@ -921,7 +921,7 @@ async function prepareResearch(root, route, task, required) {
921
921
  await writeResearchPlan(dir, task, {});
922
922
  const pipelinePlan = await writePipelinePlan(dir, { missionId: id, route, task, required, ambiguity: { required: false, status: 'direct_route' } });
923
923
  await setCurrent(root, routeState(id, route, 'RESEARCH_PREPARED', required, { prompt: task, pipeline_plan_ready: validatePipelinePlan(pipelinePlan).ok, pipeline_plan_path: PIPELINE_PLAN_ARTIFACT }));
924
- return routeContext(route, id, task, required, 'Run sks research run latest as a real long-running source-gathering pass, never an automatic mock fallback; create research-source-skill.md, maximize layered public source search, require every scout effort=xhigh plus one Eureka! idea, fill source-ledger.json, scout-ledger.json, debate-ledger.json, novelty-ledger.json, falsification-ledger.json, research-report.md, research-paper.md, genius-opinion-summary.md, and pass research-gate.json.');
924
+ return routeContext(route, id, task, required, 'Run sks research run latest as a real long-running source-gathering pass, never an automatic mock fallback; do not modify repository source code; create research-source-skill.md, maximize layered public source search, require every scout effort=xhigh plus one Eureka! idea, repeat scout/debate/falsification cycles until unanimous_consensus=true for every scout or the explicit safety cap pauses the run, fill source-ledger.json, scout-ledger.json, debate-ledger.json, novelty-ledger.json, falsification-ledger.json, research-report.md, research-paper.md, genius-opinion-summary.md, and pass research-gate.json.');
925
925
  }
926
926
 
927
927
  async function prepareAutoResearch(root, route, task, required) {
@@ -1400,6 +1400,8 @@ function normalizeComplianceReason(reason = '') {
1400
1400
  async function passedActiveGate(root, state) {
1401
1401
  const id = state?.mission_id;
1402
1402
  if (!id) return { ok: false, file: null };
1403
+ const hardBlocker = await passedHardBlocker(root, state);
1404
+ if (hardBlocker.ok) return hardBlocker;
1403
1405
  const files = gateFilesForState(state);
1404
1406
  for (const file of files) {
1405
1407
  const p = path.join(missionDir(root, id), file);
@@ -1414,8 +1416,6 @@ async function passedActiveGate(root, state) {
1414
1416
  return { ok: false, file };
1415
1417
  }
1416
1418
  }
1417
- const hardBlocker = await passedHardBlocker(root, state);
1418
- if (hardBlocker.ok) return hardBlocker;
1419
1419
  return { ok: false, file: files[0] || null };
1420
1420
  }
1421
1421