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 +1 -0
- package/package.json +1 -1
- package/src/cli/main.mjs +83 -4
- package/src/core/db-safety.mjs +135 -3
- package/src/core/decision-contract.mjs +21 -12
- package/src/core/fsx.mjs +1 -1
- package/src/core/hooks-runtime.mjs +10 -4
- package/src/core/init.mjs +1 -0
- package/src/core/pipeline.mjs +29 -4
- package/src/core/questions.mjs +45 -0
- package/src/core/routes.mjs +32 -1
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.
|
|
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
|
-
|
|
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 });
|
package/src/core/db-safety.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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.
|
|
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`,
|
package/src/core/pipeline.mjs
CHANGED
|
@@ -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
|
|
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;
|
package/src/core/questions.mjs
CHANGED
|
@@ -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() !== '';
|
package/src/core/routes.mjs
CHANGED
|
@@ -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 = '') {
|