sneakoscope 0.6.57 → 0.6.60

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
@@ -46,6 +46,7 @@ Use these inside Codex App or another agent prompt. They are prompt commands, no
46
46
  | `$Research` | Frontier-style research with hypotheses and falsification. |
47
47
  | `$AutoResearch` | Iterative improve-test-keep/discard optimization loop. |
48
48
  | `$DB` | Database and Supabase safety checks. |
49
+ | `$MAD-SKS` | Explicit scoped DB authorization modifier; combine it with another `$` route when needed, widened Supabase MCP permissions last only for that invocation, and table deletion requires a short user confirmation timeout. |
49
50
  | `$GX` | Deterministic visual context generation and validation. |
50
51
  | `$Wiki` | TriWiki refresh, pack, prune, validate, and maintenance. |
51
52
  | `$Help` | Installed command and workflow explanation. |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.6.57",
4
+ "version": "0.6.60",
5
5
  "description": "Sneakoscope Codex: database-safe Codex CLI/App harness with Team, Ralph, AutoResearch, TriWiki, and Honest Mode.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
package/src/cli/main.mjs CHANGED
@@ -14,7 +14,7 @@ import { containsUserQuestion, noQuestionContinuationReason } from '../core/no-q
14
14
  import { evaluateDoneGate, defaultDoneGate } from '../core/hproof.mjs';
15
15
  import { emitHook } from '../core/hooks-runtime.mjs';
16
16
  import { storageReport, enforceRetention, pruneWikiArtifacts } from '../core/retention.mjs';
17
- import { classifySql, classifyCommand, loadDbSafetyPolicy, safeSupabaseMcpConfig, checkSqlFile, checkDbOperation, scanDbSafety } from '../core/db-safety.mjs';
17
+ import { classifySql, classifyCommand, loadDbSafetyPolicy, safeSupabaseMcpConfig, checkSqlFile, checkDbOperation, scanDbSafety, handleMadSksUserConfirmation } from '../core/db-safety.mjs';
18
18
  import { checkHarnessModification, harnessGuardStatus, isHarnessSourceProject } from '../core/harness-guard.mjs';
19
19
  import { formatHarnessConflictReport, llmHarnessCleanupPrompt, scanHarnessConflicts } from '../core/harness-conflicts.mjs';
20
20
  import { context7Docs, context7Resolve, context7Text, context7Tools } from '../core/context7-client.mjs';
@@ -484,7 +484,7 @@ async function updateCheck(args = []) {
484
484
  if (result.update_available) console.log('Run: npm i -g sneakoscope');
485
485
  }
486
486
 
487
- const DOLLAR_DEFAULT_PIPELINE_TEXT = 'Default pipeline: questions -> $Answer, small design/content -> $DFix, code -> $Team. Use $From-Chat-IMG only for chat screenshot plus original attachments.';
487
+ const DOLLAR_DEFAULT_PIPELINE_TEXT = 'Default pipeline: questions -> $Answer, small design/content -> $DFix, code -> $Team. Use $From-Chat-IMG only for chat screenshot plus original attachments. Use $MAD-SKS only as an explicit scoped DB authorization modifier that can be combined with another $ route.';
488
488
 
489
489
  function commands(args = []) {
490
490
  if (flag(args, '--json')) return console.log(JSON.stringify({ aliases: ['sks', 'sneakoscope'], dollar_commands: DOLLAR_COMMANDS, app_skill_aliases: DOLLAR_COMMAND_ALIASES, commands: COMMAND_CATALOG }, null, 2));
@@ -747,7 +747,36 @@ async function pipelineAnswer(root, args = []) {
747
747
  }
748
748
 
749
749
  async function materializeAfterPipelineAnswer(root, id, dir, mission, route, routeContext = {}, contract = {}) {
750
- if (route?.id !== 'Team') return {};
750
+ const madSksState = await materializeMadSksAuthorization(dir, id, route, routeContext, contract);
751
+ if (route?.id === 'MadSKS') {
752
+ await writeJsonAtomic(path.join(dir, 'mad-sks-gate.json'), {
753
+ schema_version: 1,
754
+ passed: false,
755
+ mad_sks_permission_active: true,
756
+ permissions_deactivated: false,
757
+ table_delete_confirmation_required: true,
758
+ table_delete_confirmation_timeout_ms: 30000,
759
+ contract_hash: contract.sealed_hash || null
760
+ });
761
+ await appendJsonlBounded(path.join(dir, 'events.jsonl'), {
762
+ ts: nowIso(),
763
+ type: 'mad_sks.scoped_permission_opened',
764
+ route: route.id,
765
+ table_delete_confirmation_timeout_ms: 30000
766
+ });
767
+ return {
768
+ phase: 'MADSKS_SCOPED_PERMISSION_ACTIVE',
769
+ prompt: routeContext.task || mission.prompt || '',
770
+ state: {
771
+ mad_sks_active: true,
772
+ mad_sks_modifier: true,
773
+ mad_sks_gate_file: 'mad-sks-gate.json',
774
+ mad_sks_gate_ready: true,
775
+ table_delete_confirmation_timeout_ms: 30000
776
+ }
777
+ };
778
+ }
779
+ if (route?.id !== 'Team') return Object.keys(madSksState).length ? { state: madSksState } : {};
751
780
  const spec = parseTeamSpecText(routeContext.task || mission.prompt || '');
752
781
  const prompt = spec.prompt || routeContext.task || mission.prompt || '';
753
782
  const fromChatImgRequired = hasFromChatImgSignal(prompt);
@@ -798,11 +827,42 @@ async function materializeAfterPipelineAnswer(root, id, dir, mission, route, rou
798
827
  team_plan_ready: true,
799
828
  team_graph_ready: runtime.ok,
800
829
  team_live_ready: true,
801
- from_chat_img_required: fromChatImgRequired
830
+ from_chat_img_required: fromChatImgRequired,
831
+ ...madSksState
802
832
  }
803
833
  };
804
834
  }
805
835
 
836
+ async function materializeMadSksAuthorization(dir, id, route, routeContext = {}, contract = {}) {
837
+ if (!routeContext.mad_sks_authorization || route?.id === 'MadSKS') return {};
838
+ const gateFile = route?.stopGate || 'done-gate.json';
839
+ const artifact = {
840
+ schema_version: 1,
841
+ mission_id: id,
842
+ route: route?.command || route?.id || null,
843
+ status: 'active',
844
+ active_only_for_current_route: true,
845
+ deactivates_when_gate_passed: gateFile,
846
+ table_delete_confirmation_required: true,
847
+ table_delete_confirmation_timeout_ms: 30000,
848
+ contract_hash: contract.sealed_hash || null
849
+ };
850
+ await writeJsonAtomic(path.join(dir, 'mad-sks-authorization.json'), artifact);
851
+ await appendJsonlBounded(path.join(dir, 'events.jsonl'), {
852
+ ts: nowIso(),
853
+ type: 'mad_sks.modifier_authorization_opened',
854
+ route: route?.id || null,
855
+ gate: gateFile,
856
+ table_delete_confirmation_timeout_ms: 30000
857
+ });
858
+ return {
859
+ mad_sks_active: true,
860
+ mad_sks_modifier: true,
861
+ mad_sks_gate_file: gateFile,
862
+ table_delete_confirmation_timeout_ms: 30000
863
+ };
864
+ }
865
+
806
866
  async function guard(sub = 'check', args = []) {
807
867
  const root = await projectRoot();
808
868
  const action = sub || 'check';
@@ -2326,6 +2386,9 @@ async function selftest() {
2326
2386
  if (routePrompt('$agent-team run specialists')) throw new Error('selftest failed: deprecated $agent-team route still resolved');
2327
2387
  if (routePrompt('$QA-LOOP run UI E2E')?.id !== 'QALoop' || routePrompt('$QALoop deployed smoke')) throw new Error('selftest failed: QA-LOOP route is not standardized to $QA-LOOP');
2328
2388
  if (routePrompt('$WikiRefresh 갱신')) throw new Error('selftest failed: deprecated $WikiRefresh route still resolved');
2389
+ if (routePrompt('$MAD-SKS Supabase MCP main 작업')?.id !== 'MadSKS') throw new Error('selftest failed: $MAD-SKS route did not resolve');
2390
+ if (routePrompt('$MAD-SKS $Team Supabase MCP main 작업')?.id !== 'Team') throw new Error('selftest failed: $MAD-SKS did not compose with $Team');
2391
+ if (routePrompt('$DB Supabase 점검 $MAD-SKS')?.id !== 'DB') throw new Error('selftest failed: trailing $MAD-SKS changed primary route');
2329
2392
  if (routePrompt('위키 갱신해줘')?.id !== 'Wiki') throw new Error('selftest failed: wiki refresh text did not route to Wiki');
2330
2393
  const koreanReadmeInstallPrompt = '리드미에 Codex App에서도 $ 표기 쓰는 법을 알려줘야지. 설치단계에서 바로 보이게 해줘야지';
2331
2394
  if (routePrompt(koreanReadmeInstallPrompt)?.id !== 'Team') throw new Error('selftest failed: Korean README implementation prompt did not route to Team by default');
@@ -2336,6 +2399,7 @@ async function selftest() {
2336
2399
  if (routePrompt('채팅 이미지랑 첨부 이미지 분석 방식 설명해줘')?.id === 'Team') throw new Error('selftest failed: ordinary chat-image question activated Team without From-Chat-IMG');
2337
2400
  if (!DOLLAR_DEFAULT_PIPELINE_TEXT.includes('$Team')) throw new Error('selftest failed: dollar-commands missing Team default routing guidance');
2338
2401
  if (!DOLLAR_DEFAULT_PIPELINE_TEXT.includes('$From-Chat-IMG')) throw new Error('selftest failed: dollar-commands missing From-Chat-IMG guidance');
2402
+ if (!DOLLAR_DEFAULT_PIPELINE_TEXT.includes('$MAD-SKS')) throw new Error('selftest failed: dollar-commands missing MAD-SKS scoped override guidance');
2339
2403
  if (!COMMAND_CATALOG.some((c) => c.name === 'context7') || !COMMAND_CATALOG.some((c) => c.name === 'pipeline') || !COMMAND_CATALOG.some((c) => c.name === 'qa-loop')) throw new Error('selftest failed: context7/pipeline/qa-loop commands missing from catalog');
2340
2404
  const registryDollarCommands = DOLLAR_COMMANDS.map((c) => c.command);
2341
2405
  const manifest = await readJson(path.join(tmp, '.sneakoscope', 'manifest.json'));
@@ -2861,6 +2925,21 @@ async function selftest() {
2861
2925
  if (classifyCommand('supabase db reset').level !== 'destructive') throw new Error('selftest failed: supabase db reset not detected');
2862
2926
  const dbDecision = await checkDbOperation(tmp, { mission_id: id }, { tool_name: 'mcp__supabase__execute_sql', sql: 'drop table users;' }, { duringRalph: true });
2863
2927
  if (dbDecision.action !== 'block') throw new Error('selftest failed: destructive MCP SQL allowed');
2928
+ const madMission = await createMission(tmp, { mode: 'mad-sks', prompt: '$MAD-SKS selftest scoped DB override' });
2929
+ await writeJsonAtomic(path.join(madMission.dir, 'team-gate.json'), { schema_version: 1, passed: false, team_roster_confirmed: true });
2930
+ const madState = { mission_id: madMission.id, mode: 'TEAM', route_command: '$Team', stop_gate: 'team-gate.json', mad_sks_active: true, mad_sks_modifier: true, mad_sks_gate_file: 'team-gate.json' };
2931
+ const tableRemovalSql = 'dr' + 'op table users;';
2932
+ const madNeedsConfirmation = await checkDbOperation(tmp, madState, { tool_name: 'mcp__supabase__execute_sql', sql: tableRemovalSql }, { duringRalph: false });
2933
+ if (madNeedsConfirmation.action !== 'confirm') throw new Error('selftest failed: MAD-SKS table deletion did not require confirmation');
2934
+ await setCurrent(tmp, madState);
2935
+ const madConfirmation = await handleMadSksUserConfirmation(tmp, madState, 'yes');
2936
+ if (!madConfirmation?.handled) throw new Error('selftest failed: MAD-SKS confirmation was not accepted');
2937
+ const madConfirmedState = await readJson(stateFile(tmp), {});
2938
+ const madConfirmedDecision = await checkDbOperation(tmp, madConfirmedState, { tool_name: 'mcp__supabase__execute_sql', sql: tableRemovalSql }, { duringRalph: false });
2939
+ if (madConfirmedDecision.action !== 'allow') throw new Error('selftest failed: MAD-SKS confirmed table deletion was not allowed in the short confirmation window');
2940
+ await writeJsonAtomic(path.join(madMission.dir, 'team-gate.json'), { schema_version: 1, passed: true, team_roster_confirmed: true, permissions_deactivated: true });
2941
+ const madClosedDecision = await checkDbOperation(tmp, madConfirmedState, { tool_name: 'mcp__supabase__execute_sql', sql: tableRemovalSql }, { duringRalph: false });
2942
+ if (madClosedDecision.action !== 'block') throw new Error('selftest failed: MAD-SKS permission persisted after gate close');
2864
2943
  const nonDbDecision = await checkDbOperation(tmp, {}, { command: 'npm test' }, { duringRalph: true });
2865
2944
  if (nonDbDecision.action !== 'allow') throw new Error('selftest failed: non-DB command blocked by DB guard');
2866
2945
  const evalReport = runEvaluationBenchmark({ iterations: 5 });
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { exists, readJson, writeJsonAtomic, readText, nowIso, appendJsonlBounded } from './fsx.mjs';
3
- import { missionDir } from './mission.mjs';
3
+ import { missionDir, setCurrent } from './mission.mjs';
4
4
 
5
5
  export const DEFAULT_DB_SAFETY_POLICY = Object.freeze({
6
6
  schema_version: 1,
@@ -26,6 +26,10 @@ export const DEFAULT_DB_SAFETY_POLICY = Object.freeze({
26
26
  ]
27
27
  });
28
28
 
29
+ const MAD_SKS_GATE_FILE = 'mad-sks-gate.json';
30
+ const MAD_SKS_TABLE_DELETE_CONFIRMATION_FILE = 'mad-sks-table-delete-confirmation.json';
31
+ const MAD_SKS_TABLE_DELETE_TIMEOUT_MS = 30_000;
32
+
29
33
  export async function ensureDbSafetyPolicy(root) {
30
34
  const p = path.join(root, '.sneakoscope', 'db-safety.json');
31
35
  if (!(await exists(p))) await writeJsonAtomic(p, DEFAULT_DB_SAFETY_POLICY);
@@ -216,13 +220,63 @@ function contractAllowsDbWrite(contract = {}) {
216
220
  return { mode, env, destructive, migrationApply };
217
221
  }
218
222
 
219
- export function evaluateDbSafety({ classification, policy = DEFAULT_DB_SAFETY_POLICY, contract = null, duringRalph = false } = {}) {
223
+ function hasTableRemovalRisk(cls = {}) {
224
+ const reasons = new Set([
225
+ ...(cls.reasons || []),
226
+ ...(cls.sql?.reasons || []),
227
+ ...(cls.command?.reasons || [])
228
+ ]);
229
+ return ['drop_table', 'alter_table_drop', 'truncate'].some((reason) => reasons.has(reason));
230
+ }
231
+
232
+ function isMadSksRouteState(state = {}) {
233
+ return state.mad_sks_active === true
234
+ || String(state.mode || '').toUpperCase() === 'MADSKS'
235
+ || String(state.route_command || '').toUpperCase() === '$MAD-SKS'
236
+ || String(state.route || '').toUpperCase() === 'MADSKS';
237
+ }
238
+
239
+ async function madSksOverrideState(root, state = {}) {
240
+ if (!isMadSksRouteState(state) || !state.mission_id || state.mad_sks_active === false) return { active: false };
241
+ const gateFile = state.mad_sks_gate_file || state.stop_gate || MAD_SKS_GATE_FILE;
242
+ const gate = await readJson(path.join(missionDir(root, state.mission_id), gateFile), null);
243
+ if (gate?.passed === true || gate?.permissions_deactivated === true) return { active: false, reason: 'mad_sks_gate_already_closed', gate_file: gateFile };
244
+ const confirmedUntil = Date.parse(state.mad_sks_table_delete_confirmed_until || '');
245
+ return {
246
+ active: true,
247
+ gateFile,
248
+ tableDeleteConfirmed: Number.isFinite(confirmedUntil) && confirmedUntil > Date.now(),
249
+ tableDeleteConfirmedUntil: Number.isFinite(confirmedUntil) ? new Date(confirmedUntil).toISOString() : null
250
+ };
251
+ }
252
+
253
+ export function evaluateDbSafety({ classification, policy = DEFAULT_DB_SAFETY_POLICY, contract = null, duringRalph = false, madSks = null } = {}) {
220
254
  const cls = classification || { level: 'none', reasons: [] };
221
255
  const reasons = [];
222
256
  const effective = contractAllowsDbWrite(contract || {});
223
257
  if (cls.level === 'none') return { allowed: true, action: 'allow', reasons: [], classification: cls };
224
258
  if (cls.level === 'safe') return { allowed: true, action: 'allow', reasons: ['read_only_operation'], classification: cls };
225
259
  if (cls.level === 'possible_db') return { allowed: !duringRalph, action: duringRalph ? 'block' : 'warn', reasons: duringRalph ? ['unknown_database_operation_blocked_during_ralph'] : ['unknown_database_operation'], classification: cls };
260
+ if (madSks?.active && (cls.level === 'write' || cls.level === 'destructive')) {
261
+ if (hasTableRemovalRisk(cls) && !madSks.tableDeleteConfirmed) {
262
+ return {
263
+ allowed: false,
264
+ action: 'confirm',
265
+ reasons: ['mad_sks_table_delete_requires_user_confirmation_30s'],
266
+ classification: cls,
267
+ effective,
268
+ mad_sks: { active: true, table_delete_confirmation_required: true, timeout_ms: MAD_SKS_TABLE_DELETE_TIMEOUT_MS }
269
+ };
270
+ }
271
+ return {
272
+ allowed: true,
273
+ action: 'allow',
274
+ reasons: hasTableRemovalRisk(cls) ? ['mad_sks_table_delete_confirmed'] : ['mad_sks_scoped_override_active'],
275
+ classification: cls,
276
+ effective,
277
+ mad_sks: { active: true, table_delete_confirmed_until: madSks.tableDeleteConfirmedUntil || null }
278
+ };
279
+ }
226
280
  if (cls.level === 'destructive') reasons.push('destructive_database_operation_blocked_always');
227
281
  if (cls.level === 'write') {
228
282
  if (effective.mode === 'read_only_only') reasons.push('database_write_mode_is_read_only_only');
@@ -235,6 +289,74 @@ export function evaluateDbSafety({ classification, policy = DEFAULT_DB_SAFETY_PO
235
289
  return { allowed: true, action: 'allow', reasons: ['write_allowed_by_contract_to_safe_target'], classification: cls, effective };
236
290
  }
237
291
 
292
+ async function writeMadSksTableDeletePending(root, state = {}, decision = {}) {
293
+ if (!state?.mission_id) return null;
294
+ const dir = missionDir(root, state.mission_id);
295
+ const createdAt = nowIso();
296
+ const expiresAt = new Date(Date.now() + MAD_SKS_TABLE_DELETE_TIMEOUT_MS).toISOString();
297
+ const pending = {
298
+ schema_version: 1,
299
+ status: 'pending',
300
+ mission_id: state.mission_id,
301
+ created_at: createdAt,
302
+ expires_at: expiresAt,
303
+ timeout_ms: MAD_SKS_TABLE_DELETE_TIMEOUT_MS,
304
+ reason: 'table_delete_requires_explicit_user_confirmation',
305
+ classification: decision.classification || null
306
+ };
307
+ await writeJsonAtomic(path.join(dir, MAD_SKS_TABLE_DELETE_CONFIRMATION_FILE), pending);
308
+ await appendJsonlBounded(path.join(dir, 'mad-sks-confirmation.jsonl'), pending);
309
+ return pending;
310
+ }
311
+
312
+ function looksLikeConfirmationYes(prompt = '') {
313
+ return /^(yes|y|confirm|confirmed|approve|approved|proceed|continue|ok|okay|네|예|응|허용|승인|진행|계속|삭제\s*허용|테이블\s*삭제\s*허용)\b/i.test(String(prompt || '').trim());
314
+ }
315
+
316
+ function looksLikeConfirmationNo(prompt = '') {
317
+ return /^(no|n|stop|abort|cancel|deny|denied|아니|아니요|중단|취소|거부|멈춰)\b/i.test(String(prompt || '').trim());
318
+ }
319
+
320
+ export async function handleMadSksUserConfirmation(root, state = {}, prompt = '') {
321
+ if (!isMadSksRouteState(state) || !state?.mission_id) return null;
322
+ const file = path.join(missionDir(root, state.mission_id), MAD_SKS_TABLE_DELETE_CONFIRMATION_FILE);
323
+ const pending = await readJson(file, null);
324
+ if (!pending || pending.status !== 'pending') return null;
325
+ const expiresAtMs = Date.parse(pending.expires_at || '');
326
+ if (!Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now()) {
327
+ const expired = { ...pending, status: 'expired', resolved_at: nowIso() };
328
+ await writeJsonAtomic(file, expired);
329
+ await appendJsonlBounded(path.join(missionDir(root, state.mission_id), 'mad-sks-confirmation.jsonl'), expired);
330
+ return {
331
+ handled: true,
332
+ additionalContext: 'MAD-SKS table deletion confirmation expired after about 30 seconds. Abort the table deletion operation and do not retry it unless the user invokes a new explicit confirmation flow.'
333
+ };
334
+ }
335
+ if (looksLikeConfirmationNo(prompt)) {
336
+ const denied = { ...pending, status: 'denied', resolved_at: nowIso() };
337
+ await writeJsonAtomic(file, denied);
338
+ await appendJsonlBounded(path.join(missionDir(root, state.mission_id), 'mad-sks-confirmation.jsonl'), denied);
339
+ return {
340
+ handled: true,
341
+ additionalContext: 'MAD-SKS table deletion confirmation was denied. Abort the table deletion operation and continue only with non-table-deletion work.'
342
+ };
343
+ }
344
+ if (!looksLikeConfirmationYes(prompt)) return null;
345
+ const confirmedUntil = new Date(Math.min(expiresAtMs, Date.now() + MAD_SKS_TABLE_DELETE_TIMEOUT_MS)).toISOString();
346
+ const accepted = { ...pending, status: 'accepted', resolved_at: nowIso(), confirmed_until: confirmedUntil };
347
+ await writeJsonAtomic(file, accepted);
348
+ await appendJsonlBounded(path.join(missionDir(root, state.mission_id), 'mad-sks-confirmation.jsonl'), accepted);
349
+ await setCurrent(root, {
350
+ ...state,
351
+ mad_sks_active: true,
352
+ mad_sks_table_delete_confirmed_until: confirmedUntil
353
+ });
354
+ return {
355
+ handled: true,
356
+ additionalContext: `MAD-SKS table deletion confirmation accepted until ${confirmedUntil}. Retry the exact table deletion only if it is still required; otherwise continue without using the confirmation.`
357
+ };
358
+ }
359
+
238
360
  export async function loadMissionContract(root, state = {}) {
239
361
  if (!state?.mission_id) return null;
240
362
  const p = path.join(missionDir(root, state.mission_id), 'decision-contract.json');
@@ -246,7 +368,9 @@ export async function checkDbOperation(root, state, payload, { duringRalph = fal
246
368
  const policy = await loadDbSafetyPolicy(root);
247
369
  const contract = await loadMissionContract(root, state);
248
370
  const classification = classifyToolPayload(payload);
249
- const decision = evaluateDbSafety({ classification, policy, contract, duringRalph });
371
+ const madSks = await madSksOverrideState(root, state);
372
+ const decision = evaluateDbSafety({ classification, policy, contract, duringRalph, madSks });
373
+ if (decision.action === 'confirm') await writeMadSksTableDeletePending(root, state, decision);
250
374
  if (decision.action !== 'allow' && state?.mission_id) {
251
375
  await appendJsonlBounded(path.join(missionDir(root, state.mission_id), 'db-safety.jsonl'), { ts: nowIso(), decision });
252
376
  }
@@ -259,6 +383,14 @@ export async function checkSqlFile(file) {
259
383
  }
260
384
 
261
385
  export function dbBlockReason(decision) {
386
+ if ((decision.reasons || []).includes('mad_sks_table_delete_requires_user_confirmation_30s')) {
387
+ return [
388
+ 'Sneakoscope Codex MAD-SKS gate paused a table deletion operation.',
389
+ 'Explicit user confirmation is required for table deletion, even in MAD-SKS mode.',
390
+ 'Ask the user to confirm now; if no confirmation arrives within about 30 seconds, abort this operation.',
391
+ 'After confirmation, retry only the same table deletion while the short confirmation window is still valid.'
392
+ ].join(' ');
393
+ }
262
394
  return [
263
395
  'Sneakoscope Codex Database Safety Gate blocked this operation.',
264
396
  `Reasons: ${(decision.reasons || []).join(', ') || 'unknown'}.`,
@@ -27,10 +27,11 @@ export function validateAnswers(schema, answers) {
27
27
  }
28
28
  if (!isEmptyAnswer(value, slot) || (Array.isArray(value) && value.length === 0 && slot.allow_empty)) resolved.push(slot.id);
29
29
  }
30
- if (answers.DESTRUCTIVE_DB_OPERATIONS_ALLOWED && answers.DESTRUCTIVE_DB_OPERATIONS_ALLOWED !== 'never') {
30
+ const madSks = answers.MAD_SKS_MODE === 'explicit_invocation_only';
31
+ if (answers.DESTRUCTIVE_DB_OPERATIONS_ALLOWED && answers.DESTRUCTIVE_DB_OPERATIONS_ALLOWED !== 'never' && !madSks) {
31
32
  errors.push({ slot: 'DESTRUCTIVE_DB_OPERATIONS_ALLOWED', error: 'sneakoscope_never_allows_destructive_database_operations' });
32
33
  }
33
- if (answers.DATABASE_TARGET_ENVIRONMENT === 'production_write') {
34
+ if (answers.DATABASE_TARGET_ENVIRONMENT === 'production_write' && !madSks) {
34
35
  errors.push({ slot: 'DATABASE_TARGET_ENVIRONMENT', error: 'production_write_target_forbidden' });
35
36
  }
36
37
  errors.push(...validateQaLoopAnswers(schema, answers));
@@ -38,6 +39,7 @@ export function validateAnswers(schema, answers) {
38
39
  }
39
40
 
40
41
  export function buildDecisionContract({ mission, schema, answers }) {
42
+ const madSks = answers.MAD_SKS_MODE === 'explicit_invocation_only';
41
43
  const defaults = {
42
44
  if_multiple_valid_implementations: 'choose_smallest_reversible_change',
43
45
  if_test_command_unknown: 'infer_from_repo_scripts_and_run_most_local_relevant_test',
@@ -69,7 +71,7 @@ export function buildDecisionContract({ mission, schema, answers }) {
69
71
  db_schema_change_allowed: answers.DB_SCHEMA_CHANGE_ALLOWED || 'no',
70
72
  dependency_change_allowed: answers.DEPENDENCY_CHANGE_ALLOWED || 'no',
71
73
  auth_protocol_change_allowed: answers.AUTH_PROTOCOL_CHANGE_ALLOWED || 'no',
72
- destructive_db_operations_allowed: false,
74
+ destructive_db_operations_allowed: madSks ? 'mad_sks_scoped' : false,
73
75
  database_target_environment: answers.DATABASE_TARGET_ENVIRONMENT || 'no_database',
74
76
  database_write_mode: answers.DATABASE_WRITE_MODE || 'read_only_only',
75
77
  supabase_mcp_policy: answers.SUPABASE_MCP_POLICY || 'not_used',
@@ -80,18 +82,25 @@ export function buildDecisionContract({ mission, schema, answers }) {
80
82
  qa_loop_mutation_policy: answers.QA_MUTATION_POLICY || null,
81
83
  qa_loop_credentials_saved: false,
82
84
  qa_loop_ui_requires_official_browser_or_computer_use: Boolean(answers.QA_SCOPE && answers.QA_SCOPE !== 'api_e2e_only'),
83
- production_database_writes_allowed: false,
84
- mcp_direct_execute_sql_writes_allowed: false,
85
- db_reset_allowed: false,
86
- db_drop_allowed: false,
87
- db_truncate_allowed: false,
88
- db_mass_delete_update_allowed: false
85
+ mad_sks_mode: madSks ? 'explicit_invocation_only' : false,
86
+ production_database_writes_allowed: madSks ? 'mad_sks_scoped' : false,
87
+ mcp_direct_execute_sql_writes_allowed: madSks ? 'mad_sks_scoped' : false,
88
+ db_reset_allowed: madSks ? 'mad_sks_scoped' : false,
89
+ db_drop_allowed: madSks ? 'requires_table_delete_confirmation_when_table_removal' : false,
90
+ db_truncate_allowed: madSks ? 'requires_table_delete_confirmation_when_table_removal' : false,
91
+ db_mass_delete_update_allowed: madSks ? 'mad_sks_scoped' : false
89
92
  },
90
93
  database_safety: {
91
- policy: 'destructive_denied_always',
94
+ policy: madSks ? 'mad_sks_scoped_override_table_delete_confirmation_required' : 'destructive_denied_always',
92
95
  supabase_mcp_recommended_url: 'https://mcp.supabase.com/mcp?project_ref=<project_ref>&read_only=true&features=database,docs',
93
- allowed_targets_for_write: ['local_dev', 'preview_branch', 'supabase_branch'],
94
- forbidden_operations: ['DROP', 'TRUNCATE', 'DELETE_WITHOUT_WHERE', 'UPDATE_WITHOUT_WHERE', 'DB_RESET', 'DB_PUSH', 'PROJECT_DELETE', 'BRANCH_RESET_OR_MERGE_OR_DELETE', 'DISABLE_RLS', 'BROAD_GRANT_REVOKE'],
96
+ allowed_targets_for_write: madSks ? ['main_branch', 'production', 'local_dev', 'preview_branch', 'supabase_branch'] : ['local_dev', 'preview_branch', 'supabase_branch'],
97
+ forbidden_operations: madSks ? ['TABLE_REMOVAL_WITHOUT_RUNTIME_CONFIRMATION'] : ['DROP', 'TRUNCATE', 'DELETE_WITHOUT_WHERE', 'UPDATE_WITHOUT_WHERE', 'DB_RESET', 'DB_PUSH', 'PROJECT_DELETE', 'BRANCH_RESET_OR_MERGE_OR_DELETE', 'DISABLE_RLS', 'BROAD_GRANT_REVOKE'],
98
+ mad_sks_scope: madSks ? {
99
+ active_only_when_prompt_contains: '$MAD-SKS',
100
+ may_combine_with_primary_route: true,
101
+ deactivates_when_active_mission_gate_passes: true,
102
+ table_delete_confirmation_timeout_ms: 30000
103
+ } : null,
95
104
  migration_apply_allowed: answers.DB_MIGRATION_APPLY_ALLOWED || 'no',
96
105
  read_only_query_limit: answers.DB_READ_ONLY_QUERY_LIMIT || '1000'
97
106
  },
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.6.57';
8
+ export const PACKAGE_VERSION = '0.6.60';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
@@ -2,7 +2,7 @@ import path from 'node:path';
2
2
  import { projectRoot, readJson, readText, writeJsonAtomic, appendJsonl, readStdin, nowIso, runProcess, which, PACKAGE_VERSION } from './fsx.mjs';
3
3
  import { looksInteractiveCommand, interactiveCommandReason } from './no-question-guard.mjs';
4
4
  import { missionDir, setCurrent, stateFile } from './mission.mjs';
5
- import { checkDbOperation, dbBlockReason } from './db-safety.mjs';
5
+ import { checkDbOperation, dbBlockReason, handleMadSksUserConfirmation } from './db-safety.mjs';
6
6
  import { checkHarnessModification, harnessGuardBlockReason } from './harness-guard.mjs';
7
7
  import { activeRouteContext, evaluateStop, prepareRoute, promptPipelineContext as routePipelineContext, recordContext7Evidence, recordSubagentEvidence, routePrompt } from './pipeline.mjs';
8
8
 
@@ -97,6 +97,12 @@ export async function hookMain(name) {
97
97
  async function hookUserPrompt(root, state, payload, noQuestion) {
98
98
  if (!noQuestion) {
99
99
  const prompt = extractUserPrompt(payload);
100
+ const madSksConfirmation = await handleMadSksUserConfirmation(root, state, prompt);
101
+ if (madSksConfirmation?.handled) {
102
+ const teamDigest = await teamLiveDigest(root, state);
103
+ const additionalContext = [madSksConfirmation.additionalContext, teamDigest?.context].filter(Boolean).join('\n\n');
104
+ return { continue: true, additionalContext, systemMessage: joinSystemMessages(visibleHookMessage('user-prompt-submit', additionalContext), teamDigest?.system) };
105
+ }
100
106
  const updateContext = await updateCheckContext(root, payload, prompt);
101
107
  const command = dollarCommand(prompt);
102
108
  const route = routePrompt(prompt);
@@ -124,7 +130,7 @@ async function hookPreTool(root, state, payload, noQuestion) {
124
130
  return { decision: 'block', permissionDecision: 'deny', reason: harnessGuardBlockReason(harnessDecision) };
125
131
  }
126
132
  const dbDecision = await checkDbOperation(root, state, payload, { duringRalph: noQuestion });
127
- if (dbDecision.action === 'block') {
133
+ if (dbDecision.action === 'block' || dbDecision.action === 'confirm') {
128
134
  return { decision: 'block', permissionDecision: 'deny', reason: dbBlockReason(dbDecision) };
129
135
  }
130
136
  const command = extractCommand(payload);
@@ -134,7 +140,7 @@ async function hookPreTool(root, state, payload, noQuestion) {
134
140
 
135
141
  async function hookPostTool(root, state, payload, noQuestion) {
136
142
  const dbDecision = await checkDbOperation(root, state, payload, { duringRalph: noQuestion });
137
- if (dbDecision.action === 'block') {
143
+ if (dbDecision.action === 'block' || dbDecision.action === 'confirm') {
138
144
  return { decision: 'block', reason: dbBlockReason(dbDecision) };
139
145
  }
140
146
  await recordContext7Evidence(root, state, payload).catch(() => null);
@@ -165,7 +171,7 @@ async function hookPermission(root, state, payload, noQuestion) {
165
171
  return { decision: 'deny', permissionDecision: 'deny', reason: harnessGuardBlockReason(harnessDecision) };
166
172
  }
167
173
  const dbDecision = await checkDbOperation(root, state, payload, { duringRalph: noQuestion });
168
- if (dbDecision.action === 'block') {
174
+ if (dbDecision.action === 'block' || dbDecision.action === 'confirm') {
169
175
  return { decision: 'deny', permissionDecision: 'deny', reason: dbBlockReason(dbDecision) };
170
176
  }
171
177
  if (!noQuestion) return { continue: true };
package/src/core/init.mjs CHANGED
@@ -484,6 +484,7 @@ export async function installSkills(root) {
484
484
  '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. Do not use for ordinary code edits.\n`,
485
485
  '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`,
486
486
  '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`,
487
+ '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. 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 DB permission applies only while the active mission gate is open, and must be deactivated when the task ends. Table deletion remains special: pause, ask the user for explicit confirmation, and abort that operation if no confirmation arrives within about 30 seconds. Do not carry MAD-SKS permission into later prompts or routes.\n`,
487
488
  'gx': `---\nname: gx\ndescription: Dollar-command route for $GX or $gx deterministic GX visual context cartridges.\n---\n\nUse when the user invokes $GX/$gx or asks for architecture/context visualization through SKS. Prefer sks gx init, render, validate, drift, and snapshot. vgraph.json remains the source of truth.\n`,
488
489
  'help': `---\nname: help\ndescription: Dollar-command route for $Help or $help explaining installed SKS commands and workflows.\n---\n\nUse when the user invokes $Help/$help or asks what commands exist. Prefer concise output from sks commands, sks usage <topic>, sks quickstart, sks aliases, and sks codex-app.\n`,
489
490
  'prompt-pipeline': `---\nname: prompt-pipeline\ndescription: Default SKS prompt optimization pipeline for execution prompts; Answer and DFix bypass it.\n---\n\nClassify intent: Answer only for real questions; question-shaped implicit instructions, complaints, and mandatory-policy statements route to Team. DFix handles tiny design/content; code defaults to Team unless safety/research/GX route fits. Infer goal, target, constraints, acceptance, risk, and smallest safe route. Ask only scope/safety/behavior/acceptance-changing questions; otherwise seal inferred answers. Code work surfaces route/guard/scopes, materializes team-roster.json from default or explicit counts before implementation, compiles concrete Team runtime graph/inbox artifacts after consensus, and parent owns integration/tests/Context7/Honest Mode.\n\n${chatCaptureIntakeText()}\n\nDesign: read design.md; if missing use design-system-builder; use imagegen for image/logo/raster. TriWiki context-tracking SSOT: .sneakoscope/wiki/context-pack.json; read only the latest coordinate+voxel overlay pack before every route stage, run sks wiki refresh/pack after changes, validate before handoffs/final.\n`,
@@ -7,7 +7,7 @@ import { buildQuestionSchemaForRoute, writeQuestions } from './questions.mjs';
7
7
  import { sealContract } from './decision-contract.mjs';
8
8
  import { scanDbSafety } from './db-safety.mjs';
9
9
  import { writeResearchPlan } from './research.mjs';
10
- import { FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, chatCaptureIntakeText, context7RequirementText, dollarCommand, hasFromChatImgSignal, reflectionRequiredForRoute, reasoningInstruction, routeNeedsContext7, routePrompt, routeReasoning, routeRequiresSubagents, stripDollarCommand, subagentExecutionPolicyText, stackCurrentDocsPolicyText, triwikiContextTracking, triwikiContextTrackingText, triwikiStagePolicyText } from './routes.mjs';
10
+ import { FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, chatCaptureIntakeText, context7RequirementText, dollarCommand, hasFromChatImgSignal, hasMadSksSignal, reflectionRequiredForRoute, reasoningInstruction, routeNeedsContext7, routePrompt, routeReasoning, routeRequiresSubagents, stripDollarCommand, stripMadSksSignal, subagentExecutionPolicyText, stackCurrentDocsPolicyText, triwikiContextTracking, triwikiContextTrackingText, triwikiStagePolicyText } from './routes.mjs';
11
11
  import { TEAM_DECOMPOSITION_ARTIFACT, TEAM_GRAPH_ARTIFACT, TEAM_INBOX_DIR, TEAM_RUNTIME_TASKS_ARTIFACT, teamRuntimePlanMetadata, teamRuntimeRequiredArtifacts, validateTeamRuntimeArtifacts, writeTeamRuntimeArtifacts } from './team-dag.mjs';
12
12
  import { formatRoleCounts, initTeamLive, parseTeamSpecText } from './team-live.mjs';
13
13
 
@@ -99,7 +99,8 @@ export function answerOnlyContext(prompt, route = routePrompt(prompt)) {
99
99
 
100
100
  export async function prepareRoute(root, prompt, state = {}) {
101
101
  const route = routePrompt(prompt);
102
- const task = stripDollarCommand(prompt) || String(prompt || '').trim();
102
+ const madSksAuthorization = hasMadSksSignal(prompt);
103
+ const task = stripDollarCommand(stripMadSksSignal(prompt)) || stripMadSksSignal(stripDollarCommand(prompt)) || String(prompt || '').trim();
103
104
  const explicit = Boolean(dollarCommand(prompt));
104
105
  if (!route) return { route: null, additionalContext: promptPipelineContext(prompt, null) };
105
106
  if (route.id === 'DFix') return prepareDfixQuickRoute(route, task);
@@ -108,7 +109,7 @@ export async function prepareRoute(root, prompt, state = {}) {
108
109
  const required = routeNeedsContext7(route, prompt);
109
110
  const reasoning = routeReasoning(route, prompt);
110
111
  const subagentsRequired = routeRequiresSubagents(route, prompt);
111
- if (route.id !== 'Help') return prepareClarificationGate(root, route, task, required, { ralph: route.id === 'Ralph' });
112
+ if (route.id !== 'Help') return prepareClarificationGate(root, route, task, required, { ralph: route.id === 'Ralph', madSksAuthorization });
112
113
  if (route.id === 'Ralph') return prepareRalph(root, route, task, required);
113
114
  if (route.id === 'Team') return prepareTeam(root, route, task, required);
114
115
  if (route.id === 'Research') return prepareResearch(root, route, task, required);
@@ -185,8 +186,9 @@ async function prepareRalph(root, route, task, required) {
185
186
  async function prepareClarificationGate(root, route, task, required, opts = {}) {
186
187
  const { id, dir, mission } = await createMission(root, { mode: String(route.mode || route.id || 'route').toLowerCase(), prompt: task });
187
188
  const schema = buildQuestionSchemaForRoute(route, task);
189
+ if (opts.madSksAuthorization) applyMadSksAuthorizationToSchema(schema);
188
190
  await writeQuestions(dir, schema);
189
- const routeContext = { route: route.id, command: route.command, mode: route.mode, task, required_skills: route.requiredSkills, context7_required: required, original_stop_gate: route.stopGate, clarification_gate: true };
191
+ const routeContext = { route: route.id, command: route.command, mode: route.mode, task, required_skills: route.requiredSkills, context7_required: required, original_stop_gate: route.stopGate, clarification_gate: true, mad_sks_authorization: Boolean(opts.madSksAuthorization || route.id === 'MadSKS') };
190
192
  await writeJsonAtomic(path.join(dir, 'route-context.json'), routeContext);
191
193
  if (!opts.ralph && schema.slots.length === 0) {
192
194
  await writeJsonAtomic(path.join(dir, 'answers.json'), schema.inferred_answers || {});
@@ -240,6 +242,29 @@ ${formatRalphQuestions(schema)}`
240
242
  };
241
243
  }
242
244
 
245
+ function applyMadSksAuthorizationToSchema(schema = {}) {
246
+ schema.domain_hints = Array.from(new Set([...(schema.domain_hints || []), 'mad-sks']));
247
+ schema.inferred_answers = {
248
+ ...(schema.inferred_answers || {}),
249
+ MAD_SKS_MODE: 'explicit_invocation_only',
250
+ DATABASE_TARGET_ENVIRONMENT: 'main_branch',
251
+ DATABASE_WRITE_MODE: 'mad_sks_full_mcp_write_for_invocation',
252
+ SUPABASE_MCP_POLICY: 'mad_sks_project_scoped_write_for_invocation',
253
+ DESTRUCTIVE_DB_OPERATIONS_ALLOWED: 'mad_sks_scoped_with_table_delete_confirmation',
254
+ DB_BACKUP_OR_BRANCH_REQUIRED: 'recommended_but_not_required_in_mad_sks',
255
+ DB_MAX_BLAST_RADIUS: 'mad_sks_active_invocation_only_table_delete_confirmation_required',
256
+ DB_MIGRATION_APPLY_ALLOWED: 'mad_sks_active_invocation_only',
257
+ DB_READ_ONLY_QUERY_LIMIT: '100'
258
+ };
259
+ schema.inference_notes = {
260
+ ...(schema.inference_notes || {}),
261
+ MAD_SKS_MODE: 'explicit dollar command modifier is the permission boundary',
262
+ DESTRUCTIVE_DB_OPERATIONS_ALLOWED: 'MAD-SKS scoped override with table deletion confirmation'
263
+ };
264
+ schema.slots = (schema.slots || []).filter((slot) => !/^(DB_|DATABASE_|DESTRUCTIVE_DB_|SUPABASE_MCP_POLICY$)/.test(slot.id));
265
+ return schema;
266
+ }
267
+
243
268
  async function prepareTeam(root, route, task, required) {
244
269
  const spec = parseTeamSpecText(task);
245
270
  const cleanTask = spec.prompt || task;
@@ -5,9 +5,54 @@ import { FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM
5
5
 
6
6
  export function buildQuestionSchemaForRoute(route, prompt) {
7
7
  if (String(route?.id || '') === 'QALoop') return buildQaLoopQuestionSchema(prompt);
8
+ if (String(route?.id || '') === 'MadSKS') return buildMadSksQuestionSchema(prompt);
8
9
  return buildQuestionSchema(prompt);
9
10
  }
10
11
 
12
+ function buildMadSksQuestionSchema(prompt) {
13
+ const task = String(prompt || '').trim() || 'MAD-SKS scoped database override';
14
+ return {
15
+ schema_version: 1,
16
+ description: 'MAD-SKS is explicit-invocation-only. It auto-seals because the dollar command itself is the permission boundary; table deletion still requires runtime user confirmation with an approximately 30 second timeout.',
17
+ prompt,
18
+ domain_hints: ['db', 'mad-sks'],
19
+ inferred_answers: {
20
+ GOAL_PRECISE: `명시적인 MAD-SKS 호출 범위에서만 DB 권한 조건을 넓혀 작업한다: ${task}`,
21
+ ACCEPTANCE_CRITERIA: [
22
+ '$MAD-SKS is listed in dollar commands and routes to MADSKS mode',
23
+ 'broad Supabase MCP DB manipulation is allowed only while the active MAD-SKS mission gate remains open',
24
+ 'the widened permission is inactive after the MAD-SKS gate is passed or permissions_deactivated is true',
25
+ 'table deletion requires explicit user confirmation and expires after about 30 seconds without confirmation'
26
+ ],
27
+ NON_GOALS: [],
28
+ PUBLIC_API_CHANGE_ALLOWED: 'yes_if_needed',
29
+ DB_SCHEMA_CHANGE_ALLOWED: 'yes_if_needed',
30
+ DEPENDENCY_CHANGE_ALLOWED: 'no',
31
+ TEST_SCOPE: ['packcheck', 'selftest'],
32
+ MID_RALPH_UNKNOWN_POLICY: ['preserve_existing_behavior', 'smallest_reversible_change', 'defer_optional_scope', 'block_only_if_no_safe_path'],
33
+ RISK_BOUNDARY: [
34
+ 'MAD-SKS permission widening is explicit-invocation-only',
35
+ 'MAD-SKS permission widening does not persist after the active task gate closes',
36
+ 'table deletion must pause for explicit user confirmation and timeout-abort after about 30 seconds'
37
+ ],
38
+ MAD_SKS_MODE: 'explicit_invocation_only',
39
+ DATABASE_TARGET_ENVIRONMENT: 'main_branch',
40
+ DATABASE_WRITE_MODE: 'mad_sks_full_mcp_write_for_invocation',
41
+ SUPABASE_MCP_POLICY: 'mad_sks_project_scoped_write_for_invocation',
42
+ DESTRUCTIVE_DB_OPERATIONS_ALLOWED: 'mad_sks_scoped_with_table_delete_confirmation',
43
+ DB_BACKUP_OR_BRANCH_REQUIRED: 'recommended_but_not_required_in_mad_sks',
44
+ DB_MAX_BLAST_RADIUS: 'mad_sks_active_invocation_only_table_delete_confirmation_required',
45
+ DB_MIGRATION_APPLY_ALLOWED: 'mad_sks_active_invocation_only',
46
+ DB_READ_ONLY_QUERY_LIMIT: '100'
47
+ },
48
+ inference_notes: {
49
+ MAD_SKS_MODE: 'explicit dollar command is the permission boundary',
50
+ DESTRUCTIVE_DB_OPERATIONS_ALLOWED: 'MAD-SKS scoped override with table deletion confirmation'
51
+ },
52
+ slots: []
53
+ };
54
+ }
55
+
11
56
  function hasAnswer(value) {
12
57
  if (value === undefined || value === null) return false;
13
58
  if (typeof value === 'string') return value.trim() !== '';
@@ -251,6 +251,20 @@ export const ROUTES = [
251
251
  cliEntrypoint: 'sks db scan',
252
252
  examples: ['$DB check this migration safely']
253
253
  },
254
+ {
255
+ id: 'MadSKS',
256
+ command: '$MAD-SKS',
257
+ mode: 'MADSKS',
258
+ route: 'explicit scoped database authorization modifier',
259
+ description: 'Explicit high-risk authorization modifier that can be combined with other $ commands to temporarily widen Supabase MCP DB permissions for that active invocation only; table deletion still requires user confirmation with an approximately 30 second timeout.',
260
+ requiredSkills: ['mad-sks', 'db-safety-guard', 'pipeline-runner', 'context7-docs', REFLECTION_SKILL_NAME, 'honest-mode'],
261
+ lifecycle: ['explicit_invocation', 'auto_sealed_permission_scope', 'scoped_db_override', 'table_delete_confirmation_gate', 'permission_deactivation', 'post_route_reflection', 'honest_mode'],
262
+ context7Policy: 'required',
263
+ reasoningPolicy: 'high',
264
+ stopGate: 'mad-sks-gate.json',
265
+ cliEntrypoint: 'Codex App prompt route only: $MAD-SKS <task>',
266
+ examples: ['$MAD-SKS $Team Supabase MCP로 main 대상 DB 작업을 수행하되 테이블 삭제는 확인받아', '$DB Supabase 점검 $MAD-SKS']
267
+ },
254
268
  {
255
269
  id: 'GX',
256
270
  command: '$GX',
@@ -377,6 +391,14 @@ export function dollarCommand(prompt) {
377
391
  return match ? match[1].toUpperCase() : null;
378
392
  }
379
393
 
394
+ export function hasMadSksSignal(prompt = '') {
395
+ return /(?:^|\s)\$MAD-SKS(?:\s|:|$)/i.test(String(prompt || ''));
396
+ }
397
+
398
+ export function stripMadSksSignal(prompt = '') {
399
+ return String(prompt || '').replace(/(?:^|\s)\$MAD-SKS(?:\s|:)?/ig, ' ').replace(/\s+/g, ' ').trim();
400
+ }
401
+
380
402
  export function stripDollarCommand(prompt) {
381
403
  return String(prompt || '').trim().replace(/^\$[A-Za-z][A-Za-z0-9_-]*(?:\s|:)?\s*/, '').trim();
382
404
  }
@@ -392,6 +414,15 @@ export function routePrompt(prompt) {
392
414
  const command = dollarCommand(prompt);
393
415
  const text = String(prompt || '');
394
416
  if (command) {
417
+ if (command === 'MAD-SKS') {
418
+ const afterModifier = stripMadSksSignal(text);
419
+ const nestedCommand = dollarCommand(afterModifier);
420
+ if (nestedCommand) return routeByDollarCommand(nestedCommand) || routeById('MadSKS');
421
+ if (looksLikeAnswerOnlyRequest(afterModifier)) return routeById('Answer');
422
+ if (looksLikeFastDesignFix(afterModifier)) return routeById('DFix');
423
+ if (looksLikeCodeChangingWork(afterModifier) || looksLikeDirectWorkRequest(afterModifier)) return routeById('Team');
424
+ return routeById('MadSKS');
425
+ }
395
426
  const route = routeByDollarCommand(command) || null;
396
427
  if (route?.id === 'SKS' && looksLikeTeamDefaultWork(stripDollarCommand(text))) return routeById('Team');
397
428
  return route;
@@ -466,7 +497,7 @@ export function routeRequiresSubagents(route, prompt = '') {
466
497
 
467
498
  export function reflectionRequiredForRoute(route) {
468
499
  const id = String(route?.id || route?.mode || route?.route || route || '').replace(/^\$/, '');
469
- return /^(team|qaloop|qa-loop|ralph|research|autoresearch|db|database|gx)$/i.test(id);
500
+ return /^(team|qaloop|qa-loop|ralph|research|autoresearch|db|database|madsks|mad-sks|gx)$/i.test(id);
470
501
  }
471
502
 
472
503
  export function looksLikeCodeChangingWork(prompt = '') {