sneakoscope 0.6.76 → 0.6.78
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 +6 -1
- package/package.json +1 -1
- package/src/cli/main.mjs +22 -5
- package/src/cli/maintenance-commands.mjs +59 -7
- package/src/core/artifact-schemas.mjs +18 -1
- package/src/core/cmux-ui.mjs +263 -10
- package/src/core/evaluation.mjs +346 -1
- package/src/core/fsx.mjs +1 -1
- package/src/core/goal-workflow.mjs +42 -1
- package/src/core/hooks-runtime.mjs +21 -0
- package/src/core/init.mjs +1 -1
- package/src/core/memory-governor.mjs +21 -11
- package/src/core/pipeline.mjs +9 -3
- package/src/core/routes.mjs +2 -1
- package/src/core/skill-forge.mjs +16 -1
- package/src/core/team-dashboard-renderer.mjs +12 -8
- package/src/core/team-live.mjs +41 -0
package/src/core/cmux-ui.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fsp from 'node:fs/promises';
|
|
3
|
-
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
4
4
|
import { exists, nowIso, packageRoot, readJson, runProcess, sha256, sksRoot, which, writeJsonAtomic } from './fsx.mjs';
|
|
5
5
|
import { getCodexInfo } from './codex-adapter.mjs';
|
|
6
6
|
import { codexAppIntegrationStatus, formatCodexAppStatus } from './codex-app.mjs';
|
|
@@ -90,6 +90,10 @@ export function cmuxWorkspaceStatePath(plan = {}) {
|
|
|
90
90
|
return path.join(path.resolve(plan.root || process.cwd()), '.sneakoscope', 'state', 'cmux-workspaces.json');
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
export function cmuxTeamStatePath(root = process.cwd()) {
|
|
94
|
+
return path.join(path.resolve(root || process.cwd()), '.sneakoscope', 'state', 'cmux-team-workspaces.json');
|
|
95
|
+
}
|
|
96
|
+
|
|
93
97
|
export function cmuxWorkspaceStateKey(plan = {}) {
|
|
94
98
|
const root = path.resolve(plan.root || process.cwd());
|
|
95
99
|
const workspace = sanitizeCmuxWorkspaceName(plan.workspace || defaultCmuxWorkspaceName(root));
|
|
@@ -189,6 +193,23 @@ export function cmuxBinaryCandidates() {
|
|
|
189
193
|
return Array.from(new Set(candidates));
|
|
190
194
|
}
|
|
191
195
|
|
|
196
|
+
function cmuxAppExecutableCandidates() {
|
|
197
|
+
if (process.platform !== 'darwin') return [];
|
|
198
|
+
const envApps = String(process.env.SKS_CMUX_APP_PATHS || '')
|
|
199
|
+
.split(path.delimiter)
|
|
200
|
+
.map((entry) => entry.trim())
|
|
201
|
+
.filter(Boolean);
|
|
202
|
+
const appBundles = [
|
|
203
|
+
...envApps,
|
|
204
|
+
'/Applications/cmux.app',
|
|
205
|
+
'/Applications/Cmux.app',
|
|
206
|
+
'/Applications/CMUX.app',
|
|
207
|
+
path.join(process.env.HOME || '', 'Applications', 'cmux.app'),
|
|
208
|
+
'/opt/homebrew/Caskroom/cmux/latest/cmux.app'
|
|
209
|
+
].filter(Boolean);
|
|
210
|
+
return Array.from(new Set(appBundles.map((app) => path.join(app, 'Contents', 'MacOS', 'cmux'))));
|
|
211
|
+
}
|
|
212
|
+
|
|
192
213
|
export async function cmuxAvailable() {
|
|
193
214
|
const bin = await findCmuxBinary();
|
|
194
215
|
if (!bin) return { ok: false, bin: null, version: null, executable_ok: false, error: 'cmux CLI not found' };
|
|
@@ -267,14 +288,59 @@ export function codexLaunchCommand(root, codexBin, codexArgs = []) {
|
|
|
267
288
|
].join('; ');
|
|
268
289
|
}
|
|
269
290
|
|
|
291
|
+
function echoLinesCommand(lines = []) {
|
|
292
|
+
return lines.map((line) => String(line) ? `echo ${shellEscape(line)}` : 'echo').join('; ');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export const CMUX_TEAM_LANE_STYLES = Object.freeze({
|
|
296
|
+
overview: Object.freeze({ role: 'overview', label: 'overview', color_name: 'Charcoal', color: '#3E4B5E', icon: 'layout-dashboard' }),
|
|
297
|
+
scout: Object.freeze({ role: 'scout', label: 'scout', color_name: 'Aqua', color: '#0E6B8C', icon: 'search' }),
|
|
298
|
+
planning: Object.freeze({ role: 'planning', label: 'plan', color_name: 'Amber', color: '#7D6608', icon: 'messages-square' }),
|
|
299
|
+
execution: Object.freeze({ role: 'execution', label: 'exec', color_name: 'Green', color: '#196F3D', icon: 'hammer' }),
|
|
300
|
+
review: Object.freeze({ role: 'review', label: 'review', color_name: 'Crimson', color: '#922B21', icon: 'shield-check' }),
|
|
301
|
+
safety: Object.freeze({ role: 'safety', label: 'safety', color_name: 'Magenta', color: '#AD1457', icon: 'database' })
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
export function teamLaneStyle(agentId = '') {
|
|
305
|
+
const id = String(agentId || '').toLowerCase();
|
|
306
|
+
if (!id || id === 'mission_overview' || id === 'overview') return CMUX_TEAM_LANE_STYLES.overview;
|
|
307
|
+
if (/analysis|scout/.test(id)) return CMUX_TEAM_LANE_STYLES.scout;
|
|
308
|
+
if (/debate|consensus|planner|user/.test(id)) return CMUX_TEAM_LANE_STYLES.planning;
|
|
309
|
+
if (/db|safety/.test(id)) return CMUX_TEAM_LANE_STYLES.safety;
|
|
310
|
+
if (/review|qa|validation/.test(id)) return CMUX_TEAM_LANE_STYLES.review;
|
|
311
|
+
if (/executor|implementation|worker|developer/.test(id)) return CMUX_TEAM_LANE_STYLES.execution;
|
|
312
|
+
return CMUX_TEAM_LANE_STYLES.planning;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function teamLaneTitle(agentId = '') {
|
|
316
|
+
const style = teamLaneStyle(agentId);
|
|
317
|
+
return `${style.label}: ${String(agentId || 'mission_overview')}`.slice(0, 80);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function cmuxStatusKey(agentId = '') {
|
|
321
|
+
return sanitizeCmuxWorkspaceName(`sks-${String(agentId || 'overview').toLowerCase()}`).slice(0, 40);
|
|
322
|
+
}
|
|
323
|
+
|
|
270
324
|
export function teamAgentCommand(root, missionId, agentId, phase) {
|
|
325
|
+
const style = teamLaneStyle(agentId);
|
|
271
326
|
return [
|
|
272
|
-
|
|
327
|
+
'clear',
|
|
328
|
+
echoLinesCommand([...SKS_CMUX_LOGO.split('\n'), '', `Team mission: ${missionId}`, `Agent: ${agentId}`, `Lane: ${style.label} (${style.color_name} ${style.color})`, `Phase: ${phase}`, '']),
|
|
273
329
|
`cd ${shellEscape(root)}`,
|
|
274
330
|
`node ${shellEscape(path.join(packageRoot(), 'bin', 'sks.mjs'))} team lane ${shellEscape(missionId)} --agent ${shellEscape(agentId)} --phase ${shellEscape(phase)} --follow --lines 12`
|
|
275
331
|
].join('; ');
|
|
276
332
|
}
|
|
277
333
|
|
|
334
|
+
export function teamOverviewCommand(root, missionId) {
|
|
335
|
+
const style = teamLaneStyle('mission_overview');
|
|
336
|
+
return [
|
|
337
|
+
'clear',
|
|
338
|
+
echoLinesCommand([...SKS_CMUX_LOGO.split('\n'), '', `Team mission: ${missionId}`, 'View: live orchestration overview', `Lane: ${style.label} (${style.color_name} ${style.color})`, '']),
|
|
339
|
+
`cd ${shellEscape(root)}`,
|
|
340
|
+
`node ${shellEscape(path.join(packageRoot(), 'bin', 'sks.mjs'))} team watch ${shellEscape(missionId)} --follow --lines 18`
|
|
341
|
+
].join('; ');
|
|
342
|
+
}
|
|
343
|
+
|
|
278
344
|
export async function buildCmuxLaunchPlan(opts = {}) {
|
|
279
345
|
const root = path.resolve(opts.root || await sksRoot());
|
|
280
346
|
const workspace = sanitizeCmuxWorkspaceName(opts.workspace || opts.session || defaultCmuxWorkspaceName(root));
|
|
@@ -491,6 +557,14 @@ async function ensureCmuxDaemonReady(cmux = {}) {
|
|
|
491
557
|
last = await cmuxSocketProbe(cmux.bin);
|
|
492
558
|
if (last.ok) return { ...cmux, ok: true, error: null };
|
|
493
559
|
}
|
|
560
|
+
if (process.env.SKS_CMUX_SOCKET_ALLOW_ALL !== '0') {
|
|
561
|
+
await restartCmuxApp({ socketMode: 'allowAll' });
|
|
562
|
+
for (let i = 0; i < 8; i++) {
|
|
563
|
+
await new Promise((resolve) => setTimeout(resolve, 750));
|
|
564
|
+
last = await cmuxSocketProbe(cmux.bin);
|
|
565
|
+
if (last.ok) return { ...cmux, ok: true, error: null, socket_mode: 'allowAll' };
|
|
566
|
+
}
|
|
567
|
+
}
|
|
494
568
|
}
|
|
495
569
|
return { ok: false, error: last.error || 'cmux socket did not become ready' };
|
|
496
570
|
}
|
|
@@ -505,7 +579,7 @@ function isRecoverableCmuxSocketError(error) {
|
|
|
505
579
|
return /socket|broken pipe|receive timeout|connection refused/i.test(String(error || ''));
|
|
506
580
|
}
|
|
507
581
|
|
|
508
|
-
async function restartCmuxApp() {
|
|
582
|
+
async function restartCmuxApp(opts = {}) {
|
|
509
583
|
if (process.platform !== 'darwin') return { ok: false, reason: 'not_macos' };
|
|
510
584
|
const quit = await runProcess('osascript', ['-e', 'tell application "cmux" to quit'], { timeoutMs: 8000, maxOutputBytes: 16 * 1024 }).catch((err) => ({ code: 1, stderr: err.message, stdout: '' }));
|
|
511
585
|
if (quit.code !== 0) {
|
|
@@ -513,6 +587,21 @@ async function restartCmuxApp() {
|
|
|
513
587
|
}
|
|
514
588
|
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
515
589
|
await removeStaleCmuxSocket().catch(() => null);
|
|
590
|
+
if (opts.socketMode) return openCmuxAppWithSocketMode(opts.socketMode);
|
|
591
|
+
return openCmuxApp();
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async function openCmuxAppWithSocketMode(socketMode) {
|
|
595
|
+
for (const exe of cmuxAppExecutableCandidates()) {
|
|
596
|
+
if (!await exists(exe)) continue;
|
|
597
|
+
const child = spawn(exe, [], {
|
|
598
|
+
detached: true,
|
|
599
|
+
stdio: 'ignore',
|
|
600
|
+
env: { ...process.env, CMUX_SOCKET_MODE: socketMode }
|
|
601
|
+
});
|
|
602
|
+
child.unref();
|
|
603
|
+
return { ok: true, mode: socketMode, executable: exe };
|
|
604
|
+
}
|
|
516
605
|
return openCmuxApp();
|
|
517
606
|
}
|
|
518
607
|
|
|
@@ -524,7 +613,7 @@ async function removeStaleCmuxSocket() {
|
|
|
524
613
|
}
|
|
525
614
|
|
|
526
615
|
export async function launchCmuxTeamView({ root, missionId, plan = {}, promptFile = null, json = false } = {}) {
|
|
527
|
-
const launch = await buildCmuxLaunchPlan({ root, workspace: `sks-team-${missionId}
|
|
616
|
+
const launch = await buildCmuxLaunchPlan({ root, workspace: `sks-team-${missionId}`, wakeCmux: true });
|
|
528
617
|
const agents = [
|
|
529
618
|
...(plan.roster?.analysis_team || []),
|
|
530
619
|
...(plan.roster?.debate_team || []),
|
|
@@ -541,18 +630,26 @@ export async function launchCmuxTeamView({ root, missionId, plan = {}, promptFil
|
|
|
541
630
|
}
|
|
542
631
|
const commands = uniqueAgents.slice(0, Math.max(1, plan.agent_session_count || 3)).map((agentId, index) => ({
|
|
543
632
|
agent: agentId,
|
|
544
|
-
command: teamAgentCommand(launch.root, missionId, agentId, index === 0 ? 'analysis' : 'team', promptFile)
|
|
633
|
+
command: teamAgentCommand(launch.root, missionId, agentId, index === 0 ? 'analysis' : 'team', promptFile),
|
|
634
|
+
style: teamLaneStyle(agentId),
|
|
635
|
+
title: teamLaneTitle(agentId)
|
|
545
636
|
}));
|
|
546
|
-
const
|
|
637
|
+
const overview = { agent: 'mission_overview', role: 'overview', command: teamOverviewCommand(launch.root, missionId), style: teamLaneStyle('mission_overview'), title: teamLaneTitle('mission_overview') };
|
|
638
|
+
const lanes = [overview, ...commands.map((entry) => ({ ...entry, role: entry.style.role }))];
|
|
639
|
+
const result = { ready: launch.ready, cmux: launch.cmux, workspace: launch.workspace, overview, agents: commands, lanes, cleanup_policy: 'collapse-agent-lanes-to-overview', blockers: launch.blockers };
|
|
547
640
|
if (json || !launch.ready) return result;
|
|
548
|
-
const first =
|
|
641
|
+
const first = overview.command;
|
|
549
642
|
const created = spawnSync(launch.cmux.bin, buildCmuxNewWorkspaceArgs(launch, first), { encoding: 'utf8', stdio: 'pipe' });
|
|
550
643
|
result.created = created.status === 0;
|
|
551
644
|
result.stdout = created.stdout || '';
|
|
552
645
|
result.stderr = created.stderr || '';
|
|
553
|
-
const
|
|
646
|
+
const createdText = `${created.stdout || ''}\n${created.stderr || ''}`;
|
|
647
|
+
const workspaceRef = cmuxWorkspaceRefFromText(createdText);
|
|
648
|
+
let overviewSurfaceRef = cmuxSurfaceRefFromText(createdText);
|
|
554
649
|
if (workspaceRef) {
|
|
650
|
+
overviewSurfaceRef ||= firstCmuxSurfaceRef(launch.cmux.bin, workspaceRef);
|
|
555
651
|
result.workspace_ref = workspaceRef;
|
|
652
|
+
if (overviewSurfaceRef) result.overview.surface_ref = overviewSurfaceRef;
|
|
556
653
|
await writeCmuxWorkspaceRecord(launch, { ref: workspaceRef, name: launch.workspace, description: cmuxWorkspaceDescription(launch), cwd: launch.root }).catch(() => null);
|
|
557
654
|
const selected = await runProcess(launch.cmux.bin, ['select-workspace', '--workspace', workspaceRef], { timeoutMs: 5000, maxOutputBytes: 16 * 1024 }).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
|
|
558
655
|
result.selected = selected.code === 0;
|
|
@@ -564,7 +661,7 @@ export async function launchCmuxTeamView({ root, missionId, plan = {}, promptFil
|
|
|
564
661
|
if (!workspaceRef) result.blockers = [...(result.blockers || []), 'cmux new-workspace did not return a workspace ref'];
|
|
565
662
|
return result;
|
|
566
663
|
}
|
|
567
|
-
for (const entry of commands
|
|
664
|
+
for (const entry of commands) {
|
|
568
665
|
const split = spawnSync(launch.cmux.bin, ['new-split', 'right', '--workspace', workspaceRef], { encoding: 'utf8', stdio: 'pipe' });
|
|
569
666
|
const splitText = `${split.stdout || ''}\n${split.stderr || ''}`;
|
|
570
667
|
const surfaceRef = cmuxSurfaceRefFromText(splitText);
|
|
@@ -574,6 +671,8 @@ export async function launchCmuxTeamView({ root, missionId, plan = {}, promptFil
|
|
|
574
671
|
ok: split.status === 0 && Boolean(surfaceRef),
|
|
575
672
|
pane_ref: paneRef,
|
|
576
673
|
surface_ref: surfaceRef,
|
|
674
|
+
style: entry.style,
|
|
675
|
+
title: entry.title,
|
|
577
676
|
stdout: split.stdout || '',
|
|
578
677
|
stderr: split.stderr || ''
|
|
579
678
|
};
|
|
@@ -585,10 +684,35 @@ export async function launchCmuxTeamView({ root, missionId, plan = {}, promptFil
|
|
|
585
684
|
}
|
|
586
685
|
result.splits.push(splitResult);
|
|
587
686
|
}
|
|
687
|
+
const customizationLanes = [
|
|
688
|
+
{ ...overview, surface_ref: overviewSurfaceRef },
|
|
689
|
+
...result.splits.map((entry) => ({ agent: entry.agent, surface_ref: entry.surface_ref, pane_ref: entry.pane_ref, style: entry.style, title: entry.title }))
|
|
690
|
+
];
|
|
691
|
+
result.customization = await applyCmuxTeamCustomization(launch.cmux.bin, workspaceRef, customizationLanes);
|
|
692
|
+
await writeCmuxTeamRecord(launch.root, {
|
|
693
|
+
mission_id: missionId,
|
|
694
|
+
workspace: launch.workspace,
|
|
695
|
+
workspace_ref: workspaceRef,
|
|
696
|
+
overview_surface_ref: overviewSurfaceRef || null,
|
|
697
|
+
cleanup_policy: result.cleanup_policy,
|
|
698
|
+
lanes: customizationLanes.map((entry) => ({
|
|
699
|
+
agent: entry.agent,
|
|
700
|
+
role: entry.style?.role || teamLaneStyle(entry.agent).role,
|
|
701
|
+
style: entry.style || teamLaneStyle(entry.agent),
|
|
702
|
+
title: entry.title || teamLaneTitle(entry.agent),
|
|
703
|
+
surface_ref: entry.surface_ref || null,
|
|
704
|
+
pane_ref: entry.pane_ref || null
|
|
705
|
+
}))
|
|
706
|
+
}).catch(() => null);
|
|
588
707
|
result.split_count = result.splits.filter((entry) => entry.ok && entry.send_ok).length;
|
|
589
|
-
const expectedSplits =
|
|
708
|
+
const expectedSplits = commands.length;
|
|
590
709
|
result.opened_lane_count = 1 + result.split_count;
|
|
591
710
|
result.all_lanes_opened = result.created && result.selected !== false && result.split_count === expectedSplits;
|
|
711
|
+
result.screen_read_checks = readCmuxLaneScreens(launch.cmux.bin, workspaceRef, [
|
|
712
|
+
{ agent: 'mission_overview', surface_ref: overviewSurfaceRef },
|
|
713
|
+
...result.splits.map((entry) => ({ agent: entry.agent, surface_ref: entry.surface_ref }))
|
|
714
|
+
]);
|
|
715
|
+
result.screen_read_ok = result.screen_read_checks.some((entry) => entry.ok);
|
|
592
716
|
result.ready = Boolean(result.ready && result.all_lanes_opened);
|
|
593
717
|
if (!result.all_lanes_opened) {
|
|
594
718
|
result.blockers = [
|
|
@@ -600,6 +724,135 @@ export async function launchCmuxTeamView({ root, missionId, plan = {}, promptFil
|
|
|
600
724
|
return result;
|
|
601
725
|
}
|
|
602
726
|
|
|
727
|
+
async function applyCmuxTeamCustomization(bin, workspaceRef, lanes = []) {
|
|
728
|
+
if (!bin || !workspaceRef) return { ok: false, skipped: true, reason: 'missing cmux binary or workspace ref', operations: [] };
|
|
729
|
+
const operations = [];
|
|
730
|
+
const pushRun = async (label, args) => {
|
|
731
|
+
const run = await runProcess(bin, args, { timeoutMs: 5000, maxOutputBytes: 16 * 1024 }).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
|
|
732
|
+
operations.push({ label, args, ok: run.code === 0, stdout: run.stdout || '', stderr: run.stderr || '' });
|
|
733
|
+
return run.code === 0;
|
|
734
|
+
};
|
|
735
|
+
const overview = lanes.find((lane) => (lane.agent || '') === 'mission_overview') || lanes[0] || {};
|
|
736
|
+
const overviewStyle = overview.style || teamLaneStyle('mission_overview');
|
|
737
|
+
await pushRun('workspace-color', ['workspace-action', '--workspace', workspaceRef, '--action', 'set-color', '--color', overviewStyle.color]);
|
|
738
|
+
await pushRun('workspace-status', ['set-status', 'sks-team', 'Team live', '--icon', overviewStyle.icon, '--color', overviewStyle.color, '--workspace', workspaceRef]);
|
|
739
|
+
await pushRun('workspace-progress', ['set-progress', '0.15', '--label', 'Team running', '--workspace', workspaceRef]);
|
|
740
|
+
for (const lane of lanes) {
|
|
741
|
+
const style = lane.style || teamLaneStyle(lane.agent);
|
|
742
|
+
if (lane.surface_ref) await pushRun(`rename-${lane.agent}`, ['rename-tab', '--workspace', workspaceRef, '--surface', lane.surface_ref, '--title', lane.title || teamLaneTitle(lane.agent)]);
|
|
743
|
+
await pushRun(`status-${lane.agent}`, ['set-status', cmuxStatusKey(lane.agent), lane.title || teamLaneTitle(lane.agent), '--icon', style.icon, '--color', style.color, '--workspace', workspaceRef]);
|
|
744
|
+
}
|
|
745
|
+
return { ok: operations.some((entry) => entry.ok), operations };
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
async function writeCmuxTeamRecord(root, record = {}) {
|
|
749
|
+
if (!record.mission_id || !record.workspace_ref) return null;
|
|
750
|
+
const statePath = cmuxTeamStatePath(root);
|
|
751
|
+
const state = await readJson(statePath, {}).catch(() => ({}));
|
|
752
|
+
const now = nowIso();
|
|
753
|
+
const nextRecord = { ...record, schema_version: 1, root: path.resolve(root || process.cwd()), updated_at: now };
|
|
754
|
+
const missions = state.missions && typeof state.missions === 'object' ? state.missions : {};
|
|
755
|
+
await writeJsonAtomic(statePath, {
|
|
756
|
+
schema_version: 1,
|
|
757
|
+
updated_at: now,
|
|
758
|
+
missions: {
|
|
759
|
+
...missions,
|
|
760
|
+
[record.mission_id]: nextRecord
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
return nextRecord;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
async function readCmuxTeamRecord(root, missionId) {
|
|
767
|
+
const state = await readJson(cmuxTeamStatePath(root), {}).catch(() => ({}));
|
|
768
|
+
const missions = state.missions && typeof state.missions === 'object' ? state.missions : {};
|
|
769
|
+
if (missionId && missionId !== 'latest') return missions[missionId] || null;
|
|
770
|
+
const records = Object.values(missions).filter((entry) => entry && typeof entry === 'object');
|
|
771
|
+
records.sort((a, b) => String(b.updated_at || '').localeCompare(String(a.updated_at || '')));
|
|
772
|
+
return records[0] || null;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
export async function cleanupCmuxTeamView({ root, missionId = 'latest', closeWorkspace = false } = {}) {
|
|
776
|
+
const resolvedRoot = path.resolve(root || await sksRoot());
|
|
777
|
+
const record = await readCmuxTeamRecord(resolvedRoot, missionId);
|
|
778
|
+
if (!record?.workspace_ref) return { ok: false, skipped: true, reason: 'no recorded cmux Team workspace', mission_id: missionId };
|
|
779
|
+
const cmux = await cmuxReadiness({ wake: true }).catch((err) => ({ ok: false, error: err.message || 'cmux readiness failed' }));
|
|
780
|
+
if (!cmux.ok) return { ok: false, workspace_ref: record.workspace_ref, mission_id: record.mission_id, reason: cmux.error || 'cmux not ready' };
|
|
781
|
+
const operations = [];
|
|
782
|
+
const run = async (label, args) => {
|
|
783
|
+
const out = await runProcess(cmux.bin, args, { timeoutMs: 5000, maxOutputBytes: 16 * 1024 }).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
|
|
784
|
+
operations.push({ label, ok: out.code === 0, stdout: out.stdout || '', stderr: out.stderr || '' });
|
|
785
|
+
return out.code === 0;
|
|
786
|
+
};
|
|
787
|
+
if (closeWorkspace) {
|
|
788
|
+
const closed = await run('close-workspace', ['close-workspace', '--workspace', record.workspace_ref]);
|
|
789
|
+
return { ok: closed, mission_id: record.mission_id, workspace_ref: record.workspace_ref, close_workspace: true, closed_workspace: closed, operations };
|
|
790
|
+
}
|
|
791
|
+
let overviewSurfaceRef = record.overview_surface_ref || record.lanes?.find((lane) => lane.agent === 'mission_overview')?.surface_ref || null;
|
|
792
|
+
let agentLanes = (record.lanes || []).filter((lane) => lane.surface_ref && lane.surface_ref !== overviewSurfaceRef && lane.agent !== 'mission_overview');
|
|
793
|
+
if (!overviewSurfaceRef) {
|
|
794
|
+
const agentRefs = new Set(agentLanes.map((lane) => lane.surface_ref));
|
|
795
|
+
overviewSurfaceRef = listCmuxWorkspaceSurfacesSync(cmux.bin, record.workspace_ref).find((surfaceRef) => !agentRefs.has(surfaceRef)) || null;
|
|
796
|
+
agentLanes = (record.lanes || []).filter((lane) => lane.surface_ref && lane.surface_ref !== overviewSurfaceRef && lane.agent !== 'mission_overview');
|
|
797
|
+
}
|
|
798
|
+
let closed = 0;
|
|
799
|
+
for (const lane of agentLanes) {
|
|
800
|
+
if (await run(`close-${lane.agent}`, ['close-surface', '--workspace', record.workspace_ref, '--surface', lane.surface_ref])) closed += 1;
|
|
801
|
+
}
|
|
802
|
+
const completeStyle = CMUX_TEAM_LANE_STYLES.execution;
|
|
803
|
+
if (overviewSurfaceRef) await run('rename-overview-complete', ['rename-tab', '--workspace', record.workspace_ref, '--surface', overviewSurfaceRef, '--title', `complete: ${record.mission_id}`.slice(0, 80)]);
|
|
804
|
+
await run('status-complete', ['set-status', 'sks-team', 'Team complete', '--icon', 'check-circle', '--color', completeStyle.color, '--workspace', record.workspace_ref]);
|
|
805
|
+
await run('progress-complete', ['set-progress', '1.0', '--label', 'Team complete', '--workspace', record.workspace_ref]);
|
|
806
|
+
await run('select-workspace', ['select-workspace', '--workspace', record.workspace_ref]);
|
|
807
|
+
await writeCmuxTeamRecord(resolvedRoot, { ...record, cleanup_completed_at: nowIso(), closed_agent_surfaces: closed }).catch(() => null);
|
|
808
|
+
return {
|
|
809
|
+
ok: true,
|
|
810
|
+
mission_id: record.mission_id,
|
|
811
|
+
workspace_ref: record.workspace_ref,
|
|
812
|
+
close_workspace: false,
|
|
813
|
+
kept_surface: overviewSurfaceRef,
|
|
814
|
+
requested_close_surfaces: agentLanes.length,
|
|
815
|
+
closed_surfaces: closed,
|
|
816
|
+
operations
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function readCmuxLaneScreens(bin, workspaceRef, lanes = []) {
|
|
821
|
+
return lanes.map((lane) => {
|
|
822
|
+
const args = ['read-screen', '--workspace', workspaceRef, '--lines', '6'];
|
|
823
|
+
if (lane.surface_ref) args.splice(3, 0, '--surface', lane.surface_ref);
|
|
824
|
+
const read = spawnSync(bin, args, { encoding: 'utf8', stdio: 'pipe' });
|
|
825
|
+
const text = `${read.stdout || ''}\n${read.stderr || ''}`.trim();
|
|
826
|
+
return {
|
|
827
|
+
agent: lane.agent,
|
|
828
|
+
surface_ref: lane.surface_ref || null,
|
|
829
|
+
ok: read.status === 0 && Boolean(text),
|
|
830
|
+
preview: text.slice(0, 1000),
|
|
831
|
+
error: read.status === 0 ? null : text || 'cmux read-screen failed'
|
|
832
|
+
};
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function firstCmuxSurfaceRef(bin, workspaceRef) {
|
|
837
|
+
return listCmuxWorkspaceSurfacesSync(bin, workspaceRef)[0] || '';
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function listCmuxWorkspaceSurfacesSync(bin, workspaceRef) {
|
|
841
|
+
if (!bin || !workspaceRef) return [];
|
|
842
|
+
const panes = spawnSync(bin, ['list-panes', '--workspace', workspaceRef], { encoding: 'utf8', stdio: 'pipe' });
|
|
843
|
+
if (panes.status !== 0) return [];
|
|
844
|
+
const paneRefs = Array.from(new Set(String(`${panes.stdout || ''}\n${panes.stderr || ''}`).match(/\bpane:\d+\b/g) || []));
|
|
845
|
+
const surfaces = [];
|
|
846
|
+
for (const paneRef of paneRefs) {
|
|
847
|
+
const run = spawnSync(bin, ['list-pane-surfaces', '--workspace', workspaceRef, '--pane', paneRef], { encoding: 'utf8', stdio: 'pipe' });
|
|
848
|
+
if (run.status !== 0) continue;
|
|
849
|
+
for (const surfaceRef of String(`${run.stdout || ''}\n${run.stderr || ''}`).match(/\bsurface:\d+\b/g) || []) {
|
|
850
|
+
if (!surfaces.includes(surfaceRef)) surfaces.push(surfaceRef);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return surfaces;
|
|
854
|
+
}
|
|
855
|
+
|
|
603
856
|
export async function runCmuxStatus(args = [], opts = {}) {
|
|
604
857
|
const once = args.includes('--once') || !args.includes('--watch');
|
|
605
858
|
do {
|