sneakoscope 0.7.72 → 0.7.74

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.7.72",
4
+ "version": "0.7.74",
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 = REQUIRED_GENERATED_CODEX_APP_FEATURE_FLAGS.filter((name) => teamPlanFeatureFlags[name] !== true);
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');
@@ -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('## Live Chat') || !teamLane.includes('selftest mapped repo slice') || teamLane.includes('## Global Tail')) throw new Error('selftest:cht');
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');
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.72';
8
+ export const PACKAGE_VERSION = '0.7.74';
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 '';
@@ -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 command = dollarCommand(prompt);
681
- const text = String(prompt || '');
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);
@@ -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
- `## Live Chat`,
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 `- system: ${event.raw}`;
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 `- ${ts}${marker}${to}${kind}: ${String(event.message || '').slice(0, 500)}${artifact}`;
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 = '') {
@@ -223,6 +223,15 @@ function colorizedLaneBannerCommand(lines = [], color = '') {
223
223
  return `printf '\\033[1;${code}m%s\\033[0m\\n' ${shellEscape(text)}`;
224
224
  }
225
225
 
226
+ function selfClosingTeamPaneCommand(command = '') {
227
+ return [
228
+ `SKS_TEAM_PANE_SELF_CLOSE=1 ${command}`,
229
+ 'status=$?',
230
+ 'if [ "${SKS_TEAM_PANE_SELF_CLOSE:-1}" = "1" ] && [ -n "${TMUX_PANE:-}" ]; then tmux kill-pane -t "$TMUX_PANE" >/dev/null 2>&1 || true; fi',
231
+ 'exit "$status"'
232
+ ].join('; ');
233
+ }
234
+
226
235
  function compactTeamPaneBanner({ missionId, agentId, phase, style, overview = false } = {}) {
227
236
  const role = overview ? 'overview' : `${style.label} (${style.color_name})`;
228
237
  return [
@@ -263,24 +272,26 @@ function teamLaneTitle(agentId = '') {
263
272
  export function teamAgentCommand(root, missionId, agentId, phase) {
264
273
  const style = teamLaneStyle(agentId);
265
274
  const title = teamLaneTitle(agentId);
275
+ const laneCommand = `node ${shellEscape(path.join(packageRoot(), 'bin', 'sks.mjs'))} team lane ${shellEscape(missionId)} --agent ${shellEscape(agentId)} --phase ${shellEscape(phase)} --follow --lines 12`;
266
276
  return [
267
277
  terminalTitleCommand(title),
268
278
  'clear',
269
279
  colorizedLaneBannerCommand(compactTeamPaneBanner({ missionId, agentId, phase, style }), style.color),
270
280
  `cd ${shellEscape(root)}`,
271
- `node ${shellEscape(path.join(packageRoot(), 'bin', 'sks.mjs'))} team lane ${shellEscape(missionId)} --agent ${shellEscape(agentId)} --phase ${shellEscape(phase)} --follow --lines 12`
281
+ selfClosingTeamPaneCommand(laneCommand)
272
282
  ].join('; ');
273
283
  }
274
284
 
275
285
  export function teamOverviewCommand(root, missionId) {
276
286
  const style = teamLaneStyle('mission_overview');
277
287
  const title = teamLaneTitle('mission_overview');
288
+ const watchCommand = `node ${shellEscape(path.join(packageRoot(), 'bin', 'sks.mjs'))} team watch ${shellEscape(missionId)} --follow --lines 18`;
278
289
  return [
279
290
  terminalTitleCommand(title),
280
291
  'clear',
281
292
  colorizedLaneBannerCommand(compactTeamPaneBanner({ missionId, agentId: 'mission_overview', style, overview: true }), style.color),
282
293
  `cd ${shellEscape(root)}`,
283
- `node ${shellEscape(path.join(packageRoot(), 'bin', 'sks.mjs'))} team watch ${shellEscape(missionId)} --follow --lines 18`
294
+ selfClosingTeamPaneCommand(watchCommand)
284
295
  ].join('; ');
285
296
  }
286
297
 
@@ -1026,7 +1037,7 @@ export async function cleanupTmuxTeamView({ root, missionId = 'latest', closeSes
1026
1037
  };
1027
1038
  }
1028
1039
  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
1040
+ const recordedCleanup = dynamicCleanup?.ok && dynamicCleanup.closed_lane_count > 0
1030
1041
  ? null
1031
1042
  : await cleanupRecordedTmuxTeamPanes(resolvedRoot, record.mission_id || missionId, record).catch((err) => ({ ok: false, skipped: true, reason: err.message || 'recorded tmux cleanup failed' }));
1032
1043
  const legacyCleanup = (dynamicCleanup?.closed_lane_count || recordedCleanup?.closed_lane_count)
@@ -1130,10 +1141,16 @@ async function cleanupRecordedTmuxTeamPanes(root, missionId, record = {}) {
1130
1141
  const tmuxBin = await findTmuxBin() || 'tmux';
1131
1142
  const paneList = await listTmuxWindowPanes(tmuxBin, target);
1132
1143
  if (!paneList.ok) return { ok: false, skipped: true, reason: paneList.stderr, closed_lane_count: 0 };
1144
+ const recordedPaneIds = new Set([
1145
+ ...(Array.isArray(record.panes) ? record.panes : []),
1146
+ ...(Array.isArray(cockpit.panes) ? cockpit.panes : [])
1147
+ ].map((pane) => pane?.pane_id).filter(Boolean));
1133
1148
  const managed = paneList.panes.filter((pane) => pane.managed && pane.mission_id === id);
1149
+ const recorded = paneList.panes.filter((pane) => recordedPaneIds.has(pane.pane_id));
1150
+ const targets = managed.length ? managed : recorded;
1134
1151
  const closed = [];
1135
1152
  const failed = [];
1136
- for (const pane of managed) {
1153
+ for (const pane of targets) {
1137
1154
  const kill = await tmuxRun(tmuxBin, ['kill-pane', '-t', pane.pane_id], { timeoutMs: 5000 });
1138
1155
  if (kill.code === 0) closed.push({ pane_id: pane.pane_id, agent: pane.agent, role: pane.role });
1139
1156
  else failed.push({ pane_id: pane.pane_id, agent: pane.agent, stderr: kill.stderr || kill.stdout || 'tmux kill-pane failed' });
@@ -1148,9 +1165,12 @@ async function cleanupRecordedTmuxTeamPanes(root, missionId, record = {}) {
1148
1165
  session: cockpit.session || record.session,
1149
1166
  window_id: cockpit.window_id || record.window_id || null,
1150
1167
  closed_lane_count: closed.length,
1168
+ fallback_used: !managed.length && recorded.length > 0,
1151
1169
  closed,
1152
1170
  failed,
1153
- reason: closed.length ? 'closed recorded managed panes' : 'no recorded managed panes found'
1171
+ reason: closed.length
1172
+ ? (managed.length ? 'closed recorded managed panes' : 'closed panes by recorded SKS pane ids')
1173
+ : 'no recorded managed panes found'
1154
1174
  };
1155
1175
  }
1156
1176