sneakoscope 0.7.62 → 0.7.64

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 sets the Team window to `window-size latest`, installs `client-attached` and `client-resized` hooks, reapplies `resize-window -A`, preserves `window-size latest`, and recalculates the tiled layout so split panes keep fitting when Warp or another terminal is resized. 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.62",
4
+ "version": "0.7.64",
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",
@@ -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, sksAsciiLogo, 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';
@@ -204,9 +204,11 @@ Usage:
204
204
  sks goal pause|resume|clear <mission-id|latest>
205
205
  sks goal status <mission-id|latest>
206
206
  sks team "task" [executor:5 reviewer:6 user:1] [--json]
207
- 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]
208
208
  sks team event [mission-id|latest] --agent <name> --phase <phase> --message "..."
209
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]
210
212
  sks team cleanup-tmux [mission-id|latest]
211
213
  sks research prepare "topic" [--depth frontier]
212
214
  sks research run <mission-id|latest> [--mock] [--max-cycles N]
@@ -1452,7 +1454,7 @@ function usage(args = []) {
1452
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.'],
1453
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.'],
1454
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.'],
1455
- 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.'],
1456
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'],
1457
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.'],
1458
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.'],
@@ -1922,7 +1924,7 @@ function hasTopLevelCodexModeLock(text = '') {
1922
1924
  return (Boolean(model) && model !== 'gpt-5.5') || /^model_reasoning_effort\s*=/m.test(top);
1923
1925
  }
1924
1926
 
1925
- function hasLegacyHooksFeatureFlag(text = '') {
1927
+ function hasDeprecatedCodexHooksFeatureFlag(text = '') {
1926
1928
  const lines = String(text || '').split('\n');
1927
1929
  const start = lines.findIndex((line) => line.trim() === '[features]');
1928
1930
  if (start === -1) return false;
@@ -1933,7 +1935,13 @@ function hasLegacyHooksFeatureFlag(text = '') {
1933
1935
  break;
1934
1936
  }
1935
1937
  }
1936
- 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`));
1937
1945
  }
1938
1946
 
1939
1947
  async function resolveMissionId(root, arg) { return (!arg || arg === 'latest') ? findLatestMission(root) : arg; }
@@ -2150,6 +2158,8 @@ async function selftest() {
2150
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');
2151
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');
2152
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');
2153
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');
2154
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');
2155
2165
  const postinstallSetupManifest = await readJson(path.join(postinstallSetupTmp, '.sneakoscope', 'manifest.json'));
@@ -2184,6 +2194,8 @@ async function selftest() {
2184
2194
  if (postinstallNoMarker.code !== 0) throw new Error(`selftest failed: no-marker postinstall bootstrap exited ${postinstallNoMarker.code}: ${postinstallNoMarker.stderr}`);
2185
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');
2186
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');
2187
2199
  if (await exists(path.join(postinstallNoMarkerCwd, '.sneakoscope'))) throw new Error('selftest failed: no-marker postinstall polluted install cwd');
2188
2200
  if (await exists(path.join(postinstallNoMarkerGlobalRoot, '.gitignore'))) throw new Error('selftest failed: global runtime bootstrap without project git wrote shared .gitignore');
2189
2201
  const bootstrapJsonTmp = tmpdir();
@@ -2938,8 +2950,8 @@ async function selftest() {
2938
2950
  if (wikiContext.includes('MANDATORY ambiguity-removal gate activated') || wikiContext.includes('Mission:')) throw new Error('selftest failed: Wiki route created ambiguity mission state');
2939
2951
  if (!wikiJson.systemMessage?.includes('wiki refresh')) throw new Error('selftest failed: Wiki route missing system message');
2940
2952
  const codexConfigText = await safeReadText(path.join(tmp, '.codex', 'config.toml'));
2941
- if (!codexConfigText.includes('multi_agent = true')) throw new Error('selftest failed: multi_agent not enabled');
2942
- 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(', ')}`);
2943
2955
  if (!hasContext7ConfigText(codexConfigText)) throw new Error('selftest failed: Context7 MCP not configured');
2944
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');
2945
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');
@@ -2947,13 +2959,29 @@ async function selftest() {
2947
2959
  if (!codexConfigText.includes('[agents.team_consensus]')) throw new Error('selftest failed: team_consensus agent not configured');
2948
2960
  const preservedConfigTmp = tmpdir();
2949
2961
  await ensureDir(path.join(preservedConfigTmp, '.codex'));
2950
- 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');
2951
2963
  await initProject(preservedConfigTmp, {});
2952
2964
  const preservedConfig = await safeReadText(path.join(preservedConfigTmp, '.codex', 'config.toml'));
2953
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');
2954
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');
2955
- 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(', ')}`);
2956
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');
2957
2985
  const autoReviewHome = path.join(tmp, 'auto-review-home');
2958
2986
  const autoReviewEnv = { HOME: autoReviewHome };
2959
2987
  const autoReviewEnabled = await enableAutoReview({ env: autoReviewEnv, high: true });
@@ -3169,7 +3197,9 @@ async function selftest() {
3169
3197
  await writeJsonAtomic(path.join(teamDir, 'team-plan.json'), teamPlan);
3170
3198
  if (teamPlan.agent_session_count !== 5) throw new Error('selftest failed: team default sessions not 5');
3171
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');
3172
- 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(', ')}`);
3173
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');
3174
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');
3175
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');
@@ -3208,6 +3238,7 @@ async function selftest() {
3208
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');
3209
3239
  const teamWorkflow = teamWorkflowMarkdown(teamPlan);
3210
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');
3211
3242
  if (!teamWorkflow.includes(TEAM_GRAPH_ARTIFACT) || !teamWorkflow.includes(TEAM_INBOX_DIR)) throw new Error('selftest failed: team workflow missing runtime graph/inbox guidance');
3212
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');
3213
3244
  const customTeamPlan = buildTeamPlan(teamId, '병렬 구현 팀 테스트', { agentSessions: 5 });
@@ -3241,7 +3272,7 @@ async function selftest() {
3241
3272
  await ensureDir(fakeTmuxDir);
3242
3273
  const fakeTmuxLog = path.join(fakeTmuxDir, 'tmux.log');
3243
3274
  const fakeTmuxBin = path.join(fakeTmuxDir, 'tmux');
3244
- 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); }\nif (cmd === 'list-windows') { console.log('@1'); 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`);
3245
3276
  await fsp.chmod(fakeTmuxBin, 0o755);
3246
3277
  const previousFakeTmuxLog = process.env.SKS_FAKE_TMUX_LOG;
3247
3278
  process.env.SKS_FAKE_TMUX_LOG = fakeTmuxLog;
@@ -3253,6 +3284,48 @@ async function selftest() {
3253
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');
3254
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');
3255
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;
3256
3329
  await writeTextAtomic(fakeTmuxLog, '');
3257
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' });
3258
3331
  const madTmuxLogText = await safeReadText(fakeTmuxLog);
@@ -3344,6 +3417,7 @@ async function selftest() {
3344
3417
  if (!teamDashboard?.latest_messages?.some((entry) => entry.agent === 'team_consensus')) throw new Error('selftest failed: team live dashboard missing agent event');
3345
3418
  const teamLive = await readTeamLive(teamDir);
3346
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');
3347
3421
  if (!teamLive.includes('selftest mapped options')) throw new Error('selftest failed: team live transcript missing event');
3348
3422
  if (!teamLive.includes('Context tracking SSOT: TriWiki')) throw new Error('selftest failed: team live transcript missing TriWiki context tracking');
3349
3423
  if (!(await readTeamTranscriptTail(teamDir, 1)).join('\n').includes('selftest mapped options')) throw new Error('selftest failed: team transcript tail missing event');
@@ -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'),
@@ -7,6 +7,7 @@ import { getCodexInfo } from './codex-adapter.mjs';
7
7
  export const CODEX_APP_DOCS_URL = 'https://developers.openai.com/codex/app/features';
8
8
  export const CODEX_CHANGELOG_URL = 'https://developers.openai.com/codex/changelog';
9
9
  export const CODEX_REMOTE_CONTROL_MIN_VERSION = '0.130.0';
10
+ const REQUIRED_CODEX_APP_FEATURE_FLAGS = ['codex_git_commit', 'hooks', 'fast_mode', 'computer_use', 'apps', 'plugins'];
10
11
 
11
12
  export function codexAppCandidatePaths(home = os.homedir(), env = process.env) {
12
13
  const candidates = [];
@@ -106,10 +107,12 @@ export async function codexAppIntegrationStatus(opts = {}) {
106
107
  const computerUseMcpListed = /computer[-_ ]?use/i.test(mcpText);
107
108
  const browserUseMcpListed = /browser[-_ ]?use/i.test(mcpText);
108
109
  const imageGenerationReady = codexFeatureEnabled(featureText, 'image_generation');
110
+ const requiredFeatureFlags = Object.fromEntries(REQUIRED_CODEX_APP_FEATURE_FLAGS.map((name) => [name, codexFeatureEnabled(featureText, name)]));
111
+ const requiredFeatureFlagsOk = Object.values(requiredFeatureFlags).every(Boolean);
109
112
  const computerUseReady = computerUseMcpListed || Boolean(computerUsePath);
110
113
  const browserUseReady = browserUseMcpListed || Boolean(browserUsePath);
111
114
  const appInstalled = Boolean(appPath);
112
- const ready = appInstalled && Boolean(codex.bin) && mcpList.ok && featureList.ok && imageGenerationReady && computerUseReady && browserUseReady;
115
+ const ready = appInstalled && Boolean(codex.bin) && mcpList.ok && featureList.ok && requiredFeatureFlagsOk && imageGenerationReady && computerUseReady && browserUseReady;
113
116
  return {
114
117
  ok: ready,
115
118
  app: {
@@ -136,6 +139,9 @@ export async function codexAppIntegrationStatus(opts = {}) {
136
139
  features: {
137
140
  checked: featureList.checked,
138
141
  ok: featureList.ok,
142
+ ...requiredFeatureFlags,
143
+ required_flags: requiredFeatureFlags,
144
+ required_flags_ok: requiredFeatureFlagsOk,
139
145
  image_generation: imageGenerationReady,
140
146
  image_generation_source: imageGenerationReady ? 'codex_features_list' : 'missing',
141
147
  stdout: featureList.stdout,
@@ -145,7 +151,7 @@ export async function codexAppIntegrationStatus(opts = {}) {
145
151
  computer_use_cache: computerUsePath,
146
152
  browser_use_cache: browserUsePath
147
153
  },
148
- guidance: codexAppGuidance({ appInstalled, codex, mcpList, featureList, imageGenerationReady, computerUseReady, browserUseReady, computerUseMcpListed, browserUseMcpListed, remoteControl })
154
+ guidance: codexAppGuidance({ appInstalled, codex, mcpList, featureList, requiredFeatureFlags, requiredFeatureFlagsOk, imageGenerationReady, computerUseReady, browserUseReady, computerUseMcpListed, browserUseMcpListed, remoteControl })
149
155
  };
150
156
  }
151
157
 
@@ -200,7 +206,7 @@ export function formatCodexRemoteControlStatus(status) {
200
206
  return lines.filter(Boolean).join('\n');
201
207
  }
202
208
 
203
- export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, imageGenerationReady, computerUseReady, browserUseReady, computerUseMcpListed, browserUseMcpListed, remoteControl }) {
209
+ export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, requiredFeatureFlags = {}, requiredFeatureFlagsOk = true, imageGenerationReady, computerUseReady, browserUseReady, computerUseMcpListed, browserUseMcpListed, remoteControl }) {
204
210
  const lines = [];
205
211
  if (!appInstalled) {
206
212
  lines.push('Install and open Codex App for first-party MCP/plugin tools. SKS tmux launch can still run with Codex CLI alone, but Codex Computer Use and imagegen/gpt-image-2 evidence will be unavailable until Codex App is ready.');
@@ -221,6 +227,11 @@ export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, im
221
227
  lines.push(`Codex feature check failed: ${summarizeCodexMcpError(featureList.stderr || featureList.stdout)}`);
222
228
  lines.push('Verify with: codex features list');
223
229
  }
230
+ if (featureList?.checked && featureList.ok && !requiredFeatureFlagsOk) {
231
+ const missing = missingRequiredFeatureFlags(requiredFeatureFlags);
232
+ lines.push(`Codex App feature flag(s) disabled or missing: ${missing.join(', ')}. Commit message generation and app-only tool paths can fail even when CLI chat works.`);
233
+ lines.push('Verify with: codex features list | rg "codex_git_commit|hooks|fast_mode|computer_use|apps|plugins"');
234
+ }
224
235
  if (appInstalled && (!computerUseReady || !browserUseReady)) {
225
236
  lines.push('Open Codex App settings and enable recommended MCP/plugin tools. Codex CLI 0.130.0+ remote-control/app-server sessions can pick up config changes live; restart older CLI/TUI sessions.');
226
237
  lines.push('Required for SKS QA-LOOP UI/browser evidence: Codex Computer Use only. Browser Use can support non-UI browser context, but it does not satisfy UI-level E2E verification.');
@@ -232,7 +243,7 @@ export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, im
232
243
  lines.push('Codex image_generation was not visible from `codex features list`. Required imagegen/gpt-image-2 evidence must stay blocked or unverified until $imagegen is available in Codex App.');
233
244
  }
234
245
  if (computerUseReady && !computerUseMcpListed) {
235
- lines.push('Computer Use plugin files are installed, but this check cannot prove the current thread exposes the live Computer Use tools. Start a new Codex App thread and invoke @Computer or @AppName; if the tool is still absent from the model tool list, mark UI/browser evidence unverified.');
246
+ lines.push('Computer Use plugin files are installed, but this check cannot prove the current thread exposes the live Computer Use tools. Start a new Codex App thread and invoke @Computer or @AppName for the actual target app or screen; Codex App readiness itself should stay on `codex features list`, `codex mcp list`, and `sks codex-app check`.');
236
247
  }
237
248
  if (browserUseReady && !browserUseMcpListed) {
238
249
  lines.push('Browser Use plugin files are installed, but `codex mcp list` does not list a browser-use MCP server. Treat Browser Use as plugin-scoped, not as SKS UI verification evidence.');
@@ -248,6 +259,7 @@ export function formatCodexAppStatus(status, { includeRaw = false } = {}) {
248
259
  `Codex App: ${status.app.installed ? 'ok' : 'missing'}${status.app.path ? ` ${status.app.path}` : ''}`,
249
260
  `Codex CLI: ${status.codex_cli.ok ? 'ok' : 'missing'}${status.codex_cli.version ? ` ${status.codex_cli.version}` : ''}`,
250
261
  `Remote Ctrl: ${status.remote_control?.ok ? 'ok' : 'missing'}${status.remote_control?.codex_cli?.version_number ? ` min ${status.remote_control.min_version}` : ''}`,
262
+ `App Flags: ${status.features?.required_flags_ok ? 'ok' : `missing ${missingRequiredFeatureFlags(status.features?.required_flags).join(', ') || 'required flags'}`}`,
251
263
  `Computer Use:${status.mcp.has_computer_use ? status.mcp.computer_use_source === 'plugin_cache' ? ' installed (verify @Computer in thread)' : ' ok' : ' missing'}`,
252
264
  `Browser Use: ${status.mcp.has_browser_use ? status.mcp.browser_use_source === 'plugin_cache' ? 'installed (plugin scoped)' : 'ok' : 'missing'}`,
253
265
  `Image Gen: ${status.features?.image_generation ? 'ok ($imagegen/gpt-image-2)' : status.features?.checked ? 'missing' : 'not checked'}`,
@@ -273,12 +285,15 @@ function summarizeCodexMcpError(text) {
273
285
  }
274
286
 
275
287
  function codexFeatureEnabled(text, featureName) {
276
- const name = escapeRegExp(featureName);
277
- return new RegExp(`(?:^|\\n)\\s*${name}\\s+\\S+\\s+true\\b`, 'i').test(String(text || ''));
288
+ const expected = String(featureName || '').toLowerCase();
289
+ return String(text || '').split(/\r?\n/).some((line) => {
290
+ const parts = line.trim().split(/\s+/).filter(Boolean);
291
+ return parts[0]?.toLowerCase() === expected && parts[parts.length - 1]?.toLowerCase() === 'true';
292
+ });
278
293
  }
279
294
 
280
- function escapeRegExp(value) {
281
- return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
295
+ function missingRequiredFeatureFlags(flags = {}) {
296
+ return REQUIRED_CODEX_APP_FEATURE_FLAGS.filter((name) => flags?.[name] !== true);
282
297
  }
283
298
 
284
299
  function remoteControlGuidance(status = {}) {
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.62';
8
+ export const PACKAGE_VERSION = '0.7.64';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
@@ -744,6 +744,7 @@ async function teamLiveDigest(root, state = {}) {
744
744
  const lines = events.map(formatTeamDigestEvent);
745
745
  const context = boundText([
746
746
  `SKS Team live digest: mission ${id}, phase ${phase}, source ${source}.`,
747
+ `Open tmux multi-view with: sks team open-tmux ${id}`,
747
748
  `Open live view with: sks team watch ${id}`,
748
749
  'Recent events:',
749
750
  ...lines.map((line) => `- ${line}`)
package/src/core/init.mjs CHANGED
@@ -440,13 +440,17 @@ function installPolicy(scope, commandPrefix) {
440
440
  function mergeManagedCodexConfigToml(existingContent = '') {
441
441
  let next = removeLegacyTopLevelCodexModeLocks(String(existingContent || '').trimEnd());
442
442
  next = removeTomlTableKey(next, 'notice', 'fast_default_opt_out');
443
- next = removeTomlTableKey(next, 'features', 'hooks');
443
+ next = removeTomlTableKey(next, 'features', 'codex_hooks');
444
444
  next = upsertTopLevelTomlString(next, 'model', 'gpt-5.5');
445
445
  next = upsertTopLevelTomlString(next, 'service_tier', 'fast');
446
- next = upsertTomlTableKey(next, 'features', 'codex_hooks = true');
446
+ next = upsertTomlTableKey(next, 'features', 'hooks = true');
447
447
  next = upsertTomlTableKey(next, 'features', 'multi_agent = true');
448
448
  next = upsertTomlTableKey(next, 'features', 'fast_mode = true');
449
449
  next = upsertTomlTableKey(next, 'features', 'fast_mode_ui = true');
450
+ next = upsertTomlTableKey(next, 'features', 'codex_git_commit = true');
451
+ next = upsertTomlTableKey(next, 'features', 'computer_use = true');
452
+ next = upsertTomlTableKey(next, 'features', 'apps = true');
453
+ next = upsertTomlTableKey(next, 'features', 'plugins = true');
450
454
  next = upsertTomlTableKey(next, 'user.fast_mode', 'visible = true');
451
455
  next = upsertTomlTableKey(next, 'user.fast_mode', 'enabled = true');
452
456
  next = upsertTomlTableKey(next, 'user.fast_mode', 'default_profile = "sks-fast-high"');
@@ -733,7 +737,7 @@ function codexAppQuickReference(scope, commandPrefix) {
733
737
  `Context Tracking: TriWiki SSOT. Before each route phase read only the latest coordinate+voxel overlay pack at .sneakoscope/wiki/context-pack.json; coordinate-only legacy packs are invalid. Use attention.use_first for compact high-trust recall and hydrate attention.hydrate_first from source before risky/lower-trust decisions. During every stage hydrate low-trust claims from source/hash/RGBA anchors; after changes run ${commandPrefix} wiki refresh or pack; before handoff/final run ${commandPrefix} wiki validate .sneakoscope/wiki/context-pack.json.`,
734
738
  stackCurrentDocsPolicyText(commandPrefix),
735
739
  `Team review: ${MIN_TEAM_REVIEW_POLICY_TEXT}`,
736
- `Team tmux view: ${commandPrefix} team "task" prepares live watch/lane commands and opens a named Team tmux multi-pane view by default when tmux is available; add --no-open-tmux for artifact-only creation. The view has an overview watch pane plus color-coded split per-agent lanes; ${commandPrefix} team lane latest --agent analysis_scout_1 --follow shows one agent's status, assigned runtime tasks, recent agent events, direct messages, and fallback global tail; ${commandPrefix} team message latest --from analysis_scout_1 --to executor_1 --message "handoff note" mirrors bounded agent communication into transcript/lane panes; ${commandPrefix} team cleanup-tmux latest marks the SKS session record complete and asks follow panes to show a cleanup summary then stop.`,
740
+ `Team tmux view: ${commandPrefix} team "task" prepares live watch/lane commands and reconciles managed Team panes inside the current SKS-owned tmux session when available; add --no-open-tmux for artifact-only creation or --separate-session to force the named sks-team-* fallback. Existing hook-created Team missions can be opened later with ${commandPrefix} team open-tmux latest. The view keeps the main Codex pane alive, adds an overview watch pane plus color-coded split per-agent lanes, and closes only SKS-managed Team panes as agent lanes finish or cleanup is requested; ${commandPrefix} team lane latest --agent analysis_scout_1 --follow shows one agent's status, assigned runtime tasks, recent agent events, direct messages, and fallback global tail; ${commandPrefix} team message latest --from analysis_scout_1 --to executor_1 --message "handoff note" mirrors bounded agent communication into transcript/lane panes; ${commandPrefix} team cleanup-tmux latest marks the SKS session record complete and asks managed panes/follow loops to close or show a cleanup summary.`,
737
741
  `Runtime: open Codex App once, then run ${commandPrefix} bootstrap and ${commandPrefix} deps check. Bare ${commandPrefix} opens or reuses the default tmux/Codex CLI session; before launch it checks npm @openai/codex@latest and prompts Y/n when the installed Codex CLI is missing or outdated. ${commandPrefix} codex-app remote-control wraps the Codex CLI 0.130.0+ headless remote-control entrypoint. ${commandPrefix} tmux open is the explicit form for session/workspace flags.`,
738
742
  `Guard: generated harness files are immutable outside the engine source repo; check ${commandPrefix} guard check; conflicts use ${commandPrefix} conflicts prompt with human approval.`
739
743
  ].join('\n') + '\n';
@@ -62,7 +62,7 @@ export async function findLatestMission(root) {
62
62
  return candidates.at(-1)?.id || null;
63
63
  }
64
64
 
65
- export async function setCurrent(root, patch) {
66
- const current = await readJson(stateFile(root), {});
65
+ export async function setCurrent(root, patch, opts = {}) {
66
+ const current = opts.replace ? {} : await readJson(stateFile(root), {});
67
67
  await writeJsonAtomic(stateFile(root), { ...current, ...patch, updated_at: nowIso() });
68
68
  }
@@ -803,7 +803,7 @@ async function materializeAutoSealedTeam(root, id, dir, route, task, contractHas
803
803
  transcript: 'team-transcript.jsonl',
804
804
  dashboard: 'team-dashboard.json',
805
805
  tmux: 'CLI Team entrypoints open tmux live lanes for the visible Team agent budget when tmux is available.',
806
- commands: ['sks team status latest', 'sks team log latest', 'sks team tail latest', 'sks team watch latest', 'sks team lane latest --agent <name> --follow']
806
+ commands: ['sks team status latest', 'sks team log latest', 'sks team tail latest', 'sks team open-tmux latest', 'sks team watch latest', 'sks team lane latest --agent <name> --follow']
807
807
  },
808
808
  required_artifacts: ['team-roster.json', 'team-analysis.md', ...(fromChatImgRequired ? [FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT] : []), 'team-consensus.md', ...teamRuntimeRequiredArtifacts(), 'team-review.md', 'team-gate.json', TEAM_SESSION_CLEANUP_ARTIFACT, 'reflection.md', 'reflection-gate.json', 'team-live.md', 'team-transcript.jsonl', 'team-dashboard.json', '.sneakoscope/wiki/context-pack.json', 'context7-evidence.jsonl']
809
809
  };
@@ -894,7 +894,7 @@ async function prepareTeam(root, route, task, required, opts = {}) {
894
894
  transcript: 'team-transcript.jsonl',
895
895
  dashboard: 'team-dashboard.json',
896
896
  tmux: 'CLI Team entrypoints open tmux live lanes for the visible Team agent budget when tmux is available.',
897
- commands: ['sks team status latest', 'sks team log latest', 'sks team tail latest', 'sks team watch latest', 'sks team lane latest --agent <name> --follow', 'sks team event latest --agent <name> --phase <phase> --message "..."']
897
+ commands: ['sks team status latest', 'sks team log latest', 'sks team tail latest', 'sks team open-tmux latest', 'sks team watch latest', 'sks team lane latest --agent <name> --follow', 'sks team event latest --agent <name> --phase <phase> --message "..."']
898
898
  },
899
899
  required_artifacts: ['team-roster.json', 'team-analysis.md', ...(fromChatImgRequired ? [FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT] : []), 'team-consensus.md', ...teamRuntimeRequiredArtifacts(), 'team-review.md', 'team-gate.json', TEAM_SESSION_CLEANUP_ARTIFACT, 'reflection.md', 'reflection-gate.json', 'team-live.md', 'team-transcript.jsonl', 'team-dashboard.json', '.sneakoscope/wiki/context-pack.json', 'context7-evidence.jsonl']
900
900
  };
@@ -33,7 +33,7 @@ export const CODEX_COMPUTER_USE_EVIDENCE_SOURCE = 'codex_computer_use';
33
33
  export const CODEX_IMAGEGEN_EVIDENCE_SOURCE = 'codex_app_imagegen_gpt_image_2';
34
34
  export const CODEX_APP_IMAGE_GENERATION_DOC_URL = 'https://developers.openai.com/codex/app/features#image-generation';
35
35
  export const OPENAI_IMAGE_GENERATION_DOC_URL = 'https://developers.openai.com/api/docs/guides/image-generation';
36
- export const CODEX_COMPUTER_USE_ONLY_POLICY = 'Pipeline UI/browser verification and visual inspection must use Codex Computer Use only. Do not use or install Playwright packages, Chrome MCP, Browser Use, Selenium, Puppeteer, or any other browser automation substitute; if Codex Computer Use is unavailable, mark the UI/browser evidence unverified instead of substituting another tool. In Codex App prompts, invoke @Computer or @AppName in a new thread when live Computer Use tools are needed; SKS hooks and skills can require the policy but cannot attach missing host tools to an already-started turn.';
36
+ export const CODEX_COMPUTER_USE_ONLY_POLICY = 'Pipeline UI/browser verification and visual inspection must use Codex Computer Use only. Do not use or install Playwright packages, Chrome MCP, Browser Use, Selenium, Puppeteer, or any other browser automation substitute; if Codex Computer Use is unavailable for the target UI, mark the UI/browser evidence unverified instead of substituting another tool. Codex App readiness/config verification is not target-UI evidence: use the Codex-provided control surfaces `codex features list`, `codex mcp list`, `sks codex-app check`, remote-control status, and plugin/tool exposure, not direct OS Accessibility control of the Codex App bundle. In Codex App prompts, invoke @Computer or @AppName in a new thread when live Computer Use tools are needed for the actual target app or screen; SKS hooks and skills can require the policy but cannot attach missing host tools to an already-started turn.';
37
37
  export const CODEX_IMAGEGEN_REQUIRED_POLICY = 'Pipeline image generation, raster asset creation/editing, and generated image-review evidence must use real Codex App imagegen/$imagegen with gpt-image-2 when that evidence is required. Do not substitute placeholder SVG/HTML/CSS, prose-only critique, stock-like stand-ins, manually fabricated files, or missing-output ledgers for requested/generated raster assets or required generated review images. If imagegen/gpt-image-2 is unavailable, record the blocker and mark the image asset or review evidence unverified instead of passing the gate. In Codex App prompts, invoke $imagegen when live image generation is needed; SKS hooks and skills can require the policy but cannot attach missing host image-generation tools to an already-started turn.';
38
38
  export const RESERVED_CODEX_PLUGIN_SKILL_NAMES = Object.freeze(['computer-use', 'browser', 'browser-use']);
39
39
  export const FORBIDDEN_BROWSER_AUTOMATION_RE = /\b(playwright|chrome\s+mcp|browser\s+use|selenium|puppeteer)\b/i;
@@ -308,7 +308,7 @@ export const ROUTES = [
308
308
  context7Policy: 'optional',
309
309
  reasoningPolicy: 'high',
310
310
  stopGate: 'team-gate.json',
311
- cliEntrypoint: 'sks team "task" [executor:5 reviewer:6 user:1] | sks team log|tail|watch|lane|status|event|message|cleanup-tmux',
311
+ cliEntrypoint: 'sks team "task" [executor:5 reviewer:6 user:1] | sks team log|tail|watch|lane|status|event|message|open-tmux|attach-tmux|cleanup-tmux',
312
312
  examples: ['$Team executor:5 agree on the best plan and implement it', '$From-Chat-IMG 채팅+첨부 이미지 작업 지시서']
313
313
  },
314
314
  {
@@ -548,7 +548,7 @@ export const COMMAND_CATALOG = [
548
548
  { name: 'validate-artifacts', usage: 'sks validate-artifacts [mission-id|latest] [--json]', description: 'Validate schema-backed mission artifacts for work orders, effort decisions, visual maps, dogfood reports, skills, mistake memory, Team dashboard state, and Honest Mode.' },
549
549
  { name: 'wiki', usage: 'sks wiki coords|pack|refresh|prune|validate ...', description: 'Build, refresh, prune, and validate RGBA/trig LLM Wiki context packs with attention.use_first and attention.hydrate_first for compact recall plus source hydration.' },
550
550
  { name: 'hproof', usage: 'sks hproof check [mission-id|latest]', description: 'Evaluate the H-Proof done gate for a mission.' },
551
- { name: 'team', usage: 'sks team "task" [executor:5 reviewer:6 user:1]|log|tail|watch|lane|status|dashboard|event|message|cleanup-tmux ...', description: 'Create and observe a scout-first Team mission with at least five reviewer/QA validation lanes, color-coded tmux lanes, transcript messages, and cleanup-aware follow panes.' },
551
+ { name: 'team', usage: 'sks team "task" [executor:5 reviewer:6 user:1]|log|tail|watch|lane|status|dashboard|event|message|open-tmux|attach-tmux|cleanup-tmux ...', description: 'Create and observe a scout-first Team mission with at least five reviewer/QA validation lanes, current-session managed tmux lanes when available, transcript messages, and cleanup-aware follow panes.' },
552
552
  { name: 'reasoning', usage: 'sks reasoning ["prompt"] [--json]', description: 'Show SKS temporary reasoning-effort routing: medium for simple tasks, high for logic, xhigh for research.' },
553
553
  { name: 'gx', usage: 'sks gx init|render|validate|drift|snapshot [name]', description: 'Create and verify deterministic SVG/HTML visual context cartridges.' },
554
554
  { name: 'profile', usage: 'sks profile show|set <model>', description: 'Inspect or set the current SKS model profile metadata.' },
@@ -151,6 +151,7 @@ export function defaultTeamDashboard(id, prompt, opts = {}) {
151
151
  status: `sks team status ${id}`,
152
152
  log: `sks team log ${id}`,
153
153
  tail: `sks team tail ${id}`,
154
+ open_tmux: `sks team open-tmux ${id}`,
154
155
  watch: `sks team watch ${id}`,
155
156
  lane: `sks team lane ${id} --agent <agent> --follow`,
156
157
  event: `sks team event ${id} --agent <agent> --phase <phase> --message "..."`,
@@ -204,6 +205,7 @@ ${prompt}
204
205
  sks team status ${id}
205
206
  sks team log ${id}
206
207
  sks team tail ${id}
208
+ sks team open-tmux ${id}
207
209
  sks team watch ${id}
208
210
  sks team lane ${id} --agent analysis_scout_1 --follow
209
211
  sks team event ${id} --agent analysis_scout_1 --phase parallel_analysis_scouting --message "mapped repo slice"
@@ -313,7 +315,7 @@ export function parseTeamSpecArgs(args = []) {
313
315
  i++;
314
316
  continue;
315
317
  }
316
- if (arg === '--json' || arg === '--open-tmux' || arg === '--tmux-open' || arg === '--no-open-tmux' || arg === '--no-tmux' || arg === '--no-attach') continue;
318
+ if (arg === '--json' || arg === '--open-tmux' || arg === '--tmux-open' || arg === '--no-open-tmux' || arg === '--no-tmux' || arg === '--no-attach' || arg === '--separate-session' || arg === '--new-session' || arg === '--legacy-team-session' || arg === '--no-dynamic-team-tmux') continue;
317
319
  cleanArgs.push(args[i]);
318
320
  }
319
321
  return { cleanArgs, ...normalizeTeamSpec({ roleCounts, agentSessions: explicitSession }) };
@@ -444,6 +446,7 @@ export async function appendTeamEvent(dir, event) {
444
446
  dashboard.agents[agent].last_seen = record.ts;
445
447
  await writeJsonAtomic(files.dashboard, dashboard);
446
448
  }
449
+ await reconcileTeamTmuxFromEvent(dir, record).catch(() => null);
447
450
  const current = await readText(files.live, teamLiveMarkdown('unknown', 'unknown'));
448
451
  const target = record.to ? ` -> ${record.to}` : '';
449
452
  const line = `\n- ${record.ts} [${record.phase || 'general'}] ${record.agent || 'unknown'}${target}: ${record.message || ''}${record.artifact ? ` (${record.artifact})` : ''}\n`;
@@ -451,6 +454,22 @@ export async function appendTeamEvent(dir, event) {
451
454
  return record;
452
455
  }
453
456
 
457
+ async function reconcileTeamTmuxFromEvent(dir, record = {}) {
458
+ if (!process.env.TMUX || String(process.env.SKS_TMUX_EVENT_RECONCILE || '1') === '0') return null;
459
+ if (record.type === 'tmux_lane_opened') return null;
460
+ const missionId = path.basename(dir);
461
+ const root = path.resolve(dir, '..', '..', '..');
462
+ const cockpitState = await readJson(path.join(root, '.sneakoscope', 'state', 'tmux-cockpit.json'), {}).catch(() => ({}));
463
+ if (!cockpitState?.missions?.[missionId]) return null;
464
+ const plan = await readJson(path.join(dir, 'team-plan.json'), null).catch(() => null);
465
+ if (!plan) return null;
466
+ const { reconcileTmuxTeamCockpit } = await import('./tmux-ui.mjs');
467
+ const phase = String(record.phase || '');
468
+ const type = String(record.type || '');
469
+ const close = /^session_cleanup$|^team_cleanup$|^cleanup$/i.test(phase) || /^cleanup$/i.test(type);
470
+ return reconcileTmuxTeamCockpit({ root, missionId, plan, close, plannedFallback: false });
471
+ }
472
+
454
473
  export async function readTeamControl(dir) {
455
474
  return readJson(teamLogPaths(dir).control, defaultTeamControl(path.basename(dir)));
456
475
  }
@@ -466,7 +485,7 @@ export async function requestTeamSessionCleanup(dir, opts = {}) {
466
485
  cleanup_requested_at: opts.ts || nowIso(),
467
486
  cleanup_requested_by: opts.agent || 'parent_orchestrator',
468
487
  cleanup_reason: opts.reason || 'Team session cleanup requested.',
469
- final_message: opts.finalMessage || 'Team session ended. Lane follow loops may stop; tmux panes remain user-controlled.'
488
+ final_message: opts.finalMessage || 'Team session ended. Lane follow loops may stop; managed tmux Team panes may close.'
470
489
  };
471
490
  await writeJsonAtomic(files.control, next);
472
491
  return next;
@@ -486,7 +505,7 @@ export function renderTeamCleanupSummary(control = {}) {
486
505
  `Requested by: ${control.cleanup_requested_by || 'unknown'}`,
487
506
  `Reason: ${control.cleanup_reason || 'Team session cleanup requested.'}`,
488
507
  '',
489
- control.final_message || 'Team session ended. tmux panes remain user-controlled.'
508
+ control.final_message || 'Team session ended. managed tmux Team panes may close.'
490
509
  ].join('\n');
491
510
  }
492
511
 
@@ -569,6 +588,8 @@ export async function renderTeamWatch(dir, opts = {}) {
569
588
  '',
570
589
  '## Split-Screen Map',
571
590
  '- This overview pane follows the whole mission transcript.',
591
+ '- Run `sks team open-tmux ...` to materialize or reopen the split-pane Team tmux view for an existing mission.',
592
+ '- Inside an SKS-owned tmux session, Team panes are reconciled in the current window and managed panes may close as agent lanes finish.',
572
593
  '- Neighbor tmux panes follow individual `sks team lane ... --agent <name>` views.',
573
594
  '- Use `sks team event ...` to mirror scout, debate, executor, review, and verification status into the live panes.',
574
595
  '- Use `sks team message ... --from <agent> --to <agent|all>` for bounded inter-agent communication in transcript/lane views.',
@@ -7,7 +7,7 @@ import { getCodexInfo } from './codex-adapter.mjs';
7
7
  import { codexAppIntegrationStatus, formatCodexAppStatus } from './codex-app.mjs';
8
8
  import { REQUIRED_CODEX_MODEL, forceGpt55CodexArgs } from './codex-model-guard.mjs';
9
9
  import { MIN_TEAM_REVIEWER_LANES } from './team-review-policy.mjs';
10
- import { appendTeamEvent } from './team-live.mjs';
10
+ import { appendTeamEvent, readTeamControl, readTeamDashboard, teamCleanupRequested } from './team-live.mjs';
11
11
 
12
12
  const SKS_FIGLET_FONT = 'Standard';
13
13
 
@@ -97,6 +97,28 @@ export function tmuxTeamStatePath(root = process.cwd()) {
97
97
  return path.join(path.resolve(root || process.cwd()), '.sneakoscope', 'state', 'tmux-team-sessions.json');
98
98
  }
99
99
 
100
+ export function tmuxCockpitStatePath(root = process.cwd()) {
101
+ return path.join(path.resolve(root || process.cwd()), '.sneakoscope', 'state', 'tmux-cockpit.json');
102
+ }
103
+
104
+ const TERMINAL_TEAM_AGENT_STATUSES = new Set([
105
+ 'agent_closed',
106
+ 'agent_done',
107
+ 'cancelled',
108
+ 'canceled',
109
+ 'cleanup',
110
+ 'cleanup_requested',
111
+ 'closed',
112
+ 'complete',
113
+ 'completed',
114
+ 'done',
115
+ 'ended',
116
+ 'failed',
117
+ 'stopped',
118
+ 'terminal',
119
+ 'tmux_lane_closed'
120
+ ]);
121
+
100
122
  export function isTmuxShellSession(env = process.env) {
101
123
  return Boolean(String(env.TMUX || '').trim());
102
124
  }
@@ -298,7 +320,8 @@ export function formatTmuxBanner(status = null) {
298
320
  ' sks open or attach the default tmux Codex CLI session',
299
321
  ' sks tmux open open or attach a tmux Codex CLI session with explicit flags',
300
322
  ' sks --mad open one-shot MAD full-access auto-review tmux session',
301
- ' sks team "task" prepare Team mission and open the tmux multi-pane live view',
323
+ ' sks team "task" prepare Team mission and reconcile Team panes in the current SKS tmux session when available',
324
+ ' sks team open-tmux latest reopen current-session panes, or use --separate-session for the legacy view',
302
325
  '',
303
326
  'Useful terminal commands:',
304
327
  ' sks commands',
@@ -342,6 +365,116 @@ async function tmuxWindowTarget(bin, session) {
342
365
  return windowId || fallback;
343
366
  }
344
367
 
368
+ async function currentTmuxTarget(bin, env = process.env) {
369
+ if (!isTmuxShellSession(env)) return { ok: false, reason: 'not running inside tmux' };
370
+ const run = await tmuxRun(bin, ['display-message', '-p', '#{session_name}\t#{window_id}\t#{pane_id}'], { timeoutMs: 5000, maxOutputBytes: 4096 });
371
+ if (run.code !== 0) return { ok: false, reason: run.stderr || run.stdout || 'tmux display-message failed' };
372
+ const [session, windowId, paneId] = String(run.stdout || '').trim().split('\t');
373
+ if (!session || !windowId) return { ok: false, reason: 'tmux did not report current session/window' };
374
+ return { ok: true, session: sanitizeTmuxSessionName(session), window_id: windowId, pane_id: paneId || null };
375
+ }
376
+
377
+ async function isRecordedSksTmuxSession(root, session) {
378
+ const state = await readJson(tmuxStatePath(root), {}).catch(() => ({}));
379
+ const sessions = state.sessions && typeof state.sessions === 'object' ? state.sessions : {};
380
+ const record = sessions[sanitizeTmuxSessionName(session)] || null;
381
+ if (!record) return { ok: false, reason: 'current tmux session is not recorded as an SKS session' };
382
+ const recordRoot = path.resolve(record.root || root || process.cwd());
383
+ const resolvedRoot = path.resolve(root || process.cwd());
384
+ if (recordRoot !== resolvedRoot) return { ok: false, reason: `recorded SKS session belongs to ${recordRoot}` };
385
+ return { ok: true, record };
386
+ }
387
+
388
+ function isTerminalTeamAgentStatus(status = '') {
389
+ const normalized = String(status || '').trim().toLowerCase();
390
+ return TERMINAL_TEAM_AGENT_STATUSES.has(normalized) || /(?:^|_)(?:done|complete|completed|closed|cleanup|cancelled|canceled|failed|ended|stopped)(?:_|$)/.test(normalized);
391
+ }
392
+
393
+ function teamCockpitAgentIds(plan = {}, dashboard = null, control = null, opts = {}) {
394
+ if (teamCleanupRequested(control)) return [];
395
+ const visible = teamViewAgentIds(plan).filter((id) => id && id !== 'mission_overview');
396
+ const agents = dashboard?.agents && typeof dashboard.agents === 'object' ? dashboard.agents : null;
397
+ if (!agents) return opts.plannedFallback ? visible : [];
398
+ const active = [];
399
+ for (const id of visible) {
400
+ const entry = agents[id] || {};
401
+ const status = String(entry.status || '').trim().toLowerCase();
402
+ if (!status || status === 'pending') continue;
403
+ if (isTerminalTeamAgentStatus(status)) continue;
404
+ active.push(id);
405
+ }
406
+ if (!active.length && opts.plannedFallback) return visible;
407
+ return uniqueAgentIds(active);
408
+ }
409
+
410
+ function teamCockpitLanes(plan = {}, dashboard = null, control = null, opts = {}) {
411
+ const agents = teamCockpitAgentIds(plan, dashboard, control, opts);
412
+ if (!agents.length) return [];
413
+ const overview = { agent: 'mission_overview', role: 'overview', command: teamOverviewCommand(opts.root, opts.missionId), style: teamLaneStyle('mission_overview'), title: teamLaneTitle('mission_overview') };
414
+ return [
415
+ overview,
416
+ ...agents.map((agent) => {
417
+ const style = teamLaneStyle(agent);
418
+ return {
419
+ agent,
420
+ role: style.role,
421
+ command: teamAgentCommand(opts.root, opts.missionId, agent, teamLanePhase(agent)),
422
+ style,
423
+ title: teamLaneTitle(agent)
424
+ };
425
+ })
426
+ ];
427
+ }
428
+
429
+ function parseTmuxPaneLines(stdout = '') {
430
+ return String(stdout || '').split(/\r?\n/).filter(Boolean).map((line) => {
431
+ const [pane_id, title, command, managed, mission_id, agent, role] = line.split('\t');
432
+ return {
433
+ pane_id,
434
+ title: title || '',
435
+ command: command || '',
436
+ managed: managed === '1' || managed === 'true',
437
+ mission_id: mission_id || '',
438
+ agent: agent || '',
439
+ role: role || ''
440
+ };
441
+ }).filter((pane) => /^%\d+$/.test(pane.pane_id || ''));
442
+ }
443
+
444
+ async function listTmuxWindowPanes(bin, windowId) {
445
+ const format = ['#{pane_id}', '#{pane_title}', '#{pane_current_command}', '#{@sks_team_managed}', '#{@sks_mission_id}', '#{@sks_agent_id}', '#{@sks_lane_role}'].join('\t');
446
+ const run = await tmuxRun(bin, ['list-panes', '-t', windowId, '-F', format], { timeoutMs: 5000, maxOutputBytes: 32 * 1024 });
447
+ if (run.code !== 0) return { ok: false, panes: [], stderr: run.stderr || run.stdout || 'tmux list-panes failed' };
448
+ return { ok: true, panes: parseTmuxPaneLines(run.stdout) };
449
+ }
450
+
451
+ async function setTmuxPaneUserOptions(bin, paneId, options = {}) {
452
+ const applied = [];
453
+ const failed = [];
454
+ for (const [key, value] of Object.entries(options)) {
455
+ const run = await tmuxRun(bin, ['set-option', '-pt', paneId, key, String(value)], { timeoutMs: 5000 });
456
+ const command = [path.basename(bin), 'set-option', '-pt', paneId, key, String(value)].join(' ');
457
+ if (run.code === 0) applied.push(command);
458
+ else failed.push({ command, stderr: run.stderr || run.stdout || 'tmux set-option failed' });
459
+ }
460
+ return { applied, failed };
461
+ }
462
+
463
+ async function writeTmuxCockpitRecord(root, record = {}) {
464
+ if (!record.mission_id || !record.session) return null;
465
+ const statePath = tmuxCockpitStatePath(root);
466
+ const state = await readJson(statePath, {}).catch(() => ({}));
467
+ const now = nowIso();
468
+ const nextRecord = { ...record, schema_version: 1, root: path.resolve(root || process.cwd()), updated_at: now };
469
+ const missions = state.missions && typeof state.missions === 'object' ? state.missions : {};
470
+ await writeJsonAtomic(statePath, {
471
+ schema_version: 1,
472
+ updated_at: now,
473
+ missions: { ...missions, [record.mission_id]: nextRecord }
474
+ });
475
+ return nextRecord;
476
+ }
477
+
345
478
  function tmuxLayoutName(value = 'tiled') {
346
479
  const layout = String(value || 'tiled').trim();
347
480
  return /^(tiled|even-horizontal|even-vertical|main-horizontal|main-horizontal-mirrored|main-vertical|main-vertical-mirrored)$/.test(layout)
@@ -558,6 +691,97 @@ function teamLanePhase(agentId = '') {
558
691
  return 'team';
559
692
  }
560
693
 
694
+ export async function reconcileTmuxTeamCockpit({ root, missionId, plan = {}, promptFile = null, dashboard = undefined, control = undefined, close = false, plannedFallback = false, env = process.env, tmux = null } = {}) {
695
+ const resolvedRoot = path.resolve(root || await sksRoot());
696
+ const id = missionId || 'latest';
697
+ if (String(env.SKS_TMUX_DYNAMIC_TEAM || '1') === '0') return { ok: false, skipped: true, reason: 'SKS_TMUX_DYNAMIC_TEAM=0' };
698
+ const tmuxBin = tmux?.bin || await findTmuxBin() || 'tmux';
699
+ const target = await currentTmuxTarget(tmuxBin, env);
700
+ if (!target.ok) return { ok: false, skipped: true, reason: target.reason };
701
+ const ownership = await isRecordedSksTmuxSession(resolvedRoot, target.session);
702
+ if (!ownership.ok) return { ok: false, skipped: true, session: target.session, reason: ownership.reason };
703
+ const missionDir = path.join(resolvedRoot, '.sneakoscope', 'missions', id);
704
+ const loadedDashboard = dashboard === undefined ? await readTeamDashboard(missionDir).catch(() => null) : dashboard;
705
+ const loadedControl = control === undefined ? await readTeamControl(missionDir).catch(() => null) : control;
706
+ const lanes = close
707
+ ? []
708
+ : teamCockpitLanes(plan, loadedDashboard, loadedControl, { root: resolvedRoot, missionId: id, plannedFallback });
709
+ const desiredAgents = new Set(lanes.map((lane) => lane.agent));
710
+ const paneList = await listTmuxWindowPanes(tmuxBin, target.window_id);
711
+ if (!paneList.ok) return { ok: false, skipped: false, session: target.session, window_id: target.window_id, reason: paneList.stderr };
712
+ const managed = paneList.panes.filter((pane) => pane.managed && pane.mission_id === id);
713
+ const byAgent = new Map();
714
+ for (const pane of managed) {
715
+ if (pane.agent && !byAgent.has(pane.agent)) byAgent.set(pane.agent, pane);
716
+ }
717
+ const opened = [];
718
+ const closed = [];
719
+ const failed = [];
720
+ for (const pane of managed) {
721
+ if (!desiredAgents.has(pane.agent)) {
722
+ const kill = await tmuxRun(tmuxBin, ['kill-pane', '-t', pane.pane_id], { timeoutMs: 5000 });
723
+ if (kill.code === 0) closed.push({ pane_id: pane.pane_id, agent: pane.agent, role: pane.role });
724
+ else failed.push({ action: 'kill-pane', pane_id: pane.pane_id, agent: pane.agent, stderr: kill.stderr || kill.stdout || 'tmux kill-pane failed' });
725
+ }
726
+ }
727
+ for (const lane of lanes) {
728
+ if (byAgent.has(lane.agent)) continue;
729
+ const split = await tmuxRun(tmuxBin, ['split-window', '-t', target.window_id, '-d', '-P', '-F', '#{pane_id}', '-c', resolvedRoot, lane.command || 'pwd'], { timeoutMs: 5000, maxOutputBytes: 4096 });
730
+ const pane_id = paneId(split.stdout);
731
+ if (split.code !== 0 || !pane_id) {
732
+ failed.push({ action: 'split-window', agent: lane.agent, role: lane.role, stderr: split.stderr || split.stdout || 'tmux split-window failed' });
733
+ continue;
734
+ }
735
+ const optionResult = await setTmuxPaneUserOptions(tmuxBin, pane_id, {
736
+ '@sks_team_managed': '1',
737
+ '@sks_mission_id': id,
738
+ '@sks_agent_id': lane.agent,
739
+ '@sks_lane_role': lane.role || teamLaneStyle(lane.agent).role
740
+ });
741
+ failed.push(...optionResult.failed.map((entry) => ({ action: 'set-option', agent: lane.agent, role: lane.role, ...entry })));
742
+ opened.push({ pane_id, agent: lane.agent, role: lane.role, title: lane.title });
743
+ }
744
+ let relayout = null;
745
+ if (opened.length || closed.length) {
746
+ const tiled = await tmuxRun(tmuxBin, ['select-layout', '-t', target.window_id, 'tiled'], { timeoutMs: 5000 });
747
+ const even = await tmuxRun(tmuxBin, ['select-layout', '-t', target.window_id, '-E'], { timeoutMs: 5000 });
748
+ relayout = { ok: tiled.code === 0 && even.code === 0, tiled: tiled.code, even: even.code };
749
+ }
750
+ const nextPanes = [
751
+ ...managed.filter((pane) => desiredAgents.has(pane.agent) && !closed.some((entry) => entry.pane_id === pane.pane_id)),
752
+ ...opened
753
+ ];
754
+ await writeTmuxCockpitRecord(resolvedRoot, {
755
+ mission_id: id,
756
+ session: target.session,
757
+ window_id: target.window_id,
758
+ main_pane_id: target.pane_id,
759
+ mode: 'current_session_dynamic_panes',
760
+ desired_lane_count: lanes.length,
761
+ panes: nextPanes,
762
+ opened,
763
+ closed,
764
+ failed
765
+ }).catch(() => null);
766
+ return {
767
+ ok: failed.length === 0,
768
+ mode: 'current_session_dynamic_panes',
769
+ session: target.session,
770
+ window_id: target.window_id,
771
+ main_pane_id: target.pane_id,
772
+ desired_lane_count: lanes.length,
773
+ opened_lane_count: opened.length,
774
+ closed_lane_count: closed.length,
775
+ managed_lane_count: nextPanes.length,
776
+ opened,
777
+ closed,
778
+ failed,
779
+ relayout,
780
+ attach_command: `tmux attach-session -t ${target.session}`,
781
+ cleanup_policy: 'managed Team panes are reconciled in the current SKS tmux session; main Codex pane is never killed'
782
+ };
783
+ }
784
+
561
785
  export async function launchTmuxTeamView({ root, missionId, plan = {}, promptFile = null, json = false, attach = false, args = [] } = {}) {
562
786
  const launch = await buildTmuxLaunchPlan({ root, session: `sks-team-${missionId}` });
563
787
  const visibleAgents = teamViewAgentIds(plan);
@@ -594,6 +818,60 @@ export async function launchTmuxTeamView({ root, missionId, plan = {}, promptFil
594
818
  attach_command: launch.attach_command
595
819
  };
596
820
  if (json || !launch.ready) return result;
821
+ const wantsSeparateSession = args.includes('--separate-session') || args.includes('--new-session') || args.includes('--legacy-team-session') || args.includes('--no-dynamic-team-tmux');
822
+ if (!wantsSeparateSession) {
823
+ const cockpit = await reconcileTmuxTeamCockpit({ root: launch.root, missionId, plan, promptFile, plannedFallback: true });
824
+ result.dynamic_cockpit = cockpit;
825
+ if (cockpit.ok) {
826
+ result.created = true;
827
+ result.opened = cockpit;
828
+ result.session = cockpit.session;
829
+ result.workspace = cockpit.session;
830
+ result.opened_lane_count = cockpit.managed_lane_count;
831
+ result.all_lanes_opened = cockpit.desired_lane_count === cockpit.managed_lane_count;
832
+ result.ready = true;
833
+ result.attach_command = cockpit.attach_command;
834
+ result.cleanup_policy = cockpit.cleanup_policy;
835
+ result.split_ui = {
836
+ ...splitUi,
837
+ mode: cockpit.mode,
838
+ current_session: true,
839
+ window_id: cockpit.window_id,
840
+ user_attach_command: cockpit.attach_command
841
+ };
842
+ await writeTmuxTeamRecord(launch.root, {
843
+ mission_id: missionId,
844
+ session: cockpit.session,
845
+ attach_command: cockpit.attach_command,
846
+ split_ui: result.split_ui,
847
+ cleanup_policy: result.cleanup_policy,
848
+ panes: cockpit.opened || [],
849
+ lanes: lanes.map((entry) => ({
850
+ agent: entry.agent,
851
+ role: entry.style?.role || teamLaneStyle(entry.agent).role,
852
+ style: entry.style || teamLaneStyle(entry.agent),
853
+ title: entry.title || teamLaneTitle(entry.agent)
854
+ })),
855
+ mode: cockpit.mode,
856
+ window_id: cockpit.window_id
857
+ }).catch(() => null);
858
+ if (cockpit.opened?.length) {
859
+ const dir = path.join(launch.root, '.sneakoscope', 'missions', missionId);
860
+ if (await exists(dir)) {
861
+ for (const lane of cockpit.opened) {
862
+ if (!lane.agent || lane.agent === 'mission_overview') continue;
863
+ await appendTeamEvent(dir, {
864
+ agent: lane.agent,
865
+ phase: teamLanePhase(lane.agent),
866
+ type: 'tmux_lane_opened',
867
+ message: `tmux pane opened for ${lane.agent}; following live lane activity in the current SKS tmux session.`
868
+ }).catch(() => null);
869
+ }
870
+ }
871
+ }
872
+ return result;
873
+ }
874
+ }
597
875
  const panes = lanes.map((lane, index) => ({ cwd: launch.root, command: lane.command, focused: index === 0, role: lane.role, title: lane.title, vertical: index > 1 }));
598
876
  const created = await createTmuxSession(launch, panes, { layout: 'tiled', recreate: true });
599
877
  result.created = Boolean(created.ok);
@@ -685,8 +963,9 @@ export async function cleanupTmuxTeamView({ root, missionId = 'latest', closeSes
685
963
  const resolvedRoot = path.resolve(root || await sksRoot());
686
964
  const record = await readTmuxTeamRecord(resolvedRoot, missionId);
687
965
  if (!record?.session) return { ok: false, skipped: true, reason: 'no recorded tmux Team session', mission_id: missionId };
966
+ const dynamicCleanup = await reconcileTmuxTeamCockpit({ root: resolvedRoot, missionId: record.mission_id || missionId, close: true }).catch((err) => ({ ok: false, skipped: true, reason: err.message || 'dynamic tmux cleanup failed' }));
688
967
  let killed_session = false;
689
- if (closeSession || closeSession === true) {
968
+ if ((closeSession || closeSession === true) && record.mode !== 'current_session_dynamic_panes') {
690
969
  const tmuxBin = await findTmuxBin() || 'tmux';
691
970
  const kill = await tmuxRun(tmuxBin, ['kill-session', '-t', record.session], { timeoutMs: 5000 });
692
971
  killed_session = kill.code === 0;
@@ -699,9 +978,14 @@ export async function cleanupTmuxTeamView({ root, missionId = 'latest', closeSes
699
978
  attach_command: record.attach_command,
700
979
  close_session: Boolean(closeSession),
701
980
  killed_session,
702
- requested_close_surfaces: closeSession ? 1 : 0,
703
- closed_surfaces: killed_session ? 1 : 0,
704
- reason: closeSession ? 'tmux kill-session requested for recorded Team session.' : 'cleanup marks the SKS tmux Team record complete; panes remain user-controlled.'
981
+ dynamic_cleanup: dynamicCleanup,
982
+ requested_close_surfaces: closeSession ? 1 : (dynamicCleanup?.closed_lane_count || 0),
983
+ closed_surfaces: killed_session ? 1 : (dynamicCleanup?.closed_lane_count || 0),
984
+ reason: dynamicCleanup?.ok
985
+ ? 'cleanup closed managed Team panes in the current SKS tmux session.'
986
+ : closeSession
987
+ ? 'tmux kill-session requested for recorded Team session.'
988
+ : 'cleanup marks the SKS tmux Team record complete; panes remain user-controlled.'
705
989
  };
706
990
  }
707
991