sneakoscope 0.7.46 → 0.7.49

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
 
@@ -174,6 +174,7 @@ If you use [codex-lb](https://github.com/Soju06/codex-lb), start it first, creat
174
174
 
175
175
  ```sh
176
176
  sks codex-lb setup --host https://your-codex-lb.example.com --api-key "sk-clb-..."
177
+ sks codex-lb repair
177
178
  sks
178
179
  ```
179
180
 
@@ -183,7 +184,11 @@ Bare `sks` asks this before opening Codex when codex-lb is not configured:
183
184
  Authenticate and route Codex through codex-lb? [y/N]
184
185
  ```
185
186
 
186
- Answering `y` asks for the hosted domain and API key, writes `~/.codex/config.toml`, stores the key in `~/.codex/sks-codex-lb.env` with mode `0600`, syncs Codex CLI API-key auth through `codex login --with-api-key`, and sources that env file before launching Codex in tmux. When codex-lb is configured from this prompt, SKS opens a fresh tmux session for that launch so the new key is loaded by the Codex process immediately. SKS keeps Codex App Fast mode visible and defaulted by writing `service_tier = "fast"`, `[features].fast_mode = true`, and the `sks-fast-high` profile while removing only legacy top-level `model` and `model_reasoning_effort` locks; route-specific reasoning stays in named profiles or explicit tmux launch args. The generated provider config follows the codex-lb README's Codex CLI API-key setup:
187
+ Answering `y` asks for the hosted domain and API key, writes `~/.codex/config.toml`, stores the key in `~/.codex/sks-codex-lb.env` with mode `0600`, syncs Codex CLI API-key auth through `codex login --with-api-key`, and sources that env file before launching Codex in tmux. When codex-lb is configured from this prompt, SKS opens a fresh tmux session for that launch so the new key is loaded by the Codex process immediately. SKS keeps Codex App Fast mode visible and defaulted by writing `service_tier = "fast"`, `[features].fast_mode = true`, and the `sks-fast-high` profile while removing only legacy top-level `model` and `model_reasoning_effort` locks; route-specific reasoning stays in named profiles or explicit tmux launch args.
188
+
189
+ If Codex CLI auth drifts after a tmux/MAD launch, run `sks codex-lb repair` or `sks auth repair`. This reuses the stored `~/.codex/sks-codex-lb.env` key and re-syncs Codex CLI API-key auth without asking for the key again. To replace the key or host, run `sks codex-lb reconfigure --host <domain> --api-key <key>`.
190
+
191
+ The generated provider config follows the codex-lb README's Codex CLI API-key setup:
187
192
 
188
193
  ```toml
189
194
  model_provider = "codex-lb"
@@ -205,7 +210,7 @@ sks --mad
205
210
  sks --mad --yes
206
211
  ```
207
212
 
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.
213
+ This syncs existing codex-lb/Codex CLI auth before launch, 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. If codex-lb is configured and no explicit `--workspace`/`--session` was passed, SKS opens a fresh tmux session so the repaired key is loaded by the Codex process immediately. 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 unless auth repair requires a fresh codex-lb session.
209
214
 
210
215
  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
216
 
@@ -230,9 +235,9 @@ sks team dashboard latest
230
235
  sks team log latest
231
236
  ```
232
237
 
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.
238
+ 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
239
 
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`.
240
+ 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
241
 
237
242
  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
243
 
@@ -448,6 +453,7 @@ sks dollar-commands
448
453
 
449
454
  ```sh
450
455
  sks codex-lb setup --host <domain> --api-key <key>
456
+ sks codex-lb repair
451
457
  sks
452
458
  ```
453
459
 
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.49",
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",
@@ -148,7 +148,7 @@ export async function configureCodexLb(opts = {}) {
148
148
  await writeTextAtomic(envPath, `export CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`);
149
149
  await fsp.chmod(envPath, 0o600).catch(() => {});
150
150
  process.env.CODEX_LB_API_KEY = apiKey;
151
- const codexLogin = await syncCodexApiKeyLogin(apiKey, { home });
151
+ const codexLogin = await syncCodexApiKeyLogin(apiKey, { home, force: true });
152
152
  return { ok: true, status: 'configured', config_path: configPath, env_path: envPath, base_url: baseUrl, env_key: 'CODEX_LB_API_KEY', codex_login: codexLogin };
153
153
  }
154
154
 
@@ -174,6 +174,29 @@ export async function codexLbStatus(opts = {}) {
174
174
  };
175
175
  }
176
176
 
177
+ export async function repairCodexLbAuth(opts = {}) {
178
+ const status = await codexLbStatus(opts);
179
+ if (!status.ok) {
180
+ return {
181
+ ok: false,
182
+ status: 'not_configured',
183
+ config_path: status.config_path,
184
+ env_path: status.env_path,
185
+ codex_lb: status
186
+ };
187
+ }
188
+ const codexLogin = await ensureCodexLbLoginFromEnv(status, opts);
189
+ return {
190
+ ok: Boolean(codexLogin.ok),
191
+ status: codexLogin.ok ? 'repaired' : codexLogin.status,
192
+ config_path: status.config_path,
193
+ env_path: status.env_path,
194
+ base_url: status.base_url,
195
+ codex_lb: status,
196
+ codex_login: codexLogin
197
+ };
198
+ }
199
+
177
200
  export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
178
201
  if (args.includes('--json') || args.includes('--skip-codex-lb') || process.env.SKS_SKIP_CODEX_LB_PROMPT === '1') return { status: 'skipped' };
179
202
  if (!canAskYesNo()) return { status: 'non_interactive' };
@@ -198,7 +221,7 @@ async function ensureCodexLbLoginFromEnv(status = {}, opts = {}) {
198
221
  const envPath = opts.envPath || status.env_path || codexLbEnvPath(home);
199
222
  const apiKey = parseCodexLbEnvKey(await readText(envPath, ''));
200
223
  if (!apiKey) return { ok: false, status: 'missing_env_key' };
201
- return syncCodexApiKeyLogin(apiKey, { ...opts, home });
224
+ return syncCodexApiKeyLogin(apiKey, { ...opts, home, force: true });
202
225
  }
203
226
 
204
227
  async function syncCodexApiKeyLogin(apiKey, opts = {}) {
@@ -208,8 +231,10 @@ async function syncCodexApiKeyLogin(apiKey, opts = {}) {
208
231
  if (!codexBin) return { ok: false, status: 'codex_missing' };
209
232
  await ensureDir(codexHome);
210
233
  const env = { HOME: home, CODEX_HOME: codexHome, CODEX_LB_API_KEY: apiKey };
211
- const current = await runProcess(codexBin, ['login', 'status'], { env, timeoutMs: 10000, maxOutputBytes: 8192 });
212
- if (current.code === 0 && !/not logged in/i.test(`${current.stdout}\n${current.stderr}`)) return { ok: true, status: 'present' };
234
+ if (!opts.force) {
235
+ const current = await runProcess(codexBin, ['login', 'status'], { env, timeoutMs: 10000, maxOutputBytes: 8192 });
236
+ if (current.code === 0 && !/not logged in/i.test(`${current.stdout}\n${current.stderr}`)) return { ok: true, status: 'present' };
237
+ }
213
238
  const login = await runProcess(codexBin, ['login', '--with-api-key'], { input: `${apiKey}\n`, env, timeoutMs: 15000, maxOutputBytes: 8192 });
214
239
  if (login.code === 0) return { ok: true, status: 'synced' };
215
240
  return { ok: false, status: 'login_failed', error: (login.stderr || login.stdout || 'codex login failed').trim() };
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, checkDbOperation, handleMadSksUserConfirmation, loadDbSafetyPolicy, scanDbSafety } from '../core/db-safety.mjs';
17
+ import { classifySql, classifyCommand, classifyToolPayload, checkDbOperation, handleMadSksUserConfirmation, loadDbSafetyPolicy, scanDbSafety } 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';
@@ -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';
@@ -67,7 +68,7 @@ import { OPENCLAW_SKILL_NAME, installOpenClawSkill } from '../core/openclaw.mjs'
67
68
  import { buildTmuxLaunchPlan, buildTmuxOpenArgs, codexLaunchCommand, createTmuxSession, isTmuxShellSession, runTmuxLaunchPlanSyntaxCheck, shouldAutoAttachTmux, tmuxReadiness, tmuxStatusKind, defaultTmuxSessionName, formatTmuxBanner, launchTmuxTeamView, launchTmuxUi, platformTmuxInstallHint, runTmuxStatus, sanitizeTmuxSessionName, teamLaneStyle } from '../core/tmux-ui.mjs';
68
69
  import { autoReviewProfileName, autoReviewStatus, autoReviewSummary, enableAutoReview, disableAutoReview, enableMadHighProfile, madHighProfileName } from '../core/auto-review.mjs';
69
70
  import { context7Command } from './context7-command.mjs';
70
- import { askPostinstallQuestion, checkContext7, checkRequiredSkills, codexLbStatus, configureCodexLb, ensureCodexCliTool, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, globalCodexSkillsRoot, maybePromptCodexLbSetupForLaunch, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, shouldAutoApproveInstall } from './install-helpers.mjs';
71
+ import { askPostinstallQuestion, checkContext7, checkRequiredSkills, codexLbStatus, configureCodexLb, ensureCodexCliTool, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, globalCodexSkillsRoot, maybePromptCodexLbSetupForLaunch, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, repairCodexLbAuth, shouldAutoApproveInstall } from './install-helpers.mjs';
71
72
  import { buildTeamPlan, codeStructureCommand, dbCommand, defaultBeta, defaultVGraph, evalCommand, gcCommand, goalCommand, gxCommand, harnessCommand, hproofCommand, memoryCommand, migrateWikiContextPack, parseTeamCreateArgs, perfCommand, profileCommand, projectWikiClaims, proofFieldCommand, qaLoopCommand, quickstartCommand, researchCommand, skillDreamCommand, statsCommand, team, teamWorkflowMarkdown, validateArtifactsCommand, wikiCommand, wikiVoxelRowCount, writeWikiContextPack } from './maintenance-commands.mjs';
72
73
  import { openClawCommand } from './openclaw-command.mjs';
73
74
 
@@ -97,7 +98,7 @@ export async function main(args) {
97
98
  if (cmd === 'dollar-commands' || cmd === 'dollars' || cmd === '$') return dollarCommands(tail);
98
99
  if (String(cmd).toLowerCase() === 'dfix') return dfixHelp();
99
100
  const handlers = {
100
- postinstall: () => postinstall({ bootstrap }), wizard: () => wizard(tail), ui: () => wizard(tail), 'update-check': () => updateCheck(tail), help: () => help(tail), commands: () => commands(tail), usage: () => usage(tail), root: () => rootCommand(tail), quickstart: () => quickstartCommand(), 'codex-app': () => codexAppHelp(tail), 'codex-lb': () => codexLbCommand(sub, rest), openclaw: () => openClawCommand(tail), bootstrap: () => bootstrap(tail), deps: () => deps(sub, rest),
101
+ postinstall: () => postinstall({ bootstrap }), wizard: () => wizard(tail), ui: () => wizard(tail), 'update-check': () => updateCheck(tail), help: () => help(tail), commands: () => commands(tail), usage: () => usage(tail), root: () => rootCommand(tail), quickstart: () => quickstartCommand(), 'codex-app': () => codexAppHelp(tail), 'codex-lb': () => codexLbCommand(sub, rest), auth: () => codexLbCommand(sub, rest), openclaw: () => openClawCommand(tail), bootstrap: () => bootstrap(tail), deps: () => deps(sub, rest),
101
102
  'qa-loop': () => qaLoopCommand(sub, rest), ppt: () => pptCommand(sub, rest), context7: () => context7Command(sub, rest), pipeline: () => pipeline(sub, rest), guard: () => guard(sub, rest), conflicts: () => conflicts(sub, rest), versioning: () => versioning(sub, rest), reasoning: () => reasoningCommand(tail), aliases: () => aliases(), setup: () => setup(tail), 'fix-path': () => fixPath(tail), doctor: () => doctor(tail), init: () => init(tail), selftest: () => selftest(tail),
102
103
  goal: () => goalCommand(sub, rest), research: () => researchCommand(sub, rest), hook: () => emitHook(sub), profile: () => profileCommand(sub, rest), hproof: () => hproofCommand(sub, rest), 'validate-artifacts': () => validateArtifactsCommand(tail), perf: () => perfCommand(sub, rest), 'proof-field': () => proofFieldCommand(sub, rest), 'skill-dream': () => skillDreamCommand(sub, rest), 'code-structure': () => codeStructureCommand(sub, rest), memory: () => memoryCommand(sub, rest), gx: () => gxCommand(sub, rest),
103
104
  team: () => team(tail), db: () => dbCommand(sub, rest), eval: () => evalCommand(sub, rest), harness: () => harnessCommand(sub, rest), wiki: () => wikiCommand(sub, rest), gc: () => gcCommand(tail), stats: () => statsCommand(tail)
@@ -151,7 +152,8 @@ Usage:
151
152
  sks bootstrap [--install-scope global|project] [--local-only] [--json]
152
153
  sks deps check|install [tmux|codex|context7|all] [--yes] [--json]
153
154
  sks codex-app
154
- sks codex-lb setup --host <domain> --api-key <key>
155
+ sks codex-lb status|repair|setup --host <domain> --api-key <key>
156
+ sks auth status|repair|setup --host <domain> --api-key <key>
155
157
  sks openclaw install|path|print [--dir path] [--force] [--json]
156
158
  sks --mad [--high]
157
159
  sks auto-review status|enable|start [--high]
@@ -702,7 +704,11 @@ async function materializeAfterPipelineAnswer(root, id, dir, mission, route, rou
702
704
  permissions_deactivated: false,
703
705
  supabase_mcp_schema_cleanup_allowed: true,
704
706
  direct_execute_sql_allowed: true,
707
+ normal_db_writes_allowed: true,
708
+ live_server_writes_allowed: true,
709
+ migration_apply_allowed: true,
705
710
  catastrophic_safety_guard_active: true,
711
+ permission_profile: permissionGateSummary(),
706
712
  contract_hash: contract.sealed_hash || null
707
713
  });
708
714
  await appendJsonlBounded(path.join(dir, 'events.jsonl'), {
@@ -721,6 +727,9 @@ async function materializeAfterPipelineAnswer(root, id, dir, mission, route, rou
721
727
  mad_sks_gate_ready: true,
722
728
  supabase_mcp_schema_cleanup_allowed: true,
723
729
  direct_execute_sql_allowed: true,
730
+ normal_db_writes_allowed: true,
731
+ live_server_writes_allowed: true,
732
+ migration_apply_allowed: true,
724
733
  catastrophic_safety_guard_active: true
725
734
  }
726
735
  };
@@ -813,7 +822,11 @@ async function materializeMadSksAuthorization(dir, id, route, routeContext = {},
813
822
  deactivates_when_gate_passed: gateFile,
814
823
  supabase_mcp_schema_cleanup_allowed: true,
815
824
  direct_execute_sql_allowed: true,
825
+ normal_db_writes_allowed: true,
826
+ live_server_writes_allowed: true,
827
+ migration_apply_allowed: true,
816
828
  catastrophic_safety_guard_active: true,
829
+ permission_profile: permissionGateSummary(),
817
830
  contract_hash: contract.sealed_hash || null
818
831
  };
819
832
  await writeJsonAtomic(path.join(dir, 'mad-sks-authorization.json'), artifact);
@@ -830,6 +843,9 @@ async function materializeMadSksAuthorization(dir, id, route, routeContext = {},
830
843
  mad_sks_gate_file: gateFile,
831
844
  supabase_mcp_schema_cleanup_allowed: true,
832
845
  direct_execute_sql_allowed: true,
846
+ normal_db_writes_allowed: true,
847
+ live_server_writes_allowed: true,
848
+ migration_apply_allowed: true,
833
849
  catastrophic_safety_guard_active: true
834
850
  };
835
851
  }
@@ -1004,14 +1020,29 @@ async function codexLbCommand(action = 'status', args = []) {
1004
1020
  console.log(`Env file: ${status.env_file ? status.env_path : 'missing'}`);
1005
1021
  if (status.base_url) console.log(`Base URL: ${status.base_url}`);
1006
1022
  if (!status.ok) console.log('\nRun: sks codex-lb setup --host <domain> --api-key <key>');
1023
+ else console.log('\nRepair auth: sks codex-lb repair');
1007
1024
  return;
1008
1025
  }
1009
- if (sub === 'setup') {
1026
+ if (sub === 'repair' || sub === 'resync' || sub === 'login') {
1027
+ const result = await repairCodexLbAuth();
1028
+ if (json) return console.log(JSON.stringify(result, null, 2));
1029
+ if (!result.ok) {
1030
+ if (result.status === 'not_configured') console.error('codex-lb auth repair failed: codex-lb is not fully configured. Run: sks codex-lb setup --host <domain> --api-key <key>');
1031
+ else console.error(`codex-lb auth repair failed: ${result.status}${result.codex_login?.error ? `: ${result.codex_login.error}` : ''}`);
1032
+ process.exitCode = 1;
1033
+ return;
1034
+ }
1035
+ console.log('codex-lb auth repaired for Codex CLI.');
1036
+ console.log(`Config: ${result.config_path}`);
1037
+ console.log(`Key env: ${result.env_path}`);
1038
+ return;
1039
+ }
1040
+ if (sub === 'setup' || sub === 'reconfigure') {
1010
1041
  const host = readOption(args, '--host', readOption(args, '--domain', null));
1011
1042
  const apiKey = readOption(args, '--api-key', readOption(args, '--key', null));
1012
1043
  if (!host || !apiKey) {
1013
1044
  if (json) return console.log(JSON.stringify({ ok: false, reason: 'missing_host_or_api_key' }, null, 2));
1014
- console.error('Usage: sks codex-lb setup --host <domain> --api-key <key>');
1045
+ console.error('Usage: sks codex-lb setup|reconfigure --host <domain> --api-key <key>');
1015
1046
  process.exitCode = 1;
1016
1047
  return;
1017
1048
  }
@@ -1027,7 +1058,7 @@ async function codexLbCommand(action = 'status', args = []) {
1027
1058
  console.log(`Key env: ${result.env_path}`);
1028
1059
  return;
1029
1060
  }
1030
- console.error('Usage: sks codex-lb status|setup --host <domain> --api-key <key> [--json]');
1061
+ console.error('Usage: sks codex-lb status|repair|setup --host <domain> --api-key <key> [--json]');
1031
1062
  process.exitCode = 1;
1032
1063
  }
1033
1064
 
@@ -1070,17 +1101,91 @@ async function madHighCommand(args = []) {
1070
1101
  process.exitCode = 1;
1071
1102
  return;
1072
1103
  }
1104
+ const lb = await maybePromptCodexLbSetupForLaunch(args);
1105
+ if (lb.status === 'missing_api_key') {
1106
+ process.exitCode = 1;
1107
+ return;
1108
+ }
1073
1109
  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.');
1076
- const workspace = readOption(cleanArgs, '--workspace', readOption(cleanArgs, '--session', `sks-mad-${defaultTmuxSessionName(process.cwd())}`));
1110
+ const madLaunch = await activateMadTmuxPermissionState(process.cwd());
1111
+ console.log(`SKS MAD ready: ${madHighProfileName()} | gate ${madLaunch.mission_id}`);
1112
+ console.log('Live full-access active; catastrophic DB wipe/all-row/project-management guards remain.');
1113
+ const launchLb = lb.status === 'present' ? { ...lb, status: 'configured' } : lb;
1114
+ const launchOpts = codexLbImmediateLaunchOpts(cleanArgs, launchLb, {
1115
+ codexArgs: profile.launch_args,
1116
+ autoInstallTmux: !flag(args, '--no-auto-install-tmux'),
1117
+ conciseBlockers: true
1118
+ });
1119
+ const workspace = readOption(cleanArgs, '--workspace', readOption(cleanArgs, '--session', launchOpts.session || `sks-mad-${defaultTmuxSessionName(process.cwd())}`));
1077
1120
  return launchTmuxUi([...cleanArgs, '--workspace', workspace], {
1121
+ ...launchOpts,
1078
1122
  codexArgs: profile.launch_args,
1079
1123
  autoInstallTmux: !flag(args, '--no-auto-install-tmux'),
1080
1124
  conciseBlockers: true
1081
1125
  });
1082
1126
  }
1083
1127
 
1128
+ async function activateMadTmuxPermissionState(cwd = process.cwd()) {
1129
+ const root = await sksRoot();
1130
+ if (!(await exists(path.join(root, '.sneakoscope')))) await initProject(root, {});
1131
+ const { id, dir } = await createMission(root, { mode: 'mad-sks', prompt: 'sks --mad tmux live full-access session' });
1132
+ const gate = {
1133
+ schema_version: 1,
1134
+ passed: false,
1135
+ mad_sks_permission_active: true,
1136
+ permissions_deactivated: false,
1137
+ live_server_writes_allowed: true,
1138
+ supabase_mcp_schema_cleanup_allowed: true,
1139
+ direct_execute_sql_allowed: true,
1140
+ normal_db_writes_allowed: true,
1141
+ migration_apply_allowed: true,
1142
+ catastrophic_safety_guard_active: true,
1143
+ permission_profile: permissionGateSummary(),
1144
+ activated_by: 'sks --mad',
1145
+ cwd: path.resolve(cwd || process.cwd())
1146
+ };
1147
+ await writeJsonAtomic(path.join(dir, 'mad-sks-gate.json'), gate);
1148
+ await writeJsonAtomic(path.join(dir, 'route-context.json'), {
1149
+ route: 'MadSKS',
1150
+ command: '$MAD-SKS',
1151
+ mode: 'MADSKS',
1152
+ task: gate.activated_by,
1153
+ mad_sks_authorization: true,
1154
+ tmux_launch: true,
1155
+ permission_profile: gate.permission_profile
1156
+ });
1157
+ await appendJsonlBounded(path.join(dir, 'events.jsonl'), {
1158
+ ts: nowIso(),
1159
+ type: 'mad_sks.tmux_permission_opened',
1160
+ route: 'MadSKS',
1161
+ live_server_writes_allowed: true,
1162
+ catastrophic_safety_guard_active: true
1163
+ });
1164
+ await setCurrent(root, {
1165
+ mission_id: id,
1166
+ route: 'MadSKS',
1167
+ route_command: '$MAD-SKS',
1168
+ mode: 'MADSKS',
1169
+ phase: 'MADSKS_TMUX_PERMISSION_ACTIVE',
1170
+ questions_allowed: false,
1171
+ implementation_allowed: true,
1172
+ mad_sks_active: true,
1173
+ mad_sks_modifier: true,
1174
+ mad_sks_gate_file: 'mad-sks-gate.json',
1175
+ mad_sks_gate_ready: true,
1176
+ live_server_writes_allowed: true,
1177
+ supabase_mcp_schema_cleanup_allowed: true,
1178
+ direct_execute_sql_allowed: true,
1179
+ normal_db_writes_allowed: true,
1180
+ migration_apply_allowed: true,
1181
+ catastrophic_safety_guard_active: true,
1182
+ permission_profile: gate.permission_profile,
1183
+ stop_gate: 'mad-sks-gate.json',
1184
+ prompt: gate.activated_by
1185
+ });
1186
+ return { mission_id: id, dir, gate };
1187
+ }
1188
+
1084
1189
  async function maybePromptSksUpdateForLaunch(args = [], opts = {}) {
1085
1190
  if (flag(args, '--json') || flag(args, '--skip-update-check') || process.env.SKS_SKIP_UPDATE_CHECK === '1') return { status: 'skipped' };
1086
1191
  const latest = await npmPackageVersion('sneakoscope');
@@ -1361,7 +1466,7 @@ function usage(args = []) {
1361
1466
  const topic = String(args[0] || 'overview').toLowerCase();
1362
1467
  const blocks = {
1363
1468
  overview: ['ㅅㅋㅅ Usage', '', 'Discover:', ' sks commands', ' sks quickstart', ' sks root', ' sks bootstrap', ' sks deps check', ' sks codex-app check', ' sks tmux check', ' sks dollar-commands', '', `Topics: ${USAGE_TOPICS}`],
1364
- install: ['Install', '', '1. Global install:', ' npm i -g sneakoscope', '', '2. Bootstrap and check dependencies:', ' sks bootstrap', ' sks deps check', '', '3. Confirm Codex App commands:', ' sks codex-app check', ' sks dollar-commands', '', '4. Optional codex-lb key setup for CLI sks runs:', ' sks codex-lb setup --host <domain> --api-key <key>', ' sks', '', 'Fallback:', ' npx -y -p sneakoscope sks root', '', 'Project:', ' npm i -D sneakoscope', ' npx sks setup --install-scope project'],
1469
+ install: ['Install', '', '1. Global install:', ' npm i -g sneakoscope', '', '2. Bootstrap and check dependencies:', ' sks bootstrap', ' sks deps check', '', '3. Confirm Codex App commands:', ' sks codex-app check', ' sks dollar-commands', '', '4. Optional codex-lb key setup for CLI sks runs:', ' sks codex-lb setup --host <domain> --api-key <key>', ' sks codex-lb repair', ' sks', '', 'Fallback:', ' npx -y -p sneakoscope sks root', '', 'Project:', ' npm i -D sneakoscope', ' npx sks setup --install-scope project'],
1365
1470
  bootstrap: ['Bootstrap', '', ' sks bootstrap', ' sks setup --bootstrap', '', 'Creates project SKS files, Codex App skills/hooks/config, state/guard files, then checks Codex App, Context7, and tmux.'],
1366
1471
  root: ['Root', '', ' sks root [--json]', '', 'Inside a project, SKS uses that project root. Outside any project marker, runtime commands use the per-user global SKS root instead of writing .sneakoscope into the current random folder.'],
1367
1472
  deps: ['Dependencies', '', ' sks deps check [--json]', ' sks deps install [tmux|codex|context7|all] [--yes]', '', 'tmux on macOS uses Homebrew after Y/n approval for missing installs or Homebrew-managed upgrades. If PATH resolves an npm-managed tmux, SKS prompts for npm i -g tmux@latest instead. Unknown non-Homebrew tmux paths are reported as conflicts.'],
@@ -2074,10 +2179,16 @@ async function selftest() {
2074
2179
  if (defaultFastHighPlan.codexArgs.join(' ') !== '--model gpt-5.5 -c model_reasoning_effort="high"') throw new Error('selftest failed: default sks tmux launch is not fast-high');
2075
2180
  const codexLbHome = path.join(tmp, 'codex-lb-home');
2076
2181
  await ensureDir(path.join(codexLbHome, '.codex'));
2182
+ const codexLbFakeBin = path.join(tmp, 'codex-lb-fake-bin');
2183
+ await ensureDir(codexLbFakeBin);
2184
+ const codexLbFakeCodex = path.join(codexLbFakeBin, 'codex');
2185
+ await writeTextAtomic(codexLbFakeCodex, "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo \"codex-cli 99.0.0\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"status\" ]; then echo \"logged in with browser auth\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"--with-api-key\" ]; then read key; mkdir -p \"$HOME/.codex\"; printf '{\\\"auth_mode\\\":\\\"apikey\\\",\\\"key\\\":\\\"%s\\\"}\\n' \"$key\" > \"$HOME/.codex/auth.json\"; printf '%s\\n' \"$key\" >> \"$HOME/.codex/login-calls.log\"; exit 0; fi\necho \"fake codex unsupported\" >&2\nexit 1\n");
2186
+ await fsp.chmod(codexLbFakeCodex, 0o755);
2077
2187
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "high"\nservice_tier = "fast"\n\n[notice]\nfast_default_opt_out = true\n');
2188
+ const codexLbEnvForSelftest = { HOME: codexLbHome, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-global'), PATH: `${codexLbFakeBin}${path.delimiter}${process.env.PATH || ''}` };
2078
2189
  const codexLbSetup = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'setup', '--host', 'lb.example.test', '--api-key', 'sk-test', '--json'], {
2079
2190
  cwd: tmp,
2080
- env: { HOME: codexLbHome, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-global') },
2191
+ env: codexLbEnvForSelftest,
2081
2192
  timeoutMs: 15000,
2082
2193
  maxOutputBytes: 64 * 1024
2083
2194
  });
@@ -2086,11 +2197,21 @@ async function selftest() {
2086
2197
  const codexLbConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
2087
2198
  const codexLbEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
2088
2199
  const codexLbAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
2089
- if (!codexLbSetupJson.ok || codexLbSetupJson.base_url !== 'https://lb.example.test/backend-api/codex' || !codexLbConfig.includes('model_provider = "codex-lb"') || !codexLbConfig.includes('[model_providers.codex-lb]') || !codexLbEnv.includes("CODEX_LB_API_KEY='sk-test'") || !codexLbAuth.includes('"auth_mode": "apikey"')) throw new Error('selftest failed: codex-lb setup did not write provider config, env key, and Codex API-key auth');
2200
+ if (!codexLbSetupJson.ok || codexLbSetupJson.base_url !== 'https://lb.example.test/backend-api/codex' || !codexLbConfig.includes('model_provider = "codex-lb"') || !codexLbConfig.includes('[model_providers.codex-lb]') || !codexLbEnv.includes("CODEX_LB_API_KEY='sk-test'") || !/(\"auth_mode\"\s*:\s*\"apikey\")/.test(codexLbAuth)) throw new Error('selftest failed: codex-lb setup did not write provider config, env key, and Codex API-key auth');
2201
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n');
2202
+ const codexLbRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'auth', 'repair', '--json'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2203
+ if (codexLbRepair.code !== 0) throw new Error(`selftest failed: codex-lb repair exited ${codexLbRepair.code}: ${codexLbRepair.stderr}`);
2204
+ const codexLbRepairJson = JSON.parse(codexLbRepair.stdout);
2205
+ const codexLbRepairedAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
2206
+ if (!codexLbRepairJson.ok || codexLbRepairJson.status !== 'repaired' || !codexLbRepairedAuth.includes('"auth_mode":"apikey"') || !codexLbRepairedAuth.includes('sk-test')) throw new Error('selftest failed: codex-lb repair did not force API-key auth from stored env key');
2207
+ const codexLbStatusText = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'status'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2208
+ if (!String(codexLbStatusText.stdout || '').includes('Repair auth: sks codex-lb repair')) throw new Error('selftest failed: codex-lb status did not advertise repair command');
2090
2209
  if (!codexLbConfig.includes('service_tier = "fast"') || !codexLbConfig.includes('fast_mode = true') || !codexLbConfig.includes('fast_mode_ui = true') || !codexLbConfig.includes('[user.fast_mode]') || !codexLbConfig.includes('visible = true') || !codexLbConfig.includes('enabled = true') || !codexLbConfig.includes('default_profile = "sks-fast-high"') || !/\[profiles\.sks-fast-high\][\s\S]*?service_tier = "fast"/.test(codexLbConfig) || codexLbConfig.includes('fast_default_opt_out = true') || hasTopLevelCodexModeLock(codexLbConfig)) throw new Error('selftest failed: codex-lb setup did not preserve Codex App Fast mode defaults');
2091
2210
  const codexLbLaunch = codexLaunchCommand(tmp, 'codex', []);
2092
2211
  if (!codexLbLaunch.includes('sks-codex-lb.env')) throw new Error('selftest failed: tmux launch command does not source codex-lb env file');
2093
2212
  if (!codexLbLaunch.includes('SKS_TMUX_LOGO_ANIMATION') || !codexLbLaunch.includes('SNEAKOSCOPE CODEX')) throw new Error('selftest failed: tmux launch command does not include the animated SKS logo intro');
2213
+ const madLaunchSource = await safeReadText(path.join(packageRoot(), 'src', 'cli', 'main.mjs'));
2214
+ if (!madLaunchSource.includes('const lb = await maybePromptCodexLbSetupForLaunch(args)') || !madLaunchSource.includes("const launchLb = lb.status === 'present'") || !madLaunchSource.includes('codexLbImmediateLaunchOpts(cleanArgs, launchLb')) throw new Error('selftest failed: MAD launch does not sync codex-lb auth and fresh-session launch options');
2094
2215
  if (!shouldAutoAttachTmux(['--mad'], {}, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest failed: MAD tmux launch does not auto-attach in an interactive terminal');
2095
2216
  if (shouldAutoAttachTmux(['--mad', '--json'], {}, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest failed: MAD tmux json mode should not auto-attach');
2096
2217
  if (shouldAutoAttachTmux(['--mad', '--no-attach'], {}, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest failed: MAD tmux --no-attach should remain print-only');
@@ -2348,7 +2469,7 @@ async function selftest() {
2348
2469
  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
2470
  if (madStandaloneResult.code !== 0) throw new Error(`selftest failed: standalone MAD-SKS hook exited ${madStandaloneResult.code}: ${madStandaloneResult.stderr}`);
2350
2471
  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');
2472
+ 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
2473
  const madStandaloneWrite = 'cre' + 'ate table mad_selftest (id uuid primary key);';
2353
2474
  const madStandaloneCreateDecision = await checkDbOperation(madStandaloneTmp, madStandaloneState, { ['tool' + '_name']: 'mcp__data' + 'base__execute_' + 'sql', ['s' + 'ql']: madStandaloneWrite }, { duringNoQuestion: false });
2354
2475
  if (madStandaloneCreateDecision.action !== 'allow') throw new Error('selftest failed: standalone MAD-SKS did not allow ordinary DDL');
@@ -2358,7 +2479,7 @@ async function selftest() {
2358
2479
  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
2480
  if (madModifierResult.code !== 0) throw new Error(`selftest failed: MAD-SKS Team hook exited ${madModifierResult.code}: ${madModifierResult.stderr}`);
2360
2481
  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');
2482
+ 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
2483
  if (routePrompt('위키 갱신해줘')?.id !== 'Wiki') throw new Error('selftest failed: wiki refresh text did not route to Wiki');
2363
2484
  const koreanReadmeInstallPrompt = '리드미에 Codex App에서도 $ 표기 쓰는 법을 알려줘야지. 설치단계에서 바로 보이게 해줘야지';
2364
2485
  if (routePrompt(koreanReadmeInstallPrompt)?.id !== 'Team') throw new Error('selftest failed: Korean README implementation prompt did not route to Team by default');
@@ -2422,9 +2543,9 @@ async function selftest() {
2422
2543
  const hookGoalDelegationContext = hookGoalDelegationJson.hookSpecificOutput?.additionalContext || '';
2423
2544
  const hookGoalDelegationBridgeMatch = hookGoalDelegationContext.match(/Goal bridge mission: (M-[A-Za-z0-9-]+)/);
2424
2545
  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');
2546
+ 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
2547
  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');
2548
+ 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
2549
  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
2550
  const activeGoalMissionId = hookState.mission_id;
2430
2551
  const hookGoalOverlayPayload = JSON.stringify({ cwd: hookGoalTmp, prompt: '$Team 발표자료 만들어줘' });
@@ -2432,12 +2553,11 @@ async function selftest() {
2432
2553
  if (hookGoalOverlayResult.code !== 0) throw new Error(`selftest failed: active Goal overlay hook exited ${hookGoalOverlayResult.code}: ${hookGoalOverlayResult.stderr}`);
2433
2554
  const hookGoalOverlayJson = JSON.parse(hookGoalOverlayResult.stdout);
2434
2555
  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');
2556
+ 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
2557
  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
2558
  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');
2559
+ 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');
2560
+ 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
2561
  const hookUpdateCurrentTmp = tmpdir();
2442
2562
  await initProject(hookUpdateCurrentTmp, {});
2443
2563
  const hookUpdateCurrentEnv = { SKS_DISABLE_UPDATE_CHECK: '0', SKS_NPM_VIEW_SNEAKOSCOPE_VERSION: '9.9.9', SKS_INSTALLED_SKS_VERSION: '9.9.9' };
@@ -2522,7 +2642,7 @@ async function selftest() {
2522
2642
  if (hookKoreanSksResult.code !== 0) throw new Error(`selftest failed: Korean SKS hook exited ${hookKoreanSksResult.code}: ${hookKoreanSksResult.stderr}`);
2523
2643
  const hookKoreanSksJson = JSON.parse(hookKoreanSksResult.stdout);
2524
2644
  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');
2645
+ 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
2646
  if (!hookKoreanSksContext.includes('Route: $Team')) throw new Error('selftest failed: Korean implementation prompt did not promote to Team route');
2527
2647
  if (hookKoreanSksContext.includes('SKS answer-only pipeline active')) throw new Error('selftest failed: Korean implementation prompt still used answer-only pipeline');
2528
2648
  const hookKoreanSksState = await readJson(stateFile(hookKoreanSksTmp), {});
@@ -2535,12 +2655,10 @@ async function selftest() {
2535
2655
  if (hookPaymentTeamResult.code !== 0) throw new Error(`selftest failed: payment/auth Team hook exited ${hookPaymentTeamResult.code}: ${hookPaymentTeamResult.stderr}`);
2536
2656
  const hookPaymentTeamJson = JSON.parse(hookPaymentTeamResult.stdout);
2537
2657
  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');
2658
+ 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
2659
  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
2660
  const hookPaymentTeamState = await readJson(stateFile(hookPaymentTeamTmp), {});
2541
2661
  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
2662
  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
2663
  const hookTeamTmp = tmpdir();
2546
2664
  await initProject(hookTeamTmp, {});
@@ -2548,45 +2666,38 @@ async function selftest() {
2548
2666
  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
2667
  if (hookTeamResult.code !== 0) throw new Error(`selftest failed: $Team hook exited ${hookTeamResult.code}: ${hookTeamResult.stderr}`);
2550
2668
  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');
2669
+ 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');
2670
+ if (!hookTeamJson.hookSpecificOutput?.additionalContext?.includes('$Team route prepared')) throw new Error('selftest failed: $Team hook did not prepare direct Team route');
2556
2671
  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');
2672
+ 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');
2673
+ 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');
2674
+ 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
2675
  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
2676
  if (hookTeamPendingResult.code !== 0) throw new Error(`selftest failed: pending clarification hook exited ${hookTeamPendingResult.code}: ${hookTeamPendingResult.stderr}`);
2562
2677
  const hookTeamPendingJson = JSON.parse(hookTeamPendingResult.stdout);
2563
2678
  const hookTeamPendingState = await readJson(stateFile(hookTeamTmp), {});
2564
2679
  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'));
2680
+ 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');
2681
+ 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');
2682
+ const qaClarificationTmp = tmpdir();
2683
+ await initProject(qaClarificationTmp, {});
2684
+ 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 });
2685
+ if (hookQaClarificationResult.code !== 0) throw new Error(`selftest failed: QA clarification hook exited ${hookQaClarificationResult.code}: ${hookQaClarificationResult.stderr}`);
2686
+ const hookQaClarificationState = await readJson(stateFile(qaClarificationTmp), {});
2687
+ const hookQaClarificationSchema = await readJson(path.join(missionDir(qaClarificationTmp, hookQaClarificationState.mission_id), 'required-answers.schema.json'));
2688
+ const hookTeamSchema = hookQaClarificationSchema;
2578
2689
  const visibleQuestionsBlock = [
2579
2690
  'Required questions',
2580
2691
  ...hookTeamSchema.slots.map((slot, idx) => `${idx + 1}. ${slot.id}: ${slot.question}`),
2581
2692
  'Reply by slot id, then I will seal the contract with sks pipeline answer latest --stdin.'
2582
2693
  ].join('\n');
2583
- const visibleQuestionDecision = await evaluateStop(hookTeamTmp, hookTeamState, { last_assistant_message: visibleQuestionsBlock }, { noQuestion: false });
2694
+ const visibleQuestionDecision = await evaluateStop(qaClarificationTmp, hookQaClarificationState, { last_assistant_message: visibleQuestionsBlock }, { noQuestion: false });
2584
2695
  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 });
2696
+ 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
2697
  if (hookTeamPreToolBlocked.code !== 0) throw new Error(`selftest failed: pending clarification pre-tool hook exited ${hookTeamPreToolBlocked.code}: ${hookTeamPreToolBlocked.stderr}`);
2587
2698
  const hookTeamPreToolBlockedJson = JSON.parse(hookTeamPreToolBlocked.stdout);
2588
2699
  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 });
2700
+ 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
2701
  if (hookTeamAnswerToolAllowed.code !== 0) throw new Error(`selftest failed: pipeline-answer pre-tool hook exited ${hookTeamAnswerToolAllowed.code}: ${hookTeamAnswerToolAllowed.stderr}`);
2591
2702
  const hookTeamAnswerToolAllowedJson = JSON.parse(hookTeamAnswerToolAllowed.stdout);
2592
2703
  if (hookTeamAnswerToolAllowedJson.decision === 'block') throw new Error('selftest failed: pending clarification blocked the pipeline answer command');
@@ -2597,18 +2708,6 @@ async function selftest() {
2597
2708
  if (textParsedAnswers.INTENT_TARGET !== 'compact contract sealing') throw new Error('selftest failed: text answer parser did not parse slot-id answers');
2598
2709
  const textParsedImplicitAnswer = parseAnswersText({ slots: [{ id: 'INTENT_TARGET', type: 'string', required: true }] }, 'compact contract sealing');
2599
2710
  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
2711
  const honestLoopTmp = tmpdir();
2613
2712
  await initProject(honestLoopTmp, {});
2614
2713
  const { id: honestLoopId, dir: honestLoopDir } = await createMission(honestLoopTmp, { mode: 'sks', prompt: 'honest loopback selftest' });
@@ -3026,6 +3125,8 @@ async function selftest() {
3026
3125
  const tmuxTeam = await launchTmuxTeamView({ root: tmp, missionId: teamId, plan: roleTeamPlan, json: true });
3027
3126
  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
3127
  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');
3128
+ 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');
3129
+ 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
3130
  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
3131
  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
3132
  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');
@@ -3253,6 +3354,10 @@ async function selftest() {
3253
3354
  await setCurrent(tmp, { mission_id: id, mode: 'QALOOP', phase: 'QALOOP_RUNNING_NO_QUESTIONS' });
3254
3355
  if (!containsUserQuestion('확인해 주세요?')) throw new Error('selftest failed: question guard');
3255
3356
  if (classifySql('drop table users;').level !== 'destructive') throw new Error('selftest failed: destructive sql not detected');
3357
+ const patchPayloadClass = classifyToolPayload({ tool_name: 'apply_patch', command: '*** Update File: src/example.mjs\n+ok\n' });
3358
+ if (patchPayloadClass.level !== 'none') throw new Error('selftest failed: apply_patch file edits should not be classified as DB writes');
3359
+ const supabaseWritePayloadClass = classifyToolPayload({ tool_name: 'mcp__supabase__execute_sql', sql: "update users set name = 'x' where id = '1';" });
3360
+ if (supabaseWritePayloadClass.level !== 'write' || !supabaseWritePayloadClass.toolReasons.includes('database_tool')) throw new Error('selftest failed: Supabase execute_sql write classification was weakened');
3256
3361
  if (classifyCommand('supabase db reset').level !== 'destructive') throw new Error('selftest failed: supabase db reset not detected');
3257
3362
  const dbDecision = await checkDbOperation(tmp, { mission_id: id }, { tool_name: 'mcp__supabase__execute_sql', sql: 'drop table users;' }, { duringNoQuestion: true });
3258
3363
  if (dbDecision.action !== 'block') throw new Error('selftest failed: destructive MCP SQL allowed');
@@ -3263,7 +3368,9 @@ async function selftest() {
3263
3368
  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
3369
  const columnCleanupSql = 'alter table users ' + 'dr' + 'op column legacy_name;';
3265
3370
  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');
3371
+ 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');
3372
+ const madLiveDmlDecision = await checkDbOperation(tmp, madState, { tool_name: 'mcp__supabase__execute_sql', sql: "update users set name = 'fixed' where id = 'selftest';" }, { duringNoQuestion: false });
3373
+ 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
3374
  const tableRemovalSql = 'dr' + 'op table users;';
3268
3375
  const madTableRemovalDecision = await checkDbOperation(tmp, madState, { tool_name: 'mcp__supabase__execute_sql', sql: tableRemovalSql }, { duringNoQuestion: false });
3269
3376
  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,
@@ -198,6 +199,9 @@ export function classifyToolPayload(payload = {}) {
198
199
  const sqlClass = classifySql(combined);
199
200
  const commandClass = classifyCommand(strings.find((s) => /\b(supabase|psql|prisma|drizzle|knex|sequelize)\b/i.test(s)) || '');
200
201
  const toolReasons = [];
202
+ if (/\b(apply_patch|edit|write|create|remove|rename|str_replace|file_write|fs_write)\b/i.test(toolName) && !/supabase|postgres|database|execute_sql|apply_migration|sql_query|db_|_db\b|migration/.test(toolName)) {
203
+ return { level: 'none', toolName, toolReasons, sql: sqlClass, command: commandClass, stringsExamined: strings.length };
204
+ }
201
205
  if (/supabase|postgres|database|execute_sql|apply_migration|sql_query|db_|_db\b|migration/.test(toolName)) toolReasons.push('database_tool');
202
206
  if (/delete_project|pause_project|restore_project|delete_branch|reset_branch|merge_branch/.test(toolName)) toolReasons.push('dangerous_supabase_management_tool');
203
207
  let level = 'none';
@@ -229,33 +233,6 @@ function hasTableRemovalRisk(cls = {}) {
229
233
  return ['drop_table', 'truncate'].some((reason) => reasons.has(reason));
230
234
  }
231
235
 
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
236
  async function madSksOverrideState(root, state = {}) {
260
237
  if (!isMadSksRouteState(state) || !state.mission_id || state.mad_sks_active === false) return { active: false };
261
238
  const gateFile = state.mad_sks_gate_file || state.stop_gate || MAD_SKS_GATE_FILE;
@@ -279,31 +256,35 @@ export function evaluateDbSafety({ classification, policy = DEFAULT_DB_SAFETY_PO
279
256
  if (cls.level === 'safe') return { allowed: true, action: 'allow', reasons: ['read_only_operation'], classification: cls };
280
257
  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
258
  if (madSks?.active && (cls.level === 'write' || cls.level === 'destructive')) {
282
- if (hasMadSksCatastrophicDbRisk(cls)) {
259
+ const madGate = evaluateMadSksPermissionGate({ classification: cls, active: true });
260
+ if (!madGate.allowed) {
283
261
  return {
284
262
  allowed: false,
285
263
  action: 'block',
286
- reasons: ['mad_sks_catastrophic_db_operation_blocked'],
264
+ reasons: madGate.reasons,
287
265
  classification: cls,
288
266
  effective,
289
267
  mad_sks: {
290
268
  active: true,
291
269
  catastrophic_safety_guard_active: true,
292
- blocked_categories: ['whole_database_or_table_removal', 'all_rows_delete_or_update', 'dangerous_project_management']
270
+ blocked_categories: madGate.blocked_categories,
271
+ permission_profile: madGate.profile
293
272
  }
294
273
  };
295
274
  }
296
275
  return {
297
276
  allowed: true,
298
277
  action: 'allow',
299
- reasons: ['mad_sks_scoped_override_active'],
278
+ reasons: madGate.reasons,
300
279
  classification: cls,
301
280
  effective,
302
281
  mad_sks: {
303
282
  active: true,
304
283
  sks_db_constraints_removed: true,
305
284
  catastrophic_safety_guard_active: true,
306
- supabase_mcp_schema_cleanup_allowed: true
285
+ supabase_mcp_schema_cleanup_allowed: true,
286
+ live_server_writes_allowed: true,
287
+ permission_profile: madGate.profile
307
288
  }
308
289
  };
309
290
  }
@@ -416,7 +397,7 @@ export function dbBlockReason(decision) {
416
397
  if ((decision.reasons || []).includes('mad_sks_catastrophic_db_operation_blocked')) {
417
398
  return [
418
399
  '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.',
400
+ '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
401
  'Whole database/table removal, all-row value wipes, database reset, and dangerous project or branch management remain blocked.'
421
402
  ].join(' ');
422
403
  }
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.49';
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',
@@ -456,7 +456,8 @@ export const COMMAND_CATALOG = [
456
456
  { name: 'root', usage: 'sks root [--json]', description: 'Show whether SKS is using a project root or the per-user global SKS runtime root.' },
457
457
  { name: 'deps', usage: 'sks deps check|install [tmux|codex|context7|all] [--yes]', description: 'Check or guided-install Node/npm PATH, Codex CLI/App, Context7, Browser Use, Computer Use, tmux, and Homebrew on macOS.' },
458
458
  { name: 'codex-app', usage: 'sks codex-app [check|open|remote-control]', description: 'Check Codex App install and first-party MCP/plugin readiness, then show app setup files, examples, and Codex CLI 0.130.0+ remote-control availability.' },
459
- { name: 'codex-lb', usage: 'sks codex-lb status|setup --host <domain> --api-key <key>', description: 'Configure codex-lb as the Codex CLI provider by writing ~/.codex/config.toml and the CODEX_LB_API_KEY env file.' },
459
+ { name: 'codex-lb', usage: 'sks codex-lb status|repair|setup --host <domain> --api-key <key>', description: 'Configure or repair codex-lb Codex CLI auth by writing ~/.codex/config.toml, syncing auth.json, and loading the CODEX_LB_API_KEY env file.' },
460
+ { name: 'auth', usage: 'sks auth status|repair|setup --host <domain> --api-key <key>', description: 'Shortcut for codex-lb auth status, repair, and setup commands.' },
460
461
  { name: 'openclaw', usage: 'sks openclaw install|path|print [--dir path] [--force] [--json]', description: 'Generate an OpenClaw skill package so OpenClaw agents can discover and use local SKS workflows.' },
461
462
  { name: 'tmux', usage: 'sks | sks tmux open|check|status [--workspace name]', description: 'Open the default SKS tmux runtime with bare sks, or use tmux subcommands for explicit launch/check/status.' },
462
463
  { name: 'mad', usage: 'sks --mad [--high]', description: 'Open a one-shot tmux Codex CLI workspace with the SKS MAD full-access auto-review profile.' },
@@ -568,6 +569,7 @@ export function routePrompt(prompt) {
568
569
  }
569
570
  const route = routeByDollarCommand(command) || null;
570
571
  if (route?.id === 'SKS' && looksLikeTeamDefaultWork(stripDollarCommand(text))) return routeById('Team');
572
+ if (route?.id === 'Team') return route;
571
573
  return route;
572
574
  }
573
575
  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) => ({