sneakoscope 0.6.76 → 0.6.77

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
@@ -201,6 +201,10 @@ sks team log latest
201
201
 
202
202
  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 cmux Team workspace with split live lanes when cmux is available. `sks team dashboard` renders the cockpit panes for mission overview, agent lanes, task DAG, QA/dogfood, artifacts/evidence, and performance.
203
203
 
204
+ The cmux Team workspace is a live orchestration screen: 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. SKS colors and labels lanes by role, so scouts, planning/debate voices, executors, reviewers, and safety lanes are visually distinct while the same evidence is mirrored into `team-transcript.jsonl`, `team-live.md`, and `team-dashboard.json`.
205
+
206
+ When the Team route reaches `session_cleanup`, SKS collapses the cmux workspace back to the overview pane and marks the workspace complete. You can also run `sks team cleanup-cmux <mission-id|latest>` manually, or `sks team cleanup-cmux latest --close-workspace` when you want the whole Team workspace closed.
207
+
204
208
  ### QA, Goal, Research, DB, Wiki, GX
205
209
 
206
210
  ```sh
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.6.76",
4
+ "version": "0.6.77",
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",
package/src/cli/main.mjs CHANGED
@@ -40,7 +40,7 @@ import { buildPromptContext } from '../core/prompt-context-builder.mjs';
40
40
  import { renderTeamDashboardState, writeTeamDashboardState } from '../core/team-dashboard-renderer.mjs';
41
41
  import { GOAL_WORKFLOW_ARTIFACT } from '../core/goal-workflow.mjs';
42
42
  import { CODEX_APP_DOCS_URL, codexAppIntegrationStatus, formatCodexAppStatus } from '../core/codex-app.mjs';
43
- import { CMUX_BREW_COMMAND, CMUX_BREW_UPGRADE_COMMAND, buildCmuxLaunchPlan, buildCmuxNewWorkspaceArgs, cmuxSurfaceRefFromText, cmuxWorkspaceRef, cmuxWorkspaceRefFromText, cmuxReadiness, cmuxStatusKind, defaultCmuxWorkspaceName, ensureCmuxInstalled, formatCmuxBanner, launchCmuxTeamView, launchCmuxUi, matchingCmuxWorkspaces, parseCmuxWorkspaceList, platformCmuxInstallHint, readCmuxWorkspaceRecord, runCmuxStatus, sanitizeCmuxWorkspaceName, writeCmuxWorkspaceRecord } from '../core/cmux-ui.mjs';
43
+ import { CMUX_BREW_COMMAND, CMUX_BREW_UPGRADE_COMMAND, buildCmuxLaunchPlan, buildCmuxNewWorkspaceArgs, cmuxSurfaceRefFromText, cmuxWorkspaceRef, cmuxWorkspaceRefFromText, cmuxReadiness, cmuxStatusKind, defaultCmuxWorkspaceName, ensureCmuxInstalled, formatCmuxBanner, launchCmuxTeamView, launchCmuxUi, matchingCmuxWorkspaces, parseCmuxWorkspaceList, platformCmuxInstallHint, readCmuxWorkspaceRecord, runCmuxStatus, sanitizeCmuxWorkspaceName, teamLaneStyle, writeCmuxWorkspaceRecord } from '../core/cmux-ui.mjs';
44
44
  import { autoReviewProfileName, autoReviewStatus, autoReviewSummary, enableAutoReview, disableAutoReview, enableMadHighProfile, madHighProfileName } from '../core/auto-review.mjs';
45
45
  import { buildTeamPlan, codeStructureCommand, defaultBeta, defaultVGraph, evalCommand, gcCommand, goalCommand, gxCommand, hproofCommand, memoryCommand, migrateWikiContextPack, parseTeamCreateArgs, perfCommand, profileCommand, projectWikiClaims, qaLoopCommand, researchCommand, statsCommand, team, teamWorkflowMarkdown, validateArtifactsCommand, wikiCommand, wikiVoxelRowCount, writeWikiContextPack } from './maintenance-commands.mjs';
46
46
 
@@ -2700,7 +2700,10 @@ async function selftest() {
2700
2700
  if (!roleTeamPlan.roster.debate_team.some((agent) => /inconvenience/.test(agent.persona))) throw new Error('selftest failed: user friction persona missing from debate team');
2701
2701
  const cmuxTeam = await launchCmuxTeamView({ root: tmp, missionId: teamId, plan: roleTeamPlan, json: true });
2702
2702
  if (!cmuxTeam.agents?.length || !cmuxTeam.agents.some((entry) => entry.agent === 'analysis_scout_1') || !cmuxTeam.agents.every((entry) => String(entry.command || '').includes('team lane') && String(entry.command || '').includes('--agent'))) throw new Error('selftest failed: Team cmux view did not expose agent live lanes');
2703
- const cmuxTeamWorkspaceArgs = buildCmuxNewWorkspaceArgs({ root: tmp, workspace: `sks-team-${teamId}` }, cmuxTeam.agents[0].command);
2703
+ if (!cmuxTeam.overview?.command?.includes('team watch') || !cmuxTeam.lanes?.some((entry) => entry.role === 'overview') || !cmuxTeam.lanes?.some((entry) => entry.agent === 'analysis_scout_1')) throw new Error('selftest failed: Team cmux view did not expose orchestration overview plus agent lanes');
2704
+ if (teamLaneStyle('analysis_scout_1').role !== 'scout' || teamLaneStyle('executor_1').role !== 'execution' || teamLaneStyle('reviewer_1').role !== 'review') throw new Error('selftest failed: Team cmux role palette did not classify lane roles');
2705
+ if (cmuxTeam.cleanup_policy !== 'collapse-agent-lanes-to-overview' || !cmuxTeam.lanes.every((entry) => entry.style?.color && entry.title)) throw new Error('selftest failed: Team cmux view did not expose color/title metadata and cleanup policy');
2706
+ const cmuxTeamWorkspaceArgs = buildCmuxNewWorkspaceArgs({ root: tmp, workspace: `sks-team-${teamId}` }, cmuxTeam.overview.command);
2704
2707
  if (!cmuxTeamWorkspaceArgs.includes('--name') || !cmuxTeamWorkspaceArgs.includes(`sks-team-${teamId}`)) throw new Error('selftest failed: Team cmux workspace is not named for visibility');
2705
2708
  if (routeReasoning(routePrompt('$Research frontier idea'), '$Research frontier idea').effort !== 'xhigh') throw new Error('selftest failed: research reasoning not xhigh');
2706
2709
  if (routeReasoning(routePrompt('$From-Chat-IMG 채팅 이미지 작업'), '$From-Chat-IMG 채팅 이미지 작업').effort !== 'xhigh') throw new Error('selftest failed: From-Chat-IMG reasoning not xhigh');
@@ -17,7 +17,7 @@ import { contextCapsule } from '../core/triwiki-attention.mjs';
17
17
  import { rgbaKey, rgbaToWikiCoord, validateWikiCoordinateIndex } from '../core/wiki-coordinate.mjs';
18
18
  import { ALLOWED_REASONING_EFFORTS, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_SOURCE_INVENTORY_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, FROM_CHAT_IMG_VISUAL_MAP_ARTIFACT, FROM_CHAT_IMG_WORK_ORDER_ARTIFACT, ROUTES, hasFromChatImgSignal, routePrompt, stackCurrentDocsPolicy, triwikiContextTracking } from '../core/routes.mjs';
19
19
  import { TEAM_DECOMPOSITION_ARTIFACT, TEAM_GRAPH_ARTIFACT, TEAM_INBOX_DIR, TEAM_RUNTIME_TASKS_ARTIFACT, teamRuntimePlanMetadata, teamRuntimeRequiredArtifacts, writeTeamRuntimeArtifacts } from '../core/team-dag.mjs';
20
- import { appendTeamEvent, formatRoleCounts, initTeamLive, normalizeTeamSpec, parseTeamSpecArgs, readTeamDashboard, readTeamLive, readTeamTranscriptTail, renderTeamAgentLane } from '../core/team-live.mjs';
20
+ import { appendTeamEvent, formatRoleCounts, initTeamLive, normalizeTeamSpec, parseTeamSpecArgs, readTeamDashboard, readTeamLive, readTeamTranscriptTail, renderTeamAgentLane, renderTeamWatch } from '../core/team-live.mjs';
21
21
  import { ARTIFACT_FILES, writeValidationReport } from '../core/artifact-schemas.mjs';
22
22
  import { writeEffortDecision } from '../core/effort-orchestrator.mjs';
23
23
  import { createWorkOrderLedger, writeWorkOrderLedger } from '../core/work-order-ledger.mjs';
@@ -27,7 +27,7 @@ import { runPerfBench } from '../core/perf-bench.mjs';
27
27
  import { GOAL_BRIDGE_ARTIFACT, GOAL_WORKFLOW_ARTIFACT, updateGoalWorkflow, writeGoalWorkflow } from '../core/goal-workflow.mjs';
28
28
  import { scanCodeStructure, writeCodeStructureReport } from '../core/code-structure.mjs';
29
29
  import { writeMemorySweepReport } from '../core/memory-governor.mjs';
30
- import { launchCmuxTeamView } from '../core/cmux-ui.mjs';
30
+ import { cleanupCmuxTeamView, launchCmuxTeamView } from '../core/cmux-ui.mjs';
31
31
  import { writeSkillForgeReport } from '../core/skill-forge.mjs';
32
32
  import { writeMistakeMemoryReport } from '../core/mistake-memory.mjs';
33
33
  import { scanDbSafety } from '../core/db-safety.mjs';
@@ -1171,13 +1171,13 @@ export async function gxCommand(sub, args) {
1171
1171
  }
1172
1172
 
1173
1173
  export async function team(args) {
1174
- const teamSubcommands = new Set(['log', 'tail', 'watch', 'lane', 'status', 'dashboard', 'event']);
1174
+ const teamSubcommands = new Set(['log', 'tail', 'watch', 'lane', 'status', 'dashboard', 'event', 'cleanup-cmux']);
1175
1175
  if (teamSubcommands.has(args[0])) return teamCommand(args[0], args.slice(1));
1176
1176
  const opts = parseTeamCreateArgs(args);
1177
1177
  const { prompt, agentSessions, roleCounts, roster } = opts;
1178
1178
  if (!prompt) {
1179
1179
  console.error('Usage: sks team "task" [executor:5 reviewer:2 user:1] [--agents N] [--json]');
1180
- console.error(' sks team log|tail|watch|lane|status [mission-id|latest]');
1180
+ console.error(' sks team log|tail|watch|lane|status|cleanup-cmux [mission-id|latest]');
1181
1181
  console.error(' sks team event [mission-id|latest] --agent <name> --phase <phase> --message "..."');
1182
1182
  process.exitCode = 1;
1183
1183
  return;
@@ -1507,15 +1507,35 @@ async function teamCommand(sub, args) {
1507
1507
  process.exitCode = 1;
1508
1508
  return;
1509
1509
  }
1510
+ const phase = readFlagValue(args, '--phase', 'general');
1510
1511
  const record = await appendTeamEvent(dir, {
1511
1512
  agent: readFlagValue(args, '--agent', 'parent_orchestrator'),
1512
- phase: readFlagValue(args, '--phase', 'general'),
1513
+ phase,
1513
1514
  type: readFlagValue(args, '--type', 'status'),
1514
1515
  artifact: readFlagValue(args, '--artifact', ''),
1515
1516
  message
1516
1517
  });
1518
+ const cmuxCleanup = /^session_cleanup$|^team_cleanup$|^cleanup$/i.test(String(phase || ''))
1519
+ ? await cleanupCmuxTeamView({ root, missionId: id, closeWorkspace: flag(args, '--close-workspace') }).catch((err) => ({ ok: false, reason: err.message || 'cmux cleanup failed' }))
1520
+ : null;
1517
1521
  if (flag(args, '--json')) return console.log(JSON.stringify(record, null, 2));
1518
1522
  console.log(`${record.ts} [${record.phase}] ${record.agent}: ${record.message}`);
1523
+ if (cmuxCleanup) {
1524
+ if (cmuxCleanup.ok) console.log(`cmux cleanup: collapsed ${cmuxCleanup.closed_surfaces || 0} agent pane(s), kept overview ${cmuxCleanup.kept_surface || cmuxCleanup.workspace_ref}`);
1525
+ else console.log(`cmux cleanup: skipped (${cmuxCleanup.reason || 'not available'})`);
1526
+ }
1527
+ return;
1528
+ }
1529
+ if (sub === 'cleanup-cmux') {
1530
+ const cleanup = await cleanupCmuxTeamView({ root, missionId: id, closeWorkspace: flag(args, '--close-workspace') || flag(args, '--close') });
1531
+ if (flag(args, '--json')) return console.log(JSON.stringify(cleanup, null, 2));
1532
+ if (!cleanup.ok) {
1533
+ console.error(`cmux cleanup skipped: ${cleanup.reason || 'not available'}`);
1534
+ process.exitCode = cleanup.skipped ? 0 : 2;
1535
+ return;
1536
+ }
1537
+ if (cleanup.close_workspace) console.log(`cmux cleanup: closed Team workspace ${cleanup.workspace_ref}`);
1538
+ else console.log(`cmux cleanup: collapsed ${cleanup.closed_surfaces}/${cleanup.requested_close_surfaces} agent pane(s), kept overview ${cleanup.kept_surface || cleanup.workspace_ref}`);
1519
1539
  return;
1520
1540
  }
1521
1541
  if (sub === 'status') {
@@ -1573,15 +1593,26 @@ async function teamCommand(sub, args) {
1573
1593
  if (sub === 'tail' || sub === 'watch') {
1574
1594
  const lines = readFlagValue(args, '--lines', '20');
1575
1595
  const printTail = async () => {
1596
+ if (sub === 'watch' && !flag(args, '--raw')) {
1597
+ if (flag(args, '--follow') && process.stdout.isTTY) console.clear();
1598
+ console.log(await renderTeamWatch(dir, { missionId: id, lines: Number(lines) }));
1599
+ return;
1600
+ }
1576
1601
  for (const line of await readTeamTranscriptTail(dir, Number(lines))) console.log(line);
1577
1602
  };
1578
1603
  await printTail();
1579
1604
  if (sub === 'watch' && flag(args, '--follow')) {
1580
- let last = (await readTeamTranscriptTail(dir, Number(lines))).join('\n');
1605
+ let last = flag(args, '--raw')
1606
+ ? (await readTeamTranscriptTail(dir, Number(lines))).join('\n')
1607
+ : await renderTeamWatch(dir, { missionId: id, lines: Number(lines) });
1581
1608
  for (;;) {
1582
1609
  await new Promise((resolve) => setTimeout(resolve, 2000));
1583
- const next = (await readTeamTranscriptTail(dir, Number(lines))).join('\n');
1610
+ const next = flag(args, '--raw')
1611
+ ? (await readTeamTranscriptTail(dir, Number(lines))).join('\n')
1612
+ : await renderTeamWatch(dir, { missionId: id, lines: Number(lines) });
1584
1613
  if (next !== last) {
1614
+ if (process.stdout.isTTY) console.clear();
1615
+ else console.log('\n--- team watch update ---\n');
1585
1616
  console.log(next);
1586
1617
  last = next;
1587
1618
  }
@@ -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
- `printf '%s\\n' ${shellEscape(`${SKS_CMUX_LOGO}\n\nTeam mission: ${missionId}\nAgent: ${agentId}\nPhase: ${phase}\n`)}`,
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 result = { ready: launch.ready, cmux: launch.cmux, workspace: launch.workspace, agents: commands, blockers: launch.blockers };
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 = commands[0]?.command || teamAgentCommand(launch.root, missionId, 'parent_orchestrator', 'team', promptFile);
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 workspaceRef = cmuxWorkspaceRefFromText(`${created.stdout || ''}\n${created.stderr || ''}`);
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.slice(1)) {
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 = Math.max(0, commands.length - 1);
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 {
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.6.76';
8
+ export const PACKAGE_VERSION = '0.6.77';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
package/src/core/init.mjs CHANGED
@@ -486,7 +486,7 @@ function codexAppQuickReference(scope, commandPrefix) {
486
486
  `Runtime root: ${commandPrefix} root shows whether SKS is using the nearest project root or the per-user global SKS runtime root; outside any project marker, runtime commands use the global root instead of writing .sneakoscope into the current random directory.`,
487
487
  `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.`,
488
488
  stackCurrentDocsPolicyText(commandPrefix),
489
- `Team lanes: ${commandPrefix} team lane latest --agent analysis_scout_1 --follow shows one agent's status, assigned runtime tasks, recent agent events, and fallback global tail.`,
489
+ `Team cmux view: ${commandPrefix} team "task" opens a live orchestration workspace with 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, and fallback global tail; ${commandPrefix} team cleanup-cmux latest collapses agent panes back to the overview.`,
490
490
  `Runtime: open Codex App once, then run ${commandPrefix} bootstrap, ${commandPrefix} deps check, or ${commandPrefix} deps install cmux.`,
491
491
  `Guard: generated harness files are immutable outside the engine source repo; check ${commandPrefix} guard check; conflicts use ${commandPrefix} conflicts prompt with human approval.`
492
492
  ].join('\n') + '\n';
@@ -376,6 +376,43 @@ export async function renderTeamAgentLane(dir, opts = {}) {
376
376
  ].join('\n');
377
377
  }
378
378
 
379
+ export async function renderTeamWatch(dir, opts = {}) {
380
+ const lines = Math.max(1, Number(opts.lines) || 20);
381
+ const dashboard = await readTeamDashboard(dir);
382
+ const runtime = await readJson(path.join(dir, TEAM_RUNTIME_TASKS_ARTIFACT), null);
383
+ const missionId = opts.missionId || dashboard?.mission_id || runtime?.mission_id || path.basename(dir);
384
+ const agents = Object.entries(dashboard?.agents || {});
385
+ const visibleAgents = agents
386
+ .filter(([name]) => name !== 'parent_orchestrator')
387
+ .slice(0, Math.max(3, Number(dashboard?.agent_session_count) || 3));
388
+ const events = (await readTeamTranscriptTail(dir, lines)).map(parseTranscriptLine).filter(Boolean);
389
+ const runtimeTasks = Array.isArray(runtime?.tasks) ? runtime.tasks : Array.isArray(runtime) ? runtime : [];
390
+ return [
391
+ '# SKS Team Live Orchestration',
392
+ '',
393
+ `Mission: ${missionId}`,
394
+ `Updated: ${dashboard?.updated_at || 'unknown'}`,
395
+ `Agent session budget: ${dashboard?.agent_session_count || 'unknown'}`,
396
+ dashboard?.role_counts ? `Role counts: ${formatRoleCounts(dashboard.role_counts)}` : null,
397
+ '',
398
+ '## Split-Screen Map',
399
+ '- This overview pane follows the whole mission transcript.',
400
+ '- Neighbor cmux panes follow individual `sks team lane ... --agent <name>` views.',
401
+ '- Use `sks team event ...` to mirror scout, debate, executor, review, and verification status into the live panes.',
402
+ '',
403
+ '## Visible Agent Lanes',
404
+ ...(visibleAgents.length
405
+ ? visibleAgents.map(([name, status]) => `- ${name}: ${status.status || 'pending'} | ${status.phase || 'unknown'} | last_seen:${status.last_seen || 'never'}`)
406
+ : ['- No agent lanes registered yet.']),
407
+ '',
408
+ '## Runtime Task Snapshot',
409
+ ...(runtimeTasks.length ? formatRuntimeTasks(runtimeTasks.slice(0, 8)) : ['- team-runtime-tasks.json not available yet.']),
410
+ '',
411
+ '## Recent Mission Events',
412
+ ...(events.length ? events.map(formatTranscriptEvent) : ['- No transcript events yet.'])
413
+ ].filter((line) => line !== null).join('\n');
414
+ }
415
+
379
416
  function normalizeEvent(event = {}) {
380
417
  return {
381
418
  ts: event.ts || nowIso(),