sneakoscope 0.7.63 → 0.7.65
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/package.json +1 -1
- package/src/cli/main.mjs +24 -4
- package/src/cli/maintenance-commands.mjs +2 -2
- package/src/core/db-safety.mjs +15 -2
- package/src/core/fsx.mjs +1 -1
- package/src/core/hooks-runtime.mjs +9 -6
- package/src/core/pipeline.mjs +13 -6
- package/src/core/team-live.mjs +56 -11
- package/src/core/tmux-ui.mjs +151 -8
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.65",
|
|
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
|
@@ -2010,10 +2010,26 @@ async function selftest() {
|
|
|
2010
2010
|
};
|
|
2011
2011
|
for (let i = 0; i < 5; i++) {
|
|
2012
2012
|
const stop = await evaluateStop(tmp, clarificationState, { last_assistant_message: 'continuing implementation without visible questions' });
|
|
2013
|
-
if (stop?.gate
|
|
2013
|
+
if (stop?.decision !== 'block' || stop?.gate !== 'clarification' || !/paused|answers|pipeline answer/i.test(String(stop?.reason || ''))) throw new Error('selftest failed: clarification not paused');
|
|
2014
2014
|
}
|
|
2015
|
-
|
|
2016
|
-
|
|
2015
|
+
if (await exists(path.join(clarificationMission.dir, 'hard-blocker.json'))) throw new Error('selftest failed: clarification wrote hard-blocker');
|
|
2016
|
+
const visibleQuestionStop = await evaluateStop(tmp, clarificationState, { last_assistant_message: 'Required questions\n1. GOAL_PRECISE\nsks pipeline answer latest --stdin' });
|
|
2017
|
+
if (visibleQuestionStop?.continue !== true) throw new Error('selftest failed: visible clarification did not wait');
|
|
2018
|
+
const cg = await projectGateStatus(tmp, clarificationState);
|
|
2019
|
+
if (!cg.blockers.includes('clarification-gate:explicit_user_answers') || !cg.blockers.includes('clarification-gate:pipeline_answer')) throw new Error('selftest failed: missing clarification blockers');
|
|
2020
|
+
await setCurrent(tmp, clarificationState);
|
|
2021
|
+
const hookPath = path.join(packageRoot(), 'bin', 'sks.mjs');
|
|
2022
|
+
const blockedPre = await runProcess(process.execPath, [hookPath, 'hook', 'pre-tool'], { cwd: tmp, input: JSON.stringify({ cwd: tmp, tool_name: 'Bash', tool_input: { command: 'npm run selftest' } }), timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2023
|
+
if (blockedPre.code !== 0) throw new Error(`selftest failed: pre-tool exit ${blockedPre.code}: ${blockedPre.stderr}`);
|
|
2024
|
+
const bp = JSON.parse(blockedPre.stdout || '{}');
|
|
2025
|
+
if (bp.decision !== 'block' || !String(bp.reason || '').includes('waiting for explicit user answers')) throw new Error('selftest failed: pre-tool not blocked');
|
|
2026
|
+
const deniedPermission = await runProcess(process.execPath, [hookPath, 'hook', 'permission-request'], { cwd: tmp, input: JSON.stringify({ cwd: tmp, command: 'npm run selftest', action: 'Run command' }), timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2027
|
+
if (deniedPermission.code !== 0) throw new Error(`selftest failed: permission exit ${deniedPermission.code}: ${deniedPermission.stderr}`);
|
|
2028
|
+
const dp = JSON.parse(deniedPermission.stdout || '{}');
|
|
2029
|
+
if (dp.hookSpecificOutput?.decision?.behavior !== 'deny' || !String(dp.hookSpecificOutput?.decision?.message || '').includes('waiting for explicit user answers')) throw new Error('selftest failed: permission not denied');
|
|
2030
|
+
const answerTool = await runProcess(process.execPath, [hookPath, 'hook', 'pre-tool'], { cwd: tmp, input: JSON.stringify({ cwd: tmp, tool_name: 'Bash', tool_input: { command: `sks pipeline answer ${clarificationMission.id} --stdin` } }), timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
2031
|
+
if (answerTool.code !== 0) throw new Error(`selftest failed: answer hook exit ${answerTool.code}: ${answerTool.stderr}`);
|
|
2032
|
+
if (JSON.parse(answerTool.stdout || '{}').decision === 'block') throw new Error('selftest failed: answer command blocked');
|
|
2017
2033
|
await setCurrent(tmp, loopState);
|
|
2018
2034
|
const dfixPromptHook = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'hook', 'user-prompt-submit'], {
|
|
2019
2035
|
cwd: tmp,
|
|
@@ -3275,7 +3291,9 @@ async function selftest() {
|
|
|
3275
3291
|
await writeTextAtomic(fakeTmuxBin, `#!/usr/bin/env node\nconst fs = require('node:fs');\nconst log = process.env.SKS_FAKE_TMUX_LOG;\nif (log) fs.appendFileSync(log, process.argv.slice(2).join(' ') + '\\n');\nconst cmd = process.argv[2];\nif (cmd === 'has-session') process.exit(0);\nif (cmd === 'kill-session') process.exit(0);\nif (cmd === 'kill-pane') process.exit(0);\nif (cmd === 'new-session') { console.log('%1'); process.exit(0); }\nif (cmd === 'split-window') { console.log(process.env.SKS_FAKE_TMUX_SPLIT_ID || '%2'); process.exit(0); }\nif (cmd === 'list-windows') { console.log('@1'); process.exit(0); }\nif (cmd === 'display-message') { console.log(process.env.SKS_FAKE_TMUX_DISPLAY || 'sks-existing-selftest\\t@1\\t%1'); process.exit(0); }\nif (cmd === 'list-panes') { console.log(process.env.SKS_FAKE_TMUX_LIST || ''); process.exit(0); }\nif (cmd === 'set-option' || cmd === 'select-layout' || cmd === 'resize-window' || cmd === 'set-window-option' || cmd === 'set-hook') process.exit(0);\nprocess.exit(0);\n`);
|
|
3276
3292
|
await fsp.chmod(fakeTmuxBin, 0o755);
|
|
3277
3293
|
const previousFakeTmuxLog = process.env.SKS_FAKE_TMUX_LOG;
|
|
3294
|
+
const previousPath = process.env.PATH;
|
|
3278
3295
|
process.env.SKS_FAKE_TMUX_LOG = fakeTmuxLog;
|
|
3296
|
+
process.env.PATH = `${fakeTmuxDir}${path.delimiter}${previousPath || ''}`;
|
|
3279
3297
|
const recreatedTmux = await createTmuxSession({ root: tmp, session: 'sks-existing-selftest', tmux: { bin: fakeTmuxBin }, codex: { bin: process.execPath } }, [
|
|
3280
3298
|
{ cwd: tmp, command: 'pwd', role: 'overview' },
|
|
3281
3299
|
{ cwd: tmp, command: 'pwd', role: 'lane' }
|
|
@@ -3332,6 +3350,8 @@ async function selftest() {
|
|
|
3332
3350
|
if (!madCockpit.created || madCockpit.mode !== 'mad_session' || madCockpit.opened?.panes?.length !== 1 || !madTmuxLogText.includes('new-session') || madTmuxLogText.includes('split-window')) throw new Error('selftest failed: MAD tmux launch should create one pane and leave split panes to Team lanes');
|
|
3333
3351
|
if (previousFakeTmuxLog === undefined) delete process.env.SKS_FAKE_TMUX_LOG;
|
|
3334
3352
|
else process.env.SKS_FAKE_TMUX_LOG = previousFakeTmuxLog;
|
|
3353
|
+
if (previousPath === undefined) delete process.env.PATH;
|
|
3354
|
+
else process.env.PATH = previousPath;
|
|
3335
3355
|
const codexLaunchArgs = defaultCodexLaunchArgs({ SKS_CODEX_REASONING: 'low' }).join(' ');
|
|
3336
3356
|
if (!codexLaunchArgs.includes('service_tier="fast"') || !codexLaunchArgs.includes('model_reasoning_effort="low"')) throw new Error('selftest failed: Codex tmux launch args do not force Fast service tier plus dynamic reasoning');
|
|
3337
3357
|
await initTeamLive(teamId, teamDir, '역할 팀 테스트', { agentSessions: roleTeamPlan.agent_session_count, roleCounts: roleTeamPlan.role_counts, roster: roleTeamPlan.roster });
|
|
@@ -3422,7 +3442,7 @@ async function selftest() {
|
|
|
3422
3442
|
if (!teamLive.includes('Context tracking SSOT: TriWiki')) throw new Error('selftest failed: team live transcript missing TriWiki context tracking');
|
|
3423
3443
|
if (!(await readTeamTranscriptTail(teamDir, 1)).join('\n').includes('selftest mapped options')) throw new Error('selftest failed: team transcript tail missing event');
|
|
3424
3444
|
const teamLane = await renderTeamAgentLane(teamDir, { missionId: teamId, agent: 'analysis_scout_1', lines: 4 });
|
|
3425
|
-
if (!teamLane.includes('
|
|
3445
|
+
if (!teamLane.includes('selftest mapped repo slice')) throw new Error('selftest failed: team agent lane missing event context');
|
|
3426
3446
|
const teamLaneCli = await runProcess(process.execPath, [hookBin, 'team', 'lane', teamId, '--agent', 'analysis_scout_1', '--lines', '4'], { cwd: tmp, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
3427
3447
|
if (teamLaneCli.code !== 0 || !String(teamLaneCli.stdout || '').includes('SKS Team Agent Lane') || !String(teamLaneCli.stdout || '').includes('analysis_scout_1')) throw new Error('selftest failed: sks team lane CLI did not render an agent lane');
|
|
3428
3448
|
await writeTextAtomic(path.join(teamDir, 'team-analysis.md'), '- claim: analysis scout mapped route registry | source: src/core/routes.mjs | risk: high | confidence: supported\n');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fsp from 'node:fs/promises';
|
|
3
|
-
import { readJson, writeJsonAtomic, writeTextAtomic, appendJsonlBounded, nowIso, exists, ensureDir, packageRoot, dirSize, formatBytes, PACKAGE_VERSION, sksRoot } from '../core/fsx.mjs';
|
|
3
|
+
import { readJson, writeJsonAtomic, writeTextAtomic, appendJsonlBounded, nowIso, exists, ensureDir, packageRoot, dirSize, formatBytes, PACKAGE_VERSION, sksRoot, readStdin } from '../core/fsx.mjs';
|
|
4
4
|
import { initProject } from '../core/init.mjs';
|
|
5
5
|
import { getCodexInfo, runCodexExec } from '../core/codex-adapter.mjs';
|
|
6
6
|
import { createMission, loadMission, findLatestMission, missionDir, setCurrent, stateFile } from '../core/mission.mjs';
|
|
@@ -640,7 +640,7 @@ export async function dbCommand(sub, args = []) {
|
|
|
640
640
|
return;
|
|
641
641
|
}
|
|
642
642
|
if (sub === 'scan-payload') {
|
|
643
|
-
const raw = await
|
|
643
|
+
const raw = await readStdin();
|
|
644
644
|
const payload = raw.trim() ? JSON.parse(raw) : {};
|
|
645
645
|
const decision = await checkDbOperation(root, {}, payload, { duringNoQuestion: false });
|
|
646
646
|
console.log(JSON.stringify(decision, null, 2));
|
package/src/core/db-safety.mjs
CHANGED
|
@@ -204,7 +204,7 @@ function recursivelyCollectStrings(obj, out = [], depth = 0) {
|
|
|
204
204
|
if (Array.isArray(obj)) { for (const x of obj) recursivelyCollectStrings(x, out, depth + 1); return out; }
|
|
205
205
|
if (typeof obj === 'object') {
|
|
206
206
|
for (const [k, v] of Object.entries(obj)) {
|
|
207
|
-
if (/^(sql|query|statement|command|migration|body|input|text)$/i.test(k) || typeof v === 'object') recursivelyCollectStrings(v, out, depth + 1);
|
|
207
|
+
if (/^(sql|query|statement|command|migration|body|input|text|action|purpose|intent|description)$/i.test(k) || typeof v === 'object') recursivelyCollectStrings(v, out, depth + 1);
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
return out;
|
|
@@ -216,6 +216,12 @@ function looksLikeSqlText(text = '') {
|
|
|
216
216
|
|| /;\s*(select|with|show|explain|describe|insert|update|delete|drop|truncate|alter|create|grant|revoke)\b/i.test(s);
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
function hasReadOnlyDbInspectionIntent(text = '') {
|
|
220
|
+
const s = String(text || '').toLowerCase();
|
|
221
|
+
if (/\b(insert|update|delete|drop|truncate|alter|create|grant|revoke|write|mutate|migration|apply|push|reset|repair)\b|삭제|수정|변경|쓰기|삽입|생성|초기화|적용/i.test(s)) return false;
|
|
222
|
+
return /\b(read.?only|select|with|show|explain|describe|inspect|list|get|fetch|count|schema)\b|조회|확인|읽|보기|목록|스키마/i.test(s);
|
|
223
|
+
}
|
|
224
|
+
|
|
219
225
|
export function classifyToolPayload(payload = {}) {
|
|
220
226
|
const strings = recursivelyCollectStrings(payload).slice(0, 200);
|
|
221
227
|
const toolName = [payload.tool_name, payload.toolName, payload.name, payload.tool?.name, payload.server, payload.mcp_tool, payload.tool, payload.type].filter(Boolean).join(' ').toLowerCase();
|
|
@@ -241,7 +247,14 @@ export function classifyToolPayload(payload = {}) {
|
|
|
241
247
|
}
|
|
242
248
|
if (toolReasons.includes('dangerous_supabase_management_tool')) level = 'destructive';
|
|
243
249
|
if (toolReasons.includes('migration_apply_tool') && level !== 'destructive') level = 'write';
|
|
244
|
-
if (toolReasons.includes('database_tool') && level === 'none')
|
|
250
|
+
if (toolReasons.includes('database_tool') && level === 'none') {
|
|
251
|
+
if (hasReadOnlyDbInspectionIntent([toolName, ...strings].join(' '))) {
|
|
252
|
+
level = 'safe';
|
|
253
|
+
reasons.push('read_only_database_inspection_intent');
|
|
254
|
+
} else {
|
|
255
|
+
level = 'possible_db';
|
|
256
|
+
}
|
|
257
|
+
}
|
|
245
258
|
return { level, toolName, toolReasons, reasons, sql: sqlClass, command: commandClass, stringsExamined: strings.length };
|
|
246
259
|
}
|
|
247
260
|
|
package/src/core/fsx.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import os from 'node:os';
|
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
|
|
8
|
-
export const PACKAGE_VERSION = '0.7.
|
|
8
|
+
export const PACKAGE_VERSION = '0.7.65';
|
|
9
9
|
export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
|
|
10
10
|
export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
|
|
11
11
|
|
|
@@ -181,14 +181,17 @@ async function hookUserPrompt(root, state, payload, noQuestion) {
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
function isClarificationAwaiting(state = {}) {
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
const phase = String(state.phase || '');
|
|
185
|
+
const stopGate = String(state.stop_gate || '');
|
|
186
|
+
const gateAwaiting = phase.includes('CLARIFICATION_AWAITING_ANSWERS') || stopGate === 'clarification-gate';
|
|
187
|
+
if (!gateAwaiting) return false;
|
|
188
|
+
if (!state?.mission_id) return false;
|
|
189
|
+
if (state.ambiguity_gate_required !== true || state.ambiguity_gate_passed === true) return false;
|
|
190
|
+
return Boolean(state.clarification_required || state.implementation_allowed === false);
|
|
186
191
|
}
|
|
187
192
|
|
|
188
193
|
function isBlockingClarificationAwaiting(state = {}) {
|
|
189
|
-
|
|
190
|
-
return ['QALoop', 'PPT'].includes(String(state.route || ''))
|
|
191
|
-
|| ['QALOOP', 'PPT'].includes(String(state.mode || ''));
|
|
194
|
+
return isClarificationAwaiting(state);
|
|
192
195
|
}
|
|
193
196
|
|
|
194
197
|
function looksLikeClarificationCancel(prompt = '') {
|
|
@@ -316,7 +319,7 @@ function clarificationGateLocked(state = {}) {
|
|
|
316
319
|
&& state.implementation_allowed === false
|
|
317
320
|
&& state.ambiguity_gate_required === true
|
|
318
321
|
&& state.ambiguity_gate_passed !== true
|
|
319
|
-
&&
|
|
322
|
+
&& (String(state.phase || '').includes('CLARIFICATION_AWAITING_ANSWERS') || String(state.stop_gate || '') === 'clarification-gate')
|
|
320
323
|
);
|
|
321
324
|
}
|
|
322
325
|
|
package/src/core/pipeline.mjs
CHANGED
|
@@ -509,8 +509,7 @@ export async function activeRouteContext(root, state) {
|
|
|
509
509
|
return `SKS Honest Mode found unresolved gaps for ${state.route_command || state.route || state.mode}. Do not ask ambiguity questions again. Continue from the sealed decision-contract.json, inspect .sneakoscope/missions/${state.mission_id}/honest-loopback.json, fix gaps, rerun verification, refresh/validate TriWiki, then retry final Honest Mode.${reasoningNote}${planNote}`;
|
|
510
510
|
}
|
|
511
511
|
if (state.clarification_required && String(state.phase || '').includes('CLARIFICATION_AWAITING_ANSWERS')) {
|
|
512
|
-
|
|
513
|
-
return `Previous ${state.route_command || state.route || state.mode || 'SKS'} clarification state is non-blocking. Do not reprint old question sheets; prepare the current prompt normally and replace stale route state when needed.`;
|
|
512
|
+
return clarificationAwaitingAnswersContext(root, state);
|
|
514
513
|
}
|
|
515
514
|
if (state.clarification_passed && String(state.phase || '').includes('CLARIFICATION_CONTRACT_SEALED')) {
|
|
516
515
|
return `Route contract sealed for ${state.route_command || state.route || state.mode}. Use decision-contract.json and ${PIPELINE_PLAN_ARTIFACT} before executing the route. Before the next route phase, read relevant TriWiki context, hydrate low-trust claims from source, and refresh/validate TriWiki again after new findings or artifact changes. Next atomic action: continue the original route lifecycle with the inferred goal, constraints, non-goals, risk boundary, and test scope.${planNote}`;
|
|
@@ -993,7 +992,7 @@ async function clarificationAwaitingAnswersContext(root, state) {
|
|
|
993
992
|
const id = state.mission_id;
|
|
994
993
|
if (!id) return '';
|
|
995
994
|
const planNote = await activePipelinePlanNote(root, state);
|
|
996
|
-
return `Active SKS route ${state.route_command || state.route || state.mode}
|
|
995
|
+
return `Active SKS route ${state.route_command || state.route || state.mode} is paused at its ambiguity gate and waiting for explicit user answers. Do not advance to implementation, tests, route materialization, or a new pipeline stage. If the user's reply is now available, seal it with "sks pipeline answer ${id} --stdin"; otherwise show only the missing slot ids from .sneakoscope/missions/${id}/questions.md and wait.${planNote}`;
|
|
997
996
|
}
|
|
998
997
|
|
|
999
998
|
function clarificationVisibleResponseContract(id) {
|
|
@@ -1031,9 +1030,9 @@ async function clarificationStopReason(root, state, kind) {
|
|
|
1031
1030
|
const files = state?.mission_id ? `
|
|
1032
1031
|
Answer schema: .sneakoscope/missions/${state.mission_id}/required-answers.schema.json` : '';
|
|
1033
1032
|
const command = `sks pipeline answer ${id} --stdin`;
|
|
1034
|
-
const title = `SKS ${routeName}
|
|
1033
|
+
const title = `SKS ${routeName} is paused for explicit user answers.`;
|
|
1035
1034
|
return `${title}
|
|
1036
|
-
Do not
|
|
1035
|
+
Do not continue to implementation or the next pipeline stage until the ambiguity gate is sealed. Ask only the missing slot ids if they have not already been shown, then wait for the user. When the user's reply is available, seal it with "${command}".${files}
|
|
1037
1036
|
|
|
1038
1037
|
After the contract is sealed, continue the original ${routeName} route.`;
|
|
1039
1038
|
}
|
|
@@ -1329,7 +1328,15 @@ export async function evaluateStop(root, state, payload, opts = {}) {
|
|
|
1329
1328
|
}
|
|
1330
1329
|
|
|
1331
1330
|
function clarificationGatePending(state = {}) {
|
|
1332
|
-
|
|
1331
|
+
const phase = String(state.phase || '');
|
|
1332
|
+
return Boolean(state?.clarification_required && phase.includes('CLARIFICATION_AWAITING_ANSWERS'))
|
|
1333
|
+
|| Boolean(
|
|
1334
|
+
state?.mission_id
|
|
1335
|
+
&& state.implementation_allowed === false
|
|
1336
|
+
&& state.ambiguity_gate_required === true
|
|
1337
|
+
&& state.ambiguity_gate_passed !== true
|
|
1338
|
+
&& (phase.includes('CLARIFICATION_AWAITING_ANSWERS') || state.stop_gate === 'clarification-gate')
|
|
1339
|
+
);
|
|
1333
1340
|
}
|
|
1334
1341
|
|
|
1335
1342
|
async function complianceBlock(root, state = {}, reason = '', detail = {}) {
|
package/src/core/team-live.mjs
CHANGED
|
@@ -6,6 +6,7 @@ export { MIN_TEAM_REVIEWER_LANES, MIN_TEAM_REVIEW_POLICY_TEXT, MIN_TEAM_REVIEW_S
|
|
|
6
6
|
|
|
7
7
|
const MAX_LIVE_BYTES = 192 * 1024;
|
|
8
8
|
const TEAM_RUNTIME_TASKS_ARTIFACT = 'team-runtime-tasks.json';
|
|
9
|
+
const TEAM_SESSION_CLEANUP_ARTIFACT = 'team-session-cleanup.json';
|
|
9
10
|
const DEFAULT_AGENTS = ['parent_orchestrator', 'analysis_scout', 'team_consensus', 'implementation_worker', 'db_safety_reviewer', 'qa_reviewer'];
|
|
10
11
|
export const DEFAULT_TEAM_ROLE_COUNTS = { user: 1, planner: 1, reviewer: MIN_TEAM_REVIEWER_LANES, executor: 3 };
|
|
11
12
|
export const DEFAULT_MAX_TEAM_AGENT_SESSIONS = 6;
|
|
@@ -471,7 +472,19 @@ async function reconcileTeamTmuxFromEvent(dir, record = {}) {
|
|
|
471
472
|
}
|
|
472
473
|
|
|
473
474
|
export async function readTeamControl(dir) {
|
|
474
|
-
|
|
475
|
+
const control = await readJson(teamLogPaths(dir).control, defaultTeamControl(path.basename(dir)));
|
|
476
|
+
const cleanup = await readJson(path.join(dir, TEAM_SESSION_CLEANUP_ARTIFACT), null).catch(() => null);
|
|
477
|
+
if (!cleanup || (cleanup.passed !== true && cleanup.live_transcript_finalized !== true && cleanup.all_sessions_closed !== true)) return control;
|
|
478
|
+
return {
|
|
479
|
+
...defaultTeamControl(path.basename(dir)),
|
|
480
|
+
...control,
|
|
481
|
+
status: 'ended',
|
|
482
|
+
cleanup_requested: true,
|
|
483
|
+
cleanup_requested_at: cleanup.updated_at || cleanup.completed_at || cleanup.closed_at || control.cleanup_requested_at || 'artifact',
|
|
484
|
+
cleanup_requested_by: cleanup.agent || control.cleanup_requested_by || 'parent_orchestrator',
|
|
485
|
+
cleanup_reason: cleanup.reason || control.cleanup_reason || `${TEAM_SESSION_CLEANUP_ARTIFACT} passed.`,
|
|
486
|
+
final_message: cleanup.final_message || control.final_message || 'Team session ended. Lane follow loops stop and managed tmux Team panes should close.'
|
|
487
|
+
};
|
|
475
488
|
}
|
|
476
489
|
|
|
477
490
|
export async function requestTeamSessionCleanup(dir, opts = {}) {
|
|
@@ -532,26 +545,30 @@ export async function renderTeamAgentLane(dir, opts = {}) {
|
|
|
532
545
|
const missionId = opts.missionId || dashboard?.mission_id || runtime?.mission_id || path.basename(dir);
|
|
533
546
|
const status = dashboard?.agents?.[agent] || {};
|
|
534
547
|
const runtimeTasks = Array.isArray(runtime?.tasks) ? runtime.tasks : Array.isArray(runtime) ? runtime : [];
|
|
535
|
-
const assignedTasks = runtimeTasks.filter((task) => task?.worker === agent || task?.agent_hint === agent);
|
|
536
548
|
const eventWindow = await readTeamTranscriptTail(dir, Math.max(lines * 8, 80));
|
|
537
549
|
const parsedWindow = eventWindow.map(parseTranscriptLine).filter(Boolean);
|
|
538
|
-
const
|
|
539
|
-
const
|
|
540
|
-
const
|
|
550
|
+
const aliases = teamLaneAliases(agent, parsedWindow, dashboard, runtimeTasks);
|
|
551
|
+
const aliasSet = new Set(aliases);
|
|
552
|
+
const statusAliases = aliases.length > 1 ? [...aliases.slice(1), aliases[0]] : aliases;
|
|
553
|
+
const laneStatus = statusAliases.map((id) => dashboard?.agents?.[id]).find((entry) => entry && entry.status && entry.status !== 'pending') || status;
|
|
554
|
+
const assignedTasks = runtimeTasks.filter((task) => aliasSet.has(task?.worker) || aliasSet.has(task?.agent_hint));
|
|
555
|
+
const agentEvents = parsedWindow.filter((event) => aliasSet.has(event?.agent) || aliases.some((id) => eventAddressedTo(event, id))).slice(-lines);
|
|
556
|
+
const directMessages = parsedWindow.filter((event) => event?.type === 'message' && aliases.some((id) => eventAddressedTo(event, id))).slice(-lines);
|
|
541
557
|
const laneStyle = teamLaneTextStyle(agent);
|
|
542
558
|
return [
|
|
543
559
|
`# SKS Team Agent Lane`,
|
|
544
560
|
'',
|
|
545
561
|
`Mission: ${missionId}`,
|
|
546
562
|
`Agent: ${agent}`,
|
|
563
|
+
aliases.length > 1 ? `Mirrored agents: ${aliases.slice(1).join(', ')}` : null,
|
|
547
564
|
`Lane color: ${laneStyle.color_name}`,
|
|
548
565
|
`Requested phase: ${phase || 'any'}`,
|
|
549
566
|
teamCleanupRequested(control) ? `Cleanup: requested at ${control.cleanup_requested_at || 'unknown'}` : null,
|
|
550
567
|
'',
|
|
551
568
|
`## Agent Status`,
|
|
552
|
-
`- status: ${
|
|
553
|
-
`- phase: ${
|
|
554
|
-
`- last_seen: ${
|
|
569
|
+
`- status: ${laneStatus.status || 'pending'}`,
|
|
570
|
+
`- phase: ${laneStatus.phase || 'unknown'}`,
|
|
571
|
+
`- last_seen: ${laneStatus.last_seen || 'never'}`,
|
|
555
572
|
'',
|
|
556
573
|
`## Assigned Runtime Tasks`,
|
|
557
574
|
...(runtime ? formatRuntimeTasks(assignedTasks) : ['- team-runtime-tasks.json not available yet.']),
|
|
@@ -561,9 +578,11 @@ export async function renderTeamAgentLane(dir, opts = {}) {
|
|
|
561
578
|
'',
|
|
562
579
|
`## Direct Messages`,
|
|
563
580
|
...(directMessages.length ? directMessages.map(formatTranscriptEvent) : ['- No direct or broadcast messages in the bounded tail.']),
|
|
564
|
-
'',
|
|
565
|
-
`##
|
|
566
|
-
...(
|
|
581
|
+
opts.includeGlobalTail ? '' : null,
|
|
582
|
+
opts.includeGlobalTail ? `## Global Tail` : null,
|
|
583
|
+
...(opts.includeGlobalTail
|
|
584
|
+
? (await readTeamTranscriptTail(dir, lines)).map(parseTranscriptLine).filter(Boolean).map(formatTranscriptEvent)
|
|
585
|
+
: []),
|
|
567
586
|
teamCleanupRequested(control) ? ['', renderTeamCleanupSummary(control)].join('\n') : null
|
|
568
587
|
].filter((line) => line !== null).join('\n');
|
|
569
588
|
}
|
|
@@ -685,6 +704,32 @@ function eventAddressedTo(event = {}, agent = '') {
|
|
|
685
704
|
return target === name || target === 'all' || target === '*' || target === 'broadcast';
|
|
686
705
|
}
|
|
687
706
|
|
|
707
|
+
function teamLaneAliases(agent = '', events = [], dashboard = null, runtimeTasks = []) {
|
|
708
|
+
const primary = String(agent || '').trim();
|
|
709
|
+
if (!primary) return [];
|
|
710
|
+
const aliases = [primary];
|
|
711
|
+
const ordinal = numberedLaneOrdinal(primary);
|
|
712
|
+
if (!ordinal) return aliases;
|
|
713
|
+
const role = teamLaneTextStyle(primary).role;
|
|
714
|
+
const candidates = uniqueAgentIds([
|
|
715
|
+
...Object.keys(dashboard?.agents || {}),
|
|
716
|
+
...events.map((event) => event?.agent).filter(Boolean),
|
|
717
|
+
...runtimeTasks.flatMap((task) => [task?.worker, task?.agent_hint]).filter(Boolean)
|
|
718
|
+
])
|
|
719
|
+
.filter((id) => id !== primary)
|
|
720
|
+
.filter((id) => !DEFAULT_AGENTS.includes(id))
|
|
721
|
+
.filter((id) => teamLaneTextStyle(id).role === role)
|
|
722
|
+
.filter((id) => !numberedLaneOrdinal(id));
|
|
723
|
+
const concrete = candidates[ordinal - 1];
|
|
724
|
+
if (concrete) aliases.push(concrete);
|
|
725
|
+
return aliases;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function numberedLaneOrdinal(agent = '') {
|
|
729
|
+
const match = String(agent || '').match(/_(\d+)$/);
|
|
730
|
+
return match ? Number(match[1]) : 0;
|
|
731
|
+
}
|
|
732
|
+
|
|
688
733
|
function teamLaneTextStyle(agentId = '') {
|
|
689
734
|
const id = String(agentId || '').toLowerCase();
|
|
690
735
|
if (!id || id === 'mission_overview' || id === 'overview') return { role: 'overview', color_name: 'Blue' };
|
package/src/core/tmux-ui.mjs
CHANGED
|
@@ -119,6 +119,9 @@ const TERMINAL_TEAM_AGENT_STATUSES = new Set([
|
|
|
119
119
|
'tmux_lane_closed'
|
|
120
120
|
]);
|
|
121
121
|
|
|
122
|
+
const LEGACY_TEAM_PANE_TITLE_RE = /^(?:overview: mission_overview|scout: analysis_scout|plan: (?:debate|consensus|planner|user)|exec: (?:executor|implementation|worker)|review: (?:reviewer|qa|validation)|safety:)/;
|
|
123
|
+
const GENERIC_TEAM_AGENT_IDS = new Set(['parent_orchestrator', 'analysis_scout', 'team_consensus', 'implementation_worker', 'db_safety_reviewer', 'qa_reviewer']);
|
|
124
|
+
|
|
122
125
|
export function isTmuxShellSession(env = process.env) {
|
|
123
126
|
return Boolean(String(env.TMUX || '').trim());
|
|
124
127
|
}
|
|
@@ -395,6 +398,11 @@ function teamCockpitAgentIds(plan = {}, dashboard = null, control = null, opts =
|
|
|
395
398
|
const visible = teamViewAgentIds(plan).filter((id) => id && id !== 'mission_overview');
|
|
396
399
|
const agents = dashboard?.agents && typeof dashboard.agents === 'object' ? dashboard.agents : null;
|
|
397
400
|
if (!agents) return opts.plannedFallback ? visible : [];
|
|
401
|
+
const concrete = concreteDashboardAgentIds(agents, visible).filter((id) => {
|
|
402
|
+
const status = String(agents[id]?.status || '').trim().toLowerCase();
|
|
403
|
+
return status && status !== 'pending' && !isTerminalTeamAgentStatus(status);
|
|
404
|
+
});
|
|
405
|
+
if (concrete.length) return uniqueAgentIds(concrete);
|
|
398
406
|
const active = [];
|
|
399
407
|
for (const id of visible) {
|
|
400
408
|
const entry = agents[id] || {};
|
|
@@ -407,6 +415,17 @@ function teamCockpitAgentIds(plan = {}, dashboard = null, control = null, opts =
|
|
|
407
415
|
return uniqueAgentIds(active);
|
|
408
416
|
}
|
|
409
417
|
|
|
418
|
+
function concreteDashboardAgentIds(agents = {}, planned = []) {
|
|
419
|
+
const plannedSet = new Set(planned);
|
|
420
|
+
const concrete = Object.keys(agents)
|
|
421
|
+
.filter((id) => id && !plannedSet.has(id))
|
|
422
|
+
.filter((id) => !GENERIC_TEAM_AGENT_IDS.has(id))
|
|
423
|
+
.filter((id) => !/_(?:\d+)$/.test(id));
|
|
424
|
+
if (!concrete.length) return [];
|
|
425
|
+
const plannedRoles = new Set(planned.map((id) => teamLaneStyle(id).role));
|
|
426
|
+
return concrete.filter((id) => plannedRoles.has(teamLaneStyle(id).role));
|
|
427
|
+
}
|
|
428
|
+
|
|
410
429
|
function teamCockpitLanes(plan = {}, dashboard = null, control = null, opts = {}) {
|
|
411
430
|
const agents = teamCockpitAgentIds(plan, dashboard, control, opts);
|
|
412
431
|
if (!agents.length) return [];
|
|
@@ -441,6 +460,10 @@ function parseTmuxPaneLines(stdout = '') {
|
|
|
441
460
|
}).filter((pane) => /^%\d+$/.test(pane.pane_id || ''));
|
|
442
461
|
}
|
|
443
462
|
|
|
463
|
+
function isLegacyTeamPane(pane = {}) {
|
|
464
|
+
return LEGACY_TEAM_PANE_TITLE_RE.test(String(pane.title || '').trim());
|
|
465
|
+
}
|
|
466
|
+
|
|
444
467
|
async function listTmuxWindowPanes(bin, windowId) {
|
|
445
468
|
const format = ['#{pane_id}', '#{pane_title}', '#{pane_current_command}', '#{@sks_team_managed}', '#{@sks_mission_id}', '#{@sks_agent_id}', '#{@sks_lane_role}'].join('\t');
|
|
446
469
|
const run = await tmuxRun(bin, ['list-panes', '-t', windowId, '-F', format], { timeoutMs: 5000, maxOutputBytes: 32 * 1024 });
|
|
@@ -784,7 +807,13 @@ export async function reconcileTmuxTeamCockpit({ root, missionId, plan = {}, pro
|
|
|
784
807
|
|
|
785
808
|
export async function launchTmuxTeamView({ root, missionId, plan = {}, promptFile = null, json = false, attach = false, args = [] } = {}) {
|
|
786
809
|
const launch = await buildTmuxLaunchPlan({ root, session: `sks-team-${missionId}` });
|
|
787
|
-
const
|
|
810
|
+
const missionDir = path.join(launch.root, '.sneakoscope', 'missions', missionId);
|
|
811
|
+
const dashboard = await readTeamDashboard(missionDir).catch(() => null);
|
|
812
|
+
const control = await readTeamControl(missionDir).catch(() => null);
|
|
813
|
+
const plannedAgents = teamViewAgentIds(plan);
|
|
814
|
+
const concreteAgents = concreteDashboardAgentIds(dashboard?.agents || {}, plannedAgents);
|
|
815
|
+
const cleanupRequested = teamCleanupRequested(control);
|
|
816
|
+
const visibleAgents = cleanupRequested ? [] : (json ? plannedAgents : (concreteAgents.length ? concreteAgents : plannedAgents));
|
|
788
817
|
const commands = visibleAgents.map((agentId) => ({
|
|
789
818
|
agent: agentId,
|
|
790
819
|
command: teamAgentCommand(launch.root, missionId, agentId, teamLanePhase(agentId), promptFile),
|
|
@@ -792,7 +821,7 @@ export async function launchTmuxTeamView({ root, missionId, plan = {}, promptFil
|
|
|
792
821
|
title: teamLaneTitle(agentId)
|
|
793
822
|
}));
|
|
794
823
|
const overview = { agent: 'mission_overview', role: 'overview', command: teamOverviewCommand(launch.root, missionId), style: teamLaneStyle('mission_overview'), title: teamLaneTitle('mission_overview') };
|
|
795
|
-
const lanes = [overview, ...commands.map((entry) => ({ ...entry, role: entry.style.role }))];
|
|
824
|
+
const lanes = cleanupRequested ? [] : [overview, ...commands.map((entry) => ({ ...entry, role: entry.style.role }))];
|
|
796
825
|
const splitUi = {
|
|
797
826
|
mode: 'single_window_split_panes',
|
|
798
827
|
window: 'sks',
|
|
@@ -813,14 +842,14 @@ export async function launchTmuxTeamView({ root, missionId, plan = {}, promptFil
|
|
|
813
842
|
agents: commands,
|
|
814
843
|
lanes,
|
|
815
844
|
split_ui: splitUi,
|
|
816
|
-
cleanup_policy: 'mark-complete;
|
|
845
|
+
cleanup_policy: 'mark-complete; close SKS-managed Team panes; main Codex pane remains user controlled',
|
|
817
846
|
blockers: launch.blockers,
|
|
818
847
|
attach_command: launch.attach_command
|
|
819
848
|
};
|
|
820
849
|
if (json || !launch.ready) return result;
|
|
821
850
|
const wantsSeparateSession = args.includes('--separate-session') || args.includes('--new-session') || args.includes('--legacy-team-session') || args.includes('--no-dynamic-team-tmux');
|
|
822
851
|
if (!wantsSeparateSession) {
|
|
823
|
-
const cockpit = await reconcileTmuxTeamCockpit({ root: launch.root, missionId, plan, promptFile, plannedFallback: true });
|
|
852
|
+
const cockpit = await reconcileTmuxTeamCockpit({ root: launch.root, missionId, plan, promptFile, dashboard, control, plannedFallback: true });
|
|
824
853
|
result.dynamic_cockpit = cockpit;
|
|
825
854
|
if (cockpit.ok) {
|
|
826
855
|
result.created = true;
|
|
@@ -962,8 +991,25 @@ async function readTmuxTeamRecord(root, missionId) {
|
|
|
962
991
|
export async function cleanupTmuxTeamView({ root, missionId = 'latest', closeSession = false } = {}) {
|
|
963
992
|
const resolvedRoot = path.resolve(root || await sksRoot());
|
|
964
993
|
const record = await readTmuxTeamRecord(resolvedRoot, missionId);
|
|
965
|
-
if (!record?.session)
|
|
994
|
+
if (!record?.session) {
|
|
995
|
+
const legacy = await cleanupLegacyTmuxTeamSurfaces(resolvedRoot, missionId, { closeSession }).catch((err) => ({ ok: false, skipped: true, reason: err.message || 'legacy tmux cleanup failed' }));
|
|
996
|
+
return {
|
|
997
|
+
ok: legacy.ok,
|
|
998
|
+
skipped: legacy.closed_lane_count === 0 && !legacy.killed_session,
|
|
999
|
+
reason: legacy.reason,
|
|
1000
|
+
mission_id: missionId,
|
|
1001
|
+
legacy_cleanup: legacy,
|
|
1002
|
+
requested_close_surfaces: legacy.requested_close_surfaces || 0,
|
|
1003
|
+
closed_surfaces: legacy.closed_lane_count || (legacy.killed_session ? 1 : 0)
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
966
1006
|
const dynamicCleanup = await reconcileTmuxTeamCockpit({ root: resolvedRoot, missionId: record.mission_id || missionId, close: true }).catch((err) => ({ ok: false, skipped: true, reason: err.message || 'dynamic tmux cleanup failed' }));
|
|
1007
|
+
const recordedCleanup = dynamicCleanup?.ok
|
|
1008
|
+
? null
|
|
1009
|
+
: await cleanupRecordedTmuxTeamPanes(resolvedRoot, record.mission_id || missionId, record).catch((err) => ({ ok: false, skipped: true, reason: err.message || 'recorded tmux cleanup failed' }));
|
|
1010
|
+
const legacyCleanup = (dynamicCleanup?.closed_lane_count || recordedCleanup?.closed_lane_count)
|
|
1011
|
+
? null
|
|
1012
|
+
: await cleanupLegacyTmuxTeamSurfaces(resolvedRoot, record.mission_id || missionId, { closeSession: false }).catch((err) => ({ ok: false, skipped: true, reason: err.message || 'legacy tmux cleanup failed' }));
|
|
967
1013
|
let killed_session = false;
|
|
968
1014
|
if ((closeSession || closeSession === true) && record.mode !== 'current_session_dynamic_panes') {
|
|
969
1015
|
const tmuxBin = await findTmuxBin() || 'tmux';
|
|
@@ -979,13 +1025,110 @@ export async function cleanupTmuxTeamView({ root, missionId = 'latest', closeSes
|
|
|
979
1025
|
close_session: Boolean(closeSession),
|
|
980
1026
|
killed_session,
|
|
981
1027
|
dynamic_cleanup: dynamicCleanup,
|
|
982
|
-
|
|
983
|
-
|
|
1028
|
+
recorded_cleanup: recordedCleanup,
|
|
1029
|
+
legacy_cleanup: legacyCleanup,
|
|
1030
|
+
requested_close_surfaces: closeSession ? 1 : (dynamicCleanup?.closed_lane_count || recordedCleanup?.closed_lane_count || legacyCleanup?.requested_close_surfaces || 0),
|
|
1031
|
+
closed_surfaces: killed_session ? 1 : (dynamicCleanup?.closed_lane_count || recordedCleanup?.closed_lane_count || legacyCleanup?.closed_lane_count || 0),
|
|
984
1032
|
reason: dynamicCleanup?.ok
|
|
985
1033
|
? 'cleanup closed managed Team panes in the current SKS tmux session.'
|
|
1034
|
+
: recordedCleanup?.ok
|
|
1035
|
+
? 'cleanup closed recorded managed Team panes by stored tmux pane ids.'
|
|
1036
|
+
: legacyCleanup?.ok
|
|
1037
|
+
? legacyCleanup.reason
|
|
986
1038
|
: closeSession
|
|
987
1039
|
? 'tmux kill-session requested for recorded Team session.'
|
|
988
|
-
: 'cleanup marks the SKS tmux Team record complete; panes
|
|
1040
|
+
: 'cleanup marks the SKS tmux Team record complete; no managed panes were reachable.'
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
async function cleanupLegacyTmuxTeamSurfaces(root, missionId, opts = {}) {
|
|
1045
|
+
const id = String(missionId || '').trim();
|
|
1046
|
+
const tmuxBin = await findTmuxBin() || 'tmux';
|
|
1047
|
+
const current = await currentTmuxTarget(tmuxBin).catch(() => ({ ok: false }));
|
|
1048
|
+
const closed = [];
|
|
1049
|
+
const failed = [];
|
|
1050
|
+
let killed_session = false;
|
|
1051
|
+
let session_kill_requested = false;
|
|
1052
|
+
const session = id && id !== 'latest' ? sanitizeTmuxSessionName(`sks-team-${id}`) : '';
|
|
1053
|
+
if (session && await hasTmuxSession(tmuxBin, session)) {
|
|
1054
|
+
if (current.ok && current.session === session) {
|
|
1055
|
+
const panes = await listTmuxWindowPanes(tmuxBin, current.window_id);
|
|
1056
|
+
if (panes.ok) {
|
|
1057
|
+
for (const pane of panes.panes.filter((entry) => entry.pane_id !== current.pane_id && isLegacyTeamPane(entry))) {
|
|
1058
|
+
const kill = await tmuxRun(tmuxBin, ['kill-pane', '-t', pane.pane_id], { timeoutMs: 5000 });
|
|
1059
|
+
if (kill.code === 0) closed.push({ pane_id: pane.pane_id, title: pane.title });
|
|
1060
|
+
else failed.push({ pane_id: pane.pane_id, title: pane.title, stderr: kill.stderr || kill.stdout || 'tmux kill-pane failed' });
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
} else {
|
|
1064
|
+
session_kill_requested = true;
|
|
1065
|
+
const kill = await tmuxRun(tmuxBin, ['kill-session', '-t', session], { timeoutMs: 5000 });
|
|
1066
|
+
killed_session = kill.code === 0;
|
|
1067
|
+
if (!killed_session) failed.push({ session, stderr: kill.stderr || kill.stdout || 'tmux kill-session failed' });
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
if (current.ok) {
|
|
1071
|
+
const panes = await listTmuxWindowPanes(tmuxBin, current.window_id);
|
|
1072
|
+
if (panes.ok) {
|
|
1073
|
+
for (const pane of panes.panes.filter((entry) => !entry.managed && entry.pane_id !== current.pane_id && isLegacyTeamPane(entry))) {
|
|
1074
|
+
if (closed.some((entry) => entry.pane_id === pane.pane_id)) continue;
|
|
1075
|
+
const kill = await tmuxRun(tmuxBin, ['kill-pane', '-t', pane.pane_id], { timeoutMs: 5000 });
|
|
1076
|
+
if (kill.code === 0) closed.push({ pane_id: pane.pane_id, title: pane.title });
|
|
1077
|
+
else failed.push({ pane_id: pane.pane_id, title: pane.title, stderr: kill.stderr || kill.stdout || 'tmux kill-pane failed' });
|
|
1078
|
+
}
|
|
1079
|
+
if (closed.length) {
|
|
1080
|
+
await tmuxRun(tmuxBin, ['select-layout', '-t', current.window_id, 'tiled'], { timeoutMs: 5000 }).catch(() => null);
|
|
1081
|
+
await tmuxRun(tmuxBin, ['select-layout', '-t', current.window_id, '-E'], { timeoutMs: 5000 }).catch(() => null);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return {
|
|
1086
|
+
ok: failed.length === 0,
|
|
1087
|
+
skipped: !killed_session && closed.length === 0,
|
|
1088
|
+
session: session || null,
|
|
1089
|
+
killed_session,
|
|
1090
|
+
closed_lane_count: closed.length,
|
|
1091
|
+
requested_close_surfaces: (session_kill_requested ? 1 : 0) + closed.length,
|
|
1092
|
+
closed,
|
|
1093
|
+
failed,
|
|
1094
|
+
reason: killed_session
|
|
1095
|
+
? 'cleanup closed legacy Team tmux session by mission id.'
|
|
1096
|
+
: closed.length
|
|
1097
|
+
? 'cleanup closed legacy Team panes by lane title.'
|
|
1098
|
+
: 'cleanup found no legacy Team panes for this mission.'
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
async function cleanupRecordedTmuxTeamPanes(root, missionId, record = {}) {
|
|
1103
|
+
const id = record.mission_id || missionId;
|
|
1104
|
+
const cockpitState = await readJson(tmuxCockpitStatePath(root), {}).catch(() => ({}));
|
|
1105
|
+
const cockpit = cockpitState?.missions?.[id] || {};
|
|
1106
|
+
const target = cockpit.window_id || record.window_id || cockpit.session || record.session;
|
|
1107
|
+
if (!target) return { ok: false, skipped: true, reason: 'no recorded tmux target', closed_lane_count: 0 };
|
|
1108
|
+
const tmuxBin = await findTmuxBin() || 'tmux';
|
|
1109
|
+
const paneList = await listTmuxWindowPanes(tmuxBin, target);
|
|
1110
|
+
if (!paneList.ok) return { ok: false, skipped: true, reason: paneList.stderr, closed_lane_count: 0 };
|
|
1111
|
+
const managed = paneList.panes.filter((pane) => pane.managed && pane.mission_id === id);
|
|
1112
|
+
const closed = [];
|
|
1113
|
+
const failed = [];
|
|
1114
|
+
for (const pane of managed) {
|
|
1115
|
+
const kill = await tmuxRun(tmuxBin, ['kill-pane', '-t', pane.pane_id], { timeoutMs: 5000 });
|
|
1116
|
+
if (kill.code === 0) closed.push({ pane_id: pane.pane_id, agent: pane.agent, role: pane.role });
|
|
1117
|
+
else failed.push({ pane_id: pane.pane_id, agent: pane.agent, stderr: kill.stderr || kill.stdout || 'tmux kill-pane failed' });
|
|
1118
|
+
}
|
|
1119
|
+
if (closed.length) {
|
|
1120
|
+
await tmuxRun(tmuxBin, ['select-layout', '-t', target, 'tiled'], { timeoutMs: 5000 }).catch(() => null);
|
|
1121
|
+
await tmuxRun(tmuxBin, ['select-layout', '-t', target, '-E'], { timeoutMs: 5000 }).catch(() => null);
|
|
1122
|
+
}
|
|
1123
|
+
return {
|
|
1124
|
+
ok: failed.length === 0,
|
|
1125
|
+
skipped: false,
|
|
1126
|
+
session: cockpit.session || record.session,
|
|
1127
|
+
window_id: cockpit.window_id || record.window_id || null,
|
|
1128
|
+
closed_lane_count: closed.length,
|
|
1129
|
+
closed,
|
|
1130
|
+
failed,
|
|
1131
|
+
reason: closed.length ? 'closed recorded managed panes' : 'no recorded managed panes found'
|
|
989
1132
|
};
|
|
990
1133
|
}
|
|
991
1134
|
|