sneakoscope 0.7.60 → 0.7.63

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
@@ -239,9 +239,9 @@ sks team log latest
239
239
 
240
240
  By default, Team missions keep at least five QA/reviewer lanes active. Use explicit role counts only when you need to raise or otherwise pin the lane mix for a specific mission.
241
241
 
242
- 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 by default when tmux is available. Use `--no-open-tmux` for artifact-only mission creation. 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.
242
+ Team mode prepares the mission, records live events, compiles runtime tasks and worker inboxes, writes schema-backed effort/work-order/dashboard artifacts, and reconciles split live lanes inside the current SKS-owned tmux session when available. Outside an SKS tmux session, `sks team open-tmux --separate-session` keeps the named `sks-team-*` fallback view. Use `--no-open-tmux` for artifact-only mission creation. 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.
243
243
 
244
- 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`.
244
+ The tmux Team launch is a live orchestration screen in one tmux window: the main Codex pane stays alive, a managed overview pane follows `sks team watch <mission-id> --follow`, and neighboring managed 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 tags Team panes with tmux user options, closes only those managed panes when agent lanes complete or cleanup is requested, and recalculates the tiled layout after split/close operations. The separate `sks-team-*` session remains available as a fallback. 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`.
245
245
 
246
246
  Team roster and runtime artifacts now include per-agent Fast reasoning metadata. Simple bounded Team lanes can use low reasoning, tool-heavy runtime/CLI/tmux work uses medium, and knowledge, current-docs, safety, DB, release, commit, or research-heavy lanes use high or xhigh as appropriate instead of opening every scout at high.
247
247
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.7.60",
4
+ "version": "0.7.63",
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",
@@ -89,5 +89,8 @@
89
89
  "mcp-safety",
90
90
  "db-guardian"
91
91
  ],
92
- "license": "MIT"
92
+ "license": "MIT",
93
+ "dependencies": {
94
+ "figlet": "^1.11.0"
95
+ }
93
96
  }
@@ -307,12 +307,17 @@ export async function ensureGlobalCodexFastModeDuringInstall(opts = {}) {
307
307
  export function normalizeCodexFastModeUiConfig(text = '') {
308
308
  let next = removeLegacyTopLevelCodexModeLocks(text);
309
309
  next = removeTomlTableKey(next, 'notice', 'fast_default_opt_out');
310
- next = removeTomlTableKey(next, 'features', 'hooks');
310
+ next = removeTomlTableKey(next, 'features', 'codex_hooks');
311
311
  next = upsertTopLevelTomlString(next, 'model', 'gpt-5.5');
312
312
  next = upsertTopLevelTomlString(next, 'service_tier', 'fast');
313
- next = upsertTomlTableKey(next, 'features', 'codex_hooks = true');
313
+ next = upsertTomlTableKey(next, 'features', 'hooks = true');
314
+ next = upsertTomlTableKey(next, 'features', 'multi_agent = true');
314
315
  next = upsertTomlTableKey(next, 'features', 'fast_mode = true');
315
316
  next = upsertTomlTableKey(next, 'features', 'fast_mode_ui = true');
317
+ next = upsertTomlTableKey(next, 'features', 'codex_git_commit = true');
318
+ next = upsertTomlTableKey(next, 'features', 'computer_use = true');
319
+ next = upsertTomlTableKey(next, 'features', 'apps = true');
320
+ next = upsertTomlTableKey(next, 'features', 'plugins = true');
316
321
  next = upsertTomlTableKey(next, 'user.fast_mode', 'visible = true');
317
322
  next = upsertTomlTableKey(next, 'user.fast_mode', 'enabled = true');
318
323
  next = upsertTomlTableKey(next, 'user.fast_mode', 'default_profile = "sks-fast-high"');
@@ -993,7 +998,7 @@ export async function selftestCodexLb(tmp) {
993
998
  if (codexLbNotConfigured.code !== 0 || String(codexLbNotConfigured.stdout || '').includes('codex-lb auth:')) throw new Error('selftest failed: postinstall should stay quiet when codex-lb is not configured');
994
999
  const codexLbStatusText = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'status'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
995
1000
  if (!String(codexLbStatusText.stdout || '').includes('Repair auth: sks codex-lb repair')) throw new Error('selftest failed: codex-lb status did not advertise repair command');
996
- if (!/^model = "gpt-5\.5"/m.test(codexLbConfig) || !codexLbConfig.includes('service_tier = "fast"') || !codexLbConfig.includes('codex_hooks = true') || hasLegacyHooksFeatureFlag(codexLbConfig) || !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, force GPT-5.5, or migrate the hooks feature flag');
1001
+ if (!/^model = "gpt-5\.5"/m.test(codexLbConfig) || !codexLbConfig.includes('service_tier = "fast"') || !codexLbConfig.includes('hooks = true') || hasDeprecatedCodexHooksFeatureFlag(codexLbConfig) || !codexLbConfig.includes('multi_agent = true') || !codexLbConfig.includes('fast_mode = true') || !codexLbConfig.includes('fast_mode_ui = true') || !codexLbConfig.includes('codex_git_commit = true') || !codexLbConfig.includes('computer_use = true') || !codexLbConfig.includes('apps = true') || !codexLbConfig.includes('plugins = 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 feature flags, Fast mode defaults, Codex Git commit generation, force GPT-5.5, or migrate the hooks feature flag');
997
1002
  const codexLbLaunch = codexLaunchCommand(tmp, 'codex', []);
998
1003
  if (!codexLbLaunch.includes('sks-codex-lb.env')) throw new Error('selftest failed: tmux launch command does not source codex-lb env file');
999
1004
  if (!codexLbLaunch.includes("'--model' 'gpt-5.5'")) throw new Error('selftest failed: tmux launch command without args did not force GPT-5.5');
@@ -1007,7 +1012,7 @@ function hasTopLevelCodexModeLock(text = '') {
1007
1012
  return /(^|\n)\s*model\s*=\s*"codex-lb"\s*(\n|$)/.test(text) || /(^|\n)\s*model_provider\s*=\s*"openai"\s*(\n|$)/.test(text);
1008
1013
  }
1009
1014
 
1010
- function hasLegacyHooksFeatureFlag(text = '') {
1015
+ function hasDeprecatedCodexHooksFeatureFlag(text = '') {
1011
1016
  const lines = String(text || '').split('\n');
1012
1017
  const start = lines.findIndex((line) => line.trim() === '[features]');
1013
1018
  if (start === -1) return false;
@@ -1018,5 +1023,5 @@ function hasLegacyHooksFeatureFlag(text = '') {
1018
1023
  break;
1019
1024
  }
1020
1025
  }
1021
- return lines.slice(start + 1, end).some((line) => /^\s*hooks\s*=/.test(line));
1026
+ return lines.slice(start + 1, end).some((line) => /^\s*codex_hooks\s*=/.test(line));
1022
1027
  }
package/src/cli/main.mjs CHANGED
@@ -75,7 +75,7 @@ import { GOAL_WORKFLOW_ARTIFACT } from '../core/goal-workflow.mjs';
75
75
  import { CODEX_APP_DOCS_URL, codexAppIntegrationStatus, formatCodexAppStatus } from '../core/codex-app.mjs';
76
76
  import { codexAppRemoteControlCommand } from './codex-app-command.mjs';
77
77
  import { OPENCLAW_SKILL_NAME, installOpenClawSkill } from '../core/openclaw.mjs';
78
- import { buildTmuxLaunchPlan, buildTmuxOpenArgs, codexLaunchCommand, createTmuxSession, defaultCodexLaunchArgs, isTmuxShellSession, runTmuxLaunchPlanSyntaxCheck, shouldAutoAttachTmux, tmuxReadiness, tmuxStatusKind, defaultTmuxSessionName, formatTmuxBanner, launchMadTmuxUi, launchTmuxTeamView, launchTmuxUi, platformTmuxInstallHint, runTmuxStatus, sanitizeTmuxSessionName, teamLaneStyle } from '../core/tmux-ui.mjs';
78
+ import { buildTmuxLaunchPlan, buildTmuxOpenArgs, codexLaunchCommand, createTmuxSession, defaultCodexLaunchArgs, isTmuxShellSession, runTmuxLaunchPlanSyntaxCheck, shouldAutoAttachTmux, sksAsciiLogo, tmuxReadiness, tmuxStatusKind, defaultTmuxSessionName, formatTmuxBanner, launchMadTmuxUi, launchTmuxTeamView, launchTmuxUi, platformTmuxInstallHint, reconcileTmuxTeamCockpit, runTmuxStatus, sanitizeTmuxSessionName, teamLaneStyle } from '../core/tmux-ui.mjs';
79
79
  import { autoReviewProfileName, autoReviewStatus, autoReviewSummary, enableAutoReview, disableAutoReview, enableMadHighProfile, madHighProfileName } from '../core/auto-review.mjs';
80
80
  import { context7Command } from './context7-command.mjs';
81
81
  import { askPostinstallQuestion, checkContext7, checkRequiredSkills, codexLbStatus, configureCodexLb, ensureCodexCliTool, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, globalCodexSkillsRoot, maybePromptCodexLbSetupForLaunch, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, repairCodexLbAuth, selftestCodexLb, shouldAutoApproveInstall } from './install-helpers.mjs';
@@ -156,8 +156,7 @@ function codexLbImmediateLaunchOpts(args = [], lb = {}, opts = {}) {
156
156
  function help(args = []) {
157
157
  const topic = args[0];
158
158
  if (topic) return usage([topic]);
159
- console.log(`ㅅㅋㅅ
160
- Sneakoscope Codex
159
+ console.log(`${sksAsciiLogo()}
161
160
 
162
161
  Usage:
163
162
  sks
@@ -205,9 +204,11 @@ Usage:
205
204
  sks goal pause|resume|clear <mission-id|latest>
206
205
  sks goal status <mission-id|latest>
207
206
  sks team "task" [executor:5 reviewer:6 user:1] [--json]
208
- sks team log|tail|watch|lane|status|dashboard [mission-id|latest]
207
+ sks team log|tail|watch|lane|status|dashboard|open-tmux|attach-tmux [mission-id|latest]
209
208
  sks team event [mission-id|latest] --agent <name> --phase <phase> --message "..."
210
209
  sks team message [mission-id|latest] --from <agent> --to <agent|all> --message "..."
210
+ sks team open-tmux [mission-id|latest] [--no-attach|--separate-session]
211
+ sks team attach-tmux [mission-id|latest]
211
212
  sks team cleanup-tmux [mission-id|latest]
212
213
  sks research prepare "topic" [--depth frontier]
213
214
  sks research run <mission-id|latest> [--mock] [--max-cycles N]
@@ -272,7 +273,7 @@ async function wizard(args = []) {
272
273
  if (!shouldShowWizard() && !flag(args, '--force')) return help();
273
274
  const rl = readline.createInterface({ input, output });
274
275
  try {
275
- console.log('ㅅㅋㅅ Setup UI\n');
276
+ console.log(`${sksAsciiLogo()}\nSetup UI\n`);
276
277
  const currentPackage = await effectivePackageVersion();
277
278
  console.log(`Current package: ${currentPackage}`);
278
279
  const latest = await npmPackageVersion('sneakoscope');
@@ -340,7 +341,7 @@ async function updateCheck(args = []) {
340
341
  error: latest.error || null
341
342
  };
342
343
  if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
343
- console.log('ㅅㅋㅅ Update Check');
344
+ console.log(`${sksAsciiLogo()}\nUpdate Check`);
344
345
  console.log(`Current: ${result.current}`);
345
346
  console.log(`Latest: ${result.latest || 'unknown'}`);
346
347
  console.log(`Update: ${result.update_available ? 'available' : 'not needed'}`);
@@ -352,7 +353,7 @@ const DOLLAR_DEFAULT_PIPELINE_TEXT = 'Default pipeline: direct answers -> $Answe
352
353
 
353
354
  function commands(args = []) {
354
355
  if (flag(args, '--json')) return console.log(JSON.stringify({ aliases: ['sks', 'sneakoscope'], dollar_commands: DOLLAR_COMMANDS, app_skill_aliases: DOLLAR_COMMAND_ALIASES, commands: COMMAND_CATALOG }, null, 2));
355
- console.log('ㅅㅋㅅ Commands\n');
356
+ console.log(`${sksAsciiLogo()}\nCommands\n`);
356
357
  console.log('Aliases: sks, sneakoscope\n');
357
358
  const width = Math.max(...COMMAND_CATALOG.map((c) => c.usage.length));
358
359
  for (const c of COMMAND_CATALOG) console.log(`${c.usage.padEnd(width)} ${c.description}`);
@@ -375,7 +376,7 @@ async function rootCommand(args = []) {
375
376
  using_global_root: !project
376
377
  };
377
378
  if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
378
- console.log('SKS Root\n');
379
+ console.log(`${sksAsciiLogo()}\nRoot\n`);
379
380
  console.log(`Mode: ${result.mode}`);
380
381
  console.log(`Active root: ${active}`);
381
382
  console.log(`Project: ${project || 'none'}`);
@@ -385,7 +386,7 @@ async function rootCommand(args = []) {
385
386
 
386
387
  function dollarCommands(args = []) {
387
388
  if (flag(args, '--json')) return console.log(JSON.stringify({ dollar_commands: DOLLAR_COMMANDS, app_skill_aliases: DOLLAR_COMMAND_ALIASES }, null, 2));
388
- console.log('ㅅㅋㅅ $ Commands\n');
389
+ console.log(`${sksAsciiLogo()}\n$ Commands\n`);
389
390
  console.log('Use these inside Codex App or another agent prompt. Shells treat $ as variable syntax, so these are prompt commands, not terminal commands.\n');
390
391
  console.log(formatDollarCommandsDetailed());
391
392
  console.log(`\nCanonical Codex App picker skills: ${DOLLAR_COMMAND_ALIASES.map((x) => x.app_skill).join(', ')}`);
@@ -1406,7 +1407,8 @@ async function codexAppHelp(args = []) {
1406
1407
  const status = await codexAppIntegrationStatus();
1407
1408
  const skills = await codexAppSkillReadiness();
1408
1409
  console.log([
1409
- 'ㅅㅋㅅ Codex App', '',
1410
+ sksAsciiLogo(), '',
1411
+ 'Codex App', '',
1410
1412
  formatCodexAppStatus(status), '',
1411
1413
  `Skills: project=${skills.project.ok ? 'ok' : `missing ${skills.project.missing.length}`} global=${skills.global.ok ? 'ok' : `missing ${skills.global.missing.length}`}`, '',
1412
1414
  'Setup:', ' sks bootstrap', ' sks deps check', ' sks codex-app check', ' sks codex-app remote-control --status', ' sks tmux check', '',
@@ -1417,7 +1419,9 @@ async function codexAppHelp(args = []) {
1417
1419
  }
1418
1420
 
1419
1421
  function aliases() {
1420
- console.log(`ㅅㅋㅅ Aliases
1422
+ console.log(`${sksAsciiLogo()}
1423
+
1424
+ Aliases
1421
1425
 
1422
1426
  Binary aliases:
1423
1427
  sks
@@ -1443,14 +1447,14 @@ Examples:
1443
1447
  function usage(args = []) {
1444
1448
  const topic = String(args[0] || 'overview').toLowerCase();
1445
1449
  const blocks = {
1446
- 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}`],
1450
+ overview: [sksAsciiLogo(), '', '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}`],
1447
1451
  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'],
1448
1452
  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.'],
1449
1453
  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.'],
1450
1454
  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.'],
1451
1455
  tmux: ['tmux', '', ' sks', ' sks tmux open', ' sks tmux check', ' sks tmux status --once', ' sks deps install tmux', '', 'Running bare `sks` opens or reuses the default tmux Codex CLI session in fast-high mode: --model gpt-5.5 -c model_reasoning_effort="high". SKS always forces gpt-5.5; SKS_CODEX_MODEL and SKS_CODEX_FAST_HIGH=0 cannot downgrade or remove that model pin. Use SKS_CODEX_REASONING only for reasoning effort. Before launch, SKS checks npm @openai/codex@latest and prompts Y/n when the installed Codex CLI is missing or outdated. Use `sks tmux open` when you need explicit session/workspace flags, and `sks help` for CLI help.'],
1452
1456
  openclaw: ['OpenClaw', '', ' sks openclaw install', ' sks openclaw path', ' sks openclaw print SKILL.md', '', 'Installs an OpenClaw skill package under ~/.openclaw/skills/sneakoscope-codex so OpenClaw agents can attach skills: [sneakoscope-codex] with the shell tool and call local SKS commands from a project root.'],
1453
- team: ['Team', '', ' sks team "task" executor:5 reviewer:6 user:1', ' sks team watch latest', ' sks team lane latest --agent analysis_scout_1 --follow', ' sks team message latest --from analysis_scout_1 --to executor_1 --message "handoff note"', ' sks team cleanup-tmux latest', '', '$Team auto-seals a route contract, opens scout-first tmux lanes when available, then runs scouts -> TriWiki attention -> debate -> runtime graph/inbox -> fresh executors -> review -> cleanup -> reflection -> Honest.'],
1457
+ team: ['Team', '', ' sks team "task" executor:5 reviewer:6 user:1', ' sks team open-tmux latest', ' sks team watch latest', ' sks team lane latest --agent analysis_scout_1 --follow', ' sks team message latest --from analysis_scout_1 --to executor_1 --message "handoff note"', ' sks team cleanup-tmux latest', '', '$Team auto-seals a route contract, opens scout-first tmux lanes when available, then runs scouts -> TriWiki attention -> debate -> runtime graph/inbox -> fresh executors -> review -> cleanup -> reflection -> Honest.'],
1454
1458
  'qa-loop': ['QA-LOOP', '', ' sks qa-loop prepare "QA this app"', ' sks qa-loop answer <MISSION_ID> answers.json', ' sks qa-loop run <MISSION_ID> --max-cycles 8', '', 'Report: YYYY-MM-DD-v<version>-qa-report.md'],
1455
1459
  ppt: ['PPT', '', ' $PPT 투자자용 피치덱을 HTML 기반 PDF로 만들어줘', ' $PPT 우리 SaaS 소개자료 만들어줘', ' sks ppt build latest --json', ' sks ppt status latest --json', '', '$PPT infers delivery context, audience profile, STP strategy, decision context, and 3+ pain-point/solution/aha mappings before source research, design-system work, HTML/PDF export, render QA, fact-ledger validation, and bounded review-loop validation. Independent strategy/render/file-write phases run in parallel where inputs allow and are recorded in ppt-parallel-report.json. The visual system must stay simple, restrained, and information-first; editable source HTML is kept under source-html/, PPT-only temporary build files are cleaned, and installed skills/MCPs outside the $PPT allowlist are ignored. Design uses getdesign-reference plus the built-in PPT design pipeline; Codex App $imagegen/gpt-image-2 and Context7 are conditional only when the sealed PPT contract needs raster assets, slide visual critique, or current external docs. Missing required $imagegen/gpt-image-2 output blocks instead of being simulated.'],
1456
1460
  'image-ux-review': ['Image UX Review', '', ' $Image-UX-Review localhost 화면을 이미지 생성 리뷰 루프로 검수해줘', ' $UX-Review 이 스크린샷을 gpt-image-2 콜아웃 리뷰로 분석하고 고쳐줘', ' sks image-ux-review status latest --json', '', '$Image-UX-Review captures or receives source UI screenshots, runs Codex App $imagegen/gpt-image-2 to create generated annotated review images with numbered callouts, then extracts those generated images into image-ux-issue-ledger.json. Text-only screenshot critique cannot pass image-ux-review-gate.json; missing generated review images remain an explicit blocker.'],
@@ -1589,7 +1593,7 @@ async function setup(args) {
1589
1593
  next: ['sks context7 check', 'sks selftest --mock', 'sks doctor', 'sks commands']
1590
1594
  };
1591
1595
  if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
1592
- console.log('ㅅㅋㅅ Setup\n');
1596
+ console.log(`${sksAsciiLogo()}\nSetup\n`);
1593
1597
  console.log(`Project: ${root}`);
1594
1598
  console.log(`Install: ${install.ok ? 'ok' : 'missing'} ${install.scope} (${install.command_prefix})`);
1595
1599
  console.log(`CLI tools: Codex ${formatCodexCliToolStatus(cliTools.codex)}; tmux ${tmuxStatusKind(cliTools.tmux)} ${cliTools.tmux.version || cliTools.tmux.error || ''}`.trimEnd());
@@ -1713,7 +1717,7 @@ async function doctor(args) {
1713
1717
  result.ready = !result.harness_conflicts.hard_block && nodeOk && Boolean(codex.bin) && install.ok && result.sneakoscope.ok && result.context7.ok && appRuntime.ok && result.runtime.tmux.ok && result.harness_guard.ok && result.versioning.ok && result.db_guard.ok && result.codex_app.ok && result.skills.ok && result.global_skills.ok;
1714
1718
  if (result.harness_conflicts.hard_block) process.exitCode = 1;
1715
1719
  if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
1716
- console.log('ㅅㅋㅅ Doctor\n');
1720
+ console.log(`${sksAsciiLogo()}\nDoctor\n`);
1717
1721
  console.log(`Node: ${nodeOk ? 'ok' : 'fail'} ${process.version}`);
1718
1722
  console.log(`Project: ${root}`);
1719
1723
  console.log(`Codex: ${codex.bin ? 'ok' : 'missing'} ${codex.version || ''}`);
@@ -1779,7 +1783,7 @@ async function init(args) {
1779
1783
  const localOnly = flag(args, '--local-only');
1780
1784
  const globalCommand = await globalSksCommand();
1781
1785
  const res = await initProject(root, { force: flag(args, '--force'), installScope, globalCommand, localOnly });
1782
- console.log(`Initialized ㅅㅋㅅ in ${root}`);
1786
+ console.log(`Initialized SKS in ${root}`);
1783
1787
  console.log(`Install scope: ${installScope} (${sksCommandPrefix(installScope, { globalCommand })})`);
1784
1788
  if (localOnly) console.log('Git mode: local-only (.git/info/exclude)');
1785
1789
  else console.log('Git mode: shared .gitignore');
@@ -1920,7 +1924,7 @@ function hasTopLevelCodexModeLock(text = '') {
1920
1924
  return (Boolean(model) && model !== 'gpt-5.5') || /^model_reasoning_effort\s*=/m.test(top);
1921
1925
  }
1922
1926
 
1923
- function hasLegacyHooksFeatureFlag(text = '') {
1927
+ function hasDeprecatedCodexHooksFeatureFlag(text = '') {
1924
1928
  const lines = String(text || '').split('\n');
1925
1929
  const start = lines.findIndex((line) => line.trim() === '[features]');
1926
1930
  if (start === -1) return false;
@@ -1931,7 +1935,13 @@ function hasLegacyHooksFeatureFlag(text = '') {
1931
1935
  break;
1932
1936
  }
1933
1937
  }
1934
- return lines.slice(start + 1, end).some((line) => /^\s*hooks\s*=/.test(line));
1938
+ return lines.slice(start + 1, end).some((line) => /^\s*codex_hooks\s*=/.test(line));
1939
+ }
1940
+
1941
+ const REQUIRED_GENERATED_CODEX_APP_FEATURE_FLAGS = ['hooks', 'multi_agent', 'fast_mode', 'fast_mode_ui', 'codex_git_commit', 'computer_use', 'apps', 'plugins'];
1942
+
1943
+ function missingGeneratedCodexAppFeatureFlags(text = '') {
1944
+ return REQUIRED_GENERATED_CODEX_APP_FEATURE_FLAGS.filter((name) => !String(text || '').includes(`${name} = true`));
1935
1945
  }
1936
1946
 
1937
1947
  async function resolveMissionId(root, arg) { return (!arg || arg === 'latest') ? findLatestMission(root) : arg; }
@@ -2148,6 +2158,8 @@ async function selftest() {
2148
2158
  if (await exists(path.join(postinstallSetupTmp, '.agents', 'skills', 'agent-team', 'SKILL.md'))) throw new Error('selftest failed: postinstall installed deprecated agent-team fallback skill');
2149
2159
  if (!String(postinstallSetup.stdout || '').includes('SKS bootstrap: auto-running sks setup --bootstrap --install-scope global --force') || !String(postinstallSetup.stdout || '').includes('SKS Ready')) throw new Error('selftest failed: postinstall did not auto-run global forced bootstrap');
2150
2160
  if (!(await exists(path.join(postinstallSetupTmp, '.codex', 'hooks.json')))) throw new Error('selftest failed: postinstall did not create project hooks during automatic bootstrap');
2161
+ const postinstallSetupConfig = await safeReadText(path.join(postinstallSetupTmp, '.codex', 'config.toml'));
2162
+ if (missingGeneratedCodexAppFeatureFlags(postinstallSetupConfig).length || hasDeprecatedCodexHooksFeatureFlag(postinstallSetupConfig)) throw new Error('selftest failed: postinstall automatic bootstrap did not preserve required Codex App feature flags or migrate deprecated codex_hooks');
2151
2163
  if (!String(postinstallSetup.stdout || '').includes('Codex App global $ skills: installed')) throw new Error('selftest failed: postinstall did not report automatic global Codex App skills');
2152
2164
  if (!String(postinstallSetup.stdout || '').includes('Removed stale generated skill shadow(s):')) throw new Error('selftest failed: postinstall did not report stale first-party plugin shadow cleanup');
2153
2165
  const postinstallSetupManifest = await readJson(path.join(postinstallSetupTmp, '.sneakoscope', 'manifest.json'));
@@ -2182,6 +2194,8 @@ async function selftest() {
2182
2194
  if (postinstallNoMarker.code !== 0) throw new Error(`selftest failed: no-marker postinstall bootstrap exited ${postinstallNoMarker.code}: ${postinstallNoMarker.stderr}`);
2183
2195
  if (!String(postinstallNoMarker.stdout || '').includes('no project marker found; auto-running global SKS runtime bootstrap')) throw new Error('selftest failed: no-marker postinstall did not report global runtime bootstrap');
2184
2196
  if (!(await exists(path.join(postinstallNoMarkerGlobalRoot, '.sneakoscope', 'manifest.json')))) throw new Error('selftest failed: no-marker postinstall did not bootstrap global runtime root');
2197
+ const postinstallNoMarkerConfig = await safeReadText(path.join(postinstallNoMarkerGlobalRoot, '.codex', 'config.toml'));
2198
+ if (missingGeneratedCodexAppFeatureFlags(postinstallNoMarkerConfig).length || hasDeprecatedCodexHooksFeatureFlag(postinstallNoMarkerConfig)) throw new Error('selftest failed: no-marker postinstall bootstrap did not preserve required Codex App feature flags or migrate deprecated codex_hooks');
2185
2199
  if (await exists(path.join(postinstallNoMarkerCwd, '.sneakoscope'))) throw new Error('selftest failed: no-marker postinstall polluted install cwd');
2186
2200
  if (await exists(path.join(postinstallNoMarkerGlobalRoot, '.gitignore'))) throw new Error('selftest failed: global runtime bootstrap without project git wrote shared .gitignore');
2187
2201
  const bootstrapJsonTmp = tmpdir();
@@ -2474,6 +2488,13 @@ async function selftest() {
2474
2488
  const camelHookGuardResult = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'pre-tool'], { cwd: tmp, input: camelHookGuardPayload, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2475
2489
  const camelHookGuardJson = JSON.parse(camelHookGuardResult.stdout);
2476
2490
  if (camelHookGuardJson.decision !== 'block') throw new Error('selftest failed: hook did not block camelCase Codex tool payload');
2491
+ await setCurrent(tmp, { mode: 'QALOOP', phase: 'QALOOP_RUNNING_NO_QUESTIONS', route: 'QALoop', implementation_allowed: true });
2492
+ const codexGitPermissionResult = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'permission-request'], { cwd: tmp, input: JSON.stringify({ cwd: tmp, command: 'git push origin dev', action: 'Codex App Git Actions Push' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2493
+ const codexGitPermissionJson = JSON.parse(codexGitPermissionResult.stdout);
2494
+ if (codexGitPermissionJson.hookSpecificOutput?.decision?.behavior === 'deny') throw new Error('selftest failed: Codex App git push permission was denied during no-question mode');
2495
+ const codexGitForcePermissionResult = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'permission-request'], { cwd: tmp, input: JSON.stringify({ cwd: tmp, command: 'git push --force origin dev', action: 'Codex App Git Actions Push' }), env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2496
+ const codexGitForcePermissionJson = JSON.parse(codexGitForcePermissionResult.stdout);
2497
+ if (codexGitForcePermissionJson.hookSpecificOutput?.decision?.behavior !== 'deny') throw new Error('selftest failed: force-push permission should stay denied during no-question mode');
2477
2498
  if (new Set(DOLLAR_COMMANDS.map((c) => c.command)).size !== DOLLAR_COMMANDS.length) throw new Error('selftest failed: duplicate dollar commands');
2478
2499
  if (!DOLLAR_COMMAND_ALIASES.some((alias) => alias.canonical === '$QA-LOOP' && alias.app_skill === '$qa-loop')) throw new Error('selftest failed: $QA-LOOP picker skill missing');
2479
2500
  if (!DOLLAR_COMMAND_ALIASES.some((alias) => alias.canonical === '$Team' && alias.app_skill === '$from-chat-img')) throw new Error('selftest failed: $From-Chat-IMG picker skill missing');
@@ -2929,8 +2950,8 @@ async function selftest() {
2929
2950
  if (wikiContext.includes('MANDATORY ambiguity-removal gate activated') || wikiContext.includes('Mission:')) throw new Error('selftest failed: Wiki route created ambiguity mission state');
2930
2951
  if (!wikiJson.systemMessage?.includes('wiki refresh')) throw new Error('selftest failed: Wiki route missing system message');
2931
2952
  const codexConfigText = await safeReadText(path.join(tmp, '.codex', 'config.toml'));
2932
- if (!codexConfigText.includes('multi_agent = true')) throw new Error('selftest failed: multi_agent not enabled');
2933
- if (!codexConfigText.includes('codex_hooks = true') || hasLegacyHooksFeatureFlag(codexConfigText)) throw new Error('selftest failed: Codex hooks feature flag not aligned with current codex_hooks setting');
2953
+ const missingCodexConfigFlags = missingGeneratedCodexAppFeatureFlags(codexConfigText);
2954
+ if (missingCodexConfigFlags.length || hasDeprecatedCodexHooksFeatureFlag(codexConfigText)) throw new Error(`selftest failed: generated Codex App feature flags missing or deprecated: ${missingCodexConfigFlags.join(', ')}`);
2934
2955
  if (!hasContext7ConfigText(codexConfigText)) throw new Error('selftest failed: Context7 MCP not configured');
2935
2956
  if (!codexConfigText.includes('[profiles.sks-task-low]') || !codexConfigText.includes('[profiles.sks-task-medium]') || !codexConfigText.includes('[profiles.sks-logic-high]') || !codexConfigText.includes('[profiles.sks-fast-high]') || !codexConfigText.includes('[profiles.sks-research-xhigh]') || !codexConfigText.includes('[profiles.sks-mad-high]')) throw new Error('selftest failed: GPT-5.5 reasoning profiles not configured');
2936
2957
  if (!/\[profiles\.sks-mad-high\][\s\S]*?approval_policy = "never"[\s\S]*?sandbox_mode = "danger-full-access"/.test(codexConfigText)) throw new Error('selftest failed: generated sks-mad-high profile is not full access');
@@ -2938,13 +2959,29 @@ async function selftest() {
2938
2959
  if (!codexConfigText.includes('[agents.team_consensus]')) throw new Error('selftest failed: team_consensus agent not configured');
2939
2960
  const preservedConfigTmp = tmpdir();
2940
2961
  await ensureDir(path.join(preservedConfigTmp, '.codex'));
2941
- await writeTextAtomic(path.join(preservedConfigTmp, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "high"\nservice_tier = "fast"\n\n[notice]\nfast_default_opt_out = true\nkeep = true\n\n[features]\nhooks = true\nfast_mode_ui = true\n\n[user.fast_mode]\nvisible = true\n');
2962
+ await writeTextAtomic(path.join(preservedConfigTmp, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "high"\nservice_tier = "fast"\n\n[notice]\nfast_default_opt_out = true\nkeep = true\n\n[features]\ncodex_hooks = true\nfast_mode_ui = false\ncodex_git_commit = false\ncomputer_use = false\napps = false\nplugins = false\ncustom_preview = true\n\n[user.fast_mode]\nvisible = true\n');
2942
2963
  await initProject(preservedConfigTmp, {});
2943
2964
  const preservedConfig = await safeReadText(path.join(preservedConfigTmp, '.codex', 'config.toml'));
2944
2965
  if (!/^model = "gpt-5\.5"/m.test(preservedConfig) || !preservedConfig.includes('service_tier = "fast"') || !preservedConfig.includes('fast_mode = true') || !preservedConfig.includes('fast_mode_ui = true') || !preservedConfig.includes('[user.fast_mode]') || !preservedConfig.includes('visible = true') || !preservedConfig.includes('enabled = true') || !preservedConfig.includes('default_profile = "sks-fast-high"') || !/\[profiles\.sks-fast-high\][\s\S]*?service_tier = "fast"/.test(preservedConfig)) throw new Error('selftest failed: Codex config merge dropped or failed to enable Fast mode defaults and GPT-5.5');
2945
2966
  if (preservedConfig.includes('fast_default_opt_out = true') || !preservedConfig.includes('keep = true')) throw new Error('selftest failed: Codex config merge did not remove stale Fast opt-out notice while preserving other notice keys');
2946
- if (!preservedConfig.includes('codex_hooks = true') || hasLegacyHooksFeatureFlag(preservedConfig) || !preservedConfig.includes('[profiles.sks-fast-high]')) throw new Error('selftest failed: Codex config merge did not add current SKS hook settings or remove the legacy hooks flag');
2967
+ const missingPreservedFlags = missingGeneratedCodexAppFeatureFlags(preservedConfig);
2968
+ if (missingPreservedFlags.length || hasDeprecatedCodexHooksFeatureFlag(preservedConfig) || !preservedConfig.includes('custom_preview = true') || !preservedConfig.includes('[profiles.sks-fast-high]')) throw new Error(`selftest failed: Codex config merge did not add required app feature flags, preserve existing feature flags, or remove deprecated codex_hooks: ${missingPreservedFlags.join(', ')}`);
2947
2969
  if (hasTopLevelCodexModeLock(preservedConfig)) throw new Error('selftest failed: Codex config merge left top-level legacy model/reasoning locks that hide Fast mode UI');
2970
+ const appFeatureTmp = tmpdir();
2971
+ const fakeCodexApp = path.join(appFeatureTmp, 'Codex.app');
2972
+ const fakeCodexBinDir = path.join(appFeatureTmp, 'bin');
2973
+ await ensureDir(fakeCodexApp);
2974
+ await ensureDir(fakeCodexBinDir);
2975
+ const fakeCodex = path.join(fakeCodexBinDir, 'codex');
2976
+ await writeTextAtomic(fakeCodex, '#!/bin/sh\nif [ "$1" = "mcp" ] && [ "$2" = "list" ]; then printf "%s\\n" "computer-use enabled" "browser-use enabled"; exit 0; fi\nif [ "$1" = "features" ] && [ "$2" = "list" ]; then cat <<EOF\napps stable true\ncodex_git_commit under development true\ncomputer_use stable true\nfast_mode stable true\nhooks stable true\nimage_generation stable true\nplugins stable true\nEOF\nexit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
2977
+ await fsp.chmod(fakeCodex, 0o755);
2978
+ const codexAppFeatureStatus = await codexAppIntegrationStatus({ codex: { bin: fakeCodex, version: 'codex-cli 99.0.0' }, home: appFeatureTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } });
2979
+ if (!codexAppFeatureStatus.ok || !codexAppFeatureStatus.features?.required_flags_ok || !codexAppFeatureStatus.features?.codex_git_commit) throw new Error('selftest failed: codex-app check did not accept required app feature flags including under-development codex_git_commit');
2980
+ const fakeCodexMissing = path.join(fakeCodexBinDir, 'codex-missing-git-commit');
2981
+ await writeTextAtomic(fakeCodexMissing, '#!/bin/sh\nif [ "$1" = "mcp" ] && [ "$2" = "list" ]; then printf "%s\\n" "computer-use enabled" "browser-use enabled"; exit 0; fi\nif [ "$1" = "features" ] && [ "$2" = "list" ]; then cat <<EOF\napps stable true\ncodex_git_commit under development false\ncomputer_use stable true\nfast_mode stable true\nhooks stable true\nimage_generation stable true\nplugins stable true\nEOF\nexit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
2982
+ await fsp.chmod(fakeCodexMissing, 0o755);
2983
+ const codexAppMissingFeatureStatus = await codexAppIntegrationStatus({ codex: { bin: fakeCodexMissing, version: 'codex-cli 99.0.0' }, home: appFeatureTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } });
2984
+ if (codexAppMissingFeatureStatus.ok || codexAppMissingFeatureStatus.features?.required_flags_ok || codexAppMissingFeatureStatus.features?.codex_git_commit) throw new Error('selftest failed: codex-app check did not block disabled codex_git_commit feature flag');
2948
2985
  const autoReviewHome = path.join(tmp, 'auto-review-home');
2949
2986
  const autoReviewEnv = { HOME: autoReviewHome };
2950
2987
  const autoReviewEnabled = await enableAutoReview({ env: autoReviewEnv, high: true });
@@ -3160,7 +3197,9 @@ async function selftest() {
3160
3197
  await writeJsonAtomic(path.join(teamDir, 'team-plan.json'), teamPlan);
3161
3198
  if (teamPlan.agent_session_count !== 5) throw new Error('selftest failed: team default sessions not 5');
3162
3199
  if (teamPlan.role_counts.executor !== 3 || teamPlan.role_counts.user !== 1 || teamPlan.role_counts.reviewer !== 5) throw new Error('selftest failed: team default role counts invalid');
3163
- if (teamPlan.codex_config_required?.features?.codex_hooks !== true || teamPlan.codex_config_required?.features?.hooks === true) throw new Error('selftest failed: team plan Codex config still uses legacy hooks feature flag');
3200
+ const teamPlanFeatureFlags = teamPlan.codex_config_required?.features || {};
3201
+ const missingTeamPlanFeatureFlags = REQUIRED_GENERATED_CODEX_APP_FEATURE_FLAGS.filter((name) => teamPlanFeatureFlags[name] !== true);
3202
+ if (missingTeamPlanFeatureFlags.length || teamPlanFeatureFlags.codex_hooks === true) throw new Error(`selftest failed: team plan Codex config missing required app flags or still uses deprecated codex_hooks: ${missingTeamPlanFeatureFlags.join(', ')}`);
3164
3203
  if (!teamPlan.review_gate?.passed || teamPlan.review_gate.required_reviewer_lanes !== 5) throw new Error('selftest failed: team review policy gate did not pass default plan');
3165
3204
  if (teamPlan.codex_config_required?.service_tier !== 'fast' || teamPlan.reasoning?.service_tier !== 'fast') throw new Error('selftest failed: team plan did not require Fast service tier');
3166
3205
  if (!teamPlan.goal_continuation?.enabled || teamPlan.goal_continuation?.mode !== 'ambient_codex_native_goal_overlay') throw new Error('selftest failed: Team plan did not include ambient Goal continuation');
@@ -3199,6 +3238,7 @@ async function selftest() {
3199
3238
  if (!fromChatTeamPlan.invariants.some((item) => item.includes(FROM_CHAT_IMG_CHECKLIST_ARTIFACT)) || !fromChatTeamPlan.invariants.some((item) => item.includes(FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT)) || !fromChatTeamPlan.invariants.some((item) => item.includes(FROM_CHAT_IMG_QA_LOOP_ARTIFACT))) throw new Error('selftest failed: From-Chat-IMG team plan missing checklist/temp TriWiki/QA invariants');
3200
3239
  const teamWorkflow = teamWorkflowMarkdown(teamPlan);
3201
3240
  if (!teamWorkflow.includes('SSOT: triwiki') || !teamWorkflow.includes('Analysis Scouts') || !teamWorkflow.includes('sks wiki validate')) throw new Error('selftest failed: team workflow missing scout-first TriWiki context tracking');
3241
+ if (!teamWorkflow.includes('sks team open-tmux')) throw new Error('selftest failed: team workflow missing existing-mission tmux open command');
3202
3242
  if (!teamWorkflow.includes(TEAM_GRAPH_ARTIFACT) || !teamWorkflow.includes(TEAM_INBOX_DIR)) throw new Error('selftest failed: team workflow missing runtime graph/inbox guidance');
3203
3243
  if (!teamWorkflow.includes('before every stage') || !teamWorkflow.includes('after findings/artifact changes')) throw new Error('selftest failed: team workflow missing per-stage TriWiki policy');
3204
3244
  const customTeamPlan = buildTeamPlan(teamId, '병렬 구현 팀 테스트', { agentSessions: 5 });
@@ -3232,7 +3272,7 @@ async function selftest() {
3232
3272
  await ensureDir(fakeTmuxDir);
3233
3273
  const fakeTmuxLog = path.join(fakeTmuxDir, 'tmux.log');
3234
3274
  const fakeTmuxBin = path.join(fakeTmuxDir, 'tmux');
3235
- await writeTextAtomic(fakeTmuxBin, `#!/usr/bin/env node\nconst fs = require('node:fs');\nconst log = process.env.SKS_FAKE_TMUX_LOG;\nif (log) fs.appendFileSync(log, process.argv.slice(2).join(' ') + '\\n');\nconst cmd = process.argv[2];\nif (cmd === 'has-session') process.exit(0);\nif (cmd === 'kill-session') process.exit(0);\nif (cmd === 'new-session') { console.log('%1'); process.exit(0); }\nif (cmd === 'split-window') { console.log('%2'); process.exit(0); }\nprocess.exit(0);\n`);
3275
+ await writeTextAtomic(fakeTmuxBin, `#!/usr/bin/env node\nconst fs = require('node:fs');\nconst log = process.env.SKS_FAKE_TMUX_LOG;\nif (log) fs.appendFileSync(log, process.argv.slice(2).join(' ') + '\\n');\nconst cmd = process.argv[2];\nif (cmd === 'has-session') process.exit(0);\nif (cmd === 'kill-session') process.exit(0);\nif (cmd === 'kill-pane') process.exit(0);\nif (cmd === 'new-session') { console.log('%1'); process.exit(0); }\nif (cmd === 'split-window') { console.log(process.env.SKS_FAKE_TMUX_SPLIT_ID || '%2'); process.exit(0); }\nif (cmd === 'list-windows') { console.log('@1'); process.exit(0); }\nif (cmd === 'display-message') { console.log(process.env.SKS_FAKE_TMUX_DISPLAY || 'sks-existing-selftest\\t@1\\t%1'); process.exit(0); }\nif (cmd === 'list-panes') { console.log(process.env.SKS_FAKE_TMUX_LIST || ''); process.exit(0); }\nif (cmd === 'set-option' || cmd === 'select-layout' || cmd === 'resize-window' || cmd === 'set-window-option' || cmd === 'set-hook') process.exit(0);\nprocess.exit(0);\n`);
3236
3276
  await fsp.chmod(fakeTmuxBin, 0o755);
3237
3277
  const previousFakeTmuxLog = process.env.SKS_FAKE_TMUX_LOG;
3238
3278
  process.env.SKS_FAKE_TMUX_LOG = fakeTmuxLog;
@@ -3242,6 +3282,50 @@ async function selftest() {
3242
3282
  ], { recreate: true });
3243
3283
  const fakeTmuxLogText = await safeReadText(fakeTmuxLog);
3244
3284
  if (!recreatedTmux.ok || !fakeTmuxLogText.includes('kill-session -t sks-existing-selftest') || !fakeTmuxLogText.includes('new-session') || !fakeTmuxLogText.includes('split-window')) throw new Error('selftest failed: tmux recreate did not replace stale existing session with split panes');
3285
+ if (!recreatedTmux.dynamic_resize?.enabled || !fakeTmuxLogText.includes('list-windows -t sks-existing-selftest -F #{window_id}') || !fakeTmuxLogText.includes('set-window-option -t @1 window-size latest') || !fakeTmuxLogText.includes('set-hook -t sks-existing-selftest client-resized') || !fakeTmuxLogText.includes('resize-window -t @1 -A')) throw new Error('selftest failed: tmux dynamic resize hooks were not installed for split panes');
3286
+ if (recreatedTmux.layout !== 'tiled' || Number(recreatedTmux.initial_size?.width || 0) < 120 || Number(recreatedTmux.initial_size?.height || 0) < 36) throw new Error('selftest failed: tmux dynamic resize metadata missing normalized initial size/layout');
3287
+ await ensureDir(path.join(tmp, '.sneakoscope', 'state'));
3288
+ await writeJsonAtomic(path.join(tmp, '.sneakoscope', 'state', 'tmux-sessions.json'), {
3289
+ schema_version: 1,
3290
+ sessions: {
3291
+ 'sks-existing-selftest': {
3292
+ session: 'sks-existing-selftest',
3293
+ root: tmp,
3294
+ panes: [{ pane_id: '%1', role: 'codex', title: 'Codex CLI' }]
3295
+ }
3296
+ }
3297
+ });
3298
+ await writeTextAtomic(fakeTmuxLog, '');
3299
+ process.env.SKS_FAKE_TMUX_DISPLAY = 'sks-existing-selftest\t@1\t%1';
3300
+ process.env.SKS_FAKE_TMUX_LIST = '';
3301
+ process.env.SKS_FAKE_TMUX_SPLIT_ID = '%80';
3302
+ const cockpitOpen = await reconcileTmuxTeamCockpit({
3303
+ root: tmp,
3304
+ missionId: teamId,
3305
+ plan: roleTeamPlan,
3306
+ dashboard: { agents: { analysis_scout_1: { status: 'assigned' } } },
3307
+ control: { status: 'running' },
3308
+ tmux: { bin: fakeTmuxBin },
3309
+ env: { ...process.env, TMUX: '/tmp/tmux-selftest/default,1,0' }
3310
+ });
3311
+ const cockpitOpenLog = await safeReadText(fakeTmuxLog);
3312
+ if (!cockpitOpen.ok || cockpitOpen.opened_lane_count !== 2 || !cockpitOpenLog.includes('display-message -p') || !cockpitOpenLog.includes('split-window -t @1') || !cockpitOpenLog.includes('set-option -pt %80 @sks_team_managed 1') || !cockpitOpenLog.includes('select-layout -t @1 tiled')) throw new Error('selftest failed: dynamic Team cockpit did not split managed panes in the current SKS tmux session');
3313
+ await writeTextAtomic(fakeTmuxLog, '');
3314
+ process.env.SKS_FAKE_TMUX_LIST = `%81\tscout: analysis_scout_1\tnode\t1\t${teamId}\tanalysis_scout_1\tscout\n%82\tscout: analysis_scout_2\tnode\t1\t${teamId}\tanalysis_scout_2\tscout\n%83\tuser pane\tzsh\t\t\t\t`;
3315
+ const cockpitClose = await reconcileTmuxTeamCockpit({
3316
+ root: tmp,
3317
+ missionId: teamId,
3318
+ plan: roleTeamPlan,
3319
+ dashboard: { agents: { analysis_scout_1: { status: 'completed' }, analysis_scout_2: { status: 'assigned' } } },
3320
+ control: { status: 'running' },
3321
+ tmux: { bin: fakeTmuxBin },
3322
+ env: { ...process.env, TMUX: '/tmp/tmux-selftest/default,1,0' }
3323
+ });
3324
+ const cockpitCloseLog = await safeReadText(fakeTmuxLog);
3325
+ if (!cockpitClose.ok || cockpitClose.closed_lane_count !== 1 || !cockpitCloseLog.includes('kill-pane -t %81') || cockpitCloseLog.includes('kill-pane -t %83')) throw new Error('selftest failed: dynamic Team cockpit did not close only stale managed panes');
3326
+ delete process.env.SKS_FAKE_TMUX_DISPLAY;
3327
+ delete process.env.SKS_FAKE_TMUX_LIST;
3328
+ delete process.env.SKS_FAKE_TMUX_SPLIT_ID;
3245
3329
  await writeTextAtomic(fakeTmuxLog, '');
3246
3330
  const madCockpit = await launchMadTmuxUi(['--workspace', 'sks-mad-selftest-ui', '--no-attach'], { root: tmp, tmux: { ok: true, bin: fakeTmuxBin, version: '3.4' }, codex: { bin: process.execPath }, app: { ok: true, guidance: [] }, missionId: 'M-MAD-SELFTEST' });
3247
3331
  const madTmuxLogText = await safeReadText(fakeTmuxLog);
@@ -3333,6 +3417,7 @@ async function selftest() {
3333
3417
  if (!teamDashboard?.latest_messages?.some((entry) => entry.agent === 'team_consensus')) throw new Error('selftest failed: team live dashboard missing agent event');
3334
3418
  const teamLive = await readTeamLive(teamDir);
3335
3419
  if (!teamLive.includes('Analysis scouts') || !teamLive.includes('selftest mapped repo slice')) throw new Error('selftest failed: team live transcript missing analysis scout section/event');
3420
+ if (!teamLive.includes('sks team open-tmux')) throw new Error('selftest failed: team live transcript missing existing-mission tmux open command');
3336
3421
  if (!teamLive.includes('selftest mapped options')) throw new Error('selftest failed: team live transcript missing event');
3337
3422
  if (!teamLive.includes('Context tracking SSOT: TriWiki')) throw new Error('selftest failed: team live transcript missing TriWiki context tracking');
3338
3423
  if (!(await readTeamTranscriptTail(teamDir, 1)).join('\n').includes('selftest mapped options')) throw new Error('selftest failed: team transcript tail missing event');
@@ -3669,6 +3754,6 @@ async function selftest() {
3669
3754
  const gc = await enforceRetention(tmp, { dryRun: true });
3670
3755
  if (!gc.report.exists) throw new Error('selftest failed: storage report');
3671
3756
  if (!gc.actions.some((action) => action.action === 'remove_from_chat_img_temp_triwiki')) throw new Error('selftest failed: From-Chat-IMG temporary TriWiki retention action missing');
3672
- console.log('ㅅㅋㅅ selftest passed.');
3757
+ console.log(`${sksAsciiLogo()}\nselftest passed.`);
3673
3758
  console.log(`temp: ${tmp}`);
3674
3759
  }
@@ -30,7 +30,7 @@ import { PIPELINE_PLAN_ARTIFACT, validatePipelinePlan, writePipelinePlan } from
30
30
  import { GOAL_BRIDGE_ARTIFACT, GOAL_WORKFLOW_ARTIFACT, updateGoalWorkflow, writeGoalWorkflow } from '../core/goal-workflow.mjs';
31
31
  import { scanCodeStructure, writeCodeStructureReport } from '../core/code-structure.mjs';
32
32
  import { writeMemorySweepReport } from '../core/memory-governor.mjs';
33
- import { cleanupTmuxTeamView, defaultTmuxSessionName, launchMadTmuxUi, launchTmuxTeamView, sanitizeTmuxSessionName } from '../core/tmux-ui.mjs';
33
+ import { cleanupTmuxTeamView, defaultTmuxSessionName, launchMadTmuxUi, launchTmuxTeamView, reconcileTmuxTeamCockpit, sanitizeTmuxSessionName } from '../core/tmux-ui.mjs';
34
34
  import { loadSkillDreamState, recordSkillDreamEvent, runSkillDream, writeSkillForgeReport } from '../core/skill-forge.mjs';
35
35
  import { writeMistakeMemoryReport } from '../core/mistake-memory.mjs';
36
36
  import { checkDbOperation, checkSqlFile, classifyCommand, classifySql, loadDbSafetyPolicy, safeSupabaseMcpConfig, scanDbSafety } from '../core/db-safety.mjs';
@@ -516,7 +516,7 @@ async function goalCreate(args) {
516
516
  if (!prompt) throw new Error('Missing goal task prompt.');
517
517
  const { id, dir, mission } = await createMission(root, { mode: 'goal', prompt });
518
518
  const workflow = await writeGoalWorkflow(dir, mission, { action: 'create', prompt });
519
- await setCurrent(root, { mission_id: id, mode: 'GOAL', route: 'Goal', route_command: '$Goal', phase: 'GOAL_READY', questions_allowed: true, implementation_allowed: true, native_goal: workflow.native_goal, stop_gate: 'none' });
519
+ await setCurrent(root, { mission_id: id, mode: 'GOAL', route: 'Goal', route_command: '$Goal', phase: 'GOAL_READY', questions_allowed: true, implementation_allowed: true, native_goal: workflow.native_goal, stop_gate: 'none' }, { replace: true });
520
520
  console.log(`Goal mission created: ${id}`);
521
521
  console.log(`Artifact: ${path.relative(root, path.join(dir, GOAL_WORKFLOW_ARTIFACT))}`);
522
522
  console.log(`Bridge: ${path.relative(root, path.join(dir, GOAL_BRIDGE_ARTIFACT))}`);
@@ -529,7 +529,7 @@ async function goalControl(action, args) {
529
529
  if (!id) throw new Error(`Usage: sks goal ${action} <mission-id|latest>`);
530
530
  const { dir } = await loadMission(root, id);
531
531
  const workflow = await updateGoalWorkflow(dir, action);
532
- await setCurrent(root, { mission_id: id, mode: 'GOAL', route: 'Goal', route_command: '$Goal', phase: `GOAL_${String(action).toUpperCase()}`, native_goal: workflow.native_goal, questions_allowed: true, implementation_allowed: action !== 'pause' && action !== 'clear', stop_gate: 'none' });
532
+ await setCurrent(root, { mission_id: id, mode: 'GOAL', route: 'Goal', route_command: '$Goal', phase: `GOAL_${String(action).toUpperCase()}`, native_goal: workflow.native_goal, questions_allowed: true, implementation_allowed: action !== 'pause' && action !== 'clear', stop_gate: 'none' }, { replace: true });
533
533
  console.log(`Goal ${action}: ${id}`);
534
534
  console.log(`Native Codex control: ${workflow.native_goal.slash_command}`);
535
535
  }
@@ -1626,7 +1626,7 @@ export async function gxCommand(sub, args) {
1626
1626
  }
1627
1627
 
1628
1628
  export async function team(args) {
1629
- const teamSubcommands = new Set(['log', 'tail', 'watch', 'lane', 'status', 'dashboard', 'event', 'message', 'cleanup-tmux']);
1629
+ const teamSubcommands = new Set(['log', 'tail', 'watch', 'lane', 'status', 'dashboard', 'event', 'message', 'open-tmux', 'attach-tmux', 'cleanup-tmux']);
1630
1630
  if (teamSubcommands.has(args[0])) return teamCommand(args[0], args.slice(1));
1631
1631
  const jsonOutput = flag(args, '--json');
1632
1632
  const openTmux = !jsonOutput && !flag(args, '--no-open-tmux') && !flag(args, '--no-tmux');
@@ -1635,7 +1635,7 @@ export async function team(args) {
1635
1635
  const { prompt, agentSessions, roleCounts, roster } = opts;
1636
1636
  if (!prompt) {
1637
1637
  console.error('Usage: sks team "task" [executor:5 reviewer:6 user:1] [--agents N] [--no-open-tmux] [--json]');
1638
- console.error(' sks team log|tail|watch|lane|status|message|cleanup-tmux [mission-id|latest]');
1638
+ console.error(' sks team log|tail|watch|lane|status|message|open-tmux|attach-tmux|cleanup-tmux [mission-id|latest]');
1639
1639
  console.error(' sks team event [mission-id|latest] --agent <name> --phase <phase> --message "..."');
1640
1640
  console.error(' sks team message [mission-id|latest] --from <agent> --to <agent|all> --message "..."');
1641
1641
  process.exitCode = 1;
@@ -1778,7 +1778,7 @@ export function buildTeamPlan(id, prompt, opts = {}) {
1778
1778
  reasoning: teamReasoningPolicy(prompt, roster),
1779
1779
  codex_config_required: {
1780
1780
  service_tier: 'fast',
1781
- features: { multi_agent: true, codex_hooks: true },
1781
+ features: { multi_agent: true, hooks: true, fast_mode: true, fast_mode_ui: true, codex_git_commit: true, computer_use: true, apps: true, plugins: true },
1782
1782
  agents: { max_threads: 6, max_depth: 1 },
1783
1783
  custom_agents_dir: '.codex/agents'
1784
1784
  },
@@ -1887,6 +1887,7 @@ export function buildTeamPlan(id, prompt, opts = {}) {
1887
1887
  'sks team status <mission-id>',
1888
1888
  'sks team log <mission-id>',
1889
1889
  'sks team tail <mission-id>',
1890
+ 'sks team open-tmux <mission-id>',
1890
1891
  'sks team watch <mission-id>',
1891
1892
  'sks team lane <mission-id> --agent <name> --follow',
1892
1893
  'sks team event <mission-id> --agent <name> --phase <phase> --message "..."',
@@ -1967,7 +1968,7 @@ ${plan.roster.validation_team.map((agent) => `- ${agent.id}: ${agent.persona} [r
1967
1968
  - Keep team-live.md readable for the user inside Codex App.
1968
1969
  - Mirror every useful subagent status, debate result, handoff, review finding, and integration decision to team-transcript.jsonl.
1969
1970
  - Use \`sks team event ${plan.mission_id} --agent <name> --phase <phase> --message "..."\` when recording a live event from the parent thread.
1970
- - The user can inspect the flow with \`sks team log ${plan.mission_id}\`, \`sks team tail ${plan.mission_id}\`, \`sks team watch ${plan.mission_id}\`, or \`sks team lane ${plan.mission_id} --agent analysis_scout_1 --follow\`.
1971
+ - The user can inspect the flow with \`sks team open-tmux ${plan.mission_id}\`, \`sks team log ${plan.mission_id}\`, \`sks team tail ${plan.mission_id}\`, \`sks team watch ${plan.mission_id}\`, or \`sks team lane ${plan.mission_id} --agent analysis_scout_1 --follow\`.
1971
1972
 
1972
1973
  ## Phases
1973
1974
 
@@ -1989,6 +1990,37 @@ async function teamCommand(sub, args) {
1989
1990
  return;
1990
1991
  }
1991
1992
  const { dir } = await loadMission(root, id);
1993
+ if (sub === 'open-tmux' || sub === 'attach-tmux') {
1994
+ const plan = await readJson(path.join(dir, 'team-plan.json'), null);
1995
+ if (!plan) {
1996
+ console.error(`Team plan missing for ${id}; cannot open tmux Team view.`);
1997
+ process.exitCode = 2;
1998
+ return;
1999
+ }
2000
+ const tmux = await launchTmuxTeamView({
2001
+ root,
2002
+ missionId: id,
2003
+ plan,
2004
+ promptFile: path.join(dir, 'team-workflow.md'),
2005
+ json: flag(args, '--json'),
2006
+ attach: sub === 'attach-tmux' || !flag(args, '--no-attach'),
2007
+ args
2008
+ });
2009
+ if (flag(args, '--json')) return console.log(JSON.stringify(tmux, null, 2));
2010
+ if (!tmux.ready) {
2011
+ const reasons = [tmux.opened?.stderr, ...(tmux.blockers || [])].filter(Boolean);
2012
+ console.error(`tmux Team view blocked for ${id}: ${reasons.join('; ') || 'tmux creation failed'}`);
2013
+ if (tmux.attach_command) console.error(`Attach after repair: ${tmux.attach_command}`);
2014
+ process.exitCode = 2;
2015
+ return;
2016
+ }
2017
+ console.log(`tmux: opened ${tmux.opened_lane_count || tmux.lanes?.length || 0} Team lane(s) in ${tmux.session}`);
2018
+ if (tmux.split_ui?.mode) console.log(`tmux UI: ${tmux.split_ui.mode} (${tmux.split_ui.layout})`);
2019
+ if (tmux.split_ui?.current_session) console.log('tmux cockpit: reconciled inside the current SKS tmux window');
2020
+ console.log(`Attach: ${tmux.attach_command}`);
2021
+ console.log(`Watch: sks team watch ${id}`);
2022
+ return;
2023
+ }
1992
2024
  if (sub === 'event') {
1993
2025
  const message = readFlagValue(args, '--message', '');
1994
2026
  if (!message) {
@@ -1997,6 +2029,7 @@ async function teamCommand(sub, args) {
1997
2029
  return;
1998
2030
  }
1999
2031
  const phase = readFlagValue(args, '--phase', 'general');
2032
+ const plan = await readJson(path.join(dir, 'team-plan.json'), null).catch(() => null);
2000
2033
  const record = await appendTeamEvent(dir, {
2001
2034
  agent: readFlagValue(args, '--agent', 'parent_orchestrator'),
2002
2035
  phase,
@@ -2004,16 +2037,29 @@ async function teamCommand(sub, args) {
2004
2037
  artifact: readFlagValue(args, '--artifact', ''),
2005
2038
  message
2006
2039
  });
2040
+ const cockpit = plan
2041
+ ? await reconcileTmuxTeamCockpit({
2042
+ root,
2043
+ missionId: id,
2044
+ plan,
2045
+ promptFile: path.join(dir, 'team-workflow.md'),
2046
+ close: /^session_cleanup$|^team_cleanup$|^cleanup$/i.test(String(phase || '')),
2047
+ plannedFallback: false
2048
+ }).catch((err) => ({ ok: false, skipped: true, reason: err.message || 'tmux cockpit reconcile failed' }))
2049
+ : null;
2007
2050
  const tmuxCleanup = /^session_cleanup$|^team_cleanup$|^cleanup$/i.test(String(phase || ''))
2008
2051
  ? await requestTeamSessionCleanup(dir, {
2009
2052
  missionId: id,
2010
2053
  agent: readFlagValue(args, '--agent', 'parent_orchestrator'),
2011
2054
  reason: message,
2012
- finalMessage: 'Team cleanup event received. Follow panes may stop; tmux panes remain user-controlled.'
2055
+ finalMessage: 'Team cleanup event received. Managed tmux Team panes may close; follow loops may stop.'
2013
2056
  }).then(() => cleanupTmuxTeamView({ root, missionId: id, closeSession: flag(args, '--close-session') || flag(args, '--close') })).catch((err) => ({ ok: false, reason: err.message || 'tmux cleanup failed' }))
2014
2057
  : null;
2015
2058
  if (flag(args, '--json')) return console.log(JSON.stringify(record, null, 2));
2016
2059
  console.log(`${record.ts} [${record.phase}] ${record.agent}: ${record.message}`);
2060
+ if (cockpit?.ok && (cockpit.opened_lane_count || cockpit.closed_lane_count)) {
2061
+ console.log(`tmux cockpit: +${cockpit.opened_lane_count || 0} -${cockpit.closed_lane_count || 0} managed pane(s) in ${cockpit.session}`);
2062
+ }
2017
2063
  if (tmuxCleanup) {
2018
2064
  if (tmuxCleanup.ok) console.log(`tmux cleanup: marked complete (${tmuxCleanup.reason || 'record updated'})`);
2019
2065
  else console.log(`tmux cleanup: skipped (${tmuxCleanup.reason || 'not available'})`);
@@ -2045,7 +2091,7 @@ async function teamCommand(sub, args) {
2045
2091
  missionId: id,
2046
2092
  agent: readFlagValue(args, '--agent', 'parent_orchestrator'),
2047
2093
  reason: readFlagValue(args, '--reason', 'Team session ended; clean up live follow panes.'),
2048
- finalMessage: 'Team session ended. Lane/watch follow loops will stop after showing this cleanup summary; tmux panes remain user-controlled.'
2094
+ finalMessage: 'Team session ended. Lane/watch follow loops will stop after showing this cleanup summary; managed tmux Team panes may close.'
2049
2095
  });
2050
2096
  await appendTeamEvent(dir, {
2051
2097
  agent: readFlagValue(args, '--agent', 'parent_orchestrator'),