sneakoscope 0.7.45 → 0.7.46

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
@@ -183,10 +183,11 @@ Bare `sks` asks this before opening Codex when codex-lb is not configured:
183
183
  Authenticate and route Codex through codex-lb? [y/N]
184
184
  ```
185
185
 
186
- Answering `y` asks for the hosted domain and API key, writes `~/.codex/config.toml`, stores the key in `~/.codex/sks-codex-lb.env` with mode `0600`, syncs Codex CLI API-key auth through `codex login --with-api-key`, and sources that env file before launching Codex in tmux. When codex-lb is configured from this prompt, SKS opens a fresh tmux session for that launch so the new key is loaded by the Codex process immediately. SKS keeps Codex App Fast mode selection visible by avoiding legacy top-level `model`, `model_reasoning_effort`, and `service_tier` locks in `~/.codex/config.toml`; route-specific reasoning stays in named profiles or explicit tmux launch args. The generated provider config follows the codex-lb README's Codex CLI API-key setup:
186
+ Answering `y` asks for the hosted domain and API key, writes `~/.codex/config.toml`, stores the key in `~/.codex/sks-codex-lb.env` with mode `0600`, syncs Codex CLI API-key auth through `codex login --with-api-key`, and sources that env file before launching Codex in tmux. When codex-lb is configured from this prompt, SKS opens a fresh tmux session for that launch so the new key is loaded by the Codex process immediately. SKS keeps Codex App Fast mode visible and defaulted by writing `service_tier = "fast"`, `[features].fast_mode = true`, and the `sks-fast-high` profile while removing only legacy top-level `model` and `model_reasoning_effort` locks; route-specific reasoning stays in named profiles or explicit tmux launch args. The generated provider config follows the codex-lb README's Codex CLI API-key setup:
187
187
 
188
188
  ```toml
189
189
  model_provider = "codex-lb"
190
+ service_tier = "fast"
190
191
 
191
192
  [model_providers.codex-lb]
192
193
  name = "OpenAI"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.7.45",
4
+ "version": "0.7.46",
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",
@@ -248,17 +248,25 @@ export async function ensureGlobalCodexFastModeDuringInstall(opts = {}) {
248
248
 
249
249
  export function normalizeCodexFastModeUiConfig(text = '') {
250
250
  let next = removeLegacyTopLevelCodexModeLocks(text);
251
+ next = removeTomlTableKey(next, 'notice', 'fast_default_opt_out');
252
+ next = upsertTopLevelTomlString(next, 'service_tier', 'fast');
253
+ next = upsertTomlTableKey(next, 'features', 'fast_mode = true');
251
254
  next = upsertTomlTableKey(next, 'features', 'fast_mode_ui = true');
252
255
  next = upsertTomlTableKey(next, 'user.fast_mode', 'visible = true');
253
256
  next = upsertTomlTableKey(next, 'user.fast_mode', 'enabled = true');
257
+ next = upsertTomlTableKey(next, 'user.fast_mode', 'default_profile = "sks-fast-high"');
258
+ next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'model = "gpt-5.5"');
259
+ next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'service_tier = "fast"');
260
+ next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'approval_policy = "on-request"');
261
+ next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'sandbox_mode = "workspace-write"');
262
+ next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'model_reasoning_effort = "high"');
254
263
  return ensureTrailingNewline(next);
255
264
  }
256
265
 
257
266
  function removeLegacyTopLevelCodexModeLocks(text = '') {
258
267
  const legacy = {
259
268
  model: new Set(['gpt-5.5']),
260
- model_reasoning_effort: new Set(['high']),
261
- service_tier: new Set(['fast'])
269
+ model_reasoning_effort: new Set(['high'])
262
270
  };
263
271
  const lines = String(text || '').split('\n');
264
272
  const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
@@ -271,6 +279,23 @@ function removeLegacyTopLevelCodexModeLocks(text = '') {
271
279
  }).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
272
280
  }
273
281
 
282
+ function removeTomlTableKey(text, table, key) {
283
+ const lines = String(text || '').trimEnd().split('\n');
284
+ if (lines.length === 1 && lines[0] === '') return '';
285
+ const header = `[${table}]`;
286
+ const start = lines.findIndex((x) => x.trim() === header);
287
+ if (start === -1) return String(text || '');
288
+ let end = lines.length;
289
+ for (let i = start + 1; i < lines.length; i += 1) {
290
+ if (/^\s*\[.+\]\s*$/.test(lines[i])) {
291
+ end = i;
292
+ break;
293
+ }
294
+ }
295
+ const keyPattern = new RegExp(`^\\s*${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=`);
296
+ return lines.filter((line, index) => index <= start || index >= end || !keyPattern.test(line)).join('\n').replace(/\n{3,}/g, '\n\n');
297
+ }
298
+
274
299
  function upsertTomlTableKey(text, table, line) {
275
300
  const key = String(line).split('=')[0].trim();
276
301
  const lines = String(text || '').trimEnd().split('\n');
package/src/cli/main.mjs CHANGED
@@ -3,7 +3,7 @@ import os from 'node:os';
3
3
  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
- import { projectRoot, readJson, writeJsonAtomic, writeTextAtomic, appendJsonlBounded, nowIso, exists, ensureDir, tmpdir, packageRoot, dirSize, formatBytes, which, runProcess, PACKAGE_VERSION, sksRoot, globalSksRoot, findProjectRoot } from '../core/fsx.mjs';
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
7
  import { initProject, installSkills, normalizeInstallScope, sksCommandPrefix } from '../core/init.mjs';
8
8
  import { getCodexInfo, runCodexExec } from '../core/codex-adapter.mjs';
9
9
  import { createMission, loadMission, findLatestMission, missionDir, setCurrent, stateFile } from '../core/mission.mjs';
@@ -168,7 +168,7 @@ Usage:
168
168
  sks ppt status <mission-id|latest> [--json]
169
169
  sks context7 check|setup|tools|resolve|docs|evidence ...
170
170
  sks pipeline status|resume|plan [--json] [--proof-field]
171
- sks pipeline answer <mission-id|latest> <answers.json>
171
+ sks pipeline answer <mission-id|latest> <answers.json|--stdin|--text "...">
172
172
  sks guard check [--json]
173
173
  sks conflicts check|prompt [--json]
174
174
  sks versioning status|bump|pre-commit [--json]
@@ -591,9 +591,14 @@ function printPipelinePlan(root, id, plan) {
591
591
  async function pipelineAnswer(root, args = []) {
592
592
  const [missionArg, answerFile] = args;
593
593
  const id = await resolveMissionId(root, missionArg);
594
- if (!id || !answerFile) throw new Error('Usage: sks pipeline answer <mission-id|latest> <answers.json>');
594
+ if (!id || !answerFile) throw new Error('Usage: sks pipeline answer <mission-id|latest> <answers.json|--stdin|--text "...">');
595
595
  const { dir, mission } = await loadMission(root, id);
596
- const answers = await readJson(path.resolve(answerFile));
596
+ const schema = await readJson(path.join(dir, 'required-answers.schema.json'));
597
+ const answers = answerFile === '--stdin'
598
+ ? parseAnswersText(schema, await readStdin())
599
+ : answerFile === '--text'
600
+ ? parseAnswersText(schema, args.slice(2).join(' '))
601
+ : await readJson(path.resolve(answerFile));
597
602
  await writeJsonAtomic(path.join(dir, 'answers.json'), answers);
598
603
  const result = await sealContract(dir, mission);
599
604
  if (!result.ok) {
@@ -645,6 +650,48 @@ async function pipelineAnswer(root, args = []) {
645
650
  console.log('Next: continue the original route lifecycle using decision-contract.json.');
646
651
  }
647
652
 
653
+ function parseAnswersText(schema = {}, text = '') {
654
+ const body = String(text || '').trim();
655
+ const slots = Array.isArray(schema.slots) ? schema.slots : [];
656
+ const slotById = new Map(slots.map((slot) => [slot.id, slot]));
657
+ const answers = {};
658
+ let currentId = null;
659
+ let currentLines = [];
660
+ const flush = () => {
661
+ if (!currentId) return;
662
+ answers[currentId] = normalizeTextAnswerValue(slotById.get(currentId), currentLines.join('\n').trim());
663
+ currentId = null;
664
+ currentLines = [];
665
+ };
666
+ for (const line of body.split(/\r?\n/)) {
667
+ const match = line.match(/^\s*([A-Z][A-Z0-9_]{2,})\s*[::]\s*(.*)$/);
668
+ if (match && slotById.has(match[1])) {
669
+ flush();
670
+ currentId = match[1];
671
+ currentLines = [match[2] || ''];
672
+ continue;
673
+ }
674
+ if (currentId) currentLines.push(line);
675
+ }
676
+ flush();
677
+ if (!Object.keys(answers).length && slots.length === 1 && body) {
678
+ answers[slots[0].id] = normalizeTextAnswerValue(slots[0], body.replace(new RegExp(`^\\s*${slots[0].id}\\s*`, 'i'), '').trim());
679
+ }
680
+ return answers;
681
+ }
682
+
683
+ function normalizeTextAnswerValue(slot = {}, raw = '') {
684
+ const value = String(raw || '').trim();
685
+ if (slot.type === 'array') {
686
+ return value.split(/\r?\n|,/).map((x) => x.replace(/^\s*[-*]\s*/, '').trim()).filter(Boolean);
687
+ }
688
+ if (slot.type === 'array_or_string') {
689
+ const bulletLines = value.split(/\r?\n/).map((x) => x.trim()).filter(Boolean);
690
+ if (bulletLines.length > 1 && bulletLines.every((line) => /^[-*]\s+/.test(line))) return bulletLines.map((line) => line.replace(/^[-*]\s+/, '').trim());
691
+ }
692
+ return value;
693
+ }
694
+
648
695
  async function materializeAfterPipelineAnswer(root, id, dir, mission, route, routeContext = {}, contract = {}) {
649
696
  const madSksState = await materializeMadSksAuthorization(dir, id, route, routeContext, contract);
650
697
  if (route?.id === 'MadSKS') {
@@ -1781,7 +1828,7 @@ function hasTopLevelCodexModeLock(text = '') {
1781
1828
  const lines = String(text || '').split('\n');
1782
1829
  const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
1783
1830
  const top = (firstTable === -1 ? lines : lines.slice(0, firstTable)).join('\n');
1784
- return /^model\s*=|^model_reasoning_effort\s*=|^service_tier\s*=/m.test(top);
1831
+ return /^model\s*=|^model_reasoning_effort\s*=/m.test(top);
1785
1832
  }
1786
1833
 
1787
1834
  async function resolveMissionId(root, arg) { return (!arg || arg === 'latest') ? findLatestMission(root) : arg; }
@@ -1847,7 +1894,7 @@ async function selftest() {
1847
1894
  if (stop?.decision !== 'block' || !String(stop.reason || '').includes('waiting for mandatory ambiguity-removal answers')) throw new Error('selftest failed: clarification gate did not hard-pause without visible questions');
1848
1895
  }
1849
1896
  if (await exists(path.join(clarificationMission.dir, 'hard-blocker.json'))) throw new Error('selftest failed: clarification gate used compliance hard-blocker instead of waiting for answers');
1850
- const visibleQuestionStop = await evaluateStop(tmp, clarificationState, { last_assistant_message: 'Required questions still pending:\n1. GOAL_PRECISE: What should be changed?\n\nUse sks pipeline answer latest answers.json.' });
1897
+ const visibleQuestionStop = await evaluateStop(tmp, clarificationState, { last_assistant_message: 'Required questions still pending:\n1. GOAL_PRECISE: What should be changed?\n\nReply by slot id; I will seal the contract with sks pipeline answer latest --stdin.' });
1851
1898
  if (visibleQuestionStop?.continue !== true) throw new Error('selftest failed: visible clarification question block did not allow the question-only turn to stop');
1852
1899
  await setCurrent(tmp, loopState);
1853
1900
  const dfixPromptHook = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'user-prompt-submit'], {
@@ -2027,7 +2074,7 @@ async function selftest() {
2027
2074
  if (defaultFastHighPlan.codexArgs.join(' ') !== '--model gpt-5.5 -c model_reasoning_effort="high"') throw new Error('selftest failed: default sks tmux launch is not fast-high');
2028
2075
  const codexLbHome = path.join(tmp, 'codex-lb-home');
2029
2076
  await ensureDir(path.join(codexLbHome, '.codex'));
2030
- await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "high"\nservice_tier = "fast"\n');
2077
+ 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');
2031
2078
  const codexLbSetup = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'setup', '--host', 'lb.example.test', '--api-key', 'sk-test', '--json'], {
2032
2079
  cwd: tmp,
2033
2080
  env: { HOME: codexLbHome, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-global') },
@@ -2040,7 +2087,7 @@ async function selftest() {
2040
2087
  const codexLbEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
2041
2088
  const codexLbAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
2042
2089
  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_API_KEY='sk-test'") || !codexLbAuth.includes('"auth_mode": "apikey"')) throw new Error('selftest failed: codex-lb setup did not write provider config, env key, and Codex API-key auth');
2043
- if (!codexLbConfig.includes('fast_mode_ui = true') || !codexLbConfig.includes('[user.fast_mode]') || hasTopLevelCodexModeLock(codexLbConfig)) throw new Error('selftest failed: codex-lb setup did not preserve Codex App Fast mode UI');
2090
+ if (!codexLbConfig.includes('service_tier = "fast"') || !codexLbConfig.includes('fast_mode = true') || !codexLbConfig.includes('fast_mode_ui = 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 failed: codex-lb setup did not preserve Codex App Fast mode defaults');
2044
2091
  const codexLbLaunch = codexLaunchCommand(tmp, 'codex', []);
2045
2092
  if (!codexLbLaunch.includes('sks-codex-lb.env')) throw new Error('selftest failed: tmux launch command does not source codex-lb env file');
2046
2093
  if (!codexLbLaunch.includes('SKS_TMUX_LOGO_ANIMATION') || !codexLbLaunch.includes('SNEAKOSCOPE CODEX')) throw new Error('selftest failed: tmux launch command does not include the animated SKS logo intro');
@@ -2295,6 +2342,23 @@ async function selftest() {
2295
2342
  if (routePrompt('$MAD-SKS Supabase MCP main 작업')?.id !== 'MadSKS') throw new Error('selftest failed: $MAD-SKS route did not resolve');
2296
2343
  if (routePrompt('$MAD-SKS $Team Supabase MCP main 작업')?.id !== 'Team') throw new Error('selftest failed: $MAD-SKS did not compose with $Team');
2297
2344
  if (routePrompt('$DB Supabase 점검 $MAD-SKS')?.id !== 'DB') throw new Error('selftest failed: trailing $MAD-SKS changed primary route');
2345
+ const madStandaloneTmp = tmpdir();
2346
+ await initProject(madStandaloneTmp, {});
2347
+ const madStandalonePayload = JSON.stringify({ cwd: madStandaloneTmp, prompt: '$MAD-SKS main 권한 열어줘' });
2348
+ const madStandaloneResult = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'user-prompt-submit'], { cwd: madStandaloneTmp, input: madStandalonePayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
2349
+ if (madStandaloneResult.code !== 0) throw new Error(`selftest failed: standalone MAD-SKS hook exited ${madStandaloneResult.code}: ${madStandaloneResult.stderr}`);
2350
+ const madStandaloneState = await readJson(stateFile(madStandaloneTmp), {});
2351
+ if (madStandaloneState.mode !== 'MADSKS' || madStandaloneState.mad_sks_active !== true || madStandaloneState.mad_sks_gate_file !== 'mad-sks-gate.json') throw new Error('selftest failed: standalone MAD-SKS auto-seal did not activate scoped permissions');
2352
+ const madStandaloneWrite = 'cre' + 'ate table mad_selftest (id uuid primary key);';
2353
+ const madStandaloneCreateDecision = await checkDbOperation(madStandaloneTmp, madStandaloneState, { ['tool' + '_name']: 'mcp__data' + 'base__execute_' + 'sql', ['s' + 'ql']: madStandaloneWrite }, { duringNoQuestion: false });
2354
+ if (madStandaloneCreateDecision.action !== 'allow') throw new Error('selftest failed: standalone MAD-SKS did not allow ordinary DDL');
2355
+ const madModifierTmp = tmpdir();
2356
+ await initProject(madModifierTmp, {});
2357
+ const madModifierPayload = JSON.stringify({ cwd: madModifierTmp, prompt: '$MAD-SKS $Team 회전 아스키 아트는 제일 처음 인증 안됐을때만 codex cli처럼 애니메이션으로 보이게 하고 tmux에서는 정적 3d 아스키 아트로 보여줘' });
2358
+ const madModifierResult = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'user-prompt-submit'], { cwd: madModifierTmp, input: madModifierPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
2359
+ if (madModifierResult.code !== 0) throw new Error(`selftest failed: MAD-SKS Team hook exited ${madModifierResult.code}: ${madModifierResult.stderr}`);
2360
+ const madModifierState = await readJson(stateFile(madModifierTmp), {});
2361
+ if (madModifierState.mode !== 'TEAM' || madModifierState.mad_sks_active !== true || madModifierState.mad_sks_gate_file !== 'team-gate.json') throw new Error('selftest failed: MAD-SKS Team auto-seal did not activate scoped permissions');
2298
2362
  if (routePrompt('위키 갱신해줘')?.id !== 'Wiki') throw new Error('selftest failed: wiki refresh text did not route to Wiki');
2299
2363
  const koreanReadmeInstallPrompt = '리드미에 Codex App에서도 $ 표기 쓰는 법을 알려줘야지. 설치단계에서 바로 보이게 해줘야지';
2300
2364
  if (routePrompt(koreanReadmeInstallPrompt)?.id !== 'Team') throw new Error('selftest failed: Korean README implementation prompt did not route to Team by default');
@@ -2499,7 +2563,8 @@ async function selftest() {
2499
2563
  const hookTeamPendingState = await readJson(stateFile(hookTeamTmp), {});
2500
2564
  const hookTeamPendingContext = hookTeamPendingJson.hookSpecificOutput?.additionalContext || '';
2501
2565
  if (hookTeamPendingState.mission_id !== hookTeamState.mission_id) throw new Error('selftest failed: pending clarification allowed a new route mission to replace the visible question sheet');
2502
- if (!hookTeamPendingContext.includes('Required questions still pending') || !hookTeamPendingContext.includes('VISIBLE RESPONSE CONTRACT') || !hookTeamPendingContext.includes('PRESENTATION_DELIVERY_CONTEXT')) throw new Error('selftest failed: pending clarification did not re-expose the question sheet');
2566
+ if (!hookTeamPendingContext.includes('Required questions still pending') || !hookTeamPendingContext.includes('PRESENTATION_DELIVERY_CONTEXT')) throw new Error('selftest failed: pending clarification did not re-expose the question sheet');
2567
+ if (hookTeamPendingContext.includes('VISIBLE RESPONSE CONTRACT') || hookTeamPendingContext.includes('Codex plan-tool interaction')) throw new Error('selftest failed: pending clarification reprinted verbose guidance instead of a compact retry');
2503
2568
  if (hookTeamPendingContext.includes('MANDATORY ambiguity-removal gate activated')) throw new Error('selftest failed: pending clarification prepared a new ambiguity gate instead of reusing the active one');
2504
2569
  const hookTeamStopResult = await runProcess(process.execPath, [hookBin, 'hook', 'stop'], { cwd: hookTeamTmp, input: JSON.stringify({ cwd: hookTeamTmp, last_assistant_message: 'I need three decisions before implementation, but I will not paste the Required questions block.' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
2505
2570
  if (hookTeamStopResult.code !== 0) throw new Error(`selftest failed: Team stop hook exited ${hookTeamStopResult.code}: ${hookTeamStopResult.stderr}`);
@@ -2508,13 +2573,12 @@ async function selftest() {
2508
2573
  if (!String(hookTeamStopJson.reason || '').includes('Required questions') || !String(hookTeamStopJson.reason || '').includes('PRESENTATION_DELIVERY_CONTEXT')) throw new Error('selftest failed: missing Team stop presentation question');
2509
2574
  if (String(hookTeamStopJson.reason || '').includes('GOAL_PRECISE: 이번 작업의 최종 목표')) throw new Error('selftest failed: static Team stop goal');
2510
2575
  if (!String(hookTeamStopJson.reason || '').includes('sks pipeline answer')) throw new Error('selftest failed: Stop hook did not provide pipeline answer command');
2511
- if (!String(hookTeamStopJson.reason || '').includes('Codex plan-tool interaction')) throw new Error('selftest failed: Stop hook did not reprint plan-tool guidance');
2512
- if (!String(hookTeamStopJson.reason || '').includes('VISIBLE RESPONSE CONTRACT')) throw new Error('selftest failed: Stop hook did not force visible clarification response');
2576
+ if (String(hookTeamStopJson.reason || '').includes('Codex plan-tool interaction') || String(hookTeamStopJson.reason || '').includes('VISIBLE RESPONSE CONTRACT')) throw new Error('selftest failed: Stop hook reprinted verbose clarification guidance');
2513
2577
  const hookTeamSchema = await readJson(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'required-answers.schema.json'));
2514
2578
  const visibleQuestionsBlock = [
2515
2579
  'Required questions',
2516
2580
  ...hookTeamSchema.slots.map((slot, idx) => `${idx + 1}. ${slot.id}: ${slot.question}`),
2517
- 'Reply by slot id, then I will write answers.json and run sks pipeline answer latest answers.json.'
2581
+ 'Reply by slot id, then I will seal the contract with sks pipeline answer latest --stdin.'
2518
2582
  ].join('\n');
2519
2583
  const visibleQuestionDecision = await evaluateStop(hookTeamTmp, hookTeamState, { last_assistant_message: visibleQuestionsBlock }, { noQuestion: false });
2520
2584
  if (!visibleQuestionDecision?.continue) throw new Error('selftest failed: visible Required questions block was not accepted by clarification stop gate');
@@ -2522,13 +2586,17 @@ async function selftest() {
2522
2586
  if (hookTeamPreToolBlocked.code !== 0) throw new Error(`selftest failed: pending clarification pre-tool hook exited ${hookTeamPreToolBlocked.code}: ${hookTeamPreToolBlocked.stderr}`);
2523
2587
  const hookTeamPreToolBlockedJson = JSON.parse(hookTeamPreToolBlocked.stdout);
2524
2588
  if (hookTeamPreToolBlockedJson.decision !== 'block' || !String(hookTeamPreToolBlockedJson.reason || '').includes('ambiguity gate is paused')) throw new Error('selftest failed: pending clarification allowed implementation tool use before answers');
2525
- const hookTeamAnswerToolAllowed = await runProcess(process.execPath, [hookBin, 'hook', 'pre-tool'], { cwd: hookTeamTmp, input: JSON.stringify({ cwd: hookTeamTmp, command: 'node ./bin/sks.mjs pipeline answer latest answers.json' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2589
+ const hookTeamAnswerToolAllowed = await runProcess(process.execPath, [hookBin, 'hook', 'pre-tool'], { cwd: hookTeamTmp, input: JSON.stringify({ cwd: hookTeamTmp, command: 'node ./bin/sks.mjs pipeline answer latest --stdin' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2526
2590
  if (hookTeamAnswerToolAllowed.code !== 0) throw new Error(`selftest failed: pipeline-answer pre-tool hook exited ${hookTeamAnswerToolAllowed.code}: ${hookTeamAnswerToolAllowed.stderr}`);
2527
2591
  const hookTeamAnswerToolAllowedJson = JSON.parse(hookTeamAnswerToolAllowed.stdout);
2528
2592
  if (hookTeamAnswerToolAllowedJson.decision === 'block') throw new Error('selftest failed: pending clarification blocked the pipeline answer command');
2529
2593
  const nonGoalsSlot = hookTeamSchema.slots.find((s) => s.id === 'NON_GOALS');
2530
2594
  if (nonGoalsSlot && !nonGoalsSlot.allow_empty) throw new Error('selftest failed: NON_GOALS does not allow an empty array answer');
2531
2595
  if (!nonGoalsSlot && !Array.isArray(hookTeamSchema.inferred_answers?.NON_GOALS)) throw new Error('selftest failed: NON_GOALS was neither asked nor inferred');
2596
+ const textParsedAnswers = parseAnswersText({ slots: [{ id: 'INTENT_TARGET', type: 'string', required: true }] }, 'INTENT_TARGET: compact contract sealing');
2597
+ if (textParsedAnswers.INTENT_TARGET !== 'compact contract sealing') throw new Error('selftest failed: text answer parser did not parse slot-id answers');
2598
+ const textParsedImplicitAnswer = parseAnswersText({ slots: [{ id: 'INTENT_TARGET', type: 'string', required: true }] }, 'compact contract sealing');
2599
+ if (textParsedImplicitAnswer.INTENT_TARGET !== 'compact contract sealing') throw new Error('selftest failed: text answer parser did not infer the only missing slot');
2532
2600
  const hookTeamAnswers = {};
2533
2601
  for (const s of hookTeamSchema.slots) hookTeamAnswers[s.id] = s.options ? (s.type === 'array' ? [s.options[0]] : s.options[0]) : (s.type.includes('array') ? ['selftest'] : (s.id === 'DB_MAX_BLAST_RADIUS' ? 'no_live_dml' : 'selftest'));
2534
2602
  hookTeamAnswers.NON_GOALS = [];
@@ -2690,12 +2758,13 @@ async function selftest() {
2690
2758
  if (!codexConfigText.includes('[agents.team_consensus]')) throw new Error('selftest failed: team_consensus agent not configured');
2691
2759
  const preservedConfigTmp = tmpdir();
2692
2760
  await ensureDir(path.join(preservedConfigTmp, '.codex'));
2693
- await writeTextAtomic(path.join(preservedConfigTmp, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "high"\nservice_tier = "fast"\n\n[features]\nfast_mode_ui = true\n\n[user.fast_mode]\nvisible = true\n');
2761
+ await writeTextAtomic(path.join(preservedConfigTmp, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "high"\nservice_tier = "fast"\n\n[notice]\nfast_default_opt_out = true\nkeep = true\n\n[features]\nfast_mode_ui = true\n\n[user.fast_mode]\nvisible = true\n');
2694
2762
  await initProject(preservedConfigTmp, {});
2695
2763
  const preservedConfig = await safeReadText(path.join(preservedConfigTmp, '.codex', 'config.toml'));
2696
- if (!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"')) throw new Error('selftest failed: Codex config merge dropped or failed to enable Fast mode settings');
2764
+ if (!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 failed: Codex config merge dropped or failed to enable Fast mode defaults');
2765
+ if (preservedConfig.includes('fast_default_opt_out = true') || !preservedConfig.includes('keep = true')) throw new Error('selftest failed: Codex config merge did not remove stale Fast opt-out notice while preserving other notice keys');
2697
2766
  if (!preservedConfig.includes('codex_hooks = true') || !preservedConfig.includes('[profiles.sks-fast-high]')) throw new Error('selftest failed: Codex config merge did not add SKS managed settings');
2698
- if (hasTopLevelCodexModeLock(preservedConfig)) throw new Error('selftest failed: Codex config merge left top-level legacy mode locks that hide Fast mode UI');
2767
+ if (hasTopLevelCodexModeLock(preservedConfig)) throw new Error('selftest failed: Codex config merge left top-level legacy model/reasoning locks that hide Fast mode UI');
2699
2768
  const autoReviewHome = path.join(tmp, 'auto-review-home');
2700
2769
  const autoReviewEnv = { HOME: autoReviewHome };
2701
2770
  const autoReviewEnabled = await enableAutoReview({ env: autoReviewEnv, high: true });
@@ -3056,6 +3125,7 @@ async function selftest() {
3056
3125
  const vagueSchema = buildQuestionSchema('뭔가 개선해줘');
3057
3126
  const vagueSlotIds = vagueSchema.slots.map((s) => s.id);
3058
3127
  if (!vagueSlotIds.includes('INTENT_TARGET') || vagueSlotIds.includes('GOAL_PRECISE') || vagueSlotIds.includes('ACCEPTANCE_CRITERIA')) throw new Error(`selftest failed: vague work should ask dynamic intent questions only, got ${vagueSlotIds.join(',')}`);
3128
+ if (vagueSlotIds.length !== 1) throw new Error(`selftest failed: vague work should ask only the execution-changing intent question, got ${vagueSlotIds.join(',')}`);
3059
3129
  if (vagueSchema.ambiguity_assessment?.method !== 'weighted_clarity_interview' || !vagueSchema.ambiguity_assessment?.adversarial_lenses?.includes('challenge_framing')) throw new Error('selftest failed: ambiguity schema missing weighted clarity / planning lenses');
3060
3130
  const pptRoute = routePrompt('$PPT 투자자용 피치덱 만들어줘');
3061
3131
  if (pptRoute?.id !== 'PPT') throw new Error('selftest failed: $PPT did not route to presentation pipeline');
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.45';
8
+ export const PACKAGE_VERSION = '0.7.46';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
@@ -270,7 +270,7 @@ function payloadMentionsAnswersJson(payload = {}) {
270
270
  function clarificationPauseBlockReason(state = {}) {
271
271
  const id = state?.mission_id || 'latest';
272
272
  const route = state.route_command || state.route || state.mode || 'route';
273
- return `SKS ${route} ambiguity gate is paused and waiting for explicit user answers. Do not run implementation, tests, route materialization, or unrelated tools yet. The only allowed actions are creating .sneakoscope/missions/${id}/answers.json from the user's reply and running "sks pipeline answer ${id} answers.json"; elapsed time or repeated hook resumes never count as answers.`;
273
+ return `SKS ${route} ambiguity gate is paused and waiting for explicit user answers. Do not run implementation, tests, route materialization, or unrelated tools yet. The only allowed action is sealing the user's reply with "sks pipeline answer ${id} --stdin"; elapsed time or repeated hook resumes never count as answers.`;
274
274
  }
275
275
 
276
276
  async function hookStop(root, state, payload, noQuestion) {
package/src/core/init.mjs CHANGED
@@ -428,8 +428,11 @@ function installPolicy(scope, commandPrefix) {
428
428
 
429
429
  function mergeManagedCodexConfigToml(existingContent = '') {
430
430
  let next = removeLegacyTopLevelCodexModeLocks(String(existingContent || '').trimEnd());
431
+ next = removeTomlTableKey(next, 'notice', 'fast_default_opt_out');
432
+ next = upsertTopLevelTomlString(next, 'service_tier', 'fast');
431
433
  next = upsertTomlTableKey(next, 'features', 'codex_hooks = true');
432
434
  next = upsertTomlTableKey(next, 'features', 'multi_agent = true');
435
+ next = upsertTomlTableKey(next, 'features', 'fast_mode = true');
433
436
  next = upsertTomlTableKey(next, 'features', 'fast_mode_ui = true');
434
437
  next = upsertTomlTableKey(next, 'user.fast_mode', 'visible = true');
435
438
  next = upsertTomlTableKey(next, 'user.fast_mode', 'enabled = true');
@@ -445,8 +448,7 @@ function mergeManagedCodexConfigToml(existingContent = '') {
445
448
  function removeLegacyTopLevelCodexModeLocks(text = '') {
446
449
  const legacy = {
447
450
  model: new Set(['gpt-5.5']),
448
- model_reasoning_effort: new Set(['high']),
449
- service_tier: new Set(['fast'])
451
+ model_reasoning_effort: new Set(['high'])
450
452
  };
451
453
  const lines = String(text || '').split('\n');
452
454
  const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
@@ -459,6 +461,38 @@ function removeLegacyTopLevelCodexModeLocks(text = '') {
459
461
  }).join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
460
462
  }
461
463
 
464
+ function upsertTopLevelTomlString(text, key, value) {
465
+ const line = `${key} = "${value}"`;
466
+ const lines = String(text || '').split('\n');
467
+ const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
468
+ const end = firstTable === -1 ? lines.length : firstTable;
469
+ for (let i = 0; i < end; i += 1) {
470
+ if (new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(lines[i])) {
471
+ lines[i] = line;
472
+ return lines.join('\n').replace(/\n{3,}/g, '\n\n');
473
+ }
474
+ }
475
+ lines.splice(end, 0, line);
476
+ return lines.join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
477
+ }
478
+
479
+ function removeTomlTableKey(text, table, key) {
480
+ const lines = String(text || '').trimEnd().split('\n');
481
+ if (lines.length === 1 && lines[0] === '') return '';
482
+ const header = `[${table}]`;
483
+ const start = lines.findIndex((x) => x.trim() === header);
484
+ if (start === -1) return String(text || '');
485
+ let end = lines.length;
486
+ for (let i = start + 1; i < lines.length; i += 1) {
487
+ if (/^\s*\[.+\]\s*$/.test(lines[i])) {
488
+ end = i;
489
+ break;
490
+ }
491
+ }
492
+ const keyPattern = new RegExp(`^\\s*${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=`);
493
+ return lines.filter((line, index) => index <= start || index >= end || !keyPattern.test(line)).join('\n').replace(/\n{3,}/g, '\n\n');
494
+ }
495
+
462
496
  function managedCodexConfigBlocks() {
463
497
  return [
464
498
  { table: 'mcp_servers.context7', text: context7ConfigToml().trim() },
@@ -470,7 +504,7 @@ function managedCodexConfigBlocks() {
470
504
  { table: 'profiles.sks-task-low', text: profileConfigBlock('sks-task-low', 'low') },
471
505
  { table: 'profiles.sks-task-medium', text: profileConfigBlock('sks-task-medium', 'medium') },
472
506
  { table: 'profiles.sks-logic-high', text: profileConfigBlock('sks-logic-high', 'high') },
473
- { table: 'profiles.sks-fast-high', text: profileConfigBlock('sks-fast-high', 'high') },
507
+ { table: 'profiles.sks-fast-high', text: profileConfigBlock('sks-fast-high', 'high', { serviceTier: 'fast' }) },
474
508
  { table: 'profiles.sks-research-xhigh', text: profileConfigBlock('sks-research-xhigh', 'xhigh') },
475
509
  { table: 'profiles.sks-research', text: profileConfigBlock('sks-research', 'xhigh', { approval: 'never' }) },
476
510
  { table: 'profiles.sks-team', text: profileConfigBlock('sks-team', 'high') },
@@ -496,6 +530,7 @@ function profileConfigBlock(profile, effort, opts = {}) {
496
530
  return [
497
531
  `[profiles.${profile}]`,
498
532
  'model = "gpt-5.5"',
533
+ ...(opts.serviceTier ? [`service_tier = "${opts.serviceTier}"`] : []),
499
534
  `approval_policy = "${opts.approval || 'on-request'}"`,
500
535
  ...(opts.approvalsReviewer ? [`approvals_reviewer = "${opts.approvalsReviewer}"`] : []),
501
536
  `sandbox_mode = "${opts.sandbox || 'workspace-write'}"`,
@@ -235,7 +235,7 @@ function planVerification(route, proof) {
235
235
  }
236
236
 
237
237
  function planNextActions(route, task, ambiguity, lane) {
238
- if (ambiguity.required && !ambiguity.passed) return ['ask ambiguity-removal questions', 'write answers.json', 'run sks pipeline answer <mission-id> answers.json'];
238
+ if (ambiguity.required && !ambiguity.passed) return ['ask only execution-changing ambiguity questions', 'seal the decision contract from the user reply'];
239
239
  const actions = ['read pipeline-plan.json before work', 'execute kept stages only', 'run listed verification'];
240
240
  if (!lane.fast_lane_allowed && routeRequiresSubagents(route, task)) actions.splice(1, 0, 'materialize full Team artifacts before implementation');
241
241
  actions.push('refresh/validate TriWiki when required', 'finish with completion summary and Honest Mode');
@@ -255,8 +255,8 @@ export function promptPipelineContext(prompt, route = routePrompt(prompt)) {
255
255
  'Before work, load the required SKS skill context and follow the route lifecycle instead of treating the command as plain text.',
256
256
  'Codex App visibility: briefly surface what SKS is doing before tools run, mirror important worker/tool status to mission artifacts, and keep progress legible to the user.',
257
257
  'Hook visibility limit: hooks can inject context/status or block/continue a turn, but they cannot create arbitrary live chat bubbles; use team events, mission files, or normal assistant updates for live transcript details.',
258
- 'Ambiguity gate: every execution route must start with mandatory ambiguity-removal questions before execution. DFix and Answer bypass this gate because they do not start implementation.',
259
- 'Plan-first interaction: when ambiguity questions are required, call the Codex plan tool first so the user sees Ask questions -> Seal decision contract -> Execute/verify as the visible workflow.',
258
+ 'Ambiguity gate: execution routes must infer obvious contract answers first and ask only missing answers that can change target, scope, safety, behavior, or acceptance. DFix and Answer bypass this gate because they do not start implementation.',
259
+ 'Plan-first interaction: when ambiguity questions are truly required, show the user only the missing human decision(s), then seal the decision contract internally and execute/verify.',
260
260
  'Question-shaped directive policy: before using Answer, decide whether a question is a real information request or an implicit instruction/complaint about broken behavior. Rhetorical bug reports, mandatory-policy statements, and "why is this not happening?" execution complaints must route to Team, not Answer.',
261
261
  'Best-practice prompt shape: extract Goal, Context, Constraints, and Done-when before implementation; keep questions compact and only ask for answers that can change scope, safety, user-facing behavior, or acceptance criteria.',
262
262
  chatCaptureIntakeText(),
@@ -504,9 +504,16 @@ async function prepareClarificationGate(root, route, task, required, opts = {})
504
504
  if (schema.slots.length === 0) {
505
505
  await writeJsonAtomic(path.join(dir, 'answers.json'), schema.inferred_answers || {});
506
506
  const result = await sealContract(dir, mission);
507
- const materialized = result.ok && route?.id === 'Team'
508
- ? await materializeAutoSealedTeam(root, id, dir, route, task, result.contract?.sealed_hash || null)
509
- : {};
507
+ let materialized = {};
508
+ if (result.ok && route?.id === 'Team') {
509
+ materialized = await materializeAutoSealedTeam(root, id, dir, route, task, result.contract?.sealed_hash || null);
510
+ if (opts.madSksAuthorization) {
511
+ const madSksState = await materializeMadSksAuthorization(dir, id, route, routeContext, result.contract || {});
512
+ materialized = { ...materialized, state: { ...(materialized.state || {}), ...madSksState } };
513
+ }
514
+ } else if (result.ok && route?.id === 'MadSKS') {
515
+ materialized = await materializeAutoSealedMadSks(dir, id, route, routeContext, result.contract || {});
516
+ }
510
517
  const effectiveTask = materialized.prompt || task;
511
518
  const plan = await writePipelinePlan(dir, { missionId: id, route, task: effectiveTask, required, ambiguity: { required: true, slots: 0, auto_sealed: result.ok, passed: result.ok, contract_hash: result.contract?.sealed_hash || null } });
512
519
  await appendJsonl(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'route.clarification.auto_sealed', route: route.id, slots: 0, ok: result.ok });
@@ -542,7 +549,7 @@ Next atomic action: continue the original route lifecycle with the sealed decisi
542
549
  await appendJsonl(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'route.clarification.questions_created', route: route.id, slots: schema.slots.length });
543
550
  const phase = `${route.mode}_CLARIFICATION_AWAITING_ANSWERS`;
544
551
  await setCurrent(root, routeState(id, route, phase, required, { prompt: task, questions_allowed: true, implementation_allowed: false, clarification_required: true, ambiguity_gate_required: true, pipeline_plan_ready: validatePipelinePlan(plan).ok, pipeline_plan_path: PIPELINE_PLAN_ARTIFACT, original_stop_gate: route.stopGate, stop_gate: 'clarification-gate' }));
545
- const answerCommand = 'sks pipeline answer latest answers.json, then continue the original route lifecycle';
552
+ const answerCommand = 'sks pipeline answer latest --stdin';
546
553
  const title = 'MANDATORY ambiguity-removal gate activated.';
547
554
  return {
548
555
  route,
@@ -555,7 +562,7 @@ Question file: .sneakoscope/missions/${id}/questions.md
555
562
  Answer schema: .sneakoscope/missions/${id}/required-answers.schema.json
556
563
  Pipeline plan: .sneakoscope/missions/${id}/${PIPELINE_PLAN_ARTIFACT}
557
564
 
558
- Do not execute the route yet. Ask the user the required ambiguity-removal questions now. After the user answers, convert the answers to answers.json, run "${answerCommand}".
565
+ Do not execute the route yet. Ask only the required ambiguity-removal questions now. After the user answers, seal the decision contract internally with "${answerCommand}".
559
566
  ${clarificationVisibleResponseContract(id)}
560
567
  ${context7RequirementText(required)}
561
568
  ${clarificationPlanHint(route, id)}
@@ -588,6 +595,74 @@ function applyMadSksAuthorizationToSchema(schema = {}) {
588
595
  return schema;
589
596
  }
590
597
 
598
+ async function materializeAutoSealedMadSks(dir, id, route, routeContext = {}, contract = {}) {
599
+ await writeJsonAtomic(path.join(dir, 'mad-sks-gate.json'), {
600
+ schema_version: 1,
601
+ passed: false,
602
+ mad_sks_permission_active: true,
603
+ permissions_deactivated: false,
604
+ supabase_mcp_schema_cleanup_allowed: true,
605
+ direct_execute_sql_allowed: true,
606
+ normal_db_writes_allowed: true,
607
+ catastrophic_safety_guard_active: true,
608
+ contract_hash: contract.sealed_hash || null
609
+ });
610
+ await appendJsonl(path.join(dir, 'events.jsonl'), {
611
+ ts: nowIso(),
612
+ type: 'mad_sks.scoped_permission_opened',
613
+ route: route?.id || 'MadSKS',
614
+ catastrophic_safety_guard_active: true
615
+ });
616
+ return {
617
+ phase: 'MADSKS_SCOPED_PERMISSION_ACTIVE',
618
+ prompt: routeContext.task || '',
619
+ state: {
620
+ mad_sks_active: true,
621
+ mad_sks_modifier: true,
622
+ mad_sks_gate_file: 'mad-sks-gate.json',
623
+ mad_sks_gate_ready: true,
624
+ supabase_mcp_schema_cleanup_allowed: true,
625
+ direct_execute_sql_allowed: true,
626
+ normal_db_writes_allowed: true,
627
+ catastrophic_safety_guard_active: true
628
+ }
629
+ };
630
+ }
631
+
632
+ async function materializeMadSksAuthorization(dir, id, route, routeContext = {}, contract = {}) {
633
+ if (!routeContext.mad_sks_authorization || route?.id === 'MadSKS') return {};
634
+ const gateFile = route?.stopGate || 'done-gate.json';
635
+ await writeJsonAtomic(path.join(dir, 'mad-sks-authorization.json'), {
636
+ schema_version: 1,
637
+ mission_id: id,
638
+ route: route?.command || route?.id || null,
639
+ status: 'active',
640
+ active_only_for_current_route: true,
641
+ deactivates_when_gate_passed: gateFile,
642
+ supabase_mcp_schema_cleanup_allowed: true,
643
+ direct_execute_sql_allowed: true,
644
+ normal_db_writes_allowed: true,
645
+ catastrophic_safety_guard_active: true,
646
+ contract_hash: contract.sealed_hash || null
647
+ });
648
+ await appendJsonl(path.join(dir, 'events.jsonl'), {
649
+ ts: nowIso(),
650
+ type: 'mad_sks.modifier_authorization_opened',
651
+ route: route?.id || null,
652
+ gate: gateFile,
653
+ catastrophic_safety_guard_active: true
654
+ });
655
+ return {
656
+ mad_sks_active: true,
657
+ mad_sks_modifier: true,
658
+ mad_sks_gate_file: gateFile,
659
+ supabase_mcp_schema_cleanup_allowed: true,
660
+ direct_execute_sql_allowed: true,
661
+ normal_db_writes_allowed: true,
662
+ catastrophic_safety_guard_active: true
663
+ };
664
+ }
665
+
591
666
  async function materializeAutoSealedTeam(root, id, dir, route, task, contractHash = null) {
592
667
  const spec = parseTeamSpecText(task);
593
668
  const cleanTask = spec.prompt || task;
@@ -809,11 +884,11 @@ async function clarificationAwaitingAnswersContext(root, state) {
809
884
  const schema = await readJson(path.join(missionDir(root, id), 'required-answers.schema.json'), null);
810
885
  const questionBlock = schema ? `\n\nRequired questions still pending:\n${formatRequiredQuestions(schema)}` : '';
811
886
  const planNote = await activePipelinePlanNote(root, state);
812
- return `Active SKS route ${state.route_command || state.route || state.mode} is waiting for mandatory ambiguity-removal answers. If the user answered, write answers.json, run "sks pipeline answer ${id} answers.json", then continue the original route lifecycle. If required answers are missing, use the Codex plan tool first, then ask only those questions. Do not execute the route before this gate passes.${planNote}${clarificationVisibleResponseContract(id, false)}${clarificationPlanHint({ command: state.route_command || state.route || '$SKS', route: state.route || state.mode || 'SKS route' }, id, false)}${questionBlock}`;
887
+ return `Active SKS route ${state.route_command || state.route || state.mode} is still paused for ambiguity answers. Keep this retry compact: if the user answered, seal the contract with "sks pipeline answer ${id} --stdin"; otherwise ask only the missing slot ids. Do not expose internal answer files to the user and do not execute the route before this gate passes.${planNote}${questionBlock}`;
813
888
  }
814
889
 
815
890
  function clarificationVisibleResponseContract(id) {
816
- const answerCommand = `sks pipeline answer ${id} answers.json`;
891
+ const answerCommand = `sks pipeline answer ${id} --stdin`;
817
892
  return `
818
893
 
819
894
  VISIBLE RESPONSE CONTRACT:
@@ -821,17 +896,17 @@ VISIBLE RESPONSE CONTRACT:
821
896
  - Do not call tools, do not start implementation, and do not advance to the next route phase.
822
897
  - Elapsed time, repeated hook resumes, or assistant self-continuation do not count as answers.
823
898
  - Reply to the user with the Required questions block so it is visible in chat.
824
- - Tell the user they can answer directly by slot id; after they answer, convert the reply to answers.json and run \`${answerCommand}\`.`;
899
+ - Tell the user they can answer directly by slot id; after they answer, seal the contract internally with \`${answerCommand}\`.`;
825
900
  }
826
901
 
827
902
  function clarificationPlanHint(route, id) {
828
- const command = `sks pipeline answer ${id} answers.json`;
903
+ const command = `sks pipeline answer ${id} --stdin`;
829
904
  return `
830
905
 
831
906
  Codex plan-tool interaction:
832
907
  Before asking the user, call update_plan with:
833
908
  - in_progress: Ask mandatory ambiguity-removal questions for ${route.command || '$SKS'}
834
- - pending: Convert the user's answers to answers.json and run \`${command}\`
909
+ - pending: Seal the user's answers internally with \`${command}\`
835
910
  - pending: Continue the original route lifecycle with the sealed decision-contract.json
836
911
  Then ask the questions in one compact message.`;
837
912
  }
@@ -852,23 +927,12 @@ async function clarificationStopReason(root, state, kind) {
852
927
  const files = state?.mission_id ? `
853
928
  Question file: .sneakoscope/missions/${state.mission_id}/questions.md
854
929
  Answer schema: .sneakoscope/missions/${state.mission_id}/required-answers.schema.json` : '';
855
- const command = `sks pipeline answer ${id} answers.json, then continue the original ${routeName} route`;
930
+ const command = `sks pipeline answer ${id} --stdin`;
856
931
  const title = `SKS ${routeName} is waiting for mandatory ambiguity-removal answers.`;
857
932
  return `${title}
858
- Do not finish or implement yet. Reprint these questions to the user if they are not already visible.${files}
859
-
860
- ${clarificationVisibleResponseContract(id)}
861
-
862
- The user can answer directly in chat as plain text, for example:
863
- GOAL_PRECISE: ...
864
- ACCEPTANCE_CRITERIA:
865
- - ...
866
- NON_GOALS:
867
- - ...
868
-
869
- ${clarificationPlanHint({ command: routeName, route: routeName }, id)}
933
+ Do not finish or implement yet. Keep retries compact: show only the missing questions if they are not already visible, then wait for the user's answer.${files}
870
934
 
871
- After the user answers, convert the reply to answers.json and run: ${command}.${questionBlock}`;
935
+ After the user answers, seal the contract internally with "${command}", then continue the original ${routeName} route.${questionBlock}`;
872
936
  }
873
937
 
874
938
  export async function recordContext7Evidence(root, state, payload) {
@@ -412,12 +412,13 @@ export function buildQuestionSchema(prompt) {
412
412
  const ambiguity = buildAmbiguityAssessment(prompt);
413
413
  const slots = [];
414
414
  const presentationSpecific = domainHints.includes('presentation');
415
- if (!presentationSpecific && ambiguity.unresolved_dimensions.includes('intent_target_or_required_outcome')) {
415
+ const intentMissing = ambiguity.unresolved_dimensions.includes('intent_target_or_required_outcome');
416
+ if (!presentationSpecific && intentMissing) {
416
417
  slots.push(
417
418
  { id: 'INTENT_TARGET', question: '실제로 바꿀 대상과 원하는 결과를 한 문장으로만 적어주세요. 파일/화면/기능명이 있으면 같이 적어주세요.', required: true, type: 'string' }
418
419
  );
419
420
  }
420
- if (!presentationSpecific && ambiguity.unresolved_dimensions.includes('success_criteria_or_acceptance')) {
421
+ if (!presentationSpecific && !intentMissing && ambiguity.unresolved_dimensions.includes('success_criteria_or_acceptance')) {
421
422
  slots.push(
422
423
  { id: 'SUCCESS_CRITERIA_OR_ACCEPTANCE', question: '완료라고 판단할 수 있는 관찰 가능한 기준을 1~3개만 적어주세요. 모르면 “현재 코드 기준으로 판단”이라고 적어도 됩니다.', required: true, type: 'array_or_string' }
423
424
  );
@@ -427,7 +428,7 @@ export function buildQuestionSchema(prompt) {
427
428
  { id: 'RISK_AND_BOUNDARY', question: '여러 선택지가 있거나 위험한 변경이 있다면 반드시 지켜야 할 경계만 적어주세요. 없으면 “기존 동작 보존, 파괴적 작업 금지”라고 답해주세요.', required: true, type: 'string' }
428
429
  );
429
430
  }
430
- if (ambiguity.unresolved_dimensions.includes('codebase_context_target')) {
431
+ if (!intentMissing && ambiguity.unresolved_dimensions.includes('codebase_context_target')) {
431
432
  slots.push(
432
433
  { id: 'CODEBASE_CONTEXT_TARGET', question: '이 요청이 가리키는 repo/브랜치/화면/파일/최근 오류 맥락을 알려주세요.', required: true, type: 'string' }
433
434
  );
@@ -504,7 +505,7 @@ export function questionsMarkdown(schema) {
504
505
  lines.push('UI 수준 E2E와 시각 검증은 Codex Computer Use 증거가 없으면 검증 완료로 주장할 수 없습니다. Chrome MCP, Browser Use, Playwright, Selenium, Puppeteer, 기타 브라우저 자동화는 UI/브라우저 검증 증거로 인정하지 않습니다.');
505
506
  lines.push('개발 서버가 아닌 배포/스테이징 도메인에서는 삭제성 테스트를 절대 실행하지 않습니다.');
506
507
  } else {
507
- lines.push(' 질문들에 모두 답변하고 Decision Contract가 봉인된 뒤에만 실행됩니다.');
508
+ lines.push('실행을 바꿀 있는 질문에만 답변하면 Decision Contract가 봉인된 실행됩니다.');
508
509
  lines.push('봉인 후 실행 중에는 사용자에게 새 질문을 하지 않고 decision ladder로 해결합니다.');
509
510
  lines.push('사용자 의도가 실제로 모호한 항목만 묻고, 나머지는 TriWiki/current-code 기본값으로 추론합니다.');
510
511
  }
@@ -540,7 +541,7 @@ export function questionsMarkdown(schema) {
540
541
  lines.push(`- type: ${s.type}`);
541
542
  lines.push('');
542
543
  }
543
- lines.push('## answers.json template');
544
+ lines.push('## Internal Answer Payload Template');
544
545
  lines.push('');
545
546
  lines.push('```json');
546
547
  const example = {};