sneakoscope 0.7.6 → 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.
@@ -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
+ }