sneakoscope 0.7.11 → 0.7.13

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.
@@ -7,7 +7,7 @@ export const FROM_CHAT_IMG_CHECKLIST_ARTIFACT = 'from-chat-img-checklist.md';
7
7
  export const FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT = 'from-chat-img-temp-triwiki.json';
8
8
  export const FROM_CHAT_IMG_QA_LOOP_ARTIFACT = 'from-chat-img-qa-loop.json';
9
9
  export const FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS = 5;
10
- export const USAGE_TOPICS = 'install|setup|bootstrap|root|deps|warp|auto-review|team|qa-loop|ppt|goal|research|db|codex-app|dfix|design|imagegen|dollar|context7|pipeline|reasoning|guard|conflicts|versioning|eval|harness|hproof|gx|wiki|code-structure|proof-field|skill-dream';
10
+ export const USAGE_TOPICS = 'install|setup|bootstrap|root|deps|tmux|auto-review|team|qa-loop|ppt|goal|research|db|codex-app|dfix|design|imagegen|dollar|context7|pipeline|reasoning|guard|conflicts|versioning|eval|harness|hproof|gx|wiki|code-structure|proof-field|skill-dream';
11
11
  export const CODEX_COMPUTER_USE_EVIDENCE_SOURCE = 'codex_computer_use';
12
12
  export const CODEX_APP_IMAGE_GENERATION_DOC_URL = 'https://developers.openai.com/codex/app/features#image-generation';
13
13
  export const CODEX_COMPUTER_USE_ONLY_POLICY = 'Pipeline UI/browser verification and visual inspection must use Codex Computer Use only. Do not use Playwright, 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.';
@@ -231,7 +231,7 @@ export const ROUTES = [
231
231
  context7Policy: 'optional',
232
232
  reasoningPolicy: 'high',
233
233
  stopGate: 'team-gate.json',
234
- cliEntrypoint: 'sks team "task" [executor:5 reviewer:2 user:1] | sks team log|tail|watch|lane|status|event|message|cleanup-warp',
234
+ cliEntrypoint: 'sks team "task" [executor:5 reviewer:2 user:1] | sks team log|tail|watch|lane|status|event|message|cleanup-tmux',
235
235
  examples: ['$Team executor:5 agree on the best plan and implement it', '$From-Chat-IMG 채팅+첨부 이미지 작업 지시서']
236
236
  },
237
237
  {
@@ -413,13 +413,13 @@ export const COMMAND_CATALOG = [
413
413
  { name: 'commands', usage: 'sks commands [--json]', description: 'List every user-facing command with a short description.' },
414
414
  { name: 'usage', usage: `sks usage [${USAGE_TOPICS}]`, description: 'Print copy-ready workflows for common tasks.' },
415
415
  { name: 'quickstart', usage: 'sks quickstart', description: 'Show the shortest safe setup and verification flow.' },
416
- { name: 'bootstrap', usage: 'sks bootstrap [--install-scope global|project] [--local-only] [--json]', description: 'Initialize the current project, install SKS Codex App files/skills, check Context7/Codex App/warp, and print ready true/false.' },
416
+ { name: 'bootstrap', usage: 'sks bootstrap [--install-scope global|project] [--local-only] [--json]', description: 'Initialize the current project, install SKS Codex App files/skills, check Context7/Codex App/tmux, and print ready true/false.' },
417
417
  { name: 'root', usage: 'sks root [--json]', description: 'Show whether SKS is using a project root or the per-user global SKS runtime root.' },
418
- { name: 'deps', usage: 'sks deps check|install [warp|codex|context7|all] [--yes]', description: 'Check or guided-install Node/npm PATH, Codex CLI/App, Context7, Browser Use, Computer Use, warp, and Homebrew on macOS.' },
418
+ { name: 'deps', usage: 'sks deps check|install [tmux|codex|context7|all] [--yes]', description: 'Check or guided-install Node/npm PATH, Codex CLI/App, Context7, Browser Use, Computer Use, tmux, and Homebrew on macOS.' },
419
419
  { name: 'codex-app', usage: 'sks codex-app [check|open]', description: 'Check Codex App install and first-party MCP/plugin readiness, then show app setup files and examples.' },
420
- { name: 'warp', usage: 'sks warp open|check|status [--workspace name]', description: 'Explicitly open the SKS warp runtime, or check/status without launching Warp.' },
421
- { name: 'mad', usage: 'sks --mad [--high]', description: 'Open a one-shot warp Codex CLI workspace with the SKS MAD full-access auto-review profile.' },
422
- { name: 'auto-review', usage: 'sks auto-review status|enable|start [--high] | sks --Auto-review --high', description: 'Enable Codex automatic approval review and launch SKS warp with the auto-review profile.' },
420
+ { name: 'tmux', usage: 'sks tmux open|check|status [--workspace name]', description: 'Explicitly open the SKS tmux runtime, or check/status without launching tmux.' },
421
+ { name: 'mad', usage: 'sks --mad [--high]', description: 'Open a one-shot tmux Codex CLI workspace with the SKS MAD full-access auto-review profile.' },
422
+ { name: 'auto-review', usage: 'sks auto-review status|enable|start [--high] | sks --Auto-review --high', description: 'Enable Codex automatic approval review and launch SKS tmux with the auto-review profile.' },
423
423
  { name: 'dollar-commands', usage: 'sks dollar-commands [--json]', description: 'List Codex App $ commands such as $DFix and $Team.' },
424
424
  { name: 'dfix', usage: 'sks dfix', description: 'Explain $DFix ultralight design/content fix mode.' },
425
425
  { name: 'qa-loop', usage: 'sks qa-loop prepare|answer|run|status ...', description: 'Dogfood UI/API as human proxy with safety gates, safe fixes, rechecks, Codex Computer Use-only UI evidence, report.' },
@@ -439,7 +439,7 @@ export const COMMAND_CATALOG = [
439
439
  { name: 'research', usage: 'sks research prepare|run|status ...', description: 'Run frontier-style research missions with novelty and falsification gates.' },
440
440
  { name: 'db', usage: 'sks db policy|scan|mcp-config|classify|check ...', description: 'Inspect and enforce database/Supabase safety policy.' },
441
441
  { name: 'eval', usage: 'sks eval run|compare|thresholds ...', description: 'Run deterministic context-quality and performance evidence checks.' },
442
- { name: 'harness', usage: 'sks harness fixture|review [--json]', description: 'Run Harness Growth Factory fixtures for forgetting, skills, experiments, tool taxonomy, permissions, MultiAgentV2, and Warp views.' },
442
+ { name: 'harness', usage: 'sks harness fixture|review [--json]', description: 'Run Harness Growth Factory fixtures for forgetting, skills, experiments, tool taxonomy, permissions, MultiAgentV2, and tmux views.' },
443
443
  { name: 'perf', usage: 'sks perf run|workflow [--json] [--iterations N] [--intent "task"] [--changed file1,file2]', description: 'Measure structured GPT-5.5/SKS performance budgets, including Proof Field workflow decisions and fast-lane evidence.' },
444
444
  { name: 'proof-field', usage: 'sks proof-field scan [--json] [--intent "task"] [--changed file1,file2]', description: 'Analyze Potential Proof Field cones, negative-work cache, and fast-lane eligibility for a change set.' },
445
445
  { name: 'skill-dream', usage: 'sks skill-dream status|run|record [--json]', description: 'Track generated-skill usage in lightweight JSON and periodically report keep, merge, prune, and improvement candidates without deleting skills automatically.' },
@@ -447,7 +447,7 @@ export const COMMAND_CATALOG = [
447
447
  { 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.' },
448
448
  { 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.' },
449
449
  { name: 'hproof', usage: 'sks hproof check [mission-id|latest]', description: 'Evaluate the H-Proof done gate for a mission.' },
450
- { name: 'team', usage: 'sks team "task" [executor:5 reviewer:2 user:1]|log|tail|watch|lane|status|dashboard|event|message|cleanup-warp ...', description: 'Create and observe a scout-first Team mission with color-coded Warp lanes, transcript messages, and cleanup-aware follow panes.' },
450
+ { name: 'team', usage: 'sks team "task" [executor:5 reviewer:2 user:1]|log|tail|watch|lane|status|dashboard|event|message|cleanup-tmux ...', description: 'Create and observe a scout-first Team mission with color-coded tmux lanes, transcript messages, and cleanup-aware follow panes.' },
451
451
  { name: 'reasoning', usage: 'sks reasoning ["prompt"] [--json]', description: 'Show SKS temporary reasoning-effort routing: medium for simple tasks, high for logic, xhigh for research.' },
452
452
  { name: 'gx', usage: 'sks gx init|render|validate|drift|snapshot [name]', description: 'Create and verify deterministic SVG/HTML visual context cartridges.' },
453
453
  { name: 'profile', usage: 'sks profile show|set <model>', description: 'Inspect or set the current SKS model profile metadata.' },
@@ -72,7 +72,7 @@ export function defaultTeamDashboard(id, prompt, opts = {}) {
72
72
  lane: `sks team lane ${id} --agent <agent> --follow`,
73
73
  event: `sks team event ${id} --agent <agent> --phase <phase> --message "..."`,
74
74
  message: `sks team message ${id} --from <agent> --to <agent|all> --message "..."`,
75
- cleanup: `sks team cleanup-warp ${id}`
75
+ cleanup: `sks team cleanup-tmux ${id}`
76
76
  },
77
77
  agents: Object.fromEntries([...new Set([...DEFAULT_AGENTS, ...spec.roster.all_agents.map((agent) => agent.id)])].map((name) => [name, { status: 'pending', phase: null, last_seen: null }])),
78
78
  phases: ['parallel_analysis_scouting', 'triwiki_refresh', 'debate_team', 'triwiki_refresh_after_consensus', 'parallel_development_team', 'triwiki_refresh_after_implementation', 'strict_review_and_user_acceptance', 'session_cleanup'],
@@ -98,7 +98,7 @@ ${prompt}
98
98
 
99
99
  ## How to Read
100
100
 
101
- - This file is the Codex App-visible replacement for warp-style team panes.
101
+ - This file is the Codex App-visible replacement for tmux-style team panes.
102
102
  - Use at most ${spec.agentSessions} subagent sessions at a time unless the mission is recreated with a different budget.
103
103
  - Team mode has three bundles: parallel analysis scouts first, debate team second, then fresh parallel development team.
104
104
  - Use relevant TriWiki context before every stage, hydrate low-trust claims from source during the stage, refresh after findings/artifact changes, and validate before handoffs or final claims.
@@ -124,7 +124,7 @@ sks team watch ${id}
124
124
  sks team lane ${id} --agent analysis_scout_1 --follow
125
125
  sks team event ${id} --agent analysis_scout_1 --phase parallel_analysis_scouting --message "mapped repo slice"
126
126
  sks team message ${id} --from analysis_scout_1 --to executor_1 --message "handoff note"
127
- sks team cleanup-warp ${id}
127
+ sks team cleanup-tmux ${id}
128
128
  \`\`\`
129
129
 
130
130
  ## Roster
@@ -220,7 +220,7 @@ export function parseTeamSpecArgs(args = []) {
220
220
  i++;
221
221
  continue;
222
222
  }
223
- if (arg === '--json' || arg === '--open-warp' || arg === '--warp-open') continue;
223
+ if (arg === '--json' || arg === '--open-tmux' || arg === '--tmux-open') continue;
224
224
  cleanArgs.push(args[i]);
225
225
  }
226
226
  return { cleanArgs, ...normalizeTeamSpec({ roleCounts, agentSessions: explicitSession }) };
@@ -363,7 +363,7 @@ export async function requestTeamSessionCleanup(dir, opts = {}) {
363
363
  cleanup_requested_at: opts.ts || nowIso(),
364
364
  cleanup_requested_by: opts.agent || 'parent_orchestrator',
365
365
  cleanup_reason: opts.reason || 'Team session cleanup requested.',
366
- final_message: opts.finalMessage || 'Team session ended. Lane follow loops may stop; Warp panes remain user-controlled.'
366
+ final_message: opts.finalMessage || 'Team session ended. Lane follow loops may stop; tmux panes remain user-controlled.'
367
367
  };
368
368
  await writeJsonAtomic(files.control, next);
369
369
  return next;
@@ -383,7 +383,7 @@ export function renderTeamCleanupSummary(control = {}) {
383
383
  `Requested by: ${control.cleanup_requested_by || 'unknown'}`,
384
384
  `Reason: ${control.cleanup_reason || 'Team session cleanup requested.'}`,
385
385
  '',
386
- control.final_message || 'Team session ended. Warp panes remain user-controlled.'
386
+ control.final_message || 'Team session ended. tmux panes remain user-controlled.'
387
387
  ].join('\n');
388
388
  }
389
389
 
@@ -469,10 +469,10 @@ export async function renderTeamWatch(dir, opts = {}) {
469
469
  '',
470
470
  '## Split-Screen Map',
471
471
  '- This overview pane follows the whole mission transcript.',
472
- '- Neighbor warp panes follow individual `sks team lane ... --agent <name>` views.',
472
+ '- Neighbor tmux panes follow individual `sks team lane ... --agent <name>` views.',
473
473
  '- Use `sks team event ...` to mirror scout, debate, executor, review, and verification status into the live panes.',
474
474
  '- Use `sks team message ... --from <agent> --to <agent|all>` for bounded inter-agent communication in transcript/lane views.',
475
- '- Use `sks team cleanup-warp ...` at session end; follow loops show cleanup and exit while Warp panes remain user-controlled.',
475
+ '- Use `sks team cleanup-tmux ...` at session end; follow loops show cleanup and exit while tmux panes remain user-controlled.',
476
476
  '',
477
477
  '## Cockpit Views',
478
478
  '- Mission / Goal | Agents | MultiAgentV2 | Work Orders | Skills | Memory Health | Forget Queue',
@@ -0,0 +1,447 @@
1
+ import path from 'node:path';
2
+ import fsp from 'node:fs/promises';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { exists, nowIso, packageRoot, readJson, runProcess, sha256, sksRoot, which, writeJsonAtomic } from './fsx.mjs';
5
+ import { getCodexInfo } from './codex-adapter.mjs';
6
+ import { codexAppIntegrationStatus, formatCodexAppStatus } from './codex-app.mjs';
7
+
8
+ export const SKS_TMUX_LOGO = [
9
+ ' _____ __ __ _____',
10
+ ' / ___// //_// ___/',
11
+ ' \\__ \\/ ,< \\__ \\ ㅅㅋㅅ',
12
+ ' ___/ / /| | ___/ /',
13
+ '/____/_/ |_|/____/',
14
+ 'Sneakoscope Codex tmux'
15
+ ].join('\n');
16
+
17
+ export function sanitizeTmuxSessionName(input) {
18
+ const base = String(input || 'sks').trim().replace(/[^A-Za-z0-9_.:-]+/g, '-').replace(/^-+|-+$/g, '');
19
+ return (base || 'sks').slice(0, 80);
20
+ }
21
+
22
+ export function defaultTmuxSessionName(root) {
23
+ const base = sanitizeTmuxSessionName(path.basename(root || process.cwd()) || 'project');
24
+ const hash = sha256(path.resolve(root || process.cwd())).slice(0, 8);
25
+ return sanitizeTmuxSessionName(`sks-${base}-${hash}`);
26
+ }
27
+
28
+ export function shellEscape(value) {
29
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
30
+ }
31
+
32
+ export function platformTmuxInstallHint() {
33
+ if (process.platform === 'darwin') return 'Install tmux 3.x or newer: brew install tmux';
34
+ return 'Install tmux 3.x or newer with your OS package manager, then run: sks tmux check';
35
+ }
36
+
37
+ export function tmuxStatePath(root = process.cwd()) {
38
+ return path.join(path.resolve(root || process.cwd()), '.sneakoscope', 'state', 'tmux-sessions.json');
39
+ }
40
+
41
+ export function tmuxTeamStatePath(root = process.cwd()) {
42
+ return path.join(path.resolve(root || process.cwd()), '.sneakoscope', 'state', 'tmux-team-sessions.json');
43
+ }
44
+
45
+ export function isTmuxShellSession(env = process.env) {
46
+ return Boolean(String(env.TMUX || '').trim());
47
+ }
48
+
49
+ export async function findTmuxBin() {
50
+ return await which('tmux').catch(() => null);
51
+ }
52
+
53
+ function parseTmuxVersion(text = '') {
54
+ const match = String(text || '').match(/tmux\s+([0-9]+(?:\.[0-9]+)?[a-z]?)/i);
55
+ return match ? match[1] : null;
56
+ }
57
+
58
+ function tmuxVersionOk(version = '') {
59
+ const match = String(version || '').match(/^([0-9]+)(?:\.([0-9]+))?/);
60
+ if (!match) return false;
61
+ const major = Number(match[1]);
62
+ const minor = Number(match[2] || 0);
63
+ return major > 3 || (major === 3 && minor >= 0);
64
+ }
65
+
66
+ export async function tmuxReadiness(opts = {}) {
67
+ const bin = opts.bin ?? await findTmuxBin();
68
+ let version = opts.version || null;
69
+ let error = null;
70
+ if (bin && !version) {
71
+ const run = await runProcess(bin, ['-V'], { timeoutMs: 5000, maxOutputBytes: 4096 }).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
72
+ if (run.code === 0) version = parseTmuxVersion(run.stdout || run.stderr || '');
73
+ else error = run.stderr || run.stdout || 'tmux -V failed';
74
+ }
75
+ const ok = Boolean(bin && version && tmuxVersionOk(version));
76
+ return {
77
+ ok,
78
+ bin: bin || null,
79
+ version,
80
+ min_version: '3.0',
81
+ current_session: isTmuxShellSession(opts.env || process.env),
82
+ error: ok ? null : error || (bin ? `tmux ${version || 'unknown'} is older than 3.0` : 'tmux not found')
83
+ };
84
+ }
85
+
86
+ export function tmuxStatusKind(tmux = {}) {
87
+ return tmux.ok ? 'ok' : 'missing';
88
+ }
89
+
90
+ export function codexLaunchCommand(root, codexBin, codexArgs = []) {
91
+ const extraArgs = Array.isArray(codexArgs) ? codexArgs : [];
92
+ return [
93
+ 'clear',
94
+ `printf '%s\\n' ${shellEscape(SKS_TMUX_LOGO)}`,
95
+ `printf '\\nProject: %s\\n' ${shellEscape(root)}`,
96
+ 'printf \'Runtime: tmux session for Codex CLI\\n\'',
97
+ 'printf \'Prompt: use canonical $ commands, for example $Team or $QA-LOOP\\n\\n\'',
98
+ 'sleep 1',
99
+ `exec ${[shellEscape(codexBin), ...extraArgs.map(shellEscape), '--cd', shellEscape(root)].join(' ')}`
100
+ ].join('; ');
101
+ }
102
+
103
+ function terminalTitleCommand(title = '') {
104
+ return `printf '\\033]0;%s\\007' ${shellEscape(String(title || '').slice(0, 80))}`;
105
+ }
106
+
107
+ function ansiColorCode(color = '') {
108
+ return {
109
+ blue: '34',
110
+ cyan: '36',
111
+ yellow: '33',
112
+ green: '32',
113
+ red: '31',
114
+ magenta: '35'
115
+ }[String(color || '').toLowerCase()] || '37';
116
+ }
117
+
118
+ function colorizedLaneBannerCommand(lines = [], color = '') {
119
+ const code = ansiColorCode(color);
120
+ const text = lines.join('\n');
121
+ return `printf '\\033[1;${code}m%s\\033[0m\\n' ${shellEscape(text)}`;
122
+ }
123
+
124
+ export const TMUX_TEAM_LANE_STYLES = Object.freeze({
125
+ overview: Object.freeze({ role: 'overview', label: 'overview', color_name: 'Blue', color: 'blue', icon: 'layout-dashboard' }),
126
+ scout: Object.freeze({ role: 'scout', label: 'scout', color_name: 'Cyan', color: 'cyan', icon: 'search' }),
127
+ planning: Object.freeze({ role: 'planning', label: 'plan', color_name: 'Yellow', color: 'yellow', icon: 'messages-square' }),
128
+ execution: Object.freeze({ role: 'execution', label: 'exec', color_name: 'Green', color: 'green', icon: 'hammer' }),
129
+ review: Object.freeze({ role: 'review', label: 'review', color_name: 'Red', color: 'red', icon: 'shield-check' }),
130
+ safety: Object.freeze({ role: 'safety', label: 'safety', color_name: 'Magenta', color: 'magenta', icon: 'database' })
131
+ });
132
+
133
+ export function teamLaneStyle(agentId = '') {
134
+ const id = String(agentId || '').toLowerCase();
135
+ if (!id || id === 'mission_overview' || id === 'overview') return TMUX_TEAM_LANE_STYLES.overview;
136
+ if (/analysis|scout/.test(id)) return TMUX_TEAM_LANE_STYLES.scout;
137
+ if (/debate|consensus|planner|user/.test(id)) return TMUX_TEAM_LANE_STYLES.planning;
138
+ if (/db|safety/.test(id)) return TMUX_TEAM_LANE_STYLES.safety;
139
+ if (/review|qa|validation/.test(id)) return TMUX_TEAM_LANE_STYLES.review;
140
+ if (/executor|implementation|worker|developer/.test(id)) return TMUX_TEAM_LANE_STYLES.execution;
141
+ return TMUX_TEAM_LANE_STYLES.planning;
142
+ }
143
+
144
+ function teamLaneTitle(agentId = '') {
145
+ const style = teamLaneStyle(agentId);
146
+ return `${style.label}: ${String(agentId || 'mission_overview')}`.slice(0, 80);
147
+ }
148
+
149
+ export function teamAgentCommand(root, missionId, agentId, phase) {
150
+ const style = teamLaneStyle(agentId);
151
+ const title = teamLaneTitle(agentId);
152
+ return [
153
+ terminalTitleCommand(title),
154
+ 'clear',
155
+ colorizedLaneBannerCommand([...SKS_TMUX_LOGO.split('\n'), '', `Team mission: ${missionId}`, `Agent: ${agentId}`, `Lane: ${style.label} (${style.color_name})`, `Phase: ${phase}`, 'Messages: sks team message ... --to ' + agentId, 'Cleanup: sks team cleanup-tmux ' + missionId], style.color),
156
+ `cd ${shellEscape(root)}`,
157
+ `node ${shellEscape(path.join(packageRoot(), 'bin', 'sks.mjs'))} team lane ${shellEscape(missionId)} --agent ${shellEscape(agentId)} --phase ${shellEscape(phase)} --follow --lines 12`
158
+ ].join('; ');
159
+ }
160
+
161
+ export function teamOverviewCommand(root, missionId) {
162
+ const style = teamLaneStyle('mission_overview');
163
+ const title = teamLaneTitle('mission_overview');
164
+ return [
165
+ terminalTitleCommand(title),
166
+ 'clear',
167
+ colorizedLaneBannerCommand([...SKS_TMUX_LOGO.split('\n'), '', `Team mission: ${missionId}`, 'View: live orchestration overview', `Lane: ${style.label} (${style.color_name})`, 'Messages: sks team message ... --to <agent|all>', 'Cleanup: sks team cleanup-tmux ' + missionId], style.color),
168
+ `cd ${shellEscape(root)}`,
169
+ `node ${shellEscape(path.join(packageRoot(), 'bin', 'sks.mjs'))} team watch ${shellEscape(missionId)} --follow --lines 18`
170
+ ].join('; ');
171
+ }
172
+
173
+ export async function buildTmuxLaunchPlan(opts = {}) {
174
+ const root = path.resolve(opts.root || await sksRoot());
175
+ const session = sanitizeTmuxSessionName(opts.session || opts.workspace || defaultTmuxSessionName(root));
176
+ const sksBin = opts.sksBin || path.join(packageRoot(), 'bin', 'sks.mjs');
177
+ const codex = opts.codex || await getCodexInfo().catch(() => ({}));
178
+ const tmux = opts.tmux || await tmuxReadiness(opts);
179
+ const app = opts.app || await codexAppIntegrationStatus({ codex });
180
+ const codexArgs = Array.isArray(opts.codexArgs) ? opts.codexArgs : [];
181
+ return {
182
+ root,
183
+ session,
184
+ workspace: session,
185
+ sksBin,
186
+ codex,
187
+ tmux,
188
+ app,
189
+ codexArgs,
190
+ attach_command: `tmux attach-session -t ${session}`,
191
+ ready: Boolean(tmux.ok && codex.bin),
192
+ warnings: app.ok ? [] : app.guidance || [],
193
+ blockers: [
194
+ ...(!tmux.ok ? [`tmux missing or too old. ${platformTmuxInstallHint()}`] : []),
195
+ ...(!codex.bin ? ['Codex CLI missing. Install: npm i -g @openai/codex, or set SKS_CODEX_BIN.'] : [])
196
+ ]
197
+ };
198
+ }
199
+
200
+ export function formatTmuxBanner(status = null) {
201
+ const lines = [
202
+ SKS_TMUX_LOGO,
203
+ '',
204
+ 'ㅅㅋㅅ tmux runtime',
205
+ '',
206
+ 'Canonical prompt commands:',
207
+ ' $DFix $Answer $SKS $Team $QA-LOOP $PPT $Goal $Research $AutoResearch $DB $GX $Wiki $Help',
208
+ '',
209
+ 'CLI-first runtime:',
210
+ ' sks tmux open open or attach a tmux Codex CLI session',
211
+ ' sks --mad open one-shot MAD full-access auto-review tmux session',
212
+ ' sks team "task" prepare Team mission and tmux multi-pane live view',
213
+ '',
214
+ 'Useful terminal commands:',
215
+ ' sks commands',
216
+ ' sks dollar-commands',
217
+ ' sks codex-app check',
218
+ ' sks doctor --fix'
219
+ ];
220
+ if (status) lines.push('', formatCodexAppStatus(status));
221
+ return lines.join('\n');
222
+ }
223
+
224
+ function tmuxRun(bin, args, opts = {}) {
225
+ return runProcess(bin || 'tmux', args, { timeoutMs: opts.timeoutMs || 10000, maxOutputBytes: opts.maxOutputBytes || 32 * 1024 })
226
+ .catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
227
+ }
228
+
229
+ function paneId(stdout = '') {
230
+ const id = String(stdout || '').split(/\r?\n/)[0]?.trim() || '';
231
+ return id.startsWith('%') ? id : null;
232
+ }
233
+
234
+ async function hasTmuxSession(bin, session) {
235
+ const run = await tmuxRun(bin, ['has-session', '-t', session], { timeoutMs: 5000 });
236
+ return run.code === 0;
237
+ }
238
+
239
+ export function buildTmuxOpenArgs(plan = {}) {
240
+ return ['attach-session', '-t', sanitizeTmuxSessionName(plan.session || plan.workspace || defaultTmuxSessionName(plan.root))];
241
+ }
242
+
243
+ export function runTmuxLaunchPlanSyntaxCheck(plan = {}) {
244
+ const args = buildTmuxOpenArgs(plan);
245
+ return {
246
+ ok: args[0] === 'attach-session' && args[1] === '-t' && Boolean(args[2]),
247
+ has_session: Boolean(args[2]),
248
+ command: ['tmux', ...args].join(' ')
249
+ };
250
+ }
251
+
252
+ export async function createTmuxSession(plan = {}, panes = [], opts = {}) {
253
+ const tmuxBin = plan.tmux?.bin || await findTmuxBin() || 'tmux';
254
+ const session = sanitizeTmuxSessionName(plan.session || plan.workspace || defaultTmuxSessionName(plan.root));
255
+ const root = path.resolve(plan.root || process.cwd());
256
+ const normalizedPanes = panes.length ? panes : [{ cwd: root, command: plan.command || codexLaunchCommand(root, plan.codex?.bin || 'codex', plan.codexArgs), focused: true }];
257
+ if (await hasTmuxSession(tmuxBin, session)) {
258
+ return { ok: true, reused: true, session, panes: [], attach_command: `tmux attach-session -t ${session}` };
259
+ }
260
+ const first = normalizedPanes[0] || { cwd: root, command: 'pwd' };
261
+ const create = await tmuxRun(tmuxBin, ['new-session', '-d', '-s', session, '-c', path.resolve(first.cwd || root), '-n', 'sks', '-P', '-F', '#{pane_id}', first.command || 'pwd']);
262
+ if (create.code !== 0) return { ok: false, session, panes: [], stderr: create.stderr || create.stdout || 'tmux new-session failed' };
263
+ const created = [{ pane_id: paneId(create.stdout), role: first.role || 'overview', title: first.title || 'overview' }];
264
+ for (const pane of normalizedPanes.slice(1)) {
265
+ const split = await tmuxRun(tmuxBin, ['split-window', '-t', session, pane.vertical ? '-v' : '-h', '-d', '-P', '-F', '#{pane_id}', '-c', path.resolve(pane.cwd || root), pane.command || 'pwd']);
266
+ if (split.code !== 0) return { ok: false, session, panes: created, stderr: split.stderr || split.stdout || 'tmux split-window failed' };
267
+ created.push({ pane_id: paneId(split.stdout), role: pane.role || 'lane', title: pane.title || null });
268
+ }
269
+ await tmuxRun(tmuxBin, ['select-layout', '-t', session, opts.layout || 'tiled']).catch(() => null);
270
+ return { ok: true, reused: false, session, panes: created, attach_command: `tmux attach-session -t ${session}` };
271
+ }
272
+
273
+ export async function launchTmuxUi(args = [], opts = {}) {
274
+ const rootArg = readOption(args, '--root', opts.root);
275
+ const sessionArg = readOption(args, '--session', readOption(args, '--workspace', opts.session || opts.workspace));
276
+ const plan = await buildTmuxLaunchPlan({ ...opts, root: rootArg, session: sessionArg });
277
+ if (args.includes('--json')) return { plan };
278
+ if (!plan.ready && !args.includes('--status-only')) {
279
+ printTmuxLaunchBlocked(plan, { concise: opts.conciseBlockers });
280
+ process.exitCode = 1;
281
+ return { plan };
282
+ }
283
+ if (args.includes('--status-only')) return { plan };
284
+ const command = codexLaunchCommand(plan.root, plan.codex.bin, plan.codexArgs);
285
+ const created = await createTmuxSession({ ...plan, command }, [{ cwd: plan.root, command, focused: true, role: 'codex', title: 'Codex CLI' }]);
286
+ if (created.ok) await writeTmuxSessionRecord(plan.root, { session: created.session, attach_command: created.attach_command, panes: created.panes }).catch(() => null);
287
+ if (!args.includes('--quiet')) {
288
+ console.log(`SKS tmux session: ${created.session || plan.session}`);
289
+ if (created.ok && created.reused) console.log('tmux: reused existing session');
290
+ else if (created.ok) console.log(`tmux: created ${created.panes.length} pane(s)`);
291
+ else console.log(`tmux: not created (${created.stderr || 'tmux failed'})`);
292
+ if (created.ok) console.log(`Attach: ${created.attach_command}`);
293
+ }
294
+ return { plan, created: Boolean(created.ok), session: created.session || plan.session, opened: created };
295
+ }
296
+
297
+ function printTmuxLaunchBlocked(plan, opts = {}) {
298
+ if (opts.concise) {
299
+ console.error('SKS tmux launch blocked.');
300
+ if (!plan.tmux.ok) console.error(`- tmux missing: ${platformTmuxInstallHint()}`);
301
+ if (!plan.codex.bin) console.error('- Codex CLI missing. Install: npm i -g @openai/codex@latest, or set SKS_CODEX_BIN.');
302
+ return;
303
+ }
304
+ console.log(formatTmuxBanner(plan.app));
305
+ console.log('\nLaunch blocked:\n');
306
+ for (const blocker of Array.from(new Set(plan.blockers))) console.log(`- ${blocker}`);
307
+ }
308
+
309
+ export async function launchTmuxTeamView({ root, missionId, plan = {}, promptFile = null, json = false } = {}) {
310
+ const launch = await buildTmuxLaunchPlan({ root, session: `sks-team-${missionId}` });
311
+ const agents = [
312
+ ...(plan.roster?.analysis_team || []),
313
+ ...(plan.roster?.debate_team || []),
314
+ ...(plan.roster?.development_team || []),
315
+ ...(plan.roster?.validation_team || [])
316
+ ];
317
+ const uniqueAgents = [];
318
+ const seen = new Set();
319
+ for (const agent of agents) {
320
+ const id = agent.id || String(agent);
321
+ if (seen.has(id)) continue;
322
+ seen.add(id);
323
+ uniqueAgents.push(id);
324
+ }
325
+ const commands = uniqueAgents.slice(0, Math.max(1, plan.agent_session_count || 3)).map((agentId, index) => ({
326
+ agent: agentId,
327
+ command: teamAgentCommand(launch.root, missionId, agentId, index === 0 ? 'analysis' : 'team', promptFile),
328
+ style: teamLaneStyle(agentId),
329
+ title: teamLaneTitle(agentId)
330
+ }));
331
+ const overview = { agent: 'mission_overview', role: 'overview', command: teamOverviewCommand(launch.root, missionId), style: teamLaneStyle('mission_overview'), title: teamLaneTitle('mission_overview') };
332
+ const lanes = [overview, ...commands.map((entry) => ({ ...entry, role: entry.style.role }))];
333
+ const result = {
334
+ ready: launch.ready,
335
+ tmux: launch.tmux,
336
+ session: launch.session,
337
+ workspace: launch.session,
338
+ overview,
339
+ agents: commands,
340
+ lanes,
341
+ cleanup_policy: 'mark-complete; tmux panes remain user controlled',
342
+ blockers: launch.blockers,
343
+ attach_command: launch.attach_command
344
+ };
345
+ if (json || !launch.ready) return result;
346
+ const panes = lanes.map((lane, index) => ({ cwd: launch.root, command: lane.command, focused: index === 0, role: lane.role, title: lane.title, vertical: index > 1 }));
347
+ const created = await createTmuxSession(launch, panes);
348
+ result.created = Boolean(created.ok);
349
+ result.opened = created;
350
+ result.session = created.session || launch.session;
351
+ result.opened_lane_count = created.panes?.length || lanes.length;
352
+ result.all_lanes_opened = Boolean(created.ok);
353
+ result.ready = Boolean(result.ready && created.ok);
354
+ await writeTmuxTeamRecord(launch.root, {
355
+ mission_id: missionId,
356
+ session: result.session,
357
+ attach_command: created.attach_command || launch.attach_command,
358
+ cleanup_policy: result.cleanup_policy,
359
+ panes: created.panes || [],
360
+ lanes: lanes.map((entry) => ({
361
+ agent: entry.agent,
362
+ role: entry.style?.role || teamLaneStyle(entry.agent).role,
363
+ style: entry.style || teamLaneStyle(entry.agent),
364
+ title: entry.title || teamLaneTitle(entry.agent)
365
+ }))
366
+ }).catch(() => null);
367
+ return result;
368
+ }
369
+
370
+ async function writeTmuxSessionRecord(root, record = {}) {
371
+ if (!record.session) return null;
372
+ const statePath = tmuxStatePath(root);
373
+ const state = await readJson(statePath, {}).catch(() => ({}));
374
+ const now = nowIso();
375
+ const nextRecord = { ...record, schema_version: 1, root: path.resolve(root || process.cwd()), updated_at: now };
376
+ const sessions = state.sessions && typeof state.sessions === 'object' ? state.sessions : {};
377
+ await writeJsonAtomic(statePath, {
378
+ schema_version: 1,
379
+ updated_at: now,
380
+ sessions: { ...sessions, [record.session]: nextRecord }
381
+ });
382
+ return nextRecord;
383
+ }
384
+
385
+ async function writeTmuxTeamRecord(root, record = {}) {
386
+ if (!record.mission_id || !record.session) return null;
387
+ const statePath = tmuxTeamStatePath(root);
388
+ const state = await readJson(statePath, {}).catch(() => ({}));
389
+ const now = nowIso();
390
+ const nextRecord = { ...record, schema_version: 1, root: path.resolve(root || process.cwd()), updated_at: now };
391
+ const missions = state.missions && typeof state.missions === 'object' ? state.missions : {};
392
+ await writeJsonAtomic(statePath, {
393
+ schema_version: 1,
394
+ updated_at: now,
395
+ missions: { ...missions, [record.mission_id]: nextRecord }
396
+ });
397
+ return nextRecord;
398
+ }
399
+
400
+ async function readTmuxTeamRecord(root, missionId) {
401
+ const state = await readJson(tmuxTeamStatePath(root), {}).catch(() => ({}));
402
+ const missions = state.missions && typeof state.missions === 'object' ? state.missions : {};
403
+ if (missionId && missionId !== 'latest') return missions[missionId] || null;
404
+ const records = Object.values(missions).filter((entry) => entry && typeof entry === 'object');
405
+ records.sort((a, b) => String(b.updated_at || '').localeCompare(String(a.updated_at || '')));
406
+ return records[0] || null;
407
+ }
408
+
409
+ export async function cleanupTmuxTeamView({ root, missionId = 'latest', closeSession = false } = {}) {
410
+ const resolvedRoot = path.resolve(root || await sksRoot());
411
+ const record = await readTmuxTeamRecord(resolvedRoot, missionId);
412
+ if (!record?.session) return { ok: false, skipped: true, reason: 'no recorded tmux Team session', mission_id: missionId };
413
+ let killed_session = false;
414
+ if (closeSession || closeSession === true) {
415
+ const tmuxBin = await findTmuxBin() || 'tmux';
416
+ const kill = await tmuxRun(tmuxBin, ['kill-session', '-t', record.session], { timeoutMs: 5000 });
417
+ killed_session = kill.code === 0;
418
+ }
419
+ await writeTmuxTeamRecord(resolvedRoot, { ...record, cleanup_completed_at: nowIso(), killed_session }).catch(() => null);
420
+ return {
421
+ ok: true,
422
+ mission_id: record.mission_id,
423
+ session: record.session,
424
+ attach_command: record.attach_command,
425
+ close_session: Boolean(closeSession),
426
+ killed_session,
427
+ requested_close_surfaces: closeSession ? 1 : 0,
428
+ closed_surfaces: killed_session ? 1 : 0,
429
+ reason: closeSession ? 'tmux kill-session requested for recorded Team session.' : 'cleanup marks the SKS tmux Team record complete; panes remain user-controlled.'
430
+ };
431
+ }
432
+
433
+ export async function runTmuxStatus(args = [], opts = {}) {
434
+ const once = args.includes('--once') || !args.includes('--watch');
435
+ do {
436
+ const app = await codexAppIntegrationStatus();
437
+ console.clear();
438
+ console.log(formatTmuxBanner(app));
439
+ if (once) return app;
440
+ await new Promise((resolve) => setTimeout(resolve, 5000));
441
+ } while (true);
442
+ }
443
+
444
+ function readOption(args, name, fallback = null) {
445
+ const i = args.indexOf(name);
446
+ return i >= 0 && args[i + 1] ? args[i + 1] : fallback;
447
+ }