sneakoscope 0.9.9 → 0.9.11

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.9.9",
4
+ "version": "0.9.11",
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",
@@ -3,12 +3,13 @@ import os from 'node:os';
3
3
  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
- import { ensureDir, exists, globalSksRoot, packageRoot, readText, runProcess, tmpdir, which, writeTextAtomic } from '../core/fsx.mjs';
6
+ import { ensureDir, exists, globalSksRoot, packageRoot, PACKAGE_VERSION, readText, runProcess, tmpdir, which, writeTextAtomic } from '../core/fsx.mjs';
7
7
  import { getCodexInfo } from '../core/codex-adapter.mjs';
8
8
  import { formatHarnessConflictReport, llmHarnessCleanupPrompt, scanHarnessConflicts } from '../core/harness-conflicts.mjs';
9
9
  import { initProject, installSkills } from '../core/init.mjs';
10
10
  import { context7ConfigToml, DOLLAR_SKILL_NAMES, GETDESIGN_REFERENCE, hasContext7ConfigText, RECOMMENDED_SKILLS } from '../core/routes.mjs';
11
11
  import { codexLaunchCommand, platformTmuxInstallHint, tmuxReadiness } from '../core/tmux-ui.mjs';
12
+ import { reconcileCodexAppUpgradeProcesses } from '../core/codex-app.mjs';
12
13
 
13
14
  const DEFAULT_CODEX_APP_PLUGINS = [
14
15
  ['browser', 'openai-bundled'],
@@ -31,6 +32,7 @@ export async function postinstall({ bootstrap }) {
31
32
  console.log('\nSKS installed.');
32
33
  const shim = await ensureSksCommandDuringInstall();
33
34
  if (shim.status === 'present') console.log(`SKS command: available (${shim.command}).`);
35
+ else if (shim.status === 'repaired') console.log(`SKS command: stale PATH shim repaired (${shim.command}).`);
34
36
  else if (shim.status === 'created') console.log(`SKS command: shim created at ${shim.command}.`);
35
37
  else if (shim.status === 'created_not_on_path') console.log(`SKS command: shim created at ${shim.command}. Add ${path.dirname(shim.command)} to PATH, or run npx -y -p sneakoscope sks.`);
36
38
  else if (shim.status === 'skipped') console.log(`SKS command: skipped (${shim.reason}).`);
@@ -46,6 +48,11 @@ export async function postinstall({ bootstrap }) {
46
48
  else if (fastModeRepair.status === 'present') console.log('Codex App Fast mode: config already compatible.');
47
49
  else if (fastModeRepair.status === 'skipped') console.log(`Codex App Fast mode: skipped (${fastModeRepair.reason}).`);
48
50
  else if (fastModeRepair.status === 'failed') console.log(`Codex App Fast mode: auto repair failed. Run \`sks setup\`. ${fastModeRepair.error || ''}`.trim());
51
+ const appProcessRepair = await reconcileCodexAppUpgradeProcesses();
52
+ if (appProcessRepair.status === 'repaired') console.log(`Codex App reconnect repair: stopped ${appProcessRepair.killed.length} stale orphan app-server process(es). Restart Codex App to reconnect cleanly.`);
53
+ else if (appProcessRepair.status === 'partial') console.log(`Codex App reconnect repair: stopped ${appProcessRepair.killed.length} stale orphan app-server process(es); ${appProcessRepair.failed.length} could not be stopped. Restart Codex App if reconnecting continues.`);
54
+ else if (appProcessRepair.status === 'skipped' && appProcessRepair.reason !== 'platform') console.log(`Codex App reconnect repair: skipped (${appProcessRepair.reason}).`);
55
+ else if (appProcessRepair.status === 'failed') console.log(`Codex App reconnect repair: skipped (${appProcessRepair.error || appProcessRepair.reason || 'process check failed'}).`);
49
56
  const globalSkills = await ensureGlobalCodexSkillsDuringInstall();
50
57
  if (globalSkills.status === 'installed') {
51
58
  const removed = globalSkills.removed_stale_generated_skills || [];
@@ -1361,10 +1368,13 @@ function escapeRegExp(value) {
1361
1368
  export async function ensureSksCommandDuringInstall(opts = {}) {
1362
1369
  if (process.env.SKS_SKIP_POSTINSTALL_SHIM === '1' && !opts.force) return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_SHIM=1' };
1363
1370
  const pathEnv = opts.pathEnv ?? process.env.PATH ?? '';
1364
- const existing = await findCommandOnPath('sks', pathEnv);
1365
- if (isStableSksBin(existing)) return { status: 'present', command: existing };
1366
1371
  const nodeBin = opts.nodeBin || process.execPath;
1367
1372
  const target = opts.target || path.join(packageRoot(), 'bin', 'sks.mjs');
1373
+ const repair = await reconcileSksPathShimsDuringInstall({ ...opts, pathEnv, nodeBin, target });
1374
+ if (repair.status === 'repaired') return { ...repair, command: repair.command || repair.repaired?.[0]?.path || target };
1375
+ if (repair.status === 'failed') return repair;
1376
+ const existing = await findCommandOnPath('sks', pathEnv);
1377
+ if (isStableSksBin(existing)) return { status: 'present', command: existing };
1368
1378
  const dirs = candidateShimDirs(pathEnv, opts.home || process.env.HOME);
1369
1379
  const script = process.platform === 'win32'
1370
1380
  ? `@echo off\r\n"${nodeBin}" "${target}" %*\r\n`
@@ -1388,6 +1398,80 @@ export async function ensureSksCommandDuringInstall(opts = {}) {
1388
1398
  return { status: 'failed', error: lastError };
1389
1399
  }
1390
1400
 
1401
+ export async function selftestSksShimRepair() {
1402
+ const staleShimTmp = tmpdir();
1403
+ const staleBin = path.join(staleShimTmp, 'old-prefix', 'bin');
1404
+ const stalePkg = path.join(staleShimTmp, 'old-prefix', 'lib', 'node_modules', 'sneakoscope');
1405
+ await ensureDir(path.join(stalePkg, 'bin'));
1406
+ await ensureDir(staleBin);
1407
+ await writeTextAtomic(path.join(stalePkg, 'package.json'), JSON.stringify({ name: 'sneakoscope', version: '0.0.1' }, null, 2));
1408
+ await writeTextAtomic(path.join(stalePkg, 'bin', 'sks.mjs'), '#!/usr/bin/env node\nconsole.log("sneakoscope 0.0.1");\n');
1409
+ await fsp.chmod(path.join(stalePkg, 'bin', 'sks.mjs'), 0o755).catch(() => {});
1410
+ await fsp.symlink(path.join(stalePkg, 'bin', 'sks.mjs'), path.join(staleBin, 'sks'));
1411
+ const repair = await ensureSksCommandDuringInstall({ force: true, pathEnv: staleBin, home: path.join(staleShimTmp, 'home') });
1412
+ if (repair.status !== 'repaired') throw new Error(`selftest: stale global sks shim was not repaired (${repair.status})`);
1413
+ const run = await runProcess(path.join(staleBin, 'sks'), ['--version'], { timeoutMs: 10000, maxOutputBytes: 16 * 1024 });
1414
+ if (run.code !== 0 || !String(run.stdout || '').includes(PACKAGE_VERSION)) throw new Error('selftest: repaired stale sks shim does not run current package version');
1415
+ return { ok: true, repaired: repair.repaired || [] };
1416
+ }
1417
+
1418
+ async function reconcileSksPathShimsDuringInstall(opts = {}) {
1419
+ if (process.env.SKS_SKIP_POSTINSTALL_SHIM_REPAIR === '1' && !opts.force) return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_SHIM_REPAIR=1' };
1420
+ const target = opts.target || path.join(packageRoot(), 'bin', 'sks.mjs');
1421
+ const nodeBin = opts.nodeBin || process.execPath;
1422
+ const currentVersion = await installedPackageVersion(packageRoot());
1423
+ const commands = await findCommandsOnPath(['sks', 'sneakoscope'], opts.pathEnv ?? process.env.PATH ?? '');
1424
+ const repaired = [];
1425
+ const failed = [];
1426
+ for (const command of commands) {
1427
+ const info = await inspectSksPathShim(command.path, { target, currentVersion });
1428
+ if (!info.repairable) continue;
1429
+ const script = process.platform === 'win32'
1430
+ ? `@echo off\r\n"${nodeBin}" "${target}" %*\r\n`
1431
+ : `#!/bin/sh\nexec "${nodeBin}" "${target}" "$@"\n`;
1432
+ try {
1433
+ await writeTextAtomic(command.path, script);
1434
+ if (process.platform !== 'win32') await fsp.chmod(command.path, 0o755).catch(() => {});
1435
+ repaired.push({ path: command.path, name: command.name, previous_version: info.version || null, target });
1436
+ } catch (err) {
1437
+ failed.push({ path: command.path, name: command.name, previous_version: info.version || null, error: err.message });
1438
+ }
1439
+ }
1440
+ if (repaired.length) return { status: 'repaired', command: repaired[0].path, repaired, failed };
1441
+ if (failed.length) return { status: 'failed', error: failed.map((entry) => `${entry.path}: ${entry.error}`).join('; '), failed };
1442
+ return { status: 'present' };
1443
+ }
1444
+
1445
+ async function inspectSksPathShim(candidate, opts = {}) {
1446
+ if (!candidate || isTransientNpmBinPath(candidate)) return { repairable: false, reason: 'transient_or_missing' };
1447
+ const target = path.resolve(opts.target || path.join(packageRoot(), 'bin', 'sks.mjs'));
1448
+ const resolved = await fsp.realpath(candidate).catch(() => candidate);
1449
+ if (path.resolve(resolved) === target) return { repairable: false, reason: 'current_target' };
1450
+ const packageDir = sksPackageRootForBin(resolved) || sksPackageRootForBin(candidate);
1451
+ if (!packageDir) return { repairable: false, reason: 'not_sneakoscope_bin' };
1452
+ const version = await installedPackageVersion(packageDir);
1453
+ const currentVersion = opts.currentVersion || await installedPackageVersion(packageRoot());
1454
+ if (!version || !currentVersion || compareVersions(version, currentVersion) >= 0) return { repairable: false, reason: 'not_older', version, current_version: currentVersion };
1455
+ return { repairable: true, version, current_version: currentVersion, package_dir: packageDir, resolved };
1456
+ }
1457
+
1458
+ function sksPackageRootForBin(file) {
1459
+ const normalized = String(file || '').split(path.sep).join('/');
1460
+ const marker = '/node_modules/sneakoscope/bin/';
1461
+ const idx = normalized.lastIndexOf(marker);
1462
+ if (idx < 0) return null;
1463
+ return normalized.slice(0, idx + '/node_modules/sneakoscope'.length).split('/').join(path.sep);
1464
+ }
1465
+
1466
+ async function installedPackageVersion(root) {
1467
+ const pkg = await readJsonMaybe(path.join(root, 'package.json'));
1468
+ return pkg?.version || (root === packageRoot() ? PACKAGE_VERSION : null);
1469
+ }
1470
+
1471
+ async function readJsonMaybe(file) {
1472
+ try { return JSON.parse(await fsp.readFile(file, 'utf8')); } catch { return null; }
1473
+ }
1474
+
1391
1475
  function candidateShimDirs(pathEnv, home) {
1392
1476
  const seen = new Set();
1393
1477
  const out = [];
@@ -1407,14 +1491,26 @@ function candidateShimDirs(pathEnv, home) {
1407
1491
  }
1408
1492
 
1409
1493
  async function findCommandOnPath(name, pathEnv) {
1494
+ const found = await findCommandsOnPath([name], pathEnv);
1495
+ return found[0]?.path || null;
1496
+ }
1497
+
1498
+ async function findCommandsOnPath(names, pathEnv) {
1410
1499
  const suffixes = process.platform === 'win32' ? ['.cmd', '.exe', ''] : [''];
1500
+ const out = [];
1501
+ const seen = new Set();
1411
1502
  for (const dir of String(pathEnv || '').split(path.delimiter).filter(Boolean)) {
1412
- for (const suffix of suffixes) {
1413
- const candidate = path.join(dir, `${name}${suffix}`);
1414
- if (await exists(candidate)) return candidate;
1503
+ for (const name of names) {
1504
+ for (const suffix of suffixes) {
1505
+ const candidate = path.join(dir, `${name}${suffix}`);
1506
+ const key = path.resolve(candidate);
1507
+ if (seen.has(key) || !await exists(candidate)) continue;
1508
+ seen.add(key);
1509
+ out.push({ name, path: candidate });
1510
+ }
1415
1511
  }
1416
1512
  }
1417
- return null;
1513
+ return out;
1418
1514
  }
1419
1515
 
1420
1516
  async function ensureGlobalContext7DuringInstall() {
@@ -1766,6 +1862,7 @@ function codexLbPostinstallEnv(baseEnv, overrides = {}) {
1766
1862
  SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
1767
1863
  SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0',
1768
1864
  SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1',
1865
+ SKS_SKIP_CODEX_APP_UPGRADE_REPAIR: '1',
1769
1866
  ...overrides
1770
1867
  };
1771
1868
  }
@@ -1838,7 +1935,7 @@ export async function selftestCodexLb(tmp) {
1838
1935
  const codexLbPostinstallAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1839
1936
  const codexLbLoginCallsAfterPostinstall = await codexLbLoginCallCount(codexLbHome);
1840
1937
  if (!String(codexLbPostinstall.stdout || '').includes('codex-lb auth: preserved') || !codexLbPostinstallAuth.includes('"auth_mode":"browser"') || codexLbPostinstallAuth.includes('sk-test') || codexLbLoginCallsAfterPostinstall !== codexLbLoginCallsBeforePostinstall) throw new Error('selftest: postinstall auth');
1841
- const postinstallEnvKeys = ['HOME', 'PATH', 'INIT_CWD', 'SKS_GLOBAL_ROOT', 'SKS_POSTINSTALL_BOOTSTRAP', 'SKS_POSTINSTALL_NO_BOOTSTRAP', 'SKS_SKIP_POSTINSTALL_SHIM', 'SKS_SKIP_POSTINSTALL_CONTEXT7', 'SKS_SKIP_POSTINSTALL_GETDESIGN', 'SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS', 'SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH', 'SKS_SKIP_CODEX_LB_LAUNCH_ENV', 'SKS_CODEX_LB_SYNC_CODEX_LOGIN'];
1938
+ const postinstallEnvKeys = ['HOME', 'PATH', 'INIT_CWD', 'SKS_GLOBAL_ROOT', 'SKS_POSTINSTALL_BOOTSTRAP', 'SKS_POSTINSTALL_NO_BOOTSTRAP', 'SKS_SKIP_POSTINSTALL_SHIM', 'SKS_SKIP_POSTINSTALL_CONTEXT7', 'SKS_SKIP_POSTINSTALL_GETDESIGN', 'SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS', 'SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH', 'SKS_SKIP_CODEX_LB_LAUNCH_ENV', 'SKS_SKIP_CODEX_APP_UPGRADE_REPAIR', 'SKS_CODEX_LB_SYNC_CODEX_LOGIN'];
1842
1939
  const postinstallEnvBefore = Object.fromEntries(postinstallEnvKeys.map((key) => [key, process.env[key]]));
1843
1940
  const codexLbLoginCallsBeforeBootstrap = await codexLbLoginCallCount(codexLbHome);
1844
1941
  try {
@@ -1854,7 +1951,8 @@ export async function selftestCodexLb(tmp) {
1854
1951
  SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
1855
1952
  SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
1856
1953
  SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0',
1857
- SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1'
1954
+ SKS_SKIP_CODEX_LB_LAUNCH_ENV: '1',
1955
+ SKS_SKIP_CODEX_APP_UPGRADE_REPAIR: '1'
1858
1956
  });
1859
1957
  await postinstall({
1860
1958
  bootstrap: async () => {
package/src/cli/main.mjs CHANGED
@@ -73,7 +73,7 @@ import { MISTAKE_RECALL_ARTIFACT, contractConsumesMistakeRecall } from '../core/
73
73
  import { buildPromptContext } from '../core/prompt-context-builder.mjs';
74
74
  import { renderTeamDashboardState, writeTeamDashboardState } from '../core/team-dashboard-renderer.mjs';
75
75
  import { GOAL_WORKFLOW_ARTIFACT } from '../core/goal-workflow.mjs';
76
- import { CODEX_APP_DOCS_URL, codexAccessTokenStatus, codexAppIntegrationStatus, formatCodexAppStatus } from '../core/codex-app.mjs';
76
+ import { CODEX_APP_DOCS_URL, codexAccessTokenStatus, codexAppIntegrationStatus, findCodexAppUpgradeRepairTargets, formatCodexAppStatus, parseProcessRows } from '../core/codex-app.mjs';
77
77
  import { buildAllFeaturesSelftest, buildFeatureRegistry, validateFeatureRegistry } from '../core/feature-registry.mjs';
78
78
  import { codexAppRemoteControlCommand } from './codex-app-command.mjs';
79
79
  import { allFeaturesCommand, featuresCommand, hooksCommand, hooksExplainReport } from './feature-commands.mjs';
@@ -81,7 +81,7 @@ import { OPENCLAW_SKILL_NAME, installOpenClawSkill } from '../core/openclaw.mjs'
81
81
  import { buildTmuxLaunchPlan, buildTmuxOpenArgs, codexLaunchCommand, createTmuxSession, defaultCodexLaunchArgs, isTmuxShellSession, runTmuxLaunchPlanSyntaxCheck, shouldAutoAttachTmux, sksAsciiLogo, tmuxReadiness, tmuxStatusKind, defaultTmuxSessionName, formatTmuxBanner, launchMadTmuxUi, launchTmuxTeamView, launchTmuxUi, platformTmuxInstallHint, reconcileTmuxTeamCockpit, runTmuxStatus, sanitizeTmuxSessionName, sweepCodexLbTmuxSessions, sweepTmuxTeamSurfaces, teamLaneStyle } from '../core/tmux-ui.mjs';
82
82
  import { autoReviewProfileName, autoReviewStatus, autoReviewSummary, enableAutoReview, disableAutoReview, enableMadHighProfile, madHighProfileName } from '../core/auto-review.mjs';
83
83
  import { context7Command } from './context7-command.mjs';
84
- import { askPostinstallQuestion, checkCodexLbResponseChain, checkContext7, checkRequiredSkills, codexLbChatgptBackupPath, codexLbStatus, configureCodexLb, ensureCodexCliTool, ensureGlobalCodexFastModeDuringInstall, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, formatCodexLbRepairResultText, formatCodexLbStatusText, globalCodexSkillsRoot, maybePromptCodexLbSetupForLaunch, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, releaseCodexLbAuthHold, repairCodexLbAuth, selftestCodexLb, shouldAutoApproveInstall, unselectCodexLbProvider } from './install-helpers.mjs';
84
+ import { askPostinstallQuestion, checkCodexLbResponseChain, checkContext7, checkRequiredSkills, codexLbChatgptBackupPath, codexLbStatus, configureCodexLb, ensureCodexCliTool, ensureGlobalCodexFastModeDuringInstall, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, formatCodexLbRepairResultText, formatCodexLbStatusText, globalCodexSkillsRoot, maybePromptCodexLbSetupForLaunch, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, releaseCodexLbAuthHold, repairCodexLbAuth, selftestCodexLb, selftestSksShimRepair, shouldAutoApproveInstall, unselectCodexLbProvider } from './install-helpers.mjs';
85
85
  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';
86
86
  import { openClawCommand } from './openclaw-command.mjs';
87
87
  import { recallPulseCommand } from './recallpulse-command.mjs';
@@ -2289,11 +2289,11 @@ async function selftest() {
2289
2289
  await ensureDir(path.join(conflictTmp, '.omx'));
2290
2290
  const conflictScan = await scanHarnessConflicts(conflictTmp, { home: path.join(conflictTmp, 'home') });
2291
2291
  if (!conflictScan.hard_block || !formatHarnessConflictReport(conflictScan).includes('GPT-5.5')) throw new Error('selftest: OMX conflict did not block with cleanup prompt');
2292
- const postinstallConflict = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: conflictTmp, env: { INIT_CWD: conflictTmp, HOME: path.join(conflictTmp, 'home'), SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1', SKS_SKIP_POSTINSTALL_GETDESIGN: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
2292
+ const postinstallConflict = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: conflictTmp, env: { INIT_CWD: conflictTmp, HOME: path.join(conflictTmp, 'home'), SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1', SKS_SKIP_POSTINSTALL_GETDESIGN: '1', SKS_SKIP_CODEX_APP_UPGRADE_REPAIR: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
2293
2293
  if (postinstallConflict.code !== 0) throw new Error('selftest: postinstall conflict notice should not make npm install fail');
2294
2294
  const postinstallConflictOutput = String(`${postinstallConflict.stdout}\n${postinstallConflict.stderr}`);
2295
2295
  if (!postinstallConflictOutput.includes('SKS setup is blocked') || postinstallConflictOutput.includes('Cleanup prompt:')) throw new Error('selftest: postinstall conflict notice did not stay informational');
2296
- const postinstallConflictPrompt = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: conflictTmp, input: 'y\n', env: { INIT_CWD: conflictTmp, HOME: path.join(conflictTmp, 'home'), SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1', SKS_SKIP_POSTINSTALL_GETDESIGN: '1', SKS_POSTINSTALL_PROMPT: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
2296
+ const postinstallConflictPrompt = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: conflictTmp, input: 'y\n', env: { INIT_CWD: conflictTmp, HOME: path.join(conflictTmp, 'home'), SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1', SKS_SKIP_POSTINSTALL_GETDESIGN: '1', SKS_SKIP_CODEX_APP_UPGRADE_REPAIR: '1', SKS_POSTINSTALL_PROMPT: '1' }, timeoutMs: 15000, maxOutputBytes: 128 * 1024 });
2297
2297
  if (postinstallConflictPrompt.code !== 0 || !String(postinstallConflictPrompt.stdout || '').includes('Goal: completely remove the conflicting Codex harnesses')) throw new Error('selftest: conflict prompt');
2298
2298
  const postinstallSetupTmp = tmpdir();
2299
2299
  await writeJsonAtomic(path.join(postinstallSetupTmp, 'package.json'), { name: 'postinstall-setup-smoke', version: '0.0.0' });
@@ -2303,7 +2303,7 @@ async function selftest() {
2303
2303
  await ensureDir(path.join(postinstallSetupHome, '.agents', 'skills', name));
2304
2304
  await writeTextAtomic(path.join(postinstallSetupHome, '.agents', 'skills', name, 'SKILL.md'), stalePluginSkillContent(name));
2305
2305
  }
2306
- const postinstallSetup = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: postinstallSetupTmp, env: { INIT_CWD: postinstallSetupTmp, HOME: path.join(postinstallSetupTmp, 'home'), SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1', SKS_SKIP_POSTINSTALL_GETDESIGN: '1', SKS_SKIP_CLI_TOOLS: '1' }, timeoutMs: 30000, maxOutputBytes: 256 * 1024 });
2306
+ const postinstallSetup = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: postinstallSetupTmp, env: { INIT_CWD: postinstallSetupTmp, HOME: path.join(postinstallSetupTmp, 'home'), SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1', SKS_SKIP_POSTINSTALL_GETDESIGN: '1', SKS_SKIP_CODEX_APP_UPGRADE_REPAIR: '1', SKS_SKIP_CLI_TOOLS: '1' }, timeoutMs: 30000, maxOutputBytes: 256 * 1024 });
2307
2307
  if (postinstallSetup.code !== 0) throw new Error(`selftest: postinstall setup exited ${postinstallSetup.code}: ${postinstallSetup.stderr}`);
2308
2308
  if (await exists(path.join(postinstallSetupTmp, '.agents', 'skills', 'agent-team', 'SKILL.md'))) throw new Error('selftest: postinstall installed deprecated agent-team fallback skill');
2309
2309
  if (!String(postinstallSetup.stdout || '').includes('SKS bootstrap: auto-running sks setup --bootstrap --install-scope global --force') || !String(postinstallSetup.stdout || '').includes('SKS Ready')) throw new Error('selftest: postinstall bootstrap');
@@ -2330,6 +2330,7 @@ async function selftest() {
2330
2330
  if (await exists(path.join(postinstallSetupHome, '.agents', 'skills', name, 'SKILL.md'))) throw new Error(`selftest: postinstall global skills shadow the first-party ${name} plugin`);
2331
2331
  }
2332
2332
  if (!(await exists(path.join(postinstallSetupTmp, 'home', '.agents', 'skills', 'getdesign-reference', 'SKILL.md')))) throw new Error('selftest: postinstall global getdesign-reference skill not installed');
2333
+ await selftestSksShimRepair();
2333
2334
  const oldNoBootstrap = process.env.SKS_POSTINSTALL_NO_BOOTSTRAP;
2334
2335
  process.env.SKS_POSTINSTALL_NO_BOOTSTRAP = '1';
2335
2336
  const noBootstrapDecision = await postinstallBootstrapDecision(postinstallSetupTmp);
@@ -2341,7 +2342,7 @@ async function selftest() {
2341
2342
  const postinstallNoMarkerCwd = path.join(postinstallNoMarkerTmp, 'cwd');
2342
2343
  const postinstallNoMarkerGlobalRoot = path.join(postinstallNoMarkerTmp, 'global-root');
2343
2344
  await ensureDir(postinstallNoMarkerCwd);
2344
- const postinstallNoMarker = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: postinstallNoMarkerCwd, env: { INIT_CWD: postinstallNoMarkerCwd, HOME: postinstallNoMarkerHome, SKS_GLOBAL_ROOT: postinstallNoMarkerGlobalRoot, SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1', SKS_SKIP_POSTINSTALL_GETDESIGN: '1', SKS_SKIP_CLI_TOOLS: '1' }, timeoutMs: 30000, maxOutputBytes: 256 * 1024 });
2345
+ const postinstallNoMarker = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], { cwd: postinstallNoMarkerCwd, env: { INIT_CWD: postinstallNoMarkerCwd, HOME: postinstallNoMarkerHome, SKS_GLOBAL_ROOT: postinstallNoMarkerGlobalRoot, SKS_SKIP_POSTINSTALL_SHIM: '1', SKS_SKIP_POSTINSTALL_CONTEXT7: '1', SKS_SKIP_POSTINSTALL_GETDESIGN: '1', SKS_SKIP_CODEX_APP_UPGRADE_REPAIR: '1', SKS_SKIP_CLI_TOOLS: '1' }, timeoutMs: 30000, maxOutputBytes: 256 * 1024 });
2345
2346
  if (postinstallNoMarker.code !== 0) throw new Error(`selftest: no-marker postinstall bootstrap exited ${postinstallNoMarker.code}: ${postinstallNoMarker.stderr}`);
2346
2347
  if (!String(postinstallNoMarker.stdout || '').includes('no project marker found; auto-running global SKS runtime bootstrap')) throw new Error('selftest: no-marker bootstrap');
2347
2348
  if (!(await exists(path.join(postinstallNoMarkerGlobalRoot, '.sneakoscope', 'manifest.json')))) throw new Error('selftest: no-marker postinstall did not bootstrap global runtime root');
@@ -3182,6 +3183,15 @@ async function selftest() {
3182
3183
  const codexAppFixtureOpts = { codex: { bin: fakeCodex, version: 'codex-cli 99.0.0' }, home: appFeatureTmp, cwd: appFeatureTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } };
3183
3184
  const codexAppFeatureStatus = await codexAppIntegrationStatus(codexAppFixtureOpts);
3184
3185
  if (!codexAppFeatureStatus.ok || !codexAppFeatureStatus.features?.required_flags_ok || !codexAppFeatureStatus.features?.codex_git_commit || !codexAppFeatureStatus.features?.remote_control || !codexAppFeatureStatus.features?.git_actions?.ok || !codexAppFeatureStatus.features?.fast_mode_config?.ok) throw new Error('selftest: codex-app check did not accept required app feature flags, git actions, remote_control, and unlocked Fast UI config');
3186
+ const codexAppProcessRows = parseProcessRows([
3187
+ '101 1 /Applications/Codex.app/Contents/Resources/codex app-server --analytics-default-enabled',
3188
+ '200 1 /Applications/Codex.app/Contents/MacOS/Codex',
3189
+ '201 200 /Applications/Codex.app/Contents/Resources/codex app-server --analytics-default-enabled',
3190
+ '202 1 /Applications/Codex.app/Contents/Resources/codex app-server --listen stdio://',
3191
+ '203 1 /Users/me/.nvm/versions/node/bin/codex --model gpt-5.5'
3192
+ ].join('\n'));
3193
+ const codexAppRepairTargets = findCodexAppUpgradeRepairTargets(codexAppProcessRows);
3194
+ if (codexAppRepairTargets.length !== 1 || codexAppRepairTargets[0].pid !== 101) throw new Error('selftest: Codex App upgrade repair target selection is not limited to orphan desktop app-server processes');
3185
3195
  const codexAppOldCliStatus = await codexAppIntegrationStatus({ codex: { bin: fakeCodex, version: 'codex-cli 0.129.0' }, home: appFeatureTmp, cwd: appFeatureTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } });
3186
3196
  if (codexAppOldCliStatus.ok || codexAppOldCliStatus.features?.git_actions?.ok || !codexAppOldCliStatus.guidance.some((line) => line.includes('git commit/push actions are blocked'))) throw new Error('selftest: codex-app check did not block commit/push actions on old Codex CLI remote-control');
3187
3197
  const missingDefaultPluginTmp = tmpdir();
@@ -238,6 +238,65 @@ export function codexSupportsRemoteControl(versionText) {
238
238
  return Boolean(current && compareVersions(current, CODEX_REMOTE_CONTROL_MIN_VERSION) >= 0);
239
239
  }
240
240
 
241
+ export function parseProcessRows(text = '') {
242
+ return String(text || '')
243
+ .split(/\r?\n/)
244
+ .map((line) => line.trim())
245
+ .filter(Boolean)
246
+ .map((line) => {
247
+ const match = line.match(/^(\d+)\s+(\d+)\s+(.+)$/);
248
+ if (!match) return null;
249
+ return {
250
+ pid: Number.parseInt(match[1], 10),
251
+ ppid: Number.parseInt(match[2], 10),
252
+ command: match[3]
253
+ };
254
+ })
255
+ .filter((row) => Number.isFinite(row?.pid) && Number.isFinite(row?.ppid) && row.command);
256
+ }
257
+
258
+ export function findCodexAppUpgradeRepairTargets(rows = []) {
259
+ return rows.filter((row) => (
260
+ row?.ppid === 1
261
+ && /\/Codex\.app\/Contents\/Resources\/codex\s+app-server\s+--analytics-default-enabled(?:\s|$)/.test(String(row.command || ''))
262
+ ));
263
+ }
264
+
265
+ export async function reconcileCodexAppUpgradeProcesses(opts = {}) {
266
+ const platform = opts.platform || process.platform;
267
+ const env = opts.env || process.env;
268
+ if (platform !== 'darwin') return { status: 'skipped', reason: 'platform', killed: [] };
269
+ if (env.SKS_SKIP_CODEX_APP_UPGRADE_REPAIR === '1') return { status: 'skipped', reason: 'SKS_SKIP_CODEX_APP_UPGRADE_REPAIR=1', killed: [] };
270
+ const run = opts.runProcess || runProcess;
271
+ const ps = await run('ps', ['-axo', 'pid=', '-o', 'ppid=', '-o', 'command='], {
272
+ timeoutMs: opts.timeoutMs || 5000,
273
+ maxOutputBytes: opts.maxOutputBytes || 256 * 1024
274
+ }).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
275
+ if (ps.code !== 0) return { status: 'failed', reason: 'ps_failed', error: ps.stderr || ps.stdout || 'ps exited non-zero', killed: [] };
276
+ const rows = parseProcessRows(ps.stdout);
277
+ const targets = findCodexAppUpgradeRepairTargets(rows);
278
+ const killed = [];
279
+ const failed = [];
280
+ for (const target of targets) {
281
+ if (opts.dryRun) {
282
+ killed.push({ pid: target.pid, command: target.command, dry_run: true });
283
+ continue;
284
+ }
285
+ const kill = await run('kill', ['-TERM', String(target.pid)], {
286
+ timeoutMs: opts.timeoutMs || 5000,
287
+ maxOutputBytes: 8 * 1024
288
+ }).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
289
+ if (kill.code === 0) killed.push({ pid: target.pid, command: target.command });
290
+ else failed.push({ pid: target.pid, command: target.command, error: kill.stderr || kill.stdout || 'kill exited non-zero' });
291
+ }
292
+ return {
293
+ status: failed.length ? 'partial' : killed.length ? 'repaired' : 'clean',
294
+ killed,
295
+ failed,
296
+ checked: rows.length
297
+ };
298
+ }
299
+
241
300
  export function formatCodexRemoteControlStatus(status) {
242
301
  const lines = [
243
302
  'Codex remote-control',
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.9.9';
8
+ export const PACKAGE_VERSION = '0.9.11';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11