sneakoscope 0.7.72 → 0.7.75
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/install-helpers.mjs +25 -1
- package/src/cli/main.mjs +18 -34
- package/src/cli/maintenance-commands.mjs +18 -1
- package/src/core/fsx.mjs +1 -1
- package/src/core/init.mjs +64 -0
- package/src/core/pipeline.mjs +6 -1
- package/src/core/routes.mjs +12 -3
- package/src/core/team-live.mjs +13 -3
- package/src/core/tmux-ui.mjs +98 -21
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.75",
|
|
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",
|
|
@@ -361,6 +361,7 @@ export function normalizeCodexFastModeUiConfig(text = '') {
|
|
|
361
361
|
next = removeTomlTableKey(next, 'features', 'codex_hooks');
|
|
362
362
|
next = upsertTopLevelTomlString(next, 'model', 'gpt-5.5');
|
|
363
363
|
next = upsertTopLevelTomlString(next, 'service_tier', 'fast');
|
|
364
|
+
next = upsertTopLevelTomlBoolean(next, 'suppress_unstable_features_warning', true);
|
|
364
365
|
next = upsertTomlTableKey(next, 'features', 'hooks = true');
|
|
365
366
|
next = upsertTomlTableKey(next, 'features', 'multi_agent = true');
|
|
366
367
|
next = upsertTomlTableKey(next, 'features', 'fast_mode = true');
|
|
@@ -457,6 +458,21 @@ function upsertTopLevelTomlString(text, key, value) {
|
|
|
457
458
|
return lines.join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
|
|
458
459
|
}
|
|
459
460
|
|
|
461
|
+
function upsertTopLevelTomlBoolean(text, key, value) {
|
|
462
|
+
const line = `${key} = ${value ? 'true' : 'false'}`;
|
|
463
|
+
const lines = String(text || '').split('\n');
|
|
464
|
+
const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
|
|
465
|
+
const end = firstTable === -1 ? lines.length : firstTable;
|
|
466
|
+
for (let i = 0; i < end; i += 1) {
|
|
467
|
+
if (new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(lines[i])) {
|
|
468
|
+
lines[i] = line;
|
|
469
|
+
return lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
lines.splice(end, 0, line);
|
|
473
|
+
return lines.join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
|
|
474
|
+
}
|
|
475
|
+
|
|
460
476
|
function upsertTomlTable(text, table, block) {
|
|
461
477
|
let lines = String(text || '').trimEnd().split('\n');
|
|
462
478
|
if (lines.length === 1 && lines[0] === '') lines = [];
|
|
@@ -929,15 +945,18 @@ export async function selftestCodexLb(tmp) {
|
|
|
929
945
|
const codexLbEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
|
|
930
946
|
const codexLbAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
931
947
|
if (!codexLbSetupJson.ok || codexLbSetupJson.base_url !== 'https://lb.example.test/backend-api/codex' || !codexLbConfig.includes('model_provider = "codex-lb"') || !codexLbConfig.includes('[model_providers.codex-lb]') || !codexLbEnv.includes("CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'") || !codexLbEnv.includes("CODEX_LB_API_KEY='sk-test'") || !/(\"auth_mode\"\s*:\s*\"apikey\")/.test(codexLbAuth)) throw new Error('selftest: codex-lb setup');
|
|
948
|
+
if (!hasCodexUnstableFeatureWarningSuppression(codexLbConfig)) throw new Error('selftest: codex-lb setup did not suppress Codex unstable feature warning');
|
|
932
949
|
await initProject(codexLbHome, { installScope: 'global', force: true, repair: true });
|
|
933
950
|
const codexLbRepairSetupConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
|
|
934
951
|
if (!codexLbRepairSetupConfig.includes('model_provider = "codex-lb"') || !codexLbRepairSetupConfig.includes('[model_providers.codex-lb]') || !codexLbRepairSetupConfig.includes('https://lb.example.test/backend-api/codex') || codexLbRepairSetupConfig.includes('sk-test')) throw new Error('selftest: init codex-lb');
|
|
952
|
+
if (!hasCodexUnstableFeatureWarningSuppression(codexLbRepairSetupConfig)) throw new Error('selftest: init codex-lb did not suppress Codex unstable feature warning');
|
|
935
953
|
await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), `${codexLbConfig}\n[mcp_servers.supabase]\nurl = "https://mcp.supabase.com/mcp?project_ref=ref&read_only=true&features=database,docs"\n`);
|
|
936
954
|
const ptmp = path.join(tmp, 'codex-lb-project-config'), prevHome = process.env.HOME;
|
|
937
955
|
try { process.env.HOME = codexLbHome; await initProject(ptmp, { installScope: 'global' }); }
|
|
938
956
|
finally { if (prevHome === undefined) delete process.env.HOME; else process.env.HOME = prevHome; }
|
|
939
957
|
const pcfg = await safeReadText(path.join(ptmp, '.codex', 'config.toml'));
|
|
940
958
|
if (!pcfg.includes('model_provider = "codex-lb"') || !pcfg.includes('[model_providers.codex-lb]') || !pcfg.includes('[mcp_servers.supabase]') || !pcfg.includes('read_only=true')) throw new Error('selftest: project codex-lb');
|
|
959
|
+
if (!hasCodexUnstableFeatureWarningSuppression(pcfg)) throw new Error('selftest: project codex-lb config did not suppress Codex unstable feature warning');
|
|
941
960
|
await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n');
|
|
942
961
|
const codexLbRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'auth', 'repair', '--json'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
943
962
|
if (codexLbRepair.code !== 0) throw new Error(`selftest: codex-lb repair exited ${codexLbRepair.code}: ${codexLbRepair.stderr}`);
|
|
@@ -1014,7 +1033,7 @@ export async function selftestCodexLb(tmp) {
|
|
|
1014
1033
|
const codexLbDoctorJson = JSON.parse(codexLbDoctorRepair.stdout);
|
|
1015
1034
|
const codexLbDoctorAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
|
|
1016
1035
|
const codexLbDoctorConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
|
|
1017
|
-
if (!codexLbDoctorJson.repair?.codex_lb?.ok || !codexLbDoctorJson.repair.codex_lb.config_repaired || !codexLbDoctorJson.codex_lb?.ok || !codexLbDoctorAuth.includes('"auth_mode":"apikey"') || !codexLbDoctorAuth.includes('sk-test') || !codexLbDoctorConfig.includes('model_provider = "codex-lb"') || !codexLbDoctorConfig.includes('https://lb.example.test/backend-api/codex')) throw new Error('selftest: doctor codex-lb');
|
|
1036
|
+
if (!codexLbDoctorJson.repair?.codex_lb?.ok || !codexLbDoctorJson.repair.codex_lb.config_repaired || !codexLbDoctorJson.codex_lb?.ok || !codexLbDoctorAuth.includes('"auth_mode":"apikey"') || !codexLbDoctorAuth.includes('sk-test') || !codexLbDoctorConfig.includes('model_provider = "codex-lb"') || !codexLbDoctorConfig.includes('https://lb.example.test/backend-api/codex') || !hasCodexUnstableFeatureWarningSuppression(codexLbDoctorConfig)) throw new Error('selftest: doctor codex-lb');
|
|
1018
1037
|
const codexLbContext7Bin = path.join(tmp, 'codex-lb-context7-bin');
|
|
1019
1038
|
await ensureDir(codexLbContext7Bin);
|
|
1020
1039
|
await writeTextAtomic(path.join(codexLbContext7Bin, 'codex'), '#!/bin/sh\nif [ "$1" = "--version" ]; then echo "codex-cli 99.0.0"; exit 0; fi\nif [ "$CODEX_LB_API_KEY" ]; then echo "context7 leaked CODEX_LB_API_KEY" >&2; exit 77; fi\nif [ "$1" = "mcp" ] && [ "$2" = "list" ]; then echo ""; exit 0; fi\nif [ "$1" = "mcp" ] && [ "$2" = "add" ]; then echo "context7 added"; exit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
|
|
@@ -1092,6 +1111,7 @@ export async function selftestCodexLb(tmp) {
|
|
|
1092
1111
|
const codexLbStatusText = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'status'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
|
|
1093
1112
|
if (!String(codexLbStatusText.stdout || '').includes('Repair auth: sks codex-lb repair')) throw new Error('selftest: codex-lb status did not advertise repair command');
|
|
1094
1113
|
if (!/^model = "gpt-5\.5"/m.test(codexLbConfig) || !codexLbConfig.includes('service_tier = "fast"') || !codexLbConfig.includes('hooks = true') || hasDeprecatedCodexHooksFeatureFlag(codexLbConfig) || !codexLbConfig.includes('multi_agent = true') || !codexLbConfig.includes('fast_mode = true') || !codexLbConfig.includes('fast_mode_ui = true') || !codexLbConfig.includes('codex_git_commit = true') || !codexLbConfig.includes('computer_use = true') || !codexLbConfig.includes('apps = true') || !codexLbConfig.includes('plugins = true') || !codexLbConfig.includes('[user.fast_mode]') || !codexLbConfig.includes('visible = true') || !codexLbConfig.includes('enabled = true') || !codexLbConfig.includes('default_profile = "sks-fast-high"') || !/\[profiles\.sks-fast-high\][\s\S]*?service_tier = "fast"/.test(codexLbConfig) || codexLbConfig.includes('fast_default_opt_out = true') || hasTopLevelCodexModeLock(codexLbConfig)) throw new Error('selftest: codex-lb setup did not preserve Codex App feature flags, Fast mode defaults, Codex Git commit generation, force GPT-5.5, or migrate the hooks feature flag');
|
|
1114
|
+
if (!hasCodexUnstableFeatureWarningSuppression(codexLbConfig)) throw new Error('selftest: codex-lb setup did not suppress Codex unstable feature warning');
|
|
1095
1115
|
const codexLbLaunch = codexLaunchCommand(tmp, 'codex', []);
|
|
1096
1116
|
if (!codexLbLaunch.includes('sks-codex-lb.env')) throw new Error('selftest: tmux launch command does not source codex-lb env file');
|
|
1097
1117
|
if (!codexLbLaunch.includes("'--model' 'gpt-5.5'")) throw new Error('selftest: tmux launch command without args did not force GPT-5.5');
|
|
@@ -1118,3 +1138,7 @@ function hasDeprecatedCodexHooksFeatureFlag(text = '') {
|
|
|
1118
1138
|
}
|
|
1119
1139
|
return lines.slice(start + 1, end).some((line) => /^\s*codex_hooks\s*=/.test(line));
|
|
1120
1140
|
}
|
|
1141
|
+
|
|
1142
|
+
function hasCodexUnstableFeatureWarningSuppression(text = '') {
|
|
1143
|
+
return /(^|\n)\s*suppress_unstable_features_warning\s*=\s*true\s*(?:#.*)?(?=\n|$)/.test(String(text || ''));
|
|
1144
|
+
}
|
package/src/cli/main.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import fsp from 'node:fs/promises';
|
|
|
4
4
|
import readline from 'node:readline/promises';
|
|
5
5
|
import { stdin as input, stdout as output } from 'node:process';
|
|
6
6
|
import { projectRoot, readJson, writeJsonAtomic, writeTextAtomic, appendJsonlBounded, nowIso, exists, ensureDir, tmpdir, packageRoot, dirSize, formatBytes, which, runProcess, PACKAGE_VERSION, sksRoot, globalSksRoot, findProjectRoot, readStdin } from '../core/fsx.mjs';
|
|
7
|
-
import { initProject, installSkills, normalizeInstallScope, sksCommandPrefix } from '../core/init.mjs';
|
|
7
|
+
import { assertCodexWarningSuppressed as assertCodexWarn, hasDeprecatedCodexHooksFeatureFlag, hasTopLevelCodexModeLock, initProject, installSkills, missingGeneratedCodexAppFeatureFlags, normalizeInstallScope, sksCommandPrefix } from '../core/init.mjs';
|
|
8
8
|
import { buildCodexExecArgs, getCodexInfo, runCodexExec } from '../core/codex-adapter.mjs';
|
|
9
9
|
import { createMission, loadMission, findLatestMission, missionDir, setCurrent, stateFile } from '../core/mission.mjs';
|
|
10
10
|
import { buildQuestionSchema, writeQuestions } from '../core/questions.mjs';
|
|
@@ -78,7 +78,7 @@ import { OPENCLAW_SKILL_NAME, installOpenClawSkill } from '../core/openclaw.mjs'
|
|
|
78
78
|
import { buildTmuxLaunchPlan, buildTmuxOpenArgs, codexLaunchCommand, createTmuxSession, defaultCodexLaunchArgs, isTmuxShellSession, runTmuxLaunchPlanSyntaxCheck, shouldAutoAttachTmux, sksAsciiLogo, tmuxReadiness, tmuxStatusKind, defaultTmuxSessionName, formatTmuxBanner, launchMadTmuxUi, launchTmuxTeamView, launchTmuxUi, platformTmuxInstallHint, reconcileTmuxTeamCockpit, runTmuxStatus, sanitizeTmuxSessionName, teamLaneStyle } from '../core/tmux-ui.mjs';
|
|
79
79
|
import { autoReviewProfileName, autoReviewStatus, autoReviewSummary, enableAutoReview, disableAutoReview, enableMadHighProfile, madHighProfileName } from '../core/auto-review.mjs';
|
|
80
80
|
import { context7Command } from './context7-command.mjs';
|
|
81
|
-
import { askPostinstallQuestion, checkContext7, checkRequiredSkills, codexLbStatus, configureCodexLb, ensureCodexCliTool, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, globalCodexSkillsRoot, maybePromptCodexLbSetupForLaunch, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, repairCodexLbAuth, selftestCodexLb, shouldAutoApproveInstall } from './install-helpers.mjs';
|
|
81
|
+
import { askPostinstallQuestion, checkContext7, checkRequiredSkills, codexLbStatus, configureCodexLb, ensureCodexCliTool, ensureGlobalCodexFastModeDuringInstall, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, globalCodexSkillsRoot, maybePromptCodexLbSetupForLaunch, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, repairCodexLbAuth, selftestCodexLb, shouldAutoApproveInstall } from './install-helpers.mjs';
|
|
82
82
|
import { buildTeamPlan, codeStructureCommand, dbCommand, defaultBeta, defaultVGraph, evalCommand, gcCommand, goalCommand, gxCommand, harnessCommand, hproofCommand, madHighCommand as runMadHighCommand, memoryCommand, migrateWikiContextPack, parseTeamCreateArgs, perfCommand, profileCommand, projectWikiClaims, proofFieldCommand, qaLoopCommand, quickstartCommand, researchCommand, skillDreamCommand, statsCommand, team, teamWorkflowMarkdown, validateArtifactsCommand, wikiCommand, wikiVoxelRowCount, writeWikiContextPack } from './maintenance-commands.mjs';
|
|
83
83
|
import { openClawCommand } from './openclaw-command.mjs';
|
|
84
84
|
|
|
@@ -1659,6 +1659,7 @@ async function doctor(args) {
|
|
|
1659
1659
|
let conflictScan = await scanHarnessConflicts(root);
|
|
1660
1660
|
let repairApplied = false;
|
|
1661
1661
|
let globalSkillsRepair = null;
|
|
1662
|
+
let globalCodexConfigRepair = null;
|
|
1662
1663
|
let projectRepair = null;
|
|
1663
1664
|
let codexLbRepair = null;
|
|
1664
1665
|
const globalCommand = await globalSksCommand();
|
|
@@ -1666,6 +1667,7 @@ async function doctor(args) {
|
|
|
1666
1667
|
const existingManifest = await readJson(path.join(root, '.sneakoscope', 'manifest.json'), null);
|
|
1667
1668
|
const fixScope = requestedScope || normalizeInstallScope(existingManifest?.installation?.scope || 'global');
|
|
1668
1669
|
projectRepair = await initProject(root, { installScope: fixScope, globalCommand, localOnly: flag(args, '--local-only') || Boolean(existingManifest?.git?.local_only), force: true, repair: true });
|
|
1670
|
+
if (!flag(args, '--local-only')) globalCodexConfigRepair = await ensureGlobalCodexFastModeDuringInstall();
|
|
1669
1671
|
if (!flag(args, '--local-only')) globalSkillsRepair = await ensureGlobalCodexSkillsDuringInstall({ force: true });
|
|
1670
1672
|
codexLbRepair = await repairCodexLbAuth();
|
|
1671
1673
|
repairApplied = true;
|
|
@@ -1695,7 +1697,7 @@ async function doctor(args) {
|
|
|
1695
1697
|
const result = {
|
|
1696
1698
|
node: { ok: nodeOk, version: process.version }, root, codex, rust,
|
|
1697
1699
|
install,
|
|
1698
|
-
repair: { applied: repairApplied, project: projectRepair, global_skills: globalSkillsRepair, codex_lb: codexLbRepair, blocked_by_other_harness: flag(args, '--fix') && conflictScan.hard_block },
|
|
1700
|
+
repair: { applied: repairApplied, project: projectRepair, global_codex_config: globalCodexConfigRepair, global_skills: globalSkillsRepair, codex_lb: codexLbRepair, blocked_by_other_harness: flag(args, '--fix') && conflictScan.hard_block },
|
|
1699
1701
|
harness_conflicts: {
|
|
1700
1702
|
ok: conflictScan.ok,
|
|
1701
1703
|
hard_block: conflictScan.hard_block,
|
|
@@ -1729,6 +1731,7 @@ async function doctor(args) {
|
|
|
1729
1731
|
console.log(`Install: ${install.ok ? 'ok' : 'missing'} ${install.scope} (${install.command_prefix})`);
|
|
1730
1732
|
console.log(`Conflicts: ${result.harness_conflicts.hard_block ? 'blocked' : 'ok'} ${result.harness_conflicts.conflicts.length} finding(s)`);
|
|
1731
1733
|
if (repairApplied) console.log('Repair: regenerated SKS managed files from the installed package template');
|
|
1734
|
+
if (globalCodexConfigRepair) console.log(`Global Codex config: ${globalCodexConfigRepair.status} ${globalCodexConfigRepair.config_path || ''}`.trimEnd());
|
|
1732
1735
|
if (globalSkillsRepair) {
|
|
1733
1736
|
const removed = globalSkillsRepair.removed_stale_generated_skills || [];
|
|
1734
1737
|
const cleanup = removed.length ? ` removed stale generated skill shadow(s): ${removed.join(', ')}` : '';
|
|
@@ -1925,34 +1928,6 @@ async function safeReadText(file, fallback = '') {
|
|
|
1925
1928
|
try { return await fsp.readFile(file, 'utf8'); } catch { return fallback; }
|
|
1926
1929
|
}
|
|
1927
1930
|
|
|
1928
|
-
function hasTopLevelCodexModeLock(text = '') {
|
|
1929
|
-
const lines = String(text || '').split('\n');
|
|
1930
|
-
const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
|
|
1931
|
-
const top = (firstTable === -1 ? lines : lines.slice(0, firstTable)).join('\n');
|
|
1932
|
-
const model = top.match(/^model\s*=\s*"([^"]+)"/m)?.[1];
|
|
1933
|
-
return (Boolean(model) && model !== 'gpt-5.5') || /^model_reasoning_effort\s*=/m.test(top);
|
|
1934
|
-
}
|
|
1935
|
-
|
|
1936
|
-
function hasDeprecatedCodexHooksFeatureFlag(text = '') {
|
|
1937
|
-
const lines = String(text || '').split('\n');
|
|
1938
|
-
const start = lines.findIndex((line) => line.trim() === '[features]');
|
|
1939
|
-
if (start === -1) return false;
|
|
1940
|
-
let end = lines.length;
|
|
1941
|
-
for (let i = start + 1; i < lines.length; i += 1) {
|
|
1942
|
-
if (/^\s*\[.+\]\s*$/.test(lines[i])) {
|
|
1943
|
-
end = i;
|
|
1944
|
-
break;
|
|
1945
|
-
}
|
|
1946
|
-
}
|
|
1947
|
-
return lines.slice(start + 1, end).some((line) => /^\s*codex_hooks\s*=/.test(line));
|
|
1948
|
-
}
|
|
1949
|
-
|
|
1950
|
-
const REQUIRED_GENERATED_CODEX_APP_FEATURE_FLAGS = ['hooks', 'multi_agent', 'fast_mode', 'fast_mode_ui', 'codex_git_commit', 'computer_use', 'apps', 'plugins'];
|
|
1951
|
-
|
|
1952
|
-
function missingGeneratedCodexAppFeatureFlags(text = '') {
|
|
1953
|
-
return REQUIRED_GENERATED_CODEX_APP_FEATURE_FLAGS.filter((name) => !String(text || '').includes(`${name} = true`));
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
1931
|
async function resolveMissionId(root, arg) { return (!arg || arg === 'latest') ? findLatestMission(root) : arg; }
|
|
1957
1932
|
function readMaxCycles(args, fallback) {
|
|
1958
1933
|
const i = args.indexOf('--max-cycles');
|
|
@@ -2118,6 +2093,8 @@ async function selftest() {
|
|
|
2118
2093
|
if (!doctorRepairJson.repair?.applied || doctorRepairJson.install?.scope !== 'project' || !doctorRepairJson.install?.ok || !doctorRepairJson.install?.source_project) throw new Error('selftest: doctor scope');
|
|
2119
2094
|
const repairedManifest = await readJson(path.join(repairTmp, '.sneakoscope', 'manifest.json'));
|
|
2120
2095
|
if (repairedManifest.installation?.scope !== 'project' || repairedManifest.installation?.hook_command_prefix !== 'node ./bin/sks.mjs') throw new Error('selftest: manifest scope');
|
|
2096
|
+
const repairedCodexConfig = await safeReadText(path.join(repairTmp, '.codex', 'config.toml'));
|
|
2097
|
+
assertCodexWarn(repairedCodexConfig, 'doctor project config');
|
|
2121
2098
|
const repairedTeamSkill = await safeReadText(path.join(repairTmp, '.agents', 'skills', 'team', 'SKILL.md'));
|
|
2122
2099
|
if (!repairedTeamSkill.includes('SKS Team orchestration') || repairedTeamSkill.includes('tampered')) throw new Error('selftest: doctor repair did not regenerate team skill');
|
|
2123
2100
|
if (await exists(path.join(repairTmp, '.agents', 'skills', 'agent-team', 'SKILL.md'))) throw new Error('selftest: doctor repair did not remove deprecated agent-team alias skill');
|
|
@@ -2153,6 +2130,9 @@ async function selftest() {
|
|
|
2153
2130
|
});
|
|
2154
2131
|
if (doctorGlobalRepair.code !== 0) throw new Error(`selftest: doctor --fix global skill repair exited ${doctorGlobalRepair.code}: ${doctorGlobalRepair.stderr}`);
|
|
2155
2132
|
const doctorGlobalRepairJson = JSON.parse(doctorGlobalRepair.stdout || '{}');
|
|
2133
|
+
const doctorGlobalCodexConfig = await safeReadText(path.join(doctorGlobalHome, '.codex', 'config.toml'));
|
|
2134
|
+
if (!doctorGlobalRepairJson.repair?.global_codex_config) throw new Error('selftest: doctor global config repair missing');
|
|
2135
|
+
assertCodexWarn(doctorGlobalCodexConfig, 'doctor global config');
|
|
2156
2136
|
for (const name of stalePluginSkillNames) {
|
|
2157
2137
|
if (await exists(path.join(doctorGlobalHome, '.agents', 'skills', name, 'SKILL.md'))) throw new Error(`selftest: doctor --fix did not remove global generated ${name} plugin shadow skill`);
|
|
2158
2138
|
}
|
|
@@ -2185,6 +2165,7 @@ async function selftest() {
|
|
|
2185
2165
|
if (!(await exists(path.join(postinstallSetupTmp, '.codex', 'hooks.json')))) throw new Error('selftest: postinstall did not create project hooks during automatic bootstrap');
|
|
2186
2166
|
const postinstallSetupConfig = await safeReadText(path.join(postinstallSetupTmp, '.codex', 'config.toml'));
|
|
2187
2167
|
if (missingGeneratedCodexAppFeatureFlags(postinstallSetupConfig).length || hasDeprecatedCodexHooksFeatureFlag(postinstallSetupConfig)) throw new Error('selftest: postinstall flags');
|
|
2168
|
+
assertCodexWarn(postinstallSetupConfig, 'postinstall project config');
|
|
2188
2169
|
if (!String(postinstallSetup.stdout || '').includes('Codex App global $ skills: installed')) throw new Error('selftest: postinstall did not report automatic global Codex App skills');
|
|
2189
2170
|
if (!String(postinstallSetup.stdout || '').includes('Removed stale generated skill shadow(s):')) throw new Error('selftest: postinstall did not report stale first-party plugin shadow cleanup');
|
|
2190
2171
|
const postinstallSetupManifest = await readJson(path.join(postinstallSetupTmp, '.sneakoscope', 'manifest.json'));
|
|
@@ -2221,6 +2202,7 @@ async function selftest() {
|
|
|
2221
2202
|
if (!(await exists(path.join(postinstallNoMarkerGlobalRoot, '.sneakoscope', 'manifest.json')))) throw new Error('selftest: no-marker postinstall did not bootstrap global runtime root');
|
|
2222
2203
|
const postinstallNoMarkerConfig = await safeReadText(path.join(postinstallNoMarkerGlobalRoot, '.codex', 'config.toml'));
|
|
2223
2204
|
if (missingGeneratedCodexAppFeatureFlags(postinstallNoMarkerConfig).length || hasDeprecatedCodexHooksFeatureFlag(postinstallNoMarkerConfig)) throw new Error('selftest: no-marker flags');
|
|
2205
|
+
assertCodexWarn(postinstallNoMarkerConfig, 'postinstall global runtime config');
|
|
2224
2206
|
if (await exists(path.join(postinstallNoMarkerCwd, '.sneakoscope'))) throw new Error('selftest: no-marker postinstall polluted install cwd');
|
|
2225
2207
|
if (await exists(path.join(postinstallNoMarkerGlobalRoot, '.gitignore'))) throw new Error('selftest: global runtime bootstrap without project git wrote shared .gitignore');
|
|
2226
2208
|
const bootstrapJsonTmp = tmpdir();
|
|
@@ -3001,6 +2983,7 @@ async function selftest() {
|
|
|
3001
2983
|
const codexConfigText = await safeReadText(path.join(tmp, '.codex', 'config.toml'));
|
|
3002
2984
|
const missingCodexConfigFlags = missingGeneratedCodexAppFeatureFlags(codexConfigText);
|
|
3003
2985
|
if (missingCodexConfigFlags.length || hasDeprecatedCodexHooksFeatureFlag(codexConfigText)) throw new Error(`selftest: generated Codex App feature flags missing or deprecated: ${missingCodexConfigFlags.join(', ')}`);
|
|
2986
|
+
assertCodexWarn(codexConfigText, 'generated Codex App config');
|
|
3004
2987
|
if (!hasContext7ConfigText(codexConfigText)) throw new Error('selftest: Context7 MCP not configured');
|
|
3005
2988
|
if (!codexConfigText.includes('[profiles.sks-task-low]') || !codexConfigText.includes('[profiles.sks-task-medium]') || !codexConfigText.includes('[profiles.sks-logic-high]') || !codexConfigText.includes('[profiles.sks-fast-high]') || !codexConfigText.includes('[profiles.sks-research-xhigh]') || !codexConfigText.includes('[profiles.sks-mad-high]')) throw new Error('selftest: GPT-5.5 reasoning profiles not configured');
|
|
3006
2989
|
if (!/\[profiles\.sks-mad-high\][\s\S]*?approval_policy = "never"[\s\S]*?sandbox_mode = "danger-full-access"/.test(codexConfigText)) throw new Error('selftest: generated sks-mad-high profile is not full access');
|
|
@@ -3012,6 +2995,7 @@ async function selftest() {
|
|
|
3012
2995
|
await initProject(preservedConfigTmp, {});
|
|
3013
2996
|
const preservedConfig = await safeReadText(path.join(preservedConfigTmp, '.codex', 'config.toml'));
|
|
3014
2997
|
if (!/^model = "gpt-5\.5"/m.test(preservedConfig) || !preservedConfig.includes('service_tier = "fast"') || !preservedConfig.includes('fast_mode = true') || !preservedConfig.includes('fast_mode_ui = true') || !preservedConfig.includes('[user.fast_mode]') || !preservedConfig.includes('visible = true') || !preservedConfig.includes('enabled = true') || !preservedConfig.includes('default_profile = "sks-fast-high"') || !/\[profiles\.sks-fast-high\][\s\S]*?service_tier = "fast"/.test(preservedConfig)) throw new Error('selftest: Codex config merge dropped or failed to enable Fast mode defaults and GPT-5.5');
|
|
2998
|
+
assertCodexWarn(preservedConfig, 'merged Codex config');
|
|
3015
2999
|
if (preservedConfig.includes('fast_default_opt_out = true') || !preservedConfig.includes('keep = true')) throw new Error('selftest: Codex config merge did not remove stale Fast opt-out notice while preserving other notice keys');
|
|
3016
3000
|
const missingPreservedFlags = missingGeneratedCodexAppFeatureFlags(preservedConfig);
|
|
3017
3001
|
if (missingPreservedFlags.length || hasDeprecatedCodexHooksFeatureFlag(preservedConfig) || !preservedConfig.includes('custom_preview = true') || !preservedConfig.includes('[profiles.sks-fast-high]')) throw new Error(`selftest: Codex config merge did not add required app feature flags, preserve existing feature flags, or remove deprecated codex_hooks: ${missingPreservedFlags.join(', ')}`);
|
|
@@ -3247,7 +3231,7 @@ async function selftest() {
|
|
|
3247
3231
|
if (teamPlan.agent_session_count !== 5) throw new Error('selftest: team default sessions not 5');
|
|
3248
3232
|
if (teamPlan.role_counts.executor !== 3 || teamPlan.role_counts.user !== 1 || teamPlan.role_counts.reviewer !== 5) throw new Error('selftest: team default role counts invalid');
|
|
3249
3233
|
const teamPlanFeatureFlags = teamPlan.codex_config_required?.features || {};
|
|
3250
|
-
const missingTeamPlanFeatureFlags =
|
|
3234
|
+
const missingTeamPlanFeatureFlags = missingGeneratedCodexAppFeatureFlags(teamPlanFeatureFlags);
|
|
3251
3235
|
if (missingTeamPlanFeatureFlags.length || teamPlanFeatureFlags.codex_hooks === true) throw new Error(`selftest: team plan Codex config missing required app flags or still uses deprecated codex_hooks: ${missingTeamPlanFeatureFlags.join(', ')}`);
|
|
3252
3236
|
if (!teamPlan.review_gate?.passed || teamPlan.review_gate.required_reviewer_lanes !== 5) throw new Error('selftest: team review policy gate did not pass default plan');
|
|
3253
3237
|
if (teamPlan.codex_config_required?.service_tier !== 'fast' || teamPlan.reasoning?.service_tier !== 'fast') throw new Error('selftest: team plan did not require Fast service tier');
|
|
@@ -3321,7 +3305,7 @@ async function selftest() {
|
|
|
3321
3305
|
await ensureDir(fakeTmuxDir);
|
|
3322
3306
|
const fakeTmuxLog = path.join(fakeTmuxDir, 'tmux.log');
|
|
3323
3307
|
const fakeTmuxBin = path.join(fakeTmuxDir, 'tmux');
|
|
3324
|
-
await writeTextAtomic(fakeTmuxBin, `#!/usr/bin/env node\nconst{appendFileSync:a}=require('
|
|
3308
|
+
await writeTextAtomic(fakeTmuxBin, `#!/usr/bin/env node\nconst{appendFileSync:a}=require('fs'),e=process.env,r=process.argv.slice(2),c=r[0];if(e.SKS_FAKE_TMUX_LOG)a(e.SKS_FAKE_TMUX_LOG,r.join(' ')+'\\n');if(c==='new-session')console.log('%1');else if(c==='split-window')console.log(e.SKS_FAKE_TMUX_SPLIT_ID||'%2');else if(c==='list-windows')console.log('@1');else if(c==='display-message')console.log(e.SKS_FAKE_TMUX_DISPLAY||'sks-existing-selftest\\t@1\\t%1');else if(c==='list-panes'){let t=r[r.indexOf('-t')+1]||'';console.log(t[0]=='%'&&r.join(' ').includes('pane_dead')?'0\\t'+t:e.SKS_FAKE_TMUX_LIST||'')}\n`);
|
|
3325
3309
|
await fsp.chmod(fakeTmuxBin, 0o755);
|
|
3326
3310
|
const previousFakeTmuxLog = process.env.SKS_FAKE_TMUX_LOG;
|
|
3327
3311
|
const previousPath = process.env.PATH;
|
|
@@ -3478,7 +3462,7 @@ async function selftest() {
|
|
|
3478
3462
|
if (!(await readTeamTranscriptTail(teamDir, 1)).join('\n').includes('selftest mapped options')) throw new Error('selftest: team transcript tail missing event');
|
|
3479
3463
|
const teamLane = await renderTeamAgentLane(teamDir, { missionId: teamId, agent: 'analysis_scout_1', lines: 4 });
|
|
3480
3464
|
if (!teamLane.includes('selftest mapped repo slice')) throw new Error('selftest: team agent lane missing event context');
|
|
3481
|
-
if (!teamLane.includes('##
|
|
3465
|
+
if (!teamLane.includes('## Codex Chat') || !teamLane.includes('+-- me [status]') || !teamLane.includes('selftest mapped repo slice') || teamLane.includes('## Global Tail')) throw new Error('selftest: chat lane');
|
|
3482
3466
|
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 });
|
|
3483
3467
|
if (teamLaneCli.code !== 0 || !String(teamLaneCli.stdout || '').includes('SKS Team Agent Lane') || !String(teamLaneCli.stdout || '').includes('analysis_scout_1')) throw new Error('selftest: sks team lane CLI did not render an agent lane');
|
|
3484
3468
|
await writeTextAtomic(path.join(teamDir, 'team-analysis.md'), '- claim: analysis scout mapped route registry | source: src/core/routes.mjs | risk: high | confidence: supported\n');
|
|
@@ -761,15 +761,32 @@ export async function validateArtifactsCommand(args = []) {
|
|
|
761
761
|
const root = await sksRoot();
|
|
762
762
|
const missionArg = args[0] && !String(args[0]).startsWith('--') ? args[0] : 'latest';
|
|
763
763
|
const id = await resolveMissionId(root, missionArg);
|
|
764
|
-
const
|
|
764
|
+
const loaded = id ? await loadMission(root, id) : null;
|
|
765
|
+
const targetDir = loaded ? loaded.dir : root;
|
|
765
766
|
const requiredRaw = readFlagValue(args, '--required', '');
|
|
766
767
|
const required = requiredRaw === 'all'
|
|
767
768
|
? Object.keys(ARTIFACT_FILES)
|
|
768
769
|
: String(requiredRaw || '').split(',').map((x) => x.trim()).filter(Boolean);
|
|
769
770
|
const report = await writeValidationReport(targetDir, { required });
|
|
771
|
+
const missionMode = String(loaded?.mission?.mode || '').toLowerCase();
|
|
772
|
+
if (missionMode === 'research' || await exists(path.join(targetDir, 'research-gate.json'))) {
|
|
773
|
+
const researchGate = await evaluateResearchGate(targetDir);
|
|
774
|
+
report.route_gate = {
|
|
775
|
+
route: 'Research',
|
|
776
|
+
ok: researchGate.passed === true,
|
|
777
|
+
gate_file: 'research-gate.evaluated.json',
|
|
778
|
+
reasons: researchGate.reasons || []
|
|
779
|
+
};
|
|
780
|
+
if (!report.route_gate.ok) {
|
|
781
|
+
report.ok = false;
|
|
782
|
+
report.errors = [...(report.errors || []), ...report.route_gate.reasons.map((reason) => `research-gate:${reason}`)];
|
|
783
|
+
}
|
|
784
|
+
await writeJsonAtomic(path.join(targetDir, 'artifact-validation.json'), report);
|
|
785
|
+
}
|
|
770
786
|
if (flag(args, '--json')) return console.log(JSON.stringify(report, null, 2));
|
|
771
787
|
console.log(`Artifact validation: ${report.ok ? 'pass' : 'fail'}`);
|
|
772
788
|
console.log(`Target: ${path.relative(root, targetDir) || '.'}`);
|
|
789
|
+
if (report.route_gate) console.log(`Route gate: ${report.route_gate.route} ${report.route_gate.ok ? 'pass' : `fail (${report.route_gate.reasons.join(', ')})`}`);
|
|
773
790
|
if (report.missing.length) console.log(`Missing: ${report.missing.join(', ')}`);
|
|
774
791
|
for (const [schema, result] of Object.entries(report.results)) console.log(`${schema}: ${result.ok ? 'pass' : `fail (${result.errors.join(', ')})`}`);
|
|
775
792
|
if (!report.ok) process.exitCode = 2;
|
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.75';
|
|
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
|
@@ -15,6 +15,54 @@ const SKS_GENERATED_GIT_PATTERNS = ['.sneakoscope/', '.codex/', '.agents/', 'AGE
|
|
|
15
15
|
const SKS_SKILL_MANIFEST_FILE = '.sks-generated.json';
|
|
16
16
|
const GENERATED_PRUNE_POLICY = 'remove_previous_sks_generated_paths_absent_from_current_manifest';
|
|
17
17
|
|
|
18
|
+
export const REQUIRED_GENERATED_CODEX_APP_FEATURE_FLAGS = [
|
|
19
|
+
'hooks',
|
|
20
|
+
'multi_agent',
|
|
21
|
+
'fast_mode',
|
|
22
|
+
'fast_mode_ui',
|
|
23
|
+
'codex_git_commit',
|
|
24
|
+
'computer_use',
|
|
25
|
+
'apps',
|
|
26
|
+
'plugins'
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export function hasTopLevelCodexModeLock(text = '') {
|
|
30
|
+
const lines = String(text || '').split('\n');
|
|
31
|
+
const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
|
|
32
|
+
const top = (firstTable === -1 ? lines : lines.slice(0, firstTable)).join('\n');
|
|
33
|
+
const model = top.match(/^model\s*=\s*"([^"]+)"/m)?.[1];
|
|
34
|
+
return (Boolean(model) && model !== 'gpt-5.5') || /^model_reasoning_effort\s*=/m.test(top);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function hasDeprecatedCodexHooksFeatureFlag(text = '') {
|
|
38
|
+
const lines = String(text || '').split('\n');
|
|
39
|
+
const start = lines.findIndex((line) => line.trim() === '[features]');
|
|
40
|
+
if (start === -1) return false;
|
|
41
|
+
let end = lines.length;
|
|
42
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
43
|
+
if (/^\s*\[.+\]\s*$/.test(lines[i])) {
|
|
44
|
+
end = i;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return lines.slice(start + 1, end).some((line) => /^\s*codex_hooks\s*=/.test(line));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function missingGeneratedCodexAppFeatureFlags(text = '') {
|
|
52
|
+
if (text && typeof text === 'object') return REQUIRED_GENERATED_CODEX_APP_FEATURE_FLAGS.filter((name) => text[name] !== true);
|
|
53
|
+
return REQUIRED_GENERATED_CODEX_APP_FEATURE_FLAGS.filter((name) => !String(text || '').includes(`${name} = true`));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function hasCodexUnstableFeatureWarningSuppression(text = '') {
|
|
57
|
+
return /(^|\n)\s*suppress_unstable_features_warning\s*=\s*true\s*(?:#.*)?(?=\n|$)/.test(String(text || ''));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function assertCodexWarningSuppressed(text = '', label = 'Codex config') {
|
|
61
|
+
if (!hasCodexUnstableFeatureWarningSuppression(text)) {
|
|
62
|
+
throw new Error(`selftest: ${label} missing suppress_unstable_features_warning`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
18
66
|
function reflectionInstructionText(commandPrefix = 'sks') {
|
|
19
67
|
return `Post-route reflection: full routes load \`reflection\` after work/tests and before final; DFix/Answer/Help/Wiki/SKS discovery are exempt. Write reflection.md; record only real misses/gaps, or no_issue_acknowledged. For lessons, append TriWiki claim rows to ${REFLECTION_MEMORY_PATH}. Run "${commandPrefix} wiki refresh" or pack, validate, then pass reflection-gate.json.`;
|
|
20
68
|
}
|
|
@@ -444,6 +492,7 @@ function mergeManagedCodexConfigToml(existingContent = '') {
|
|
|
444
492
|
next = removeTomlTableKey(next, 'features', 'codex_hooks');
|
|
445
493
|
next = upsertTopLevelTomlString(next, 'model', 'gpt-5.5');
|
|
446
494
|
next = upsertTopLevelTomlString(next, 'service_tier', 'fast');
|
|
495
|
+
next = upsertTopLevelTomlBoolean(next, 'suppress_unstable_features_warning', true);
|
|
447
496
|
next = upsertTomlTableKey(next, 'features', 'hooks = true');
|
|
448
497
|
next = upsertTomlTableKey(next, 'features', 'multi_agent = true');
|
|
449
498
|
next = upsertTomlTableKey(next, 'features', 'fast_mode = true');
|
|
@@ -546,6 +595,21 @@ function upsertTopLevelTomlString(text, key, value) {
|
|
|
546
595
|
return lines.join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
|
|
547
596
|
}
|
|
548
597
|
|
|
598
|
+
function upsertTopLevelTomlBoolean(text, key, value) {
|
|
599
|
+
const line = `${key} = ${value ? 'true' : 'false'}`;
|
|
600
|
+
const lines = String(text || '').split('\n');
|
|
601
|
+
const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
|
|
602
|
+
const end = firstTable === -1 ? lines.length : firstTable;
|
|
603
|
+
for (let i = 0; i < end; i += 1) {
|
|
604
|
+
if (new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(lines[i])) {
|
|
605
|
+
lines[i] = line;
|
|
606
|
+
return lines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
lines.splice(end, 0, line);
|
|
610
|
+
return lines.join('\n').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
|
|
611
|
+
}
|
|
612
|
+
|
|
549
613
|
function removeTomlTableKey(text, table, key) {
|
|
550
614
|
const lines = String(text || '').trimEnd().split('\n');
|
|
551
615
|
if (lines.length === 1 && lines[0] === '') return '';
|
package/src/core/pipeline.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { writeMemorySweepReport } from './memory-governor.mjs';
|
|
|
12
12
|
import { writeMistakeMemoryReport } from './mistake-memory.mjs';
|
|
13
13
|
import { MISTAKE_RECALL_ARTIFACT, mistakeRecallGateStatus } from './mistake-recall.mjs';
|
|
14
14
|
import { recordSkillDreamEvent, skillDreamPolicyText, writeSkillForgeReport } from './skill-forge.mjs';
|
|
15
|
-
import { writeResearchPlan } from './research.mjs';
|
|
15
|
+
import { evaluateResearchGate, writeResearchPlan } from './research.mjs';
|
|
16
16
|
import { PPT_REQUIRED_GATE_FIELDS, writePptRouteArtifacts } from './ppt.mjs';
|
|
17
17
|
import { writeQaLoopArtifacts } from './qa-loop.mjs';
|
|
18
18
|
import { IMAGE_UX_REVIEW_GATE_ARTIFACT, IMAGE_UX_REVIEW_POLICY_ARTIFACT, IMAGE_UX_REVIEW_SCREEN_INVENTORY_ARTIFACT, IMAGE_UX_REVIEW_GENERATED_REVIEW_LEDGER_ARTIFACT, IMAGE_UX_REVIEW_ISSUE_LEDGER_ARTIFACT, IMAGE_UX_REVIEW_ITERATION_REPORT_ARTIFACT, IMAGE_UX_REVIEW_REQUIRED_GATE_FIELDS, writeImageUxReviewRouteArtifacts } from './image-ux-review.mjs';
|
|
@@ -1456,6 +1456,11 @@ function missingRequiredGateFields(file, state, gate = {}) {
|
|
|
1456
1456
|
|
|
1457
1457
|
async function missingRequiredGateArtifacts(root, file, state, gate = {}) {
|
|
1458
1458
|
const mode = String(state?.mode || '').toUpperCase();
|
|
1459
|
+
if (file === 'research-gate.json' || mode === 'RESEARCH') {
|
|
1460
|
+
const evaluated = await evaluateResearchGate(missionDir(root, state.mission_id));
|
|
1461
|
+
if (evaluated.passed === true) return [];
|
|
1462
|
+
return (evaluated.reasons || ['research_gate_blocked']).map((reason) => `research-gate:${reason}`);
|
|
1463
|
+
}
|
|
1459
1464
|
if (file === IMAGE_UX_REVIEW_GATE_ARTIFACT || mode === 'IMAGE_UX_REVIEW') return missingImageUxReviewArtifacts(root, state, gate);
|
|
1460
1465
|
if (file !== 'team-gate.json' && mode !== 'TEAM') return [];
|
|
1461
1466
|
const missing = [];
|
package/src/core/routes.mjs
CHANGED
|
@@ -149,13 +149,22 @@ export function dollarSkillName(commandOrId) {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
export function stripVisibleDecisionAnswerBlocks(value = '') {
|
|
152
|
-
return String(value || '')
|
|
152
|
+
return stripNonAuthoritativeLiveChatBlocks(String(value || ''))
|
|
153
153
|
.replace(/\s*\[(?=[^\]]*\b[A-Z][A-Z0-9_]{2,}\s*:)[^\]]{0,6000}\]\s*/g, ' ')
|
|
154
154
|
.replace(/[ \t]{2,}/g, ' ')
|
|
155
155
|
.replace(/\n{3,}/g, '\n\n')
|
|
156
156
|
.trim();
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
export function stripNonAuthoritativeLiveChatBlocks(value = '') {
|
|
160
|
+
return String(value || '')
|
|
161
|
+
.replace(/(?:^|\n)\s*[›>]\s*\[## Live Chat[\s\S]*?\]\s*(?=(?:이|이거|그리고|근데|계속|고쳐|수정|해결|Pane|pane|please|fix|also|and|$))/g, '\n')
|
|
162
|
+
.replace(/(?:^|\n)\s*\[## Live Chat[\s\S]*?\]\s*(?=(?:이|이거|그리고|근데|계속|고쳐|수정|해결|Pane|pane|please|fix|also|and|$))/g, '\n')
|
|
163
|
+
.replace(/^\s*-?\s*\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\s+\S+\s+\[[^\]]+\]:.*$/gm, '')
|
|
164
|
+
.replace(/^\s*## Live Chat\s*$/gm, '')
|
|
165
|
+
.trim();
|
|
166
|
+
}
|
|
167
|
+
|
|
159
168
|
export function triwikiContextTracking(commandPrefix = 'sks') {
|
|
160
169
|
const prefix = String(commandPrefix || 'sks');
|
|
161
170
|
return {
|
|
@@ -677,8 +686,8 @@ export function looksLikeImageUxReviewRequest(prompt = '') {
|
|
|
677
686
|
}
|
|
678
687
|
|
|
679
688
|
export function routePrompt(prompt) {
|
|
680
|
-
const
|
|
681
|
-
const
|
|
689
|
+
const text = stripVisibleDecisionAnswerBlocks(prompt);
|
|
690
|
+
const command = dollarCommand(text);
|
|
682
691
|
if (command) {
|
|
683
692
|
if (command === 'MAD-SKS') {
|
|
684
693
|
const afterModifier = stripMadSksSignal(text);
|
package/src/core/team-live.mjs
CHANGED
|
@@ -576,7 +576,7 @@ export async function renderTeamAgentLane(dir, opts = {}) {
|
|
|
576
576
|
`## Assigned Runtime Tasks`,
|
|
577
577
|
...(runtime ? formatRuntimeTasks(assignedTasks) : ['- team-runtime-tasks.json not available yet.']),
|
|
578
578
|
'',
|
|
579
|
-
`##
|
|
579
|
+
`## Codex Chat`,
|
|
580
580
|
...(chatEvents.length ? chatEvents.map((event) => formatChatTranscriptEvent(event, aliases[0])) : ['- waiting for live agent messages...']),
|
|
581
581
|
opts.includeGlobalTail ? '' : null,
|
|
582
582
|
opts.includeGlobalTail ? `## Global Tail` : null,
|
|
@@ -710,14 +710,24 @@ function uniqueTranscriptEvents(events = []) {
|
|
|
710
710
|
}
|
|
711
711
|
|
|
712
712
|
function formatChatTranscriptEvent(event = {}, laneAgent = '') {
|
|
713
|
-
if (event.raw) return
|
|
713
|
+
if (event.raw) return codexChatBlock({ speaker: 'system', message: event.raw });
|
|
714
714
|
const from = event.agent || 'unknown';
|
|
715
715
|
const to = event.to ? ` -> ${event.to}` : '';
|
|
716
716
|
const kind = event.type && event.type !== 'message' ? ` [${event.type}]` : '';
|
|
717
717
|
const ts = event.ts ? `${event.ts} ` : '';
|
|
718
718
|
const artifact = event.artifact ? ` (${event.artifact})` : '';
|
|
719
719
|
const marker = String(from) === String(laneAgent) ? 'me' : from;
|
|
720
|
-
return
|
|
720
|
+
return codexChatBlock({
|
|
721
|
+
speaker: `${marker}${to}${kind}`,
|
|
722
|
+
meta: ts.trim(),
|
|
723
|
+
message: `${String(event.message || '').slice(0, 500)}${artifact}`
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function codexChatBlock({ speaker = 'agent', meta = '', message = '' } = {}) {
|
|
728
|
+
const header = [speaker, meta].filter(Boolean).join(' | ');
|
|
729
|
+
const body = String(message || '').split(/\r?\n/).map((line) => `| ${line}`).join('\n');
|
|
730
|
+
return [`+-- ${header}`, body || '|', '+--'].join('\n');
|
|
721
731
|
}
|
|
722
732
|
|
|
723
733
|
function eventAddressedTo(event = {}, agent = '') {
|
package/src/core/tmux-ui.mjs
CHANGED
|
@@ -122,6 +122,8 @@ const TERMINAL_TEAM_AGENT_STATUSES = new Set([
|
|
|
122
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
123
|
const GENERIC_TEAM_AGENT_IDS = new Set(['parent_orchestrator', 'analysis_scout', 'team_consensus', 'implementation_worker', 'db_safety_reviewer', 'qa_reviewer']);
|
|
124
124
|
const DYNAMIC_TEAM_TMUX_LAYOUT = 'main-vertical';
|
|
125
|
+
const TEAM_TMUX_MAIN_PANE_MIN_WIDTH = 48;
|
|
126
|
+
const TEAM_TMUX_MAIN_PANE_WIDTH_RATIO = 0.5;
|
|
125
127
|
|
|
126
128
|
export function isTmuxShellSession(env = process.env) {
|
|
127
129
|
return Boolean(String(env.TMUX || '').trim());
|
|
@@ -223,6 +225,15 @@ function colorizedLaneBannerCommand(lines = [], color = '') {
|
|
|
223
225
|
return `printf '\\033[1;${code}m%s\\033[0m\\n' ${shellEscape(text)}`;
|
|
224
226
|
}
|
|
225
227
|
|
|
228
|
+
function selfClosingTeamPaneCommand(command = '') {
|
|
229
|
+
return [
|
|
230
|
+
`SKS_TEAM_PANE_SELF_CLOSE=1 ${command}`,
|
|
231
|
+
'status=$?',
|
|
232
|
+
'if [ "${SKS_TEAM_PANE_SELF_CLOSE:-1}" = "1" ] && [ -n "${TMUX_PANE:-}" ]; then tmux kill-pane -t "$TMUX_PANE" >/dev/null 2>&1 || true; fi',
|
|
233
|
+
'exit "$status"'
|
|
234
|
+
].join('; ');
|
|
235
|
+
}
|
|
236
|
+
|
|
226
237
|
function compactTeamPaneBanner({ missionId, agentId, phase, style, overview = false } = {}) {
|
|
227
238
|
const role = overview ? 'overview' : `${style.label} (${style.color_name})`;
|
|
228
239
|
return [
|
|
@@ -263,24 +274,26 @@ function teamLaneTitle(agentId = '') {
|
|
|
263
274
|
export function teamAgentCommand(root, missionId, agentId, phase) {
|
|
264
275
|
const style = teamLaneStyle(agentId);
|
|
265
276
|
const title = teamLaneTitle(agentId);
|
|
277
|
+
const laneCommand = `node ${shellEscape(path.join(packageRoot(), 'bin', 'sks.mjs'))} team lane ${shellEscape(missionId)} --agent ${shellEscape(agentId)} --phase ${shellEscape(phase)} --follow --lines 12`;
|
|
266
278
|
return [
|
|
267
279
|
terminalTitleCommand(title),
|
|
268
280
|
'clear',
|
|
269
281
|
colorizedLaneBannerCommand(compactTeamPaneBanner({ missionId, agentId, phase, style }), style.color),
|
|
270
282
|
`cd ${shellEscape(root)}`,
|
|
271
|
-
|
|
283
|
+
selfClosingTeamPaneCommand(laneCommand)
|
|
272
284
|
].join('; ');
|
|
273
285
|
}
|
|
274
286
|
|
|
275
287
|
export function teamOverviewCommand(root, missionId) {
|
|
276
288
|
const style = teamLaneStyle('mission_overview');
|
|
277
289
|
const title = teamLaneTitle('mission_overview');
|
|
290
|
+
const watchCommand = `node ${shellEscape(path.join(packageRoot(), 'bin', 'sks.mjs'))} team watch ${shellEscape(missionId)} --follow --lines 18`;
|
|
278
291
|
return [
|
|
279
292
|
terminalTitleCommand(title),
|
|
280
293
|
'clear',
|
|
281
294
|
colorizedLaneBannerCommand(compactTeamPaneBanner({ missionId, agentId: 'mission_overview', style, overview: true }), style.color),
|
|
282
295
|
`cd ${shellEscape(root)}`,
|
|
283
|
-
|
|
296
|
+
selfClosingTeamPaneCommand(watchCommand)
|
|
284
297
|
].join('; ');
|
|
285
298
|
}
|
|
286
299
|
|
|
@@ -472,6 +485,16 @@ async function listTmuxWindowPanes(bin, windowId) {
|
|
|
472
485
|
return { ok: true, panes: parseTmuxPaneLines(run.stdout) };
|
|
473
486
|
}
|
|
474
487
|
|
|
488
|
+
async function tmuxPaneExists(bin, paneId) {
|
|
489
|
+
if (!paneId || !String(paneId).startsWith('%')) return false;
|
|
490
|
+
const run = await tmuxRun(bin, ['list-panes', '-t', paneId, '-F', '#{pane_dead}\t#{pane_id}'], { timeoutMs: 5000, maxOutputBytes: 4096 });
|
|
491
|
+
if (run.code !== 0) return false;
|
|
492
|
+
return String(run.stdout || '').split(/\r?\n/).some((line) => {
|
|
493
|
+
const [dead = '', id = ''] = line.trim().split('\t');
|
|
494
|
+
return id === paneId && dead !== '1';
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
475
498
|
async function setTmuxPaneUserOptions(bin, paneId, options = {}) {
|
|
476
499
|
const applied = [];
|
|
477
500
|
const failed = [];
|
|
@@ -506,11 +529,52 @@ function tmuxLayoutName(value = 'tiled') {
|
|
|
506
529
|
: 'tiled';
|
|
507
530
|
}
|
|
508
531
|
|
|
532
|
+
function teamMainPaneWidthFromWindow(width) {
|
|
533
|
+
const n = Number(width);
|
|
534
|
+
if (!Number.isFinite(n) || n <= 0) return TEAM_TMUX_MAIN_PANE_MIN_WIDTH;
|
|
535
|
+
return Math.max(TEAM_TMUX_MAIN_PANE_MIN_WIDTH, Math.floor(n * TEAM_TMUX_MAIN_PANE_WIDTH_RATIO));
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function applyStableTeamLayout(tmuxBin, target, mainPaneId = null, opts = {}) {
|
|
539
|
+
const layout = tmuxLayoutName(opts.layout || DYNAMIC_TEAM_TMUX_LAYOUT);
|
|
540
|
+
const windowTarget = target || mainPaneId;
|
|
541
|
+
const applied = [];
|
|
542
|
+
const failed = [];
|
|
543
|
+
const runAndRecord = async (args) => {
|
|
544
|
+
const run = await tmuxRun(tmuxBin, args, { timeoutMs: 5000 });
|
|
545
|
+
const command = [path.basename(tmuxBin), ...args].join(' ');
|
|
546
|
+
if (run.code === 0) applied.push(command);
|
|
547
|
+
else failed.push({ command, stderr: run.stderr || run.stdout || 'tmux command failed' });
|
|
548
|
+
return run;
|
|
549
|
+
};
|
|
550
|
+
if (mainPaneId) await runAndRecord(['select-pane', '-t', mainPaneId]);
|
|
551
|
+
const width = await tmuxRun(tmuxBin, ['display-message', '-p', '-t', windowTarget, '#{window_width}'], { timeoutMs: 5000, maxOutputBytes: 1024 });
|
|
552
|
+
if (width.code === 0) {
|
|
553
|
+
const mainWidth = teamMainPaneWidthFromWindow(String(width.stdout || '').trim());
|
|
554
|
+
await runAndRecord(['set-window-option', '-t', windowTarget, 'main-pane-width', String(mainWidth)]);
|
|
555
|
+
}
|
|
556
|
+
await runAndRecord(['select-layout', '-t', windowTarget, layout]);
|
|
557
|
+
return { ok: failed.length === 0, layout_name: layout, applied, failed };
|
|
558
|
+
}
|
|
559
|
+
|
|
509
560
|
async function enableTmuxDynamicResize(tmuxBin, session, opts = {}) {
|
|
510
561
|
const layout = tmuxLayoutName(opts.layout || 'tiled');
|
|
511
562
|
const safeSession = sanitizeTmuxSessionName(session);
|
|
512
563
|
const target = await tmuxWindowTarget(tmuxBin, safeSession);
|
|
513
|
-
const
|
|
564
|
+
const stableMainVertical = layout === DYNAMIC_TEAM_TMUX_LAYOUT && opts.stableTeamLayout;
|
|
565
|
+
const tmuxShell = shellEscape(tmuxBin || 'tmux');
|
|
566
|
+
const targetShell = shellEscape(target);
|
|
567
|
+
const stableRelayoutShell = [
|
|
568
|
+
`${tmuxShell} resize-window -t ${targetShell} -A >/dev/null 2>&1 || true`,
|
|
569
|
+
`${tmuxShell} set-window-option -t ${targetShell} window-size latest >/dev/null 2>&1 || true`,
|
|
570
|
+
`w=$(${tmuxShell} display-message -p -t ${targetShell} '#{window_width}' 2>/dev/null || printf 120)`,
|
|
571
|
+
`if [ "$w" -gt 0 ] 2>/dev/null; then ${tmuxShell} set-window-option -t ${targetShell} main-pane-width $((w / 2)) >/dev/null 2>&1 || true; fi`,
|
|
572
|
+
`${tmuxShell} select-layout -t ${targetShell} ${layout} >/dev/null 2>&1 || true`,
|
|
573
|
+
`${tmuxShell} set-window-option -t ${targetShell} window-size latest >/dev/null 2>&1 || true`
|
|
574
|
+
].join('; ');
|
|
575
|
+
const relayout = stableMainVertical
|
|
576
|
+
? `run-shell -b ${shellEscape(stableRelayoutShell)}`
|
|
577
|
+
: `resize-window -t ${target} -A; set-window-option -t ${target} window-size latest; select-layout -t ${target} ${layout}; select-layout -t ${target} -E; set-window-option -t ${target} window-size latest`;
|
|
514
578
|
const commands = [
|
|
515
579
|
['set-window-option', '-t', target, 'window-size', 'latest'],
|
|
516
580
|
['set-window-option', '-t', target, 'aggressive-resize', 'on'],
|
|
@@ -518,8 +582,8 @@ async function enableTmuxDynamicResize(tmuxBin, session, opts = {}) {
|
|
|
518
582
|
['set-hook', '-t', safeSession, 'client-resized', relayout],
|
|
519
583
|
['resize-window', '-t', target, '-A'],
|
|
520
584
|
['set-window-option', '-t', target, 'window-size', 'latest'],
|
|
521
|
-
['select-layout', '-t', target, layout],
|
|
522
|
-
['
|
|
585
|
+
...(stableMainVertical ? [] : [['select-layout', '-t', target, layout], ['select-layout', '-t', target, '-E']]),
|
|
586
|
+
...(stableMainVertical ? [['display-message', '-p', '-t', target, '#{window_width}'], ['select-layout', '-t', target, layout]] : []),
|
|
523
587
|
['set-window-option', '-t', target, 'window-size', 'latest']
|
|
524
588
|
];
|
|
525
589
|
const applied = [];
|
|
@@ -588,19 +652,21 @@ export async function createTmuxSession(plan = {}, panes = [], opts = {}) {
|
|
|
588
652
|
const create = await tmuxRun(tmuxBin, ['new-session', '-d', '-x', dimensions.width, '-y', dimensions.height, '-s', session, '-c', path.resolve(first.cwd || root), '-n', 'sks', '-P', '-F', '#{pane_id}', first.command || 'pwd']);
|
|
589
653
|
if (create.code !== 0) return { ok: false, session, panes: [], stderr: create.stderr || create.stdout || 'tmux new-session failed' };
|
|
590
654
|
const created = [{ pane_id: paneId(create.stdout), role: first.role || 'overview', title: first.title || 'overview' }];
|
|
591
|
-
let
|
|
655
|
+
let rightStackRootPaneId = null;
|
|
592
656
|
for (const pane of normalizedPanes.slice(1)) {
|
|
593
657
|
const direction = rightSidePanes ? (created.length === 1 ? '-h' : '-v') : (pane.vertical ? '-v' : '-h');
|
|
594
|
-
const splitTarget = rightSidePanes ?
|
|
658
|
+
const splitTarget = rightSidePanes ? (rightStackRootPaneId || created[0].pane_id || session) : session;
|
|
595
659
|
const split = await tmuxRun(tmuxBin, ['split-window', '-t', splitTarget, direction, '-d', '-P', '-F', '#{pane_id}', '-c', path.resolve(pane.cwd || root), pane.command || 'pwd']);
|
|
596
660
|
if (split.code !== 0) return { ok: false, session, panes: created, stderr: split.stderr || split.stdout || 'tmux split-window failed' };
|
|
597
661
|
const newPaneId = paneId(split.stdout);
|
|
662
|
+
if (newPaneId && !(await tmuxPaneExists(tmuxBin, newPaneId))) return { ok: false, session, panes: created, stderr: `tmux split-window returned pane ${newPaneId}, but the pane was not present after creation` };
|
|
598
663
|
created.push({ pane_id: newPaneId, role: pane.role || 'lane', title: pane.title || null });
|
|
599
|
-
if (rightSidePanes && newPaneId)
|
|
600
|
-
await tmuxRun(tmuxBin, ['select-layout', '-t', session, layout]).catch(() => null);
|
|
664
|
+
if (rightSidePanes && !rightStackRootPaneId && newPaneId) rightStackRootPaneId = newPaneId;
|
|
665
|
+
if (!rightSidePanes) await tmuxRun(tmuxBin, ['select-layout', '-t', session, layout]).catch(() => null);
|
|
601
666
|
}
|
|
602
|
-
const
|
|
603
|
-
|
|
667
|
+
const stable_layout = rightSidePanes ? await applyStableTeamLayout(tmuxBin, session, created[0].pane_id, { layout }) : null;
|
|
668
|
+
const dynamic_resize = await enableTmuxDynamicResize(tmuxBin, session, { layout, stableTeamLayout: rightSidePanes });
|
|
669
|
+
return { ok: true, reused: false, session, panes: created, attach_command: `tmux attach-session -t ${session}`, layout, initial_size: dimensions, stable_layout, dynamic_resize };
|
|
604
670
|
}
|
|
605
671
|
|
|
606
672
|
export async function launchTmuxUi(args = [], opts = {}) {
|
|
@@ -759,18 +825,23 @@ export async function reconcileTmuxTeamCockpit({ root, missionId, plan = {}, pro
|
|
|
759
825
|
}
|
|
760
826
|
}
|
|
761
827
|
const remainingManaged = managed.filter((pane) => desiredAgents.has(pane.agent) && !closed.some((entry) => entry.pane_id === pane.pane_id));
|
|
762
|
-
let
|
|
828
|
+
let rightStackRootPaneId = remainingManaged[0]?.pane_id || null;
|
|
763
829
|
for (const lane of lanes) {
|
|
764
830
|
if (byAgent.has(lane.agent)) continue;
|
|
765
831
|
const firstRightPane = remainingManaged.length === 0 && opened.length === 0;
|
|
766
832
|
const direction = firstRightPane ? '-h' : '-v';
|
|
767
|
-
const
|
|
833
|
+
const splitTarget = firstRightPane ? (mainPaneId || target.window_id) : (rightStackRootPaneId || mainPaneId || target.window_id);
|
|
834
|
+
const split = await tmuxRun(tmuxBin, ['split-window', direction, '-t', splitTarget, '-d', '-P', '-F', '#{pane_id}', '-c', resolvedRoot, lane.command || 'pwd'], { timeoutMs: 5000, maxOutputBytes: 4096 });
|
|
768
835
|
const pane_id = paneId(split.stdout);
|
|
769
836
|
if (split.code !== 0 || !pane_id) {
|
|
770
837
|
failed.push({ action: 'split-window', agent: lane.agent, role: lane.role, stderr: split.stderr || split.stdout || 'tmux split-window failed' });
|
|
771
838
|
continue;
|
|
772
839
|
}
|
|
773
|
-
|
|
840
|
+
if (!(await tmuxPaneExists(tmuxBin, pane_id))) {
|
|
841
|
+
failed.push({ action: 'verify-pane', pane_id, agent: lane.agent, role: lane.role, stderr: 'tmux split-window returned a pane id, but the pane was not present after creation' });
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
if (!rightStackRootPaneId) rightStackRootPaneId = pane_id;
|
|
774
845
|
const optionResult = await setTmuxPaneUserOptions(tmuxBin, pane_id, {
|
|
775
846
|
'@sks_team_managed': '1',
|
|
776
847
|
'@sks_mission_id': id,
|
|
@@ -782,10 +853,7 @@ export async function reconcileTmuxTeamCockpit({ root, missionId, plan = {}, pro
|
|
|
782
853
|
}
|
|
783
854
|
let relayout = null;
|
|
784
855
|
if (opened.length || closed.length) {
|
|
785
|
-
|
|
786
|
-
const tiled = await tmuxRun(tmuxBin, ['select-layout', '-t', target.window_id, DYNAMIC_TEAM_TMUX_LAYOUT], { timeoutMs: 5000 });
|
|
787
|
-
const even = await tmuxRun(tmuxBin, ['select-layout', '-t', target.window_id, '-E'], { timeoutMs: 5000 });
|
|
788
|
-
relayout = { ok: selectedMain.code === 0 && tiled.code === 0 && even.code === 0, selected_main: selectedMain.code, layout: tiled.code, even: even.code, layout_name: DYNAMIC_TEAM_TMUX_LAYOUT };
|
|
856
|
+
relayout = await applyStableTeamLayout(tmuxBin, target.window_id, mainPaneId, { layout: DYNAMIC_TEAM_TMUX_LAYOUT });
|
|
789
857
|
}
|
|
790
858
|
const nextPanes = [
|
|
791
859
|
...managed.filter((pane) => desiredAgents.has(pane.agent) && !closed.some((entry) => entry.pane_id === pane.pane_id)),
|
|
@@ -1026,7 +1094,7 @@ export async function cleanupTmuxTeamView({ root, missionId = 'latest', closeSes
|
|
|
1026
1094
|
};
|
|
1027
1095
|
}
|
|
1028
1096
|
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' }));
|
|
1029
|
-
const recordedCleanup = dynamicCleanup?.ok
|
|
1097
|
+
const recordedCleanup = dynamicCleanup?.ok && dynamicCleanup.closed_lane_count > 0
|
|
1030
1098
|
? null
|
|
1031
1099
|
: await cleanupRecordedTmuxTeamPanes(resolvedRoot, record.mission_id || missionId, record).catch((err) => ({ ok: false, skipped: true, reason: err.message || 'recorded tmux cleanup failed' }));
|
|
1032
1100
|
const legacyCleanup = (dynamicCleanup?.closed_lane_count || recordedCleanup?.closed_lane_count)
|
|
@@ -1130,10 +1198,16 @@ async function cleanupRecordedTmuxTeamPanes(root, missionId, record = {}) {
|
|
|
1130
1198
|
const tmuxBin = await findTmuxBin() || 'tmux';
|
|
1131
1199
|
const paneList = await listTmuxWindowPanes(tmuxBin, target);
|
|
1132
1200
|
if (!paneList.ok) return { ok: false, skipped: true, reason: paneList.stderr, closed_lane_count: 0 };
|
|
1201
|
+
const recordedPaneIds = new Set([
|
|
1202
|
+
...(Array.isArray(record.panes) ? record.panes : []),
|
|
1203
|
+
...(Array.isArray(cockpit.panes) ? cockpit.panes : [])
|
|
1204
|
+
].map((pane) => pane?.pane_id).filter(Boolean));
|
|
1133
1205
|
const managed = paneList.panes.filter((pane) => pane.managed && pane.mission_id === id);
|
|
1206
|
+
const recorded = paneList.panes.filter((pane) => recordedPaneIds.has(pane.pane_id));
|
|
1207
|
+
const targets = managed.length ? managed : recorded;
|
|
1134
1208
|
const closed = [];
|
|
1135
1209
|
const failed = [];
|
|
1136
|
-
for (const pane of
|
|
1210
|
+
for (const pane of targets) {
|
|
1137
1211
|
const kill = await tmuxRun(tmuxBin, ['kill-pane', '-t', pane.pane_id], { timeoutMs: 5000 });
|
|
1138
1212
|
if (kill.code === 0) closed.push({ pane_id: pane.pane_id, agent: pane.agent, role: pane.role });
|
|
1139
1213
|
else failed.push({ pane_id: pane.pane_id, agent: pane.agent, stderr: kill.stderr || kill.stdout || 'tmux kill-pane failed' });
|
|
@@ -1148,9 +1222,12 @@ async function cleanupRecordedTmuxTeamPanes(root, missionId, record = {}) {
|
|
|
1148
1222
|
session: cockpit.session || record.session,
|
|
1149
1223
|
window_id: cockpit.window_id || record.window_id || null,
|
|
1150
1224
|
closed_lane_count: closed.length,
|
|
1225
|
+
fallback_used: !managed.length && recorded.length > 0,
|
|
1151
1226
|
closed,
|
|
1152
1227
|
failed,
|
|
1153
|
-
reason: closed.length
|
|
1228
|
+
reason: closed.length
|
|
1229
|
+
? (managed.length ? 'closed recorded managed panes' : 'closed panes by recorded SKS pane ids')
|
|
1230
|
+
: 'no recorded managed panes found'
|
|
1154
1231
|
};
|
|
1155
1232
|
}
|
|
1156
1233
|
|