sneakoscope 0.7.46 → 0.7.48

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
@@ -79,7 +79,7 @@ The default `sks` runtime checks npm for newer `sneakoscope` and `@openai/codex`
79
79
  - Checks npm for newer `sneakoscope` and `@openai/codex` versions before launch and asks whether to update when the terminal can answer y/n.
80
80
  - Installs the latest Codex CLI with `npm i -g @openai/codex@latest` when it is missing and you approve or pass `--yes`.
81
81
  - Requires tmux 3.x or newer before opening the session.
82
- - Creates or reuses a named detached tmux session, splits panes, and prints the attach command.
82
+ - Creates or reuses a named detached tmux session and prints only the session, gate, attach, and blocker details needed to act.
83
83
 
84
84
  ## Installation
85
85
 
@@ -205,7 +205,7 @@ sks --mad
205
205
  sks --mad --yes
206
206
  ```
207
207
 
208
- This creates/uses the `sks-mad-high` Codex profile for a one-shot full-access, high-reasoning tmux session with `sandbox_mode = "danger-full-access"` and `approval_policy = "never"`, then launches Codex with `--sandbox danger-full-access --ask-for-approval never` and attaches to the session in an interactive terminal. It is scoped to that explicit command and does not change normal SKS/DB safety defaults. Repeat launches reuse the same named SKS MAD tmux session.
208
+ This creates/uses the `sks-mad-high` Codex profile for a one-shot full-access, high-reasoning tmux session with `sandbox_mode = "danger-full-access"` and `approval_policy = "never"`, opens an active MAD-SKS permission gate for that tmux run, then launches Codex with `--sandbox danger-full-access --ask-for-approval never` and attaches to the session in an interactive terminal. While the gate is active, live server work, Supabase MCP database writes, direct SQL, targeted DML, schema cleanup, and needed migrations are allowed. Catastrophic database wipe/all-row/project-management safeguards remain active. Repeat launches reuse the same named SKS MAD tmux session.
209
209
 
210
210
  MAD does not disable the pipeline contract: stages, executors, reviewers, and auto-review policy still must not invent unrequested fallback implementation code. If the requested path cannot be implemented, SKS should block with evidence rather than add substitute behavior.
211
211
 
@@ -230,9 +230,9 @@ sks team dashboard latest
230
230
  sks team log latest
231
231
  ```
232
232
 
233
- Team mode prepares the mission, records live events, compiles runtime tasks and worker inboxes, writes schema-backed effort/work-order/dashboard artifacts, and opens a named tmux Team session with split live lanes when tmux is available. `sks team dashboard` renders the cockpit panes for mission overview, agent lanes, task DAG, QA/dogfood, artifacts/evidence, and performance.
233
+ Team mode prepares the mission, records live events, compiles runtime tasks and worker inboxes, writes schema-backed effort/work-order/dashboard artifacts, and opens a named tmux Team session with split live lanes when tmux is available. The default terminal output stays compact: mission id, agent count, role count, tmux status, watch command, and artifact directory. `sks team dashboard` renders the cockpit panes for mission overview, agent lanes, task DAG, QA/dogfood, artifacts/evidence, and performance.
234
234
 
235
- The tmux Team launch is a live orchestration screen: the first pane follows `sks team watch <mission-id> --follow` as the mission overview, and neighboring split panes follow individual `sks team lane <mission-id> --agent <name> --follow` views. SKS gives lanes role-specific colors, labels, and terminal titles, so scouts, planning/debate voices, executors, reviewers, and safety lanes are visually distinct while the same evidence is mirrored into `team-transcript.jsonl`, `team-live.md`, and `team-dashboard.json`.
235
+ The tmux Team launch is a live orchestration screen in one tmux window: the first pane follows `sks team watch <mission-id> --follow` as the mission overview, and neighboring split panes follow individual `sks team lane <mission-id> --agent <name> --follow` views. Pane headers show only mission, lane, phase, follow command, and cleanup command. SKS gives lanes role-specific colors, labels, and terminal titles, so scouts, planning/debate voices, executors, reviewers, and safety lanes are visually distinct while detailed evidence is mirrored into `team-transcript.jsonl`, `team-live.md`, and `team-dashboard.json`.
236
236
 
237
237
  Agent sessions communicate through the bounded Team transcript. Use `sks team message <mission-id|latest> --from <agent> --to <agent|all> --message "..."` to add direct or broadcast messages; lane panes show messages addressed to that agent plus the fallback global tail.
238
238
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.7.46",
4
+ "version": "0.7.48",
5
5
  "description": "Sneakoscope Codex: database-safe Codex CLI/App harness with Team, Goal, AutoResearch, TriWiki, and Honest Mode.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
package/src/cli/main.mjs CHANGED
@@ -56,6 +56,7 @@ import { createSkillCandidate, decideSkillInjection, skillDreamFixture, writeSki
56
56
  import { classifyToolError, harnessGrowthReport } from '../core/evaluation.mjs';
57
57
  import { runWorkflowPerfBench, validateWorkflowPerfReport } from '../core/perf-bench.mjs';
58
58
  import { buildProofField, proofFieldFixture, validateProofFieldReport } from '../core/proof-field.mjs';
59
+ import { permissionGateSummary } from '../core/permission-gates.mjs';
59
60
  import { recordMistake, writeMistakeMemoryReport } from '../core/mistake-memory.mjs';
60
61
  import { MISTAKE_RECALL_ARTIFACT, contractConsumesMistakeRecall } from '../core/mistake-recall.mjs';
61
62
  import { buildPromptContext } from '../core/prompt-context-builder.mjs';
@@ -702,7 +703,11 @@ async function materializeAfterPipelineAnswer(root, id, dir, mission, route, rou
702
703
  permissions_deactivated: false,
703
704
  supabase_mcp_schema_cleanup_allowed: true,
704
705
  direct_execute_sql_allowed: true,
706
+ normal_db_writes_allowed: true,
707
+ live_server_writes_allowed: true,
708
+ migration_apply_allowed: true,
705
709
  catastrophic_safety_guard_active: true,
710
+ permission_profile: permissionGateSummary(),
706
711
  contract_hash: contract.sealed_hash || null
707
712
  });
708
713
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), {
@@ -721,6 +726,9 @@ async function materializeAfterPipelineAnswer(root, id, dir, mission, route, rou
721
726
  mad_sks_gate_ready: true,
722
727
  supabase_mcp_schema_cleanup_allowed: true,
723
728
  direct_execute_sql_allowed: true,
729
+ normal_db_writes_allowed: true,
730
+ live_server_writes_allowed: true,
731
+ migration_apply_allowed: true,
724
732
  catastrophic_safety_guard_active: true
725
733
  }
726
734
  };
@@ -813,7 +821,11 @@ async function materializeMadSksAuthorization(dir, id, route, routeContext = {},
813
821
  deactivates_when_gate_passed: gateFile,
814
822
  supabase_mcp_schema_cleanup_allowed: true,
815
823
  direct_execute_sql_allowed: true,
824
+ normal_db_writes_allowed: true,
825
+ live_server_writes_allowed: true,
826
+ migration_apply_allowed: true,
816
827
  catastrophic_safety_guard_active: true,
828
+ permission_profile: permissionGateSummary(),
817
829
  contract_hash: contract.sealed_hash || null
818
830
  };
819
831
  await writeJsonAtomic(path.join(dir, 'mad-sks-authorization.json'), artifact);
@@ -830,6 +842,9 @@ async function materializeMadSksAuthorization(dir, id, route, routeContext = {},
830
842
  mad_sks_gate_file: gateFile,
831
843
  supabase_mcp_schema_cleanup_allowed: true,
832
844
  direct_execute_sql_allowed: true,
845
+ normal_db_writes_allowed: true,
846
+ live_server_writes_allowed: true,
847
+ migration_apply_allowed: true,
833
848
  catastrophic_safety_guard_active: true
834
849
  };
835
850
  }
@@ -1071,8 +1086,9 @@ async function madHighCommand(args = []) {
1071
1086
  return;
1072
1087
  }
1073
1088
  const profile = await enableMadHighProfile();
1074
- console.log(`SKS MAD full-access profile ready: ${madHighProfileName()}`);
1075
- console.log('Scope: explicit tmux launch only; Codex opens with danger-full-access sandbox and approval_policy=never.');
1089
+ const madLaunch = await activateMadTmuxPermissionState(process.cwd());
1090
+ console.log(`SKS MAD ready: ${madHighProfileName()} | gate ${madLaunch.mission_id}`);
1091
+ console.log('Live full-access active; catastrophic DB wipe/all-row/project-management guards remain.');
1076
1092
  const workspace = readOption(cleanArgs, '--workspace', readOption(cleanArgs, '--session', `sks-mad-${defaultTmuxSessionName(process.cwd())}`));
1077
1093
  return launchTmuxUi([...cleanArgs, '--workspace', workspace], {
1078
1094
  codexArgs: profile.launch_args,
@@ -1081,6 +1097,67 @@ async function madHighCommand(args = []) {
1081
1097
  });
1082
1098
  }
1083
1099
 
1100
+ async function activateMadTmuxPermissionState(cwd = process.cwd()) {
1101
+ const root = await sksRoot();
1102
+ if (!(await exists(path.join(root, '.sneakoscope')))) await initProject(root, {});
1103
+ const { id, dir } = await createMission(root, { mode: 'mad-sks', prompt: 'sks --mad tmux live full-access session' });
1104
+ const gate = {
1105
+ schema_version: 1,
1106
+ passed: false,
1107
+ mad_sks_permission_active: true,
1108
+ permissions_deactivated: false,
1109
+ live_server_writes_allowed: true,
1110
+ supabase_mcp_schema_cleanup_allowed: true,
1111
+ direct_execute_sql_allowed: true,
1112
+ normal_db_writes_allowed: true,
1113
+ migration_apply_allowed: true,
1114
+ catastrophic_safety_guard_active: true,
1115
+ permission_profile: permissionGateSummary(),
1116
+ activated_by: 'sks --mad',
1117
+ cwd: path.resolve(cwd || process.cwd())
1118
+ };
1119
+ await writeJsonAtomic(path.join(dir, 'mad-sks-gate.json'), gate);
1120
+ await writeJsonAtomic(path.join(dir, 'route-context.json'), {
1121
+ route: 'MadSKS',
1122
+ command: '$MAD-SKS',
1123
+ mode: 'MADSKS',
1124
+ task: gate.activated_by,
1125
+ mad_sks_authorization: true,
1126
+ tmux_launch: true,
1127
+ permission_profile: gate.permission_profile
1128
+ });
1129
+ await appendJsonlBounded(path.join(dir, 'events.jsonl'), {
1130
+ ts: nowIso(),
1131
+ type: 'mad_sks.tmux_permission_opened',
1132
+ route: 'MadSKS',
1133
+ live_server_writes_allowed: true,
1134
+ catastrophic_safety_guard_active: true
1135
+ });
1136
+ await setCurrent(root, {
1137
+ mission_id: id,
1138
+ route: 'MadSKS',
1139
+ route_command: '$MAD-SKS',
1140
+ mode: 'MADSKS',
1141
+ phase: 'MADSKS_TMUX_PERMISSION_ACTIVE',
1142
+ questions_allowed: false,
1143
+ implementation_allowed: true,
1144
+ mad_sks_active: true,
1145
+ mad_sks_modifier: true,
1146
+ mad_sks_gate_file: 'mad-sks-gate.json',
1147
+ mad_sks_gate_ready: true,
1148
+ live_server_writes_allowed: true,
1149
+ supabase_mcp_schema_cleanup_allowed: true,
1150
+ direct_execute_sql_allowed: true,
1151
+ normal_db_writes_allowed: true,
1152
+ migration_apply_allowed: true,
1153
+ catastrophic_safety_guard_active: true,
1154
+ permission_profile: gate.permission_profile,
1155
+ stop_gate: 'mad-sks-gate.json',
1156
+ prompt: gate.activated_by
1157
+ });
1158
+ return { mission_id: id, dir, gate };
1159
+ }
1160
+
1084
1161
  async function maybePromptSksUpdateForLaunch(args = [], opts = {}) {
1085
1162
  if (flag(args, '--json') || flag(args, '--skip-update-check') || process.env.SKS_SKIP_UPDATE_CHECK === '1') return { status: 'skipped' };
1086
1163
  const latest = await npmPackageVersion('sneakoscope');
@@ -2348,7 +2425,7 @@ async function selftest() {
2348
2425
  const madStandaloneResult = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'user-prompt-submit'], { cwd: madStandaloneTmp, input: madStandalonePayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
2349
2426
  if (madStandaloneResult.code !== 0) throw new Error(`selftest failed: standalone MAD-SKS hook exited ${madStandaloneResult.code}: ${madStandaloneResult.stderr}`);
2350
2427
  const madStandaloneState = await readJson(stateFile(madStandaloneTmp), {});
2351
- if (madStandaloneState.mode !== 'MADSKS' || madStandaloneState.mad_sks_active !== true || madStandaloneState.mad_sks_gate_file !== 'mad-sks-gate.json') throw new Error('selftest failed: standalone MAD-SKS auto-seal did not activate scoped permissions');
2428
+ if (madStandaloneState.mode !== 'MADSKS' || madStandaloneState.mad_sks_active !== true || madStandaloneState.mad_sks_gate_file !== 'mad-sks-gate.json' || madStandaloneState.normal_db_writes_allowed !== true || madStandaloneState.live_server_writes_allowed !== true || madStandaloneState.migration_apply_allowed !== true) throw new Error('selftest failed: standalone MAD-SKS auto-seal did not activate live full-access scoped permissions');
2352
2429
  const madStandaloneWrite = 'cre' + 'ate table mad_selftest (id uuid primary key);';
2353
2430
  const madStandaloneCreateDecision = await checkDbOperation(madStandaloneTmp, madStandaloneState, { ['tool' + '_name']: 'mcp__data' + 'base__execute_' + 'sql', ['s' + 'ql']: madStandaloneWrite }, { duringNoQuestion: false });
2354
2431
  if (madStandaloneCreateDecision.action !== 'allow') throw new Error('selftest failed: standalone MAD-SKS did not allow ordinary DDL');
@@ -2358,7 +2435,7 @@ async function selftest() {
2358
2435
  const madModifierResult = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'user-prompt-submit'], { cwd: madModifierTmp, input: madModifierPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
2359
2436
  if (madModifierResult.code !== 0) throw new Error(`selftest failed: MAD-SKS Team hook exited ${madModifierResult.code}: ${madModifierResult.stderr}`);
2360
2437
  const madModifierState = await readJson(stateFile(madModifierTmp), {});
2361
- if (madModifierState.mode !== 'TEAM' || madModifierState.mad_sks_active !== true || madModifierState.mad_sks_gate_file !== 'team-gate.json') throw new Error('selftest failed: MAD-SKS Team auto-seal did not activate scoped permissions');
2438
+ if (madModifierState.mode !== 'TEAM' || madModifierState.mad_sks_active !== true || madModifierState.mad_sks_gate_file !== 'team-gate.json' || madModifierState.normal_db_writes_allowed !== true || madModifierState.live_server_writes_allowed !== true || madModifierState.migration_apply_allowed !== true) throw new Error('selftest failed: MAD-SKS Team auto-seal did not activate live full-access scoped permissions');
2362
2439
  if (routePrompt('위키 갱신해줘')?.id !== 'Wiki') throw new Error('selftest failed: wiki refresh text did not route to Wiki');
2363
2440
  const koreanReadmeInstallPrompt = '리드미에 Codex App에서도 $ 표기 쓰는 법을 알려줘야지. 설치단계에서 바로 보이게 해줘야지';
2364
2441
  if (routePrompt(koreanReadmeInstallPrompt)?.id !== 'Team') throw new Error('selftest failed: Korean README implementation prompt did not route to Team by default');
@@ -2422,9 +2499,9 @@ async function selftest() {
2422
2499
  const hookGoalDelegationContext = hookGoalDelegationJson.hookSpecificOutput?.additionalContext || '';
2423
2500
  const hookGoalDelegationBridgeMatch = hookGoalDelegationContext.match(/Goal bridge mission: (M-[A-Za-z0-9-]+)/);
2424
2501
  if (!hookGoalDelegationBridgeMatch || !hookGoalDelegationContext.includes('Delegated execution route: $Team')) throw new Error('selftest failed: $Goal implementation prompt did not prepare a bridge plus Team delegation');
2425
- if (!hookGoalDelegationContext.includes('MANDATORY ambiguity-removal gate activated') || !hookGoalDelegationContext.includes('Route: $Team')) throw new Error('selftest failed: $Goal implementation delegation did not prepare Team ambiguity gate');
2502
+ if (hookGoalDelegationContext.includes('MANDATORY ambiguity-removal gate activated') || !hookGoalDelegationContext.includes('$Team route prepared')) throw new Error('selftest failed: $Goal implementation delegation did not prepare direct Team route');
2426
2503
  const hookGoalDelegationState = await readJson(stateFile(hookGoalDelegationTmp), {});
2427
- if (hookGoalDelegationState.mode !== 'TEAM' || hookGoalDelegationState.phase !== 'TEAM_CLARIFICATION_AWAITING_ANSWERS' || hookGoalDelegationState.implementation_allowed !== false) throw new Error('selftest failed: $Goal implementation delegation did not leave Team gate current');
2504
+ if (hookGoalDelegationState.mode !== 'TEAM' || hookGoalDelegationState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || hookGoalDelegationState.implementation_allowed === false || !hookGoalDelegationState.team_plan_ready) throw new Error('selftest failed: $Goal implementation delegation did not leave direct Team ready');
2428
2505
  if (!(await exists(path.join(missionDir(hookGoalDelegationTmp, hookGoalDelegationBridgeMatch[1]), GOAL_WORKFLOW_ARTIFACT)))) throw new Error('selftest failed: $Goal implementation delegation did not write bridge workflow artifact');
2429
2506
  const activeGoalMissionId = hookState.mission_id;
2430
2507
  const hookGoalOverlayPayload = JSON.stringify({ cwd: hookGoalTmp, prompt: '$Team 발표자료 만들어줘' });
@@ -2432,12 +2509,11 @@ async function selftest() {
2432
2509
  if (hookGoalOverlayResult.code !== 0) throw new Error(`selftest failed: active Goal overlay hook exited ${hookGoalOverlayResult.code}: ${hookGoalOverlayResult.stderr}`);
2433
2510
  const hookGoalOverlayJson = JSON.parse(hookGoalOverlayResult.stdout);
2434
2511
  const hookGoalOverlayContext = hookGoalOverlayJson.hookSpecificOutput?.additionalContext || '';
2435
- if (!hookGoalOverlayContext.includes('MANDATORY ambiguity-removal gate activated') || !hookGoalOverlayContext.includes('Route: $Team')) throw new Error('selftest failed: active Goal hijacked a plain Korean implementation prompt instead of preparing Team');
2512
+ if (hookGoalOverlayContext.includes('MANDATORY ambiguity-removal gate activated') || !hookGoalOverlayContext.includes('$Team route prepared')) throw new Error('selftest failed: active Goal hijacked a plain Korean implementation prompt instead of preparing direct Team');
2436
2513
  if (!hookGoalOverlayContext.includes(`Active Goal overlay: existing Goal mission ${activeGoalMissionId}`) || !hookGoalOverlayContext.includes('goal-workflow.json')) throw new Error('selftest failed: active Goal overlay context was not included with the new route');
2437
- if (hookGoalOverlayContext.indexOf('MANDATORY ambiguity-removal gate activated') > hookGoalOverlayContext.indexOf('Active Goal overlay:')) throw new Error('selftest failed: active Goal overlay appeared before the newly prepared Team gate');
2438
2514
  const hookGoalOverlayState = await readJson(stateFile(hookGoalTmp), {});
2439
- if (hookGoalOverlayState.mission_id === activeGoalMissionId || hookGoalOverlayState.mode !== 'TEAM' || hookGoalOverlayState.phase !== 'TEAM_CLARIFICATION_AWAITING_ANSWERS' || hookGoalOverlayState.implementation_allowed !== false) throw new Error('selftest failed: active Goal overlay did not leave a new Team ambiguity mission current');
2440
- if (!(await exists(path.join(missionDir(hookGoalTmp, hookGoalOverlayState.mission_id), 'required-answers.schema.json')))) throw new Error('selftest failed: active Goal overlay Team mission did not write ambiguity schema');
2515
+ if (hookGoalOverlayState.mission_id === activeGoalMissionId || hookGoalOverlayState.mode !== 'TEAM' || hookGoalOverlayState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || hookGoalOverlayState.implementation_allowed === false || !hookGoalOverlayState.team_plan_ready) throw new Error('selftest failed: active Goal overlay did not leave a new direct Team mission current');
2516
+ if (!(await exists(path.join(missionDir(hookGoalTmp, hookGoalOverlayState.mission_id), 'team-plan.json')))) throw new Error('selftest failed: active Goal overlay Team mission did not write team-plan.json');
2441
2517
  const hookUpdateCurrentTmp = tmpdir();
2442
2518
  await initProject(hookUpdateCurrentTmp, {});
2443
2519
  const hookUpdateCurrentEnv = { SKS_DISABLE_UPDATE_CHECK: '0', SKS_NPM_VIEW_SNEAKOSCOPE_VERSION: '9.9.9', SKS_INSTALLED_SKS_VERSION: '9.9.9' };
@@ -2522,7 +2598,7 @@ async function selftest() {
2522
2598
  if (hookKoreanSksResult.code !== 0) throw new Error(`selftest failed: Korean SKS hook exited ${hookKoreanSksResult.code}: ${hookKoreanSksResult.stderr}`);
2523
2599
  const hookKoreanSksJson = JSON.parse(hookKoreanSksResult.stdout);
2524
2600
  const hookKoreanSksContext = hookKoreanSksJson.hookSpecificOutput?.additionalContext || '';
2525
- if (!hookKoreanSksContext.includes('Ambiguity gate auto-sealed') || hookKoreanSksContext.includes('GOAL_PRECISE: 이번 작업의 최종 목표')) throw new Error('selftest failed: Korean prompt did not auto-infer');
2601
+ if (!hookKoreanSksContext.includes('$Team route prepared') || hookKoreanSksContext.includes('GOAL_PRECISE: 이번 작업의 최종 목표') || hookKoreanSksContext.includes('MANDATORY ambiguity-removal gate activated')) throw new Error('selftest failed: Korean prompt did not prepare direct Team route');
2526
2602
  if (!hookKoreanSksContext.includes('Route: $Team')) throw new Error('selftest failed: Korean implementation prompt did not promote to Team route');
2527
2603
  if (hookKoreanSksContext.includes('SKS answer-only pipeline active')) throw new Error('selftest failed: Korean implementation prompt still used answer-only pipeline');
2528
2604
  const hookKoreanSksState = await readJson(stateFile(hookKoreanSksTmp), {});
@@ -2535,12 +2611,10 @@ async function selftest() {
2535
2611
  if (hookPaymentTeamResult.code !== 0) throw new Error(`selftest failed: payment/auth Team hook exited ${hookPaymentTeamResult.code}: ${hookPaymentTeamResult.stderr}`);
2536
2612
  const hookPaymentTeamJson = JSON.parse(hookPaymentTeamResult.stdout);
2537
2613
  const hookPaymentTeamContext = hookPaymentTeamJson.hookSpecificOutput?.additionalContext || '';
2538
- if (!hookPaymentTeamContext.includes('Ambiguity gate auto-sealed')) throw new Error('selftest failed: predictable payment/auth Team prompt did not auto-seal');
2614
+ if (!hookPaymentTeamContext.includes('$Team route prepared') || hookPaymentTeamContext.includes('MANDATORY ambiguity-removal gate activated')) throw new Error('selftest failed: predictable payment/auth Team prompt did not prepare direct Team route');
2539
2615
  if (hookPaymentTeamContext.includes('PAYMENT_RETRY_POLICY') || hookPaymentTeamContext.includes('AUTH_PROTOCOL_CHANGE_ALLOWED')) throw new Error('selftest failed: predictable payment/auth policy defaults were asked instead of inferred');
2540
2616
  const hookPaymentTeamState = await readJson(stateFile(hookPaymentTeamTmp), {});
2541
2617
  if (hookPaymentTeamState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || hookPaymentTeamState.implementation_allowed !== true || !hookPaymentTeamState.ambiguity_gate_passed || !hookPaymentTeamState.team_plan_ready) throw new Error('selftest failed: predictable payment/auth Team did not materialize after auto-seal');
2542
- const hookPaymentTeamSchema = await readJson(path.join(missionDir(hookPaymentTeamTmp, hookPaymentTeamState.mission_id), 'required-answers.schema.json'));
2543
- if (hookPaymentTeamSchema.slots.length !== 0 || hookPaymentTeamSchema.inferred_answers?.PAYMENT_RETRY_POLICY === undefined || hookPaymentTeamSchema.inferred_answers?.AUTH_SESSION_EXPIRED_BEHAVIOR === undefined) throw new Error('selftest failed: predictable payment/auth defaults were not recorded as inferred answers');
2544
2618
  if (!(await exists(path.join(missionDir(hookPaymentTeamTmp, hookPaymentTeamState.mission_id), 'team-plan.json')))) throw new Error('selftest failed: predictable payment/auth Team auto-seal did not write team-plan.json');
2545
2619
  const hookTeamTmp = tmpdir();
2546
2620
  await initProject(hookTeamTmp, {});
@@ -2548,45 +2622,38 @@ async function selftest() {
2548
2622
  const hookTeamResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookTeamTmp, input: hookTeamPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
2549
2623
  if (hookTeamResult.code !== 0) throw new Error(`selftest failed: $Team hook exited ${hookTeamResult.code}: ${hookTeamResult.stderr}`);
2550
2624
  const hookTeamJson = JSON.parse(hookTeamResult.stdout);
2551
- if (!hookTeamJson.hookSpecificOutput?.additionalContext?.includes('MANDATORY ambiguity-removal gate activated')) throw new Error('selftest failed: $Team hook did not force ambiguity gate before Team execution');
2552
- if (!hookTeamJson.hookSpecificOutput?.additionalContext?.includes('VISIBLE RESPONSE CONTRACT') || !String(hookTeamJson.systemMessage || '').includes('clarification questions')) throw new Error('selftest failed: $Team ambiguity gate did not force visible question response');
2553
- if (hookTeamJson.hookSpecificOutput?.additionalContext?.includes('GOAL_PRECISE: 이번 작업의 최종 목표')) throw new Error('selftest failed: static Team goal');
2554
- if (!hookTeamJson.hookSpecificOutput?.additionalContext?.includes('PRESENTATION_DELIVERY_CONTEXT')) throw new Error('selftest failed: missing Team presentation question');
2555
- if (!hookTeamJson.hookSpecificOutput?.additionalContext?.includes('Codex plan-tool interaction')) throw new Error('selftest failed: $Team ambiguity gate did not inject plan-tool guidance');
2625
+ if (hookTeamJson.hookSpecificOutput?.additionalContext?.includes('MANDATORY ambiguity-removal gate activated') || hookTeamJson.hookSpecificOutput?.additionalContext?.includes('VISIBLE RESPONSE CONTRACT')) throw new Error('selftest failed: $Team hook still forced ambiguity questions');
2626
+ if (!hookTeamJson.hookSpecificOutput?.additionalContext?.includes('$Team route prepared')) throw new Error('selftest failed: $Team hook did not prepare direct Team route');
2556
2627
  const hookTeamState = await readJson(stateFile(hookTeamTmp), {});
2557
- if (hookTeamState.phase !== 'TEAM_CLARIFICATION_AWAITING_ANSWERS' || hookTeamState.implementation_allowed !== false) throw new Error('selftest failed: $Team hook did not lock execution behind ambiguity gate');
2558
- if (!hookTeamState.pipeline_plan_ready || !(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), PIPELINE_PLAN_ARTIFACT)))) throw new Error('selftest failed: $Team hook did not write a pending pipeline plan');
2559
- if (await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'team-plan.json'))) throw new Error('selftest failed: Team plan was created before ambiguity gate passed');
2628
+ if (hookTeamState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || hookTeamState.implementation_allowed === false || !hookTeamState.team_plan_ready) throw new Error('selftest failed: $Team hook did not prepare direct Team mission');
2629
+ if (!hookTeamState.pipeline_plan_ready || !(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), PIPELINE_PLAN_ARTIFACT)))) throw new Error('selftest failed: $Team hook did not write a pipeline plan');
2630
+ if (!(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'team-plan.json')))) throw new Error('selftest failed: Team plan was not created directly');
2560
2631
  const hookTeamPendingResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: hookTeamTmp, input: JSON.stringify({ cwd: hookTeamTmp, prompt: '$Team 새 작업으로 넘어가' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
2561
2632
  if (hookTeamPendingResult.code !== 0) throw new Error(`selftest failed: pending clarification hook exited ${hookTeamPendingResult.code}: ${hookTeamPendingResult.stderr}`);
2562
2633
  const hookTeamPendingJson = JSON.parse(hookTeamPendingResult.stdout);
2563
2634
  const hookTeamPendingState = await readJson(stateFile(hookTeamTmp), {});
2564
2635
  const hookTeamPendingContext = hookTeamPendingJson.hookSpecificOutput?.additionalContext || '';
2565
- if (hookTeamPendingState.mission_id !== hookTeamState.mission_id) throw new Error('selftest failed: pending clarification allowed a new route mission to replace the visible question sheet');
2566
- if (!hookTeamPendingContext.includes('Required questions still pending') || !hookTeamPendingContext.includes('PRESENTATION_DELIVERY_CONTEXT')) throw new Error('selftest failed: pending clarification did not re-expose the question sheet');
2567
- if (hookTeamPendingContext.includes('VISIBLE RESPONSE CONTRACT') || hookTeamPendingContext.includes('Codex plan-tool interaction')) throw new Error('selftest failed: pending clarification reprinted verbose guidance instead of a compact retry');
2568
- if (hookTeamPendingContext.includes('MANDATORY ambiguity-removal gate activated')) throw new Error('selftest failed: pending clarification prepared a new ambiguity gate instead of reusing the active one');
2569
- const hookTeamStopResult = await runProcess(process.execPath, [hookBin, 'hook', 'stop'], { cwd: hookTeamTmp, input: JSON.stringify({ cwd: hookTeamTmp, last_assistant_message: 'I need three decisions before implementation, but I will not paste the Required questions block.' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
2570
- if (hookTeamStopResult.code !== 0) throw new Error(`selftest failed: Team stop hook exited ${hookTeamStopResult.code}: ${hookTeamStopResult.stderr}`);
2571
- const hookTeamStopJson = JSON.parse(hookTeamStopResult.stdout);
2572
- if (hookTeamStopJson.decision !== 'block' || !String(hookTeamStopJson.reason || '').includes('mandatory ambiguity-removal')) throw new Error('selftest failed: Stop hook did not block missing Team ambiguity answers');
2573
- if (!String(hookTeamStopJson.reason || '').includes('Required questions') || !String(hookTeamStopJson.reason || '').includes('PRESENTATION_DELIVERY_CONTEXT')) throw new Error('selftest failed: missing Team stop presentation question');
2574
- if (String(hookTeamStopJson.reason || '').includes('GOAL_PRECISE: 이번 작업의 최종 목표')) throw new Error('selftest failed: static Team stop goal');
2575
- if (!String(hookTeamStopJson.reason || '').includes('sks pipeline answer')) throw new Error('selftest failed: Stop hook did not provide pipeline answer command');
2576
- if (String(hookTeamStopJson.reason || '').includes('Codex plan-tool interaction') || String(hookTeamStopJson.reason || '').includes('VISIBLE RESPONSE CONTRACT')) throw new Error('selftest failed: Stop hook reprinted verbose clarification guidance');
2577
- const hookTeamSchema = await readJson(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'required-answers.schema.json'));
2636
+ if (hookTeamPendingState.mission_id === hookTeamState.mission_id || hookTeamPendingContext.includes('Required questions still pending') || hookTeamPendingContext.includes('MANDATORY ambiguity-removal gate activated')) throw new Error('selftest failed: direct Team follow-up was blocked by stale clarification behavior');
2637
+ if (hookTeamPendingState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || !hookTeamPendingState.team_plan_ready) throw new Error('selftest failed: direct Team follow-up did not prepare a fresh Team mission');
2638
+ const qaClarificationTmp = tmpdir();
2639
+ await initProject(qaClarificationTmp, {});
2640
+ const hookQaClarificationResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], { cwd: qaClarificationTmp, input: JSON.stringify({ cwd: qaClarificationTmp, prompt: '$QA-LOOP 로그인 QA 해줘' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 256 * 1024 });
2641
+ if (hookQaClarificationResult.code !== 0) throw new Error(`selftest failed: QA clarification hook exited ${hookQaClarificationResult.code}: ${hookQaClarificationResult.stderr}`);
2642
+ const hookQaClarificationState = await readJson(stateFile(qaClarificationTmp), {});
2643
+ const hookQaClarificationSchema = await readJson(path.join(missionDir(qaClarificationTmp, hookQaClarificationState.mission_id), 'required-answers.schema.json'));
2644
+ const hookTeamSchema = hookQaClarificationSchema;
2578
2645
  const visibleQuestionsBlock = [
2579
2646
  'Required questions',
2580
2647
  ...hookTeamSchema.slots.map((slot, idx) => `${idx + 1}. ${slot.id}: ${slot.question}`),
2581
2648
  'Reply by slot id, then I will seal the contract with sks pipeline answer latest --stdin.'
2582
2649
  ].join('\n');
2583
- const visibleQuestionDecision = await evaluateStop(hookTeamTmp, hookTeamState, { last_assistant_message: visibleQuestionsBlock }, { noQuestion: false });
2650
+ const visibleQuestionDecision = await evaluateStop(qaClarificationTmp, hookQaClarificationState, { last_assistant_message: visibleQuestionsBlock }, { noQuestion: false });
2584
2651
  if (!visibleQuestionDecision?.continue) throw new Error('selftest failed: visible Required questions block was not accepted by clarification stop gate');
2585
- const hookTeamPreToolBlocked = await runProcess(process.execPath, [hookBin, 'hook', 'pre-tool'], { cwd: hookTeamTmp, input: JSON.stringify({ cwd: hookTeamTmp, command: 'npm run selftest' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2652
+ const hookTeamPreToolBlocked = await runProcess(process.execPath, [hookBin, 'hook', 'pre-tool'], { cwd: qaClarificationTmp, input: JSON.stringify({ cwd: qaClarificationTmp, command: 'npm run selftest' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2586
2653
  if (hookTeamPreToolBlocked.code !== 0) throw new Error(`selftest failed: pending clarification pre-tool hook exited ${hookTeamPreToolBlocked.code}: ${hookTeamPreToolBlocked.stderr}`);
2587
2654
  const hookTeamPreToolBlockedJson = JSON.parse(hookTeamPreToolBlocked.stdout);
2588
2655
  if (hookTeamPreToolBlockedJson.decision !== 'block' || !String(hookTeamPreToolBlockedJson.reason || '').includes('ambiguity gate is paused')) throw new Error('selftest failed: pending clarification allowed implementation tool use before answers');
2589
- const hookTeamAnswerToolAllowed = await runProcess(process.execPath, [hookBin, 'hook', 'pre-tool'], { cwd: hookTeamTmp, input: JSON.stringify({ cwd: hookTeamTmp, command: 'node ./bin/sks.mjs pipeline answer latest --stdin' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2656
+ const hookTeamAnswerToolAllowed = await runProcess(process.execPath, [hookBin, 'hook', 'pre-tool'], { cwd: qaClarificationTmp, input: JSON.stringify({ cwd: qaClarificationTmp, command: 'node ./bin/sks.mjs pipeline answer latest --stdin' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2590
2657
  if (hookTeamAnswerToolAllowed.code !== 0) throw new Error(`selftest failed: pipeline-answer pre-tool hook exited ${hookTeamAnswerToolAllowed.code}: ${hookTeamAnswerToolAllowed.stderr}`);
2591
2658
  const hookTeamAnswerToolAllowedJson = JSON.parse(hookTeamAnswerToolAllowed.stdout);
2592
2659
  if (hookTeamAnswerToolAllowedJson.decision === 'block') throw new Error('selftest failed: pending clarification blocked the pipeline answer command');
@@ -2597,18 +2664,6 @@ async function selftest() {
2597
2664
  if (textParsedAnswers.INTENT_TARGET !== 'compact contract sealing') throw new Error('selftest failed: text answer parser did not parse slot-id answers');
2598
2665
  const textParsedImplicitAnswer = parseAnswersText({ slots: [{ id: 'INTENT_TARGET', type: 'string', required: true }] }, 'compact contract sealing');
2599
2666
  if (textParsedImplicitAnswer.INTENT_TARGET !== 'compact contract sealing') throw new Error('selftest failed: text answer parser did not infer the only missing slot');
2600
- const hookTeamAnswers = {};
2601
- for (const s of hookTeamSchema.slots) hookTeamAnswers[s.id] = s.options ? (s.type === 'array' ? [s.options[0]] : s.options[0]) : (s.type.includes('array') ? ['selftest'] : (s.id === 'DB_MAX_BLAST_RADIUS' ? 'no_live_dml' : 'selftest'));
2602
- hookTeamAnswers.NON_GOALS = [];
2603
- const hookTeamAnswersPath = path.join(hookTeamTmp, 'team-answers.json');
2604
- await writeJsonAtomic(hookTeamAnswersPath, hookTeamAnswers);
2605
- const pipelineAnswerResult = await runProcess(process.execPath, [hookBin, 'pipeline', 'answer', 'latest', hookTeamAnswersPath], { cwd: hookTeamTmp, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2606
- if (pipelineAnswerResult.code !== 0) throw new Error(`selftest failed: pipeline answer exited ${pipelineAnswerResult.code}: ${pipelineAnswerResult.stderr}`);
2607
- const answeredTeamState = await readJson(stateFile(hookTeamTmp), {});
2608
- if (answeredTeamState.phase !== 'TEAM_PARALLEL_ANALYSIS_SCOUTING' || !answeredTeamState.ambiguity_gate_passed || answeredTeamState.implementation_allowed !== true || !answeredTeamState.team_plan_ready || !answeredTeamState.pipeline_plan_ready) throw new Error('selftest failed: pipeline answer did not materialize Team after ambiguity gate');
2609
- if (!(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'decision-contract.json')))) throw new Error('selftest failed: pipeline answer did not seal decision contract');
2610
- if (validatePipelinePlan(await readJson(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), PIPELINE_PLAN_ARTIFACT))).ok !== true) throw new Error('selftest failed: pipeline answer did not refresh a valid pipeline plan');
2611
- if (!(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'team-plan.json'))) || !(await exists(path.join(missionDir(hookTeamTmp, hookTeamState.mission_id), 'team-live.md')))) throw new Error('selftest failed: Team artifacts missing after ambiguity gate passed');
2612
2667
  const honestLoopTmp = tmpdir();
2613
2668
  await initProject(honestLoopTmp, {});
2614
2669
  const { id: honestLoopId, dir: honestLoopDir } = await createMission(honestLoopTmp, { mode: 'sks', prompt: 'honest loopback selftest' });
@@ -3026,6 +3081,8 @@ async function selftest() {
3026
3081
  const tmuxTeam = await launchTmuxTeamView({ root: tmp, missionId: teamId, plan: roleTeamPlan, json: true });
3027
3082
  if (!tmuxTeam.agents?.length || !tmuxTeam.agents.some((entry) => entry.agent === 'analysis_scout_1') || !tmuxTeam.agents.every((entry) => String(entry.command || '').includes('team lane') && String(entry.command || '').includes('--agent'))) throw new Error('selftest failed: Team tmux view did not expose agent live lanes');
3028
3083
  if (!tmuxTeam.overview?.command?.includes('team watch') || !tmuxTeam.lanes?.some((entry) => entry.role === 'overview') || !tmuxTeam.lanes?.some((entry) => entry.agent === 'analysis_scout_1')) throw new Error('selftest failed: Team tmux view did not expose orchestration overview plus agent lanes');
3084
+ if (tmuxTeam.split_ui?.mode !== 'single_window_split_panes' || tmuxTeam.split_ui?.layout !== 'tiled' || tmuxTeam.split_ui?.live_updates !== true) throw new Error('selftest failed: Team tmux view did not expose single-window split UI metadata');
3085
+ if (String(tmuxTeam.overview?.command || '').includes('SNEAKOSCOPE CODEX') || !String(tmuxTeam.overview?.command || '').includes('Follow: team watch')) throw new Error('selftest failed: Team tmux pane banner is too noisy or missing compact follow hint');
3029
3086
  if (teamLaneStyle('analysis_scout_1').role !== 'scout' || teamLaneStyle('executor_1').role !== 'execution' || teamLaneStyle('reviewer_1').role !== 'review') throw new Error('selftest failed: Team tmux role palette did not classify lane roles');
3030
3087
  if (!String(tmuxTeam.cleanup_policy || '').includes('mark-complete') || !tmuxTeam.lanes.every((entry) => entry.style?.color && entry.title)) throw new Error('selftest failed: Team tmux view did not expose color/title metadata and cleanup policy');
3031
3088
  if (tmuxTeam.session !== `sks-team-${teamId}` || !tmuxTeam.attach_command?.includes(`sks-team-${teamId}`)) throw new Error('selftest failed: Team tmux session is not named for visibility');
@@ -3263,7 +3320,9 @@ async function selftest() {
3263
3320
  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' };
3264
3321
  const columnCleanupSql = 'alter table users ' + 'dr' + 'op column legacy_name;';
3265
3322
  const madColumnCleanupDecision = await checkDbOperation(tmp, madState, { tool_name: 'mcp__supabase__execute_sql', sql: columnCleanupSql }, { duringNoQuestion: false });
3266
- if (madColumnCleanupDecision.action !== 'allow') throw new Error('selftest failed: MAD-SKS column cleanup was not allowed');
3323
+ if (madColumnCleanupDecision.action !== 'allow' || !madColumnCleanupDecision.mad_sks?.permission_profile?.allowed?.includes('direct_execute_sql_writes')) throw new Error('selftest failed: MAD-SKS column cleanup was not allowed through the modular permission gate');
3324
+ const madLiveDmlDecision = await checkDbOperation(tmp, madState, { tool_name: 'mcp__supabase__execute_sql', sql: "update users set name = 'fixed' where id = 'selftest';" }, { duringNoQuestion: false });
3325
+ if (madLiveDmlDecision.action !== 'allow' || !madLiveDmlDecision.mad_sks?.live_server_writes_allowed) throw new Error('selftest failed: MAD-SKS targeted live DML was not allowed');
3267
3326
  const tableRemovalSql = 'dr' + 'op table users;';
3268
3327
  const madTableRemovalDecision = await checkDbOperation(tmp, madState, { tool_name: 'mcp__supabase__execute_sql', sql: tableRemovalSql }, { duringNoQuestion: false });
3269
3328
  if (madTableRemovalDecision.action !== 'block') throw new Error('selftest failed: MAD-SKS catastrophic table removal was not blocked');
@@ -1542,21 +1542,16 @@ export async function team(args) {
1542
1542
  result.tmux = await launchTmuxTeamView({ root, missionId: id, plan, promptFile: result.workflow, json: flag(args, '--json') || !openTmux });
1543
1543
  if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
1544
1544
  console.log(`Team mission created: ${id}`);
1545
- console.log(`Plan: ${path.relative(root, result.plan)}`);
1546
- console.log(`Pipeline plan: ${path.relative(root, result.pipeline_plan)}`);
1547
1545
  console.log(`Agent sessions: ${agentSessions}`);
1548
1546
  console.log(`Role counts: ${formatRoleCounts(roleCounts)}`);
1549
- console.log(`Workflow: ${path.relative(root, result.workflow)}`);
1550
- console.log(`Runtime graph: ${path.relative(root, result.team_graph)}`);
1551
- console.log(`Worker inbox: ${path.relative(root, result.worker_inbox_dir)}`);
1552
- console.log(`Live: ${path.relative(root, result.live)}`);
1553
1547
  if (result.tmux.ready) {
1554
1548
  const tmuxState = result.tmux.created ? 'opened' : 'not opened; use --open-tmux for a tmux session';
1555
1549
  console.log(`tmux: ${tmuxState} ${result.tmux.opened_lane_count || result.tmux.agents.length} agent lane(s) in ${result.tmux.session || result.tmux.workspace}`);
1550
+ if (result.tmux.split_ui?.mode) console.log(`tmux UI: ${result.tmux.split_ui.mode} (${result.tmux.split_ui.layout})`);
1556
1551
  }
1557
1552
  else console.log(`tmux: blocked (${Array.from(new Set(result.tmux.blockers || [])).join('; ')})`);
1558
1553
  console.log(`Watch: sks team watch ${id}`);
1559
- console.log('Use $Team in Codex App or the tmux launch view from this CLI flow to run scouts, debate/consensus, runtime graph/inbox handoff, then a fresh implementation team with disjoint ownership.');
1554
+ console.log(`Artifacts: .sneakoscope/missions/${id}`);
1560
1555
  }
1561
1556
 
1562
1557
  export function parseTeamCreateArgs(args) {
@@ -180,7 +180,7 @@ function upsertProfile(text, profile, effort, reviewer = AUTO_REVIEW_REVIEWER) {
180
180
  function upsertAutoReviewPolicy(text) {
181
181
  const policy = [
182
182
  '[auto_review]',
183
- 'policy = "Deny destructive database operations, credential exfiltration, persistent security weakening, broad file deletion, writes outside the workspace, and unrequested fallback implementation code unless explicitly authorized by the user or sealed decision contract."'
183
+ 'policy = "In MAD launches, allow live-server work, normal DB writes, Supabase MCP DB writes, direct execute SQL, schema cleanup, and migration application for the active invocation. Deny only catastrophic database wipes, all-row value deletion/update, dangerous project or branch management, credential exfiltration, persistent security weakening, broad unrelated file deletion, and unrequested fallback implementation code."'
184
184
  ].join('\n');
185
185
  const existing = readTableString(text, 'auto_review', 'policy');
186
186
  if (existing && /unrequested fallback implementation code/i.test(existing)) return text;
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { exists, readJson, writeJsonAtomic, readText, nowIso, appendJsonlBounded } from './fsx.mjs';
3
3
  import { missionDir, setCurrent } from './mission.mjs';
4
+ import { evaluateMadSksPermissionGate, isMadSksRouteState } from './permission-gates.mjs';
4
5
 
5
6
  export const DEFAULT_DB_SAFETY_POLICY = Object.freeze({
6
7
  schema_version: 1,
@@ -229,33 +230,6 @@ function hasTableRemovalRisk(cls = {}) {
229
230
  return ['drop_table', 'truncate'].some((reason) => reasons.has(reason));
230
231
  }
231
232
 
232
- function hasMadSksCatastrophicDbRisk(cls = {}) {
233
- const reasons = new Set([
234
- ...(cls.reasons || []),
235
- ...(cls.sql?.reasons || []),
236
- ...(cls.command?.reasons || [])
237
- ]);
238
- return [
239
- 'drop_database',
240
- 'drop_schema',
241
- 'drop_table',
242
- 'truncate',
243
- 'delete_without_where',
244
- 'update_without_where',
245
- 'supabase_db_reset',
246
- 'prisma_migrate_reset',
247
- 'postgres_database_admin_command'
248
- ].some((reason) => reasons.has(reason))
249
- || cls.toolReasons?.includes?.('dangerous_supabase_management_tool');
250
- }
251
-
252
- function isMadSksRouteState(state = {}) {
253
- return state.mad_sks_active === true
254
- || String(state.mode || '').toUpperCase() === 'MADSKS'
255
- || String(state.route_command || '').toUpperCase() === '$MAD-SKS'
256
- || String(state.route || '').toUpperCase() === 'MADSKS';
257
- }
258
-
259
233
  async function madSksOverrideState(root, state = {}) {
260
234
  if (!isMadSksRouteState(state) || !state.mission_id || state.mad_sks_active === false) return { active: false };
261
235
  const gateFile = state.mad_sks_gate_file || state.stop_gate || MAD_SKS_GATE_FILE;
@@ -279,31 +253,35 @@ export function evaluateDbSafety({ classification, policy = DEFAULT_DB_SAFETY_PO
279
253
  if (cls.level === 'safe') return { allowed: true, action: 'allow', reasons: ['read_only_operation'], classification: cls };
280
254
  if (cls.level === 'possible_db') return { allowed: !noQuestion, action: noQuestion ? 'block' : 'warn', reasons: noQuestion ? ['unknown_database_operation_blocked_during_no_question_run'] : ['unknown_database_operation'], classification: cls };
281
255
  if (madSks?.active && (cls.level === 'write' || cls.level === 'destructive')) {
282
- if (hasMadSksCatastrophicDbRisk(cls)) {
256
+ const madGate = evaluateMadSksPermissionGate({ classification: cls, active: true });
257
+ if (!madGate.allowed) {
283
258
  return {
284
259
  allowed: false,
285
260
  action: 'block',
286
- reasons: ['mad_sks_catastrophic_db_operation_blocked'],
261
+ reasons: madGate.reasons,
287
262
  classification: cls,
288
263
  effective,
289
264
  mad_sks: {
290
265
  active: true,
291
266
  catastrophic_safety_guard_active: true,
292
- blocked_categories: ['whole_database_or_table_removal', 'all_rows_delete_or_update', 'dangerous_project_management']
267
+ blocked_categories: madGate.blocked_categories,
268
+ permission_profile: madGate.profile
293
269
  }
294
270
  };
295
271
  }
296
272
  return {
297
273
  allowed: true,
298
274
  action: 'allow',
299
- reasons: ['mad_sks_scoped_override_active'],
275
+ reasons: madGate.reasons,
300
276
  classification: cls,
301
277
  effective,
302
278
  mad_sks: {
303
279
  active: true,
304
280
  sks_db_constraints_removed: true,
305
281
  catastrophic_safety_guard_active: true,
306
- supabase_mcp_schema_cleanup_allowed: true
282
+ supabase_mcp_schema_cleanup_allowed: true,
283
+ live_server_writes_allowed: true,
284
+ permission_profile: madGate.profile
307
285
  }
308
286
  };
309
287
  }
@@ -416,7 +394,7 @@ export function dbBlockReason(decision) {
416
394
  if ((decision.reasons || []).includes('mad_sks_catastrophic_db_operation_blocked')) {
417
395
  return [
418
396
  'Sneakoscope Codex MAD-SKS catastrophic database safeguard blocked this operation.',
419
- 'MAD-SKS opens Supabase MCP column/schema cleanup, direct execute SQL, and normal DB writes only while the mission gate is active.',
397
+ 'MAD-SKS opens live-server changes, Supabase MCP column/schema cleanup, direct execute SQL, migrations when required, and normal DB writes only while the mission gate is active.',
420
398
  'Whole database/table removal, all-row value wipes, database reset, and dangerous project or branch management remain blocked.'
421
399
  ].join(' ');
422
400
  }
package/src/core/fsx.mjs CHANGED
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import crypto from 'node:crypto';
6
6
  import { spawn } from 'node:child_process';
7
7
 
8
- export const PACKAGE_VERSION = '0.7.46';
8
+ export const PACKAGE_VERSION = '0.7.48';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
@@ -113,7 +113,7 @@ async function hookUserPrompt(root, state, payload, noQuestion) {
113
113
  const route = routePrompt(prompt);
114
114
  const bypassActiveRoute = route?.id === 'DFix' || route?.id === 'Answer';
115
115
  const goalOverlay = activeGoalOverlayContext(state, route);
116
- if (isClarificationAwaiting(state) && !looksLikeClarificationCancel(prompt)) {
116
+ if (isBlockingClarificationAwaiting(state) && !looksLikeClarificationCancel(prompt)) {
117
117
  const activeContext = await activeRouteContext(root, state);
118
118
  const teamDigest = await teamLiveDigest(root, state);
119
119
  const additionalContext = [updateContext, activeContext, teamDigest?.context].filter(Boolean).join('\n\n');
@@ -142,6 +142,12 @@ function isClarificationAwaiting(state = {}) {
142
142
  || ['QALOOP_CLARIFICATION_AWAITING_ANSWERS'].includes(String(state.phase || ''));
143
143
  }
144
144
 
145
+ function isBlockingClarificationAwaiting(state = {}) {
146
+ if (!isClarificationAwaiting(state)) return false;
147
+ return ['QALoop', 'PPT'].includes(String(state.route || ''))
148
+ || ['QALOOP', 'PPT'].includes(String(state.mode || ''));
149
+ }
150
+
145
151
  function looksLikeClarificationCancel(prompt = '') {
146
152
  return /^(cancel|reset|restart|new mission|새로|취소|중단|리셋|다시 시작)\b/i.test(String(prompt || '').trim());
147
153
  }
@@ -241,12 +247,13 @@ async function hookPermission(root, state, payload, noQuestion) {
241
247
  }
242
248
 
243
249
  function clarificationGateLocked(state = {}) {
244
- if (isClarificationAwaiting(state)) return true;
250
+ if (isBlockingClarificationAwaiting(state)) return true;
245
251
  return Boolean(
246
252
  state?.mission_id
247
253
  && state.implementation_allowed === false
248
254
  && state.ambiguity_gate_required === true
249
255
  && state.ambiguity_gate_passed !== true
256
+ && isBlockingClarificationAwaiting(state)
250
257
  );
251
258
  }
252
259
 
package/src/core/init.mjs CHANGED
@@ -200,7 +200,7 @@ export async function initProject(root, opts = {}) {
200
200
  state: 'git-common-dir/sks-version-state.json'
201
201
  }
202
202
  },
203
- database_safety: 'destructive_db_operations_denied_always',
203
+ database_safety: 'default_safe; $MAD-SKS live-full-access profile is centralized in src/core/permission-gates.mjs and keeps only catastrophic DB safeguards',
204
204
  gx_renderer: 'deterministic_svg_html'
205
205
  };
206
206
  await writeJsonAtomic(manifestPath, manifest);
@@ -331,7 +331,11 @@ export async function initProject(root, opts = {}) {
331
331
  required_before_final: true,
332
332
  verify_goal_evidence_tests_gaps: true
333
333
  },
334
- database_safety: DEFAULT_DB_SAFETY_POLICY,
334
+ database_safety: {
335
+ ...DEFAULT_DB_SAFETY_POLICY,
336
+ mad_sks_live_full_access: true,
337
+ mad_sks_gate_module: 'src/core/permission-gates.mjs'
338
+ },
335
339
  performance: {
336
340
  max_parallel_sessions: 2,
337
341
  process_tail_bytes: 262144,
@@ -511,7 +515,7 @@ function managedCodexConfigBlocks() {
511
515
  { table: 'profiles.sks-mad-high', text: profileConfigBlock('sks-mad-high', 'high', { approval: 'never', sandbox: 'danger-full-access', approvalsReviewer: 'auto_review' }) },
512
516
  {
513
517
  table: 'auto_review',
514
- text: '[auto_review]\npolicy = "Deny destructive database operations, credential exfiltration, persistent security weakening, broad file deletion, writes outside the workspace, and unrequested fallback implementation code unless explicitly authorized by the user or sealed decision contract."'
518
+ text: '[auto_review]\npolicy = "In MAD launches, allow live-server work, normal DB writes, Supabase MCP DB writes, direct execute SQL, schema cleanup, and migration application for the active invocation. Deny only catastrophic database wipes, all-row value deletion/update, dangerous project or branch management, credential exfiltration, persistent security weakening, broad unrelated file deletion, and unrequested fallback implementation code."'
515
519
  },
516
520
  { table: 'profiles.sks-default', text: profileConfigBlock('sks-default', 'high') }
517
521
  ];
@@ -731,7 +735,7 @@ export async function installSkills(root) {
731
735
  'answer': `---\nname: answer\ndescription: Answer-only research route for ordinary questions that should not start implementation.\n---\n\nUse for explanations, comparisons, status, facts, source-backed research, or docs guidance. Use repo/TriWiki first for project-local facts; hydrate low-trust claims from source. Browse or use Context7 for current external package/API/framework/MCP docs. End with a concise answer summary plus Honest Mode; do not create missions, subagents, or file edits.\n`,
732
736
  'sks': `---\nname: sks\ndescription: General Sneakoscope Codex command route for $SKS or $sks usage, setup, status, and workflow help.\n---\n\nUse local SKS commands: bootstrap, deps, commands, quickstart, codex-app, context7, guard, conflicts, reasoning, wiki, pipeline status, pipeline plan, skill-dream. Promote code-changing work to Team unless Answer/DFix/Help/Wiki/safety route fits. Surface route/guard/scope, use TriWiki, do not edit installed harness files outside this engine repo, and require human-approved conflict cleanup. ${skillDreamPolicyText()}\n`,
733
737
  'wiki': `---\nname: wiki\ndescription: Dollar-command route for $Wiki TriWiki refresh, pack, validate, and prune commands.\n---\n\nUse for $Wiki or Korean wiki-refresh requests. Refresh/update/갱신: run sks wiki refresh, then validate .sneakoscope/wiki/context-pack.json. Pack: run sks wiki pack, then validate. Prune/clean/정리: use sks wiki refresh --prune, or sks wiki prune --dry-run for inspection. Report claims, anchors, trust, attention.use_first/hydrate_first, validation, and blockers. Do not start ambiguity-gated implementation, subagents, or unrelated work.\n`,
734
- 'team': `---\nname: team\ndescription: SKS Team orchestration for $Team/code work; $From-Chat-IMG is the explicit chat-image alias.\n---\n\nUse for $Team/code work. Ambiguity gate first, but score goal, constraints, success criteria, and codebase context before asking; ask only the lowest-clarity scope/safety/behavior/acceptance question(s), otherwise auto-seal inferred answers. Read pipeline-plan.json or run sks pipeline plan to see the runtime lane, kept/skipped stages, and verification before implementation. Write team-roster.json; team-gate.json needs team_roster_confirmed=true. executor:N means N scouts, N debate voices, then fresh N executors. After consensus, compile team-graph.json, team-runtime-tasks.json, team-decomposition-report.json, and team-inbox/ so worker handoff uses concrete runtime task ids with role/path/domain/lane hints. Refresh/validate TriWiki before debate, implementation, review, and final; consume attention.use_first and hydrate attention.hydrate_first before risky decisions. ${outcomeRubricPolicyText()} ${speedLanePolicyText()} ${skillDreamPolicyText()} Log events and use sks team message for bounded inter-agent communication in transcript/lane panes. Color-coded tmux lanes distinguish overview/scout/planning/execution/review/safety sessions. End with cleanup-tmux or a cleanup event so follow panes show cleanup and stop; pass team-session-cleanup.json, then reflection and Honest Mode. Parent integrates/verifies.\n\n${chatCaptureIntakeText()}\n`,
738
+ 'team': `---\nname: team\ndescription: SKS Team orchestration for $Team/code work; $From-Chat-IMG is the explicit chat-image alias.\n---\n\nUse for $Team/code work. Ambiguity gate first, but score goal, constraints, success criteria, and codebase context before asking; ask only the lowest-clarity scope/safety/behavior/acceptance question(s), otherwise auto-seal inferred answers. Read pipeline-plan.json or run sks pipeline plan to see the runtime lane, kept/skipped stages, and verification before implementation. Write team-roster.json; team-gate.json needs team_roster_confirmed=true. executor:N means N scouts, N debate voices, then fresh N executors. After consensus, compile team-graph.json, team-runtime-tasks.json, team-decomposition-report.json, and team-inbox/ so worker handoff uses concrete runtime task ids with role/path/domain/lane hints. Refresh/validate TriWiki before debate, implementation, review, and final; consume attention.use_first and hydrate attention.hydrate_first before risky decisions. ${outcomeRubricPolicyText()} ${speedLanePolicyText()} ${skillDreamPolicyText()} Log events and use sks team message for bounded inter-agent communication in transcript/lane panes. Color-coded tmux lanes distinguish overview/scout/planning/execution/review/safety sessions in one tmux window using split panes when tmux is available. $Team/$team plus sks --mad uses the MAD-SKS permission gate module: live server work, normal DB writes, Supabase MCP writes, direct SQL, schema cleanup, and needed migrations are open for the active invocation; only catastrophic DB wipe/all-row/project-management guards remain. End with cleanup-tmux or a cleanup event so follow panes show cleanup and stop; pass team-session-cleanup.json, then reflection and Honest Mode. Parent integrates/verifies.\n\n${chatCaptureIntakeText()}\n`,
735
739
  'from-chat-img': `---\nname: from-chat-img\ndescription: Explicit $From-Chat-IMG Team alias for chat screenshot plus attachment analysis.\n---\n\nUse only for From-Chat-IMG/$From-Chat-IMG. It enters the normal Team pipeline. Treat uploads as chat screenshot plus originals. Use Codex Computer Use visual inspection when available, list requirements first, match regions to attachments with confidence, write ${FROM_CHAT_IMG_COVERAGE_ARTIFACT}, ${FROM_CHAT_IMG_CHECKLIST_ARTIFACT}, ${FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT}, and ${FROM_CHAT_IMG_QA_LOOP_ARTIFACT}, then continue Team gates, review, reflection, and Honest Mode. ${CODEX_COMPUTER_USE_ONLY_POLICY} The ledger must account for every visible customer request, screenshot image region, and separate attachment; ${FROM_CHAT_IMG_CHECKLIST_ARTIFACT} must have a checked item for each request, image-region/attachment match, work item, scoped QA-LOOP, and verification step; ${FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT} stores temporary TriWiki-backed session context with expires_after_sessions=${FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS}. ${FROM_CHAT_IMG_QA_LOOP_ARTIFACT} must prove QA-LOOP ran over the exact customer-request work-order range after implementation, with every work item covered, post-fix verification complete, and zero unresolved findings. team-gate.json cannot pass From-Chat-IMG completion until unresolved_items is empty, every checklist box is checked, and scoped_qa_loop_completed=true.\n`,
736
740
  'qa-loop': `---\nname: qa-loop\ndescription: $QA-LOOP dogfoods UI/API as human proxy with safety gates, Codex Computer Use-only UI evidence, safe fixes, rechecks, and a QA report.\n---\n\nUse only $QA-LOOP. Ask scope, target, mutation, login. Credentials are runtime-only; never save secrets. UI-level E2E needs Codex Computer Use evidence or must be marked unverified; Chrome MCP, Browser Use, Playwright, Selenium, Puppeteer, and other browser automation do not satisfy UI/browser verification. Deployed targets are read-only; destructive removal is forbidden. After answer/run, dogfood real flows, apply safe contract-allowed code/test/docs fixes, recheck, and do not pass qa-gate.json with unresolved findings or without post_fix_verification_complete. Finish qa-ledger, date/version report, gate, completion summary, and Honest Mode.\n`,
737
741
  'ppt': `---\nname: ppt\ndescription: $PPT information-first HTML/PDF presentation pipeline with STP, audience, pain-point, format, research, design-system, and verification questions.\n---\n\nUse only when the user invokes $PPT or asks to create a presentation, deck, slides, pitch deck, proposal deck, HTML presentation, or PDF presentation artifact. Before artifact work, seal presentation-specific ambiguity answers: delivery context, target audience profile including role/average age/job/industry/topic familiarity/decision power, STP strategy, decision context and objections, and 3+ pain-point to solution mappings with expected aha moments. Presentation design must be simple, restrained, and information-first: avoid over-designed decoration, ornamental gradients, nested cards, and effects that compete with the message. Design detail should be embedded through typography hierarchy, spacing, alignment, thin rules, source clarity, and subtle accents. ${pptPipelineAllowlistPolicyText()} Use design.md as the only design decision SSOT. If design.md is missing, use docs/Design-Sys-Prompt.md plus getdesign-reference and curated DESIGN.md examples from ${AWESOME_DESIGN_MD_REFERENCE.url} only as source inputs, then fuse them into route-local PPT style tokens with a recorded design_ssot instead of treating references as parallel authorities. If generated image assets or slide visual critique are needed, use imagegen/gpt-image-2 only when that asset/review need is explicitly sealed in the $PPT contract; prefer Codex App built-in image generation (${CODEX_APP_IMAGE_GENERATION_DOC_URL}) and use the OpenAI Image API with OPENAI_API_KEY when CLI-side required image assets can be generated. Use web or Context7 evidence only when external facts/libraries/current docs are required by the PPT contract, record verified claims in ppt-fact-ledger.json, record generated image asset plans/results/blockers in ppt-image-asset-ledger.json, then create the PDF plus editable source HTML under source-html/, keep independent strategy/render/file-write phases parallel where inputs allow, record ppt-parallel-report.json, run the bounded ppt-review-policy/ppt-review-ledger/ppt-iteration-report loop, and verify readability, overlap, format fit, source coverage, export state, unsupported-claim status, image-asset completion, review-loop termination, and temporary build files cleanup. Finish with reflection and Honest Mode; do not skip STP/audience questions for presentation artifacts.\n`,
@@ -742,7 +746,7 @@ export async function installSkills(root) {
742
746
  '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. Keep the loop short: frame outcome, compare a few mechanisms, falsify, keep the smallest useful probe, and avoid adding background process unless it reduces net route weight. Do not use for ordinary code edits.\n`,
743
747
  '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`,
744
748
  '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`,
745
- '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, must be deactivated when the task ends, and opens Supabase MCP column/schema cleanup, direct execute SQL, and normal DB write permissions. Keep only catastrophic database-wipe safeguards: whole database/table removal, all-row delete/update, reset, and dangerous project/branch management remain blocked. Do not carry MAD-SKS permission into later prompts or routes.\n`,
749
+ 'mad-sks': `---\nname: mad-sks\ndescription: Explicit high-risk authorization modifier for $MAD-SKS scoped Supabase MCP DB permission widening.\n---\n\nUse only when the user explicitly invokes $MAD-SKS or top-level sks --mad. It can be combined with another route, such as $MAD-SKS $Team or $DB ... $MAD-SKS; in that case the other command remains the primary workflow and MAD-SKS is only the temporary permission grant. The widened permission applies only while the active mission gate is open, must be deactivated when the task ends, and opens live server work, Supabase MCP database writes, column/schema cleanup, direct execute SQL, migration application when required, and normal targeted DB writes. Keep only catastrophic safeguards: whole database/schema/table removal, truncate, all-row delete/update, reset, dangerous project/branch management, credential exfiltration, persistent security weakening, and unrequested fallback implementation remain blocked. Do not carry MAD-SKS permission into later prompts or routes. The permission profile is centralized in src/core/permission-gates.mjs so skill/hook/MCP-style gates share one decision function.\n`,
746
750
  '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`,
747
751
  '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`,
748
752
  '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. Score ambiguity first using goal, constraints, success criteria, and codebase context; ask only the lowest-clarity scope/safety/behavior/acceptance-changing questions within a small question budget, otherwise seal inferred answers. Materialize pipeline-plan.json for the runtime lane, kept/skipped stages, no-fallback invariant, and verification; inspect with sks pipeline plan, adding --proof-field when changed files are known. 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. ${outcomeRubricPolicyText()} ${speedLanePolicyText()} ${skillDreamPolicyText()}\n\n${chatCaptureIntakeText()}\n\nDesign: non-PPT UI/UX reads design.md; if missing use design-system-builder; use imagegen for image/logo/raster, and imagegen must prefer Codex App built-in image generation (${CODEX_APP_IMAGE_GENERATION_DOC_URL}) before API generation. For $PPT, ${pptPipelineAllowlistPolicyText()} ${getdesignReferencePolicyText()} 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`,
@@ -0,0 +1,99 @@
1
+ export const PERMISSION_GATE_SCHEMA_VERSION = 1;
2
+
3
+ export const MAD_SKS_PERMISSION_PROFILE = Object.freeze({
4
+ schema_version: PERMISSION_GATE_SCHEMA_VERSION,
5
+ id: 'mad_sks_live_full_access',
6
+ command: '$MAD-SKS',
7
+ intent: 'explicit_live_server_intervention',
8
+ scope: 'active_invocation_only',
9
+ authority_surface: 'skill_or_mcp_gate_function',
10
+ allowed: Object.freeze([
11
+ 'live_server_changes',
12
+ 'supabase_mcp_database_writes',
13
+ 'direct_execute_sql_writes',
14
+ 'schema_cleanup',
15
+ 'column_cleanup',
16
+ 'migration_apply_when_required',
17
+ 'normal_dml_with_targeted_scope'
18
+ ]),
19
+ blocked: Object.freeze([
20
+ 'drop_database',
21
+ 'drop_schema',
22
+ 'drop_table',
23
+ 'truncate_table',
24
+ 'delete_without_where',
25
+ 'update_without_where',
26
+ 'database_reset',
27
+ 'dangerous_project_or_branch_management',
28
+ 'credential_exfiltration',
29
+ 'persistent_security_weakening',
30
+ 'unrequested_fallback_implementation'
31
+ ]),
32
+ deactivation: 'mission_gate_passed_or_permissions_deactivated'
33
+ });
34
+
35
+ export function permissionGateSummary(profile = MAD_SKS_PERMISSION_PROFILE) {
36
+ return {
37
+ schema_version: PERMISSION_GATE_SCHEMA_VERSION,
38
+ id: profile.id,
39
+ scope: profile.scope,
40
+ authority_surface: profile.authority_surface,
41
+ allowed: [...profile.allowed],
42
+ blocked: [...profile.blocked],
43
+ deactivation: profile.deactivation
44
+ };
45
+ }
46
+
47
+ export function isMadSksRouteState(state = {}) {
48
+ return state.mad_sks_active === true
49
+ || String(state.mode || '').toUpperCase() === 'MADSKS'
50
+ || String(state.route_command || '').toUpperCase() === '$MAD-SKS'
51
+ || String(state.route || '').toUpperCase() === 'MADSKS'
52
+ || state.permission_profile?.id === MAD_SKS_PERMISSION_PROFILE.id;
53
+ }
54
+
55
+ export function madSksCatastrophicDbReasons(cls = {}) {
56
+ const reasons = new Set([
57
+ ...(cls.reasons || []),
58
+ ...(cls.sql?.reasons || []),
59
+ ...(cls.command?.reasons || [])
60
+ ]);
61
+ const blocked = [
62
+ 'drop_database',
63
+ 'drop_schema',
64
+ 'drop_table',
65
+ 'truncate',
66
+ 'delete_without_where',
67
+ 'update_without_where',
68
+ 'supabase_db_reset',
69
+ 'prisma_migrate_reset',
70
+ 'postgres_database_admin_command'
71
+ ].filter((reason) => reasons.has(reason));
72
+ if (cls.toolReasons?.includes?.('dangerous_supabase_management_tool')) blocked.push('dangerous_project_or_branch_management');
73
+ return [...new Set(blocked)];
74
+ }
75
+
76
+ export function evaluateMadSksPermissionGate({ classification, active = false } = {}) {
77
+ const cls = classification || { level: 'none', reasons: [] };
78
+ if (!active || !['write', 'destructive'].includes(cls.level)) return { matched: false, active: Boolean(active), profile: permissionGateSummary() };
79
+ const catastrophic = madSksCatastrophicDbReasons(cls);
80
+ if (catastrophic.length) {
81
+ return {
82
+ matched: true,
83
+ active: true,
84
+ allowed: false,
85
+ action: 'block',
86
+ reasons: ['mad_sks_catastrophic_db_operation_blocked'],
87
+ blocked_categories: catastrophic,
88
+ profile: permissionGateSummary()
89
+ };
90
+ }
91
+ return {
92
+ matched: true,
93
+ active: true,
94
+ allowed: true,
95
+ action: 'allow',
96
+ reasons: ['mad_sks_scoped_live_full_access_active'],
97
+ profile: permissionGateSummary()
98
+ };
99
+ }
@@ -15,6 +15,7 @@ import { recordSkillDreamEvent, skillDreamPolicyText, writeSkillForgeReport } fr
15
15
  import { writeResearchPlan } from './research.mjs';
16
16
  import { PPT_REQUIRED_GATE_FIELDS } from './ppt.mjs';
17
17
  import { SPEED_LANE_POLICY } from './proof-field.mjs';
18
+ import { permissionGateSummary } from './permission-gates.mjs';
18
19
  import { CODEX_APP_IMAGE_GENERATION_DOC_URL, CODEX_COMPUTER_USE_EVIDENCE_SOURCE, CODEX_COMPUTER_USE_ONLY_POLICY, 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, evidenceMentionsForbiddenBrowserAutomation, getdesignReferencePolicyText, hasFromChatImgSignal, hasMadSksSignal, noUnrequestedFallbackCodePolicyText, outcomeRubricPolicyText, pptPipelineAllowlistPolicyText, reflectionRequiredForRoute, reasoningInstruction, routeNeedsContext7, routePrompt, routeReasoning, routeRequiresSubagents, speedLanePolicyText, stripDollarCommand, stripMadSksSignal, subagentExecutionPolicyText, stackCurrentDocsPolicyText, triwikiContextTracking, triwikiContextTrackingText, triwikiStagePolicyText } from './routes.mjs';
19
20
  import { TEAM_DECOMPOSITION_ARTIFACT, TEAM_GRAPH_ARTIFACT, TEAM_INBOX_DIR, TEAM_RUNTIME_TASKS_ARTIFACT, teamRuntimePlanMetadata, teamRuntimeRequiredArtifacts, validateTeamRuntimeArtifacts, writeTeamRuntimeArtifacts } from './team-dag.mjs';
20
21
  import { formatRoleCounts, initTeamLive, parseTeamSpecText } from './team-live.mjs';
@@ -31,6 +32,7 @@ const COMPLIANCE_LOOP_GUARD_ARTIFACT = 'compliance-loop-guard.json';
31
32
  const HARD_BLOCKER_ARTIFACT = 'hard-blocker.json';
32
33
  const DEFAULT_COMPLIANCE_LOOP_LIMIT = 3;
33
34
  const CLARIFICATION_BYPASS_ROUTES = new Set(['Answer', 'DFix', 'Help', 'Wiki', 'ComputerUse', 'Goal']);
35
+ const QUESTION_GATE_ROUTES = new Set(['QALoop', 'PPT']);
34
36
  const LIGHTWEIGHT_ROUTES = new Set(['Answer', 'DFix', 'Help', 'Wiki']);
35
37
  const FULL_ROUTE_STAGES = Object.freeze([
36
38
  'route_classification',
@@ -337,8 +339,8 @@ export async function prepareRoute(root, prompt, state = {}) {
337
339
  const required = routeNeedsContext7(route, prompt);
338
340
  const reasoning = routeReasoning(route, prompt);
339
341
  const subagentsRequired = routeRequiresSubagents(route, prompt);
340
- if (route.id !== 'Help') return withSkillDreamContext(await prepareClarificationGate(root, route, task, required, { madSksAuthorization }), dreamContext);
341
- if (route.id === 'Team') return withSkillDreamContext(await prepareTeam(root, route, task, required), dreamContext);
342
+ if (QUESTION_GATE_ROUTES.has(route.id) || route.id === 'MadSKS') return withSkillDreamContext(await prepareClarificationGate(root, route, task, required, { madSksAuthorization }), dreamContext);
343
+ if (route.id === 'Team') return withSkillDreamContext(await prepareTeam(root, route, task, required, { madSksAuthorization }), dreamContext);
342
344
  if (route.id === 'Research') return withSkillDreamContext(await prepareResearch(root, route, task, required), dreamContext);
343
345
  if (route.id === 'AutoResearch') return withSkillDreamContext(await prepareAutoResearch(root, route, task, required), dreamContext);
344
346
  if (route.id === 'DB') return withSkillDreamContext(await prepareDb(root, route, task, required), dreamContext);
@@ -434,7 +436,10 @@ export async function activeRouteContext(root, state) {
434
436
  if (state.honest_loop_required || /HONEST_LOOPBACK_AFTER_CLARIFICATION/.test(String(state.phase || ''))) {
435
437
  return `SKS Honest Mode found unresolved gaps for ${state.route_command || state.route || state.mode}. Do not ask ambiguity questions again. Continue from the sealed decision-contract.json, inspect .sneakoscope/missions/${state.mission_id}/honest-loopback.json, fix gaps, rerun verification, refresh/validate TriWiki, then retry final Honest Mode.${reasoningNote}${planNote}`;
436
438
  }
437
- if (state.clarification_required && String(state.phase || '').includes('CLARIFICATION_AWAITING_ANSWERS')) return clarificationAwaitingAnswersContext(root, state);
439
+ if (state.clarification_required && String(state.phase || '').includes('CLARIFICATION_AWAITING_ANSWERS')) {
440
+ if (['QALoop', 'PPT'].includes(String(state.route || '')) || ['QALOOP', 'PPT'].includes(String(state.mode || ''))) return clarificationAwaitingAnswersContext(root, state);
441
+ return `Previous ${state.route_command || state.route || state.mode || 'SKS'} clarification state is non-blocking. Do not reprint old question sheets; prepare the current prompt normally and replace stale route state when needed.`;
442
+ }
438
443
  if (state.clarification_passed && String(state.phase || '').includes('CLARIFICATION_CONTRACT_SEALED')) {
439
444
  return `Mandatory ambiguity-removal gate passed for ${state.route_command || state.route || state.mode}. Use the sealed decision-contract.json and ${PIPELINE_PLAN_ARTIFACT} before executing the route. Before the next route phase, read relevant TriWiki context, hydrate low-trust claims from source, and refresh/validate TriWiki again after new findings or artifact changes. Next atomic action: continue the original route lifecycle with the clarified goal, constraints, non-goals, risk boundary, and test scope.${planNote}`;
440
445
  }
@@ -589,7 +594,7 @@ function applyMadSksAuthorizationToSchema(schema = {}) {
589
594
  schema.inference_notes = {
590
595
  ...(schema.inference_notes || {}),
591
596
  MAD_SKS_MODE: 'explicit dollar command modifier is the permission boundary',
592
- DESTRUCTIVE_DB_OPERATIONS_ALLOWED: 'MAD-SKS opens Supabase MCP DB cleanup while blocking only catastrophic database wipe operations'
597
+ DESTRUCTIVE_DB_OPERATIONS_ALLOWED: 'MAD-SKS opens live-server DB changes, Supabase MCP cleanup, direct SQL, and needed migrations while blocking only catastrophic database wipe operations'
593
598
  };
594
599
  schema.slots = (schema.slots || []).filter((slot) => !/^(DB_|DATABASE_|DESTRUCTIVE_DB_|SUPABASE_MCP_POLICY$)/.test(slot.id));
595
600
  return schema;
@@ -604,7 +609,10 @@ async function materializeAutoSealedMadSks(dir, id, route, routeContext = {}, co
604
609
  supabase_mcp_schema_cleanup_allowed: true,
605
610
  direct_execute_sql_allowed: true,
606
611
  normal_db_writes_allowed: true,
612
+ live_server_writes_allowed: true,
613
+ migration_apply_allowed: true,
607
614
  catastrophic_safety_guard_active: true,
615
+ permission_profile: permissionGateSummary(),
608
616
  contract_hash: contract.sealed_hash || null
609
617
  });
610
618
  await appendJsonl(path.join(dir, 'events.jsonl'), {
@@ -624,6 +632,8 @@ async function materializeAutoSealedMadSks(dir, id, route, routeContext = {}, co
624
632
  supabase_mcp_schema_cleanup_allowed: true,
625
633
  direct_execute_sql_allowed: true,
626
634
  normal_db_writes_allowed: true,
635
+ live_server_writes_allowed: true,
636
+ migration_apply_allowed: true,
627
637
  catastrophic_safety_guard_active: true
628
638
  }
629
639
  };
@@ -642,7 +652,10 @@ async function materializeMadSksAuthorization(dir, id, route, routeContext = {},
642
652
  supabase_mcp_schema_cleanup_allowed: true,
643
653
  direct_execute_sql_allowed: true,
644
654
  normal_db_writes_allowed: true,
655
+ live_server_writes_allowed: true,
656
+ migration_apply_allowed: true,
645
657
  catastrophic_safety_guard_active: true,
658
+ permission_profile: permissionGateSummary(),
646
659
  contract_hash: contract.sealed_hash || null
647
660
  });
648
661
  await appendJsonl(path.join(dir, 'events.jsonl'), {
@@ -659,6 +672,8 @@ async function materializeMadSksAuthorization(dir, id, route, routeContext = {},
659
672
  supabase_mcp_schema_cleanup_allowed: true,
660
673
  direct_execute_sql_allowed: true,
661
674
  normal_db_writes_allowed: true,
675
+ live_server_writes_allowed: true,
676
+ migration_apply_allowed: true,
662
677
  catastrophic_safety_guard_active: true
663
678
  };
664
679
  }
@@ -748,7 +763,7 @@ async function materializeAutoSealedTeam(root, id, dir, route, task, contractHas
748
763
  };
749
764
  }
750
765
 
751
- async function prepareTeam(root, route, task, required) {
766
+ async function prepareTeam(root, route, task, required, opts = {}) {
752
767
  const spec = parseTeamSpecText(task);
753
768
  const cleanTask = spec.prompt || task;
754
769
  const fromChatImgRequired = hasFromChatImgSignal(cleanTask);
@@ -803,8 +818,11 @@ async function prepareTeam(root, route, task, required) {
803
818
  await writeMistakeMemoryReport(dir, { mission_id: id, route: 'team', task: cleanTask }).catch(() => null);
804
819
  await writeCodeStructureReport(root, dir, { missionId: id, exception: 'Team prepare records split-review risk; extraction happens only when the mission scope includes the touched file.' }).catch(() => null);
805
820
  await writeJsonAtomic(path.join(dir, 'team-gate.json'), { passed: false, team_roster_confirmed: true, analysis_artifact: false, triwiki_refreshed: false, triwiki_validated: false, consensus_artifact: false, ...runtime.gate_fields, implementation_team_fresh: false, review_artifact: false, integration_evidence: false, session_cleanup: false, context7_evidence: false, ...(fromChatImgRequired ? { from_chat_img_required: true, from_chat_img_request_coverage: false } : {}) });
821
+ const madSksState = opts.madSksAuthorization
822
+ ? await materializeMadSksAuthorization(dir, id, route, { mad_sks_authorization: true }, {})
823
+ : {};
806
824
  const pipelinePlan = await writePipelinePlan(dir, { missionId: id, route, task: cleanTask, required, ambiguity: { required: false, status: 'direct_team_cli' } });
807
- await setCurrent(root, routeState(id, route, 'TEAM_PARALLEL_ANALYSIS_SCOUTING', required, { prompt: cleanTask, agent_sessions: agentSessions, role_counts: roleCounts, team_roster_confirmed: true, team_graph_ready: runtime.ok, context_tracking: 'triwiki', from_chat_img_required: fromChatImgRequired, pipeline_plan_ready: validatePipelinePlan(pipelinePlan).ok, pipeline_plan_path: PIPELINE_PLAN_ARTIFACT }));
825
+ await setCurrent(root, routeState(id, route, 'TEAM_PARALLEL_ANALYSIS_SCOUTING', required, { prompt: cleanTask, implementation_allowed: true, ambiguity_gate_required: false, ambiguity_gate_passed: true, agent_sessions: agentSessions, role_counts: roleCounts, team_roster_confirmed: true, team_plan_ready: true, team_graph_ready: runtime.ok, context_tracking: 'triwiki', from_chat_img_required: fromChatImgRequired, pipeline_plan_ready: validatePipelinePlan(pipelinePlan).ok, pipeline_plan_path: PIPELINE_PLAN_ARTIFACT, ...madSksState }));
808
826
  return routeContext(route, id, cleanTask, required, `Run scouts, refresh/validate TriWiki, debate, close debate agents, form a fresh ${roster.bundle_size}-person executor team, then close/clean Team sessions and write ${TEAM_SESSION_CLEANUP_ARTIFACT} before reflection.`);
809
827
  }
810
828
 
@@ -378,7 +378,7 @@ export const ROUTES = [
378
378
  command: '$MAD-SKS',
379
379
  mode: 'MADSKS',
380
380
  route: 'explicit scoped database authorization modifier',
381
- description: 'Explicit high-risk authorization modifier that can be combined with other $ commands to temporarily open Supabase MCP column/schema cleanup, direct execute SQL, and normal DB write permissions for the active invocation, while blocking only catastrophic database-wipe operations.',
381
+ description: 'Explicit high-risk authorization modifier that can be combined with other $ commands to temporarily open live server work, Supabase MCP DB writes, direct execute SQL, schema cleanup, migration application, and normal targeted DB writes for the active invocation, while blocking only catastrophic database-wipe/all-row/project-management operations.',
382
382
  requiredSkills: ['mad-sks', 'db-safety-guard', 'pipeline-runner', 'context7-docs', REFLECTION_SKILL_NAME, 'honest-mode'],
383
383
  lifecycle: ['explicit_invocation', 'auto_sealed_permission_scope', 'scoped_db_cleanup_override', 'catastrophic_db_guard', 'permission_deactivation', 'post_route_reflection', 'honest_mode'],
384
384
  context7Policy: 'required',
@@ -568,6 +568,7 @@ export function routePrompt(prompt) {
568
568
  }
569
569
  const route = routeByDollarCommand(command) || null;
570
570
  if (route?.id === 'SKS' && looksLikeTeamDefaultWork(stripDollarCommand(text))) return routeById('Team');
571
+ if (route?.id === 'Team') return route;
571
572
  return route;
572
573
  }
573
574
  if (hasFromChatImgSignal(text)) return routeById('Team');
@@ -254,6 +254,18 @@ function colorizedLaneBannerCommand(lines = [], color = '') {
254
254
  return `printf '\\033[1;${code}m%s\\033[0m\\n' ${shellEscape(text)}`;
255
255
  }
256
256
 
257
+ function compactTeamPaneBanner({ missionId, agentId, phase, style, overview = false } = {}) {
258
+ const role = overview ? 'overview' : `${style.label} (${style.color_name})`;
259
+ return [
260
+ `SKS Team ${missionId}`,
261
+ overview ? 'Overview: live orchestration' : `Agent: ${agentId}`,
262
+ `Lane: ${role}${phase ? ` | Phase: ${phase}` : ''}`,
263
+ overview ? 'Follow: team watch' : `Follow: team lane --agent ${agentId}`,
264
+ `Cleanup: sks team cleanup-tmux ${missionId}`,
265
+ ''
266
+ ];
267
+ }
268
+
257
269
  export const TMUX_TEAM_LANE_STYLES = Object.freeze({
258
270
  overview: Object.freeze({ role: 'overview', label: 'overview', color_name: 'Blue', color: 'blue', icon: 'layout-dashboard' }),
259
271
  scout: Object.freeze({ role: 'scout', label: 'scout', color_name: 'Cyan', color: 'cyan', icon: 'search' }),
@@ -285,7 +297,7 @@ export function teamAgentCommand(root, missionId, agentId, phase) {
285
297
  return [
286
298
  terminalTitleCommand(title),
287
299
  'clear',
288
- colorizedLaneBannerCommand([...SKS_TMUX_LOGO.split('\n'), '', `Team mission: ${missionId}`, `Agent: ${agentId}`, `Lane: ${style.label} (${style.color_name})`, `Phase: ${phase}`, 'Messages: sks team message ... --to ' + agentId, 'Cleanup: sks team cleanup-tmux ' + missionId], style.color),
300
+ colorizedLaneBannerCommand(compactTeamPaneBanner({ missionId, agentId, phase, style }), style.color),
289
301
  `cd ${shellEscape(root)}`,
290
302
  `node ${shellEscape(path.join(packageRoot(), 'bin', 'sks.mjs'))} team lane ${shellEscape(missionId)} --agent ${shellEscape(agentId)} --phase ${shellEscape(phase)} --follow --lines 12`
291
303
  ].join('; ');
@@ -297,7 +309,7 @@ export function teamOverviewCommand(root, missionId) {
297
309
  return [
298
310
  terminalTitleCommand(title),
299
311
  'clear',
300
- colorizedLaneBannerCommand([...SKS_TMUX_LOGO.split('\n'), '', `Team mission: ${missionId}`, 'View: live orchestration overview', `Lane: ${style.label} (${style.color_name})`, 'Messages: sks team message ... --to <agent|all>', 'Cleanup: sks team cleanup-tmux ' + missionId], style.color),
312
+ colorizedLaneBannerCommand(compactTeamPaneBanner({ missionId, agentId: 'mission_overview', style, overview: true }), style.color),
301
313
  `cd ${shellEscape(root)}`,
302
314
  `node ${shellEscape(path.join(packageRoot(), 'bin', 'sks.mjs'))} team watch ${shellEscape(missionId)} --follow --lines 18`
303
315
  ].join('; ');
@@ -495,6 +507,14 @@ export async function launchTmuxTeamView({ root, missionId, plan = {}, promptFil
495
507
  }));
496
508
  const overview = { agent: 'mission_overview', role: 'overview', command: teamOverviewCommand(launch.root, missionId), style: teamLaneStyle('mission_overview'), title: teamLaneTitle('mission_overview') };
497
509
  const lanes = [overview, ...commands.map((entry) => ({ ...entry, role: entry.style.role }))];
510
+ const splitUi = {
511
+ mode: 'single_window_split_panes',
512
+ window: 'sks',
513
+ layout: 'tiled',
514
+ live_updates: true,
515
+ panes_show: ['overview', 'scout', 'planning', 'execution', 'review', 'safety'],
516
+ user_attach_command: launch.attach_command
517
+ };
498
518
  const result = {
499
519
  ready: launch.ready,
500
520
  tmux: launch.tmux,
@@ -503,6 +523,7 @@ export async function launchTmuxTeamView({ root, missionId, plan = {}, promptFil
503
523
  overview,
504
524
  agents: commands,
505
525
  lanes,
526
+ split_ui: splitUi,
506
527
  cleanup_policy: 'mark-complete; tmux panes remain user controlled',
507
528
  blockers: launch.blockers,
508
529
  attach_command: launch.attach_command
@@ -520,6 +541,7 @@ export async function launchTmuxTeamView({ root, missionId, plan = {}, promptFil
520
541
  mission_id: missionId,
521
542
  session: result.session,
522
543
  attach_command: created.attach_command || launch.attach_command,
544
+ split_ui: splitUi,
523
545
  cleanup_policy: result.cleanup_policy,
524
546
  panes: created.panes || [],
525
547
  lanes: lanes.map((entry) => ({