sneakoscope 0.7.75 → 0.7.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -150,11 +150,12 @@ For [codex-lb](https://github.com/Soju06/codex-lb), start the server, create a d
150
150
 
151
151
  ```sh
152
152
  sks codex-lb setup --host https://your-codex-lb.example.com --api-key "sk-clb-..."
153
+ sks codex-lb health
153
154
  sks codex-lb repair
154
155
  sks
155
156
  ```
156
157
 
157
- Bare `sks` can also prompt for codex-lb auth; SKS stores the base URL/key in `~/.codex/sks-codex-lb.env`, syncs `codex login --with-api-key`, and loads it in tmux.
158
+ Bare `sks` can also prompt for codex-lb auth; SKS stores the base URL/key in `~/.codex/sks-codex-lb.env`, syncs `codex login --with-api-key`, and loads it in tmux. When codex-lb is active, SKS opens a fresh `sks-codex-lb-*` tmux session and sweeps older detached codex-lb sessions for the same repo before launch so stale Responses API chains are not reused. Configured launch paths, including non-interactive runs, verify that codex-lb can continue a Responses API chain with `previous_response_id`; if that check fails, SKS bypasses codex-lb for that launch with `model_provider="openai"` instead of letting the Codex session fail mid-work.
158
159
 
159
160
  If Codex CLI auth drifts after launch/reinstall, run `sks doctor --fix` or `sks codex-lb repair`; to replace it, run `sks codex-lb reconfigure --host <domain> --api-key <key>`.
160
161
 
@@ -278,6 +279,8 @@ Default setup adds these generated SKS paths to the project `.gitignore`; `--loc
278
279
 
279
280
  Use `sks dollar-commands` to confirm that terminal discovery and Codex App prompt commands agree.
280
281
 
282
+ SKS does not install Git pre-commit hooks. Release metadata is changed only by explicit commands such as `sks versioning bump`, and `sks versioning hook` is intentionally blocked so Codex App commit/push flows stay unobstructed.
283
+
281
284
  TriWiki is intentionally sparse: `sks wiki sweep` records demote, soft-forget, archive, delete, promote-to-skill, and promote-to-rule candidates instead of injecting every old claim into future prompts. `sks harness fixture` validates the broader Harness Growth Factory contract: deliberate forgetting fixtures, skill card metadata, experiment schema, tool-error taxonomy, permission profiles, MultiAgentV2 defaults, and tmux cockpit view coverage. `sks code-structure scan` flags handwritten files above 1000/2000/3000-line thresholds so new logic can be extracted before command files become harder to maintain.
282
285
 
283
286
  ## OpenClaw Agent Usage
@@ -416,6 +419,8 @@ npm run publish:dry
416
419
 
417
420
  `release:check` runs audit, changelog, syntax, selftest, size, and registry checks. `publish:dry` runs that same gate and then performs an npm dry-run publish against the public registry.
418
421
 
422
+ Version bumps are manual. Run `sks versioning bump` only when preparing release metadata; SKS will not create `.git/hooks/pre-commit` or auto-bump during ordinary commits.
423
+
419
424
  ## License
420
425
 
421
426
  MIT
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.7.75",
4
+ "version": "0.7.78",
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",
@@ -217,6 +217,126 @@ export async function codexLbStatus(opts = {}) {
217
217
  };
218
218
  }
219
219
 
220
+ function codexLbResponsesEndpoint(baseUrl = '') {
221
+ const base = String(baseUrl || '').trim().replace(/\/+$/, '');
222
+ if (!base) return '';
223
+ return /\/responses$/i.test(base) ? base : `${base}/responses`;
224
+ }
225
+
226
+ function codexLbChainCheckEnabled(env = process.env) {
227
+ return env.SKS_CODEX_LB_CHAIN_CHECK !== '0' && env.SKS_SKIP_CODEX_LB_CHAIN_CHECK !== '1';
228
+ }
229
+
230
+ function isPreviousResponseNotFound(payload = {}) {
231
+ const error = payload?.error || payload?.response?.error || payload;
232
+ const text = typeof error === 'string'
233
+ ? error
234
+ : [error?.type, error?.code, error?.message, error?.param, JSON.stringify(error || {})].filter(Boolean).join(' ');
235
+ return /previous_response_not_found|previous_response_id.*not found|previous_response_id/i.test(text);
236
+ }
237
+
238
+ function parseCodexLbSseEvents(text = '') {
239
+ const events = [];
240
+ for (const line of String(text || '').split(/\r?\n/)) {
241
+ if (!line.startsWith('data:')) continue;
242
+ const data = line.slice(5).trim();
243
+ if (!data || data === '[DONE]') continue;
244
+ try {
245
+ events.push(JSON.parse(data));
246
+ } catch {}
247
+ }
248
+ return events;
249
+ }
250
+
251
+ function codexLbResponseId(payload = {}) {
252
+ if (typeof payload?.id === 'string' && payload.id) return payload.id;
253
+ if (typeof payload?.response?.id === 'string' && payload.response.id) return payload.response.id;
254
+ if (typeof payload?.data?.id === 'string' && payload.data.id) return payload.data.id;
255
+ if (typeof payload?.data?.response?.id === 'string' && payload.data.response.id) return payload.data.response.id;
256
+ return null;
257
+ }
258
+
259
+ function codexLbResponseError(json, events = []) {
260
+ if (json?.error) return json;
261
+ for (const event of events) {
262
+ if (event?.error || event?.response?.error || event?.type === 'response.failed' || event?.type === 'error') return event;
263
+ }
264
+ return null;
265
+ }
266
+
267
+ async function fetchCodexLbResponse(fetchImpl, endpoint, apiKey, body, timeoutMs) {
268
+ const controller = new AbortController();
269
+ const timer = setTimeout(() => controller.abort(), timeoutMs).unref?.();
270
+ try {
271
+ const response = await fetchImpl(endpoint, {
272
+ method: 'POST',
273
+ headers: {
274
+ authorization: `Bearer ${apiKey}`,
275
+ 'content-type': 'application/json'
276
+ },
277
+ body: JSON.stringify(body),
278
+ signal: controller.signal
279
+ });
280
+ const text = await response.text();
281
+ let json = null;
282
+ try { json = text ? JSON.parse(text) : null; } catch {}
283
+ const events = json ? [] : parseCodexLbSseEvents(text);
284
+ const responseId = codexLbResponseId(json) || events.map((event) => codexLbResponseId(event)).find(Boolean) || null;
285
+ const errorPayload = codexLbResponseError(json, events);
286
+ return { ok: response.ok && !errorPayload, status: response.status, json, text, events, response_id: responseId, error_payload: errorPayload };
287
+ } catch (err) {
288
+ return { ok: false, status: 0, json: null, text: err.name === 'AbortError' ? 'request timed out' : err.message, events: [], response_id: null, error_payload: null };
289
+ } finally {
290
+ clearTimeout(timer);
291
+ }
292
+ }
293
+
294
+ export async function checkCodexLbResponseChain(status = {}, opts = {}) {
295
+ const env = opts.env || process.env;
296
+ if (!codexLbChainCheckEnabled(env) && !opts.force) return { ok: true, status: 'skipped', skipped: true, reason: 'SKS_CODEX_LB_CHAIN_CHECK=0' };
297
+ const endpoint = codexLbResponsesEndpoint(opts.baseUrl || status.base_url);
298
+ if (!endpoint) return { ok: false, status: 'missing_base_url', chain_unhealthy: true };
299
+ const apiKey = opts.apiKey || parseCodexLbEnvKey(await readText(opts.envPath || status.env_path || codexLbEnvPath(opts.home || env.HOME || os.homedir()), ''));
300
+ if (!apiKey) return { ok: false, status: 'missing_env_key', chain_unhealthy: true };
301
+ const fetchImpl = opts.fetch || globalThis.fetch;
302
+ if (typeof fetchImpl !== 'function') return { ok: true, status: 'skipped', skipped: true, reason: 'fetch unavailable' };
303
+ const model = opts.model || env.SKS_CODEX_MODEL || 'gpt-5.5';
304
+ const timeoutMs = Number(opts.timeoutMs || env.SKS_CODEX_LB_CHAIN_CHECK_TIMEOUT_MS || 8000);
305
+ const baseBody = {
306
+ model,
307
+ instructions: 'You are running a short SKS codex-lb response-chain health check.',
308
+ input: 'SKS codex-lb response-chain health check. Reply with OK.',
309
+ stream: true,
310
+ store: true,
311
+ parallel_tool_calls: false,
312
+ tool_choice: 'auto',
313
+ reasoning: { effort: 'low' }
314
+ };
315
+ const first = await fetchCodexLbResponse(fetchImpl, endpoint, apiKey, baseBody, timeoutMs);
316
+ if (!first.ok || !first.response_id) {
317
+ return {
318
+ ok: false,
319
+ status: first.ok ? 'missing_response_id' : 'first_request_failed',
320
+ chain_unhealthy: true,
321
+ endpoint,
322
+ http_status: first.status,
323
+ error: redactSecretText(first.error_payload?.error?.message || first.error_payload?.response?.error?.message || first.text || 'codex-lb first Responses request failed', [apiKey])
324
+ };
325
+ }
326
+ const second = await fetchCodexLbResponse(fetchImpl, endpoint, apiKey, { ...baseBody, previous_response_id: first.response_id }, timeoutMs);
327
+ if (second.ok) return { ok: true, status: 'chain_ok', endpoint, response_id: first.response_id, chained_response_id: second.response_id || null, http_status: second.status };
328
+ const previousMissing = isPreviousResponseNotFound(second.error_payload || second.json || second.text);
329
+ return {
330
+ ok: false,
331
+ status: previousMissing ? 'previous_response_not_found' : 'second_request_failed',
332
+ chain_unhealthy: true,
333
+ endpoint,
334
+ response_id: first.response_id,
335
+ http_status: second.status,
336
+ error: redactSecretText(second.error_payload?.error?.message || second.error_payload?.response?.error?.message || second.text || 'codex-lb chained Responses request failed', [apiKey])
337
+ };
338
+ }
339
+
220
340
  function hasTopLevelCodexLbSelected(text = '') {
221
341
  const topLevel = String(text || '').split(/\n\s*\[/)[0] || '';
222
342
  return /(^|\n)\s*model_provider\s*=\s*"codex-lb"\s*(?:#.*)?(?=\n|$)/.test(topLevel);
@@ -283,13 +403,18 @@ export async function ensureCodexLbAuthDuringInstall(opts = {}) {
283
403
 
284
404
  export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
285
405
  if (args.includes('--json') || args.includes('--skip-codex-lb') || process.env.SKS_SKIP_CODEX_LB_PROMPT === '1') return { status: 'skipped' };
286
- if (!canAskYesNo()) return { status: 'non_interactive' };
287
406
  const status = await codexLbStatus(opts);
288
407
  if (status.ok) {
289
408
  const codexLogin = await ensureCodexLbLoginFromEnv(status, opts);
290
409
  if (codexLogin.status === 'synced') console.log('codex-lb auth synced with Codex CLI.');
291
- return { status: 'present', ...status, codex_login: codexLogin };
410
+ const chainHealth = await checkCodexLbResponseChain(status, opts);
411
+ if (!chainHealth.ok && chainHealth.chain_unhealthy) {
412
+ console.log(`codex-lb response chain check failed (${chainHealth.status}); bypassing codex-lb for this launch.`);
413
+ return { status: 'chain_unhealthy', ...status, ok: false, codex_login: codexLogin, chain_health: chainHealth, bypass_codex_lb: true };
414
+ }
415
+ return { status: 'present', ...status, codex_login: codexLogin, chain_health: chainHealth };
292
416
  }
417
+ if (!canAskYesNo()) return { status: 'non_interactive', codex_lb: status };
293
418
  const useCodexLb = (await askPostinstallQuestion('\nAuthenticate and route Codex through codex-lb? [y/N] ')).trim();
294
419
  if (!/^(y|yes|예|네|응)$/i.test(useCodexLb)) return { status: 'continued_to_codex' };
295
420
  const host = (await askPostinstallQuestion('codex-lb host domain [http://127.0.0.1:2455]: ')).trim() || 'http://127.0.0.1:2455';
@@ -1110,6 +1235,56 @@ export async function selftestCodexLb(tmp) {
1110
1235
  if (codexLbNotConfigured.code !== 0 || String(codexLbNotConfigured.stdout || '').includes('codex-lb auth:')) throw new Error('selftest: postinstall should stay quiet when codex-lb is not configured');
1111
1236
  const codexLbStatusText = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'status'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
1112
1237
  if (!String(codexLbStatusText.stdout || '').includes('Repair auth: sks codex-lb repair')) throw new Error('selftest: codex-lb status did not advertise repair command');
1238
+ const nonInteractiveLaunchChainCalls = [];
1239
+ const nonInteractiveLaunch = await maybePromptCodexLbSetupForLaunch([], {
1240
+ home: codexLbHome,
1241
+ apiKey: 'sk-test',
1242
+ codexBin: path.join(codexLbFakeBin, 'codex'),
1243
+ timeoutMs: 1000,
1244
+ fetch: async (url, init) => {
1245
+ nonInteractiveLaunchChainCalls.push({ url, body: JSON.parse(init.body) });
1246
+ return new Response(JSON.stringify({ id: nonInteractiveLaunchChainCalls.length === 1 ? 'resp_noninteractive_1' : 'resp_noninteractive_2' }), { status: 200, headers: { 'content-type': 'application/json' } });
1247
+ }
1248
+ });
1249
+ if (!nonInteractiveLaunch.ok || nonInteractiveLaunch.status !== 'present' || nonInteractiveLaunch.chain_health?.status !== 'chain_ok' || nonInteractiveLaunchChainCalls.length !== 2 || nonInteractiveLaunchChainCalls[1].body.previous_response_id !== 'resp_noninteractive_1') throw new Error('selftest: non-interactive codex-lb launch path did not run response-chain preflight');
1250
+ const nonInteractiveBrokenLaunch = await maybePromptCodexLbSetupForLaunch([], {
1251
+ home: codexLbHome,
1252
+ apiKey: 'sk-test',
1253
+ codexBin: path.join(codexLbFakeBin, 'codex'),
1254
+ timeoutMs: 1000,
1255
+ fetch: async (_url, init) => {
1256
+ const body = JSON.parse(init.body);
1257
+ if (!body.previous_response_id) return new Response(JSON.stringify({ id: 'resp_noninteractive_broken' }), { status: 200, headers: { 'content-type': 'application/json' } });
1258
+ return new Response(JSON.stringify({ error: { type: 'invalid_request_error', code: 'previous_response_not_found', message: 'Previous response not found.', param: 'previous_response_id' } }), { status: 400, headers: { 'content-type': 'application/json' } });
1259
+ }
1260
+ });
1261
+ if (nonInteractiveBrokenLaunch.status !== 'chain_unhealthy' || nonInteractiveBrokenLaunch.bypass_codex_lb !== true || nonInteractiveBrokenLaunch.chain_health?.status !== 'previous_response_not_found') throw new Error('selftest: non-interactive codex-lb launch path did not bypass on previous_response_not_found');
1262
+ const chainCalls = [];
1263
+ const okChain = await checkCodexLbResponseChain(
1264
+ { base_url: 'https://lb.example.test/backend-api/codex', env_path: path.join(codexLbHome, '.codex', 'sks-codex-lb.env') },
1265
+ {
1266
+ apiKey: 'sk-test',
1267
+ timeoutMs: 1000,
1268
+ fetch: async (url, init) => {
1269
+ chainCalls.push({ url, body: JSON.parse(init.body) });
1270
+ return new Response(JSON.stringify({ id: chainCalls.length === 1 ? 'resp_selftest_1' : 'resp_selftest_2' }), { status: 200, headers: { 'content-type': 'application/json' } });
1271
+ }
1272
+ }
1273
+ );
1274
+ if (!okChain.ok || okChain.status !== 'chain_ok' || chainCalls.length !== 2 || !String(chainCalls[0].url).endsWith('/backend-api/codex/responses') || chainCalls[1].body.previous_response_id !== 'resp_selftest_1') throw new Error('selftest: codex-lb response chain health check did not verify previous_response_id continuity');
1275
+ const brokenChain = await checkCodexLbResponseChain(
1276
+ { base_url: 'https://lb.example.test/backend-api/codex', env_path: path.join(codexLbHome, '.codex', 'sks-codex-lb.env') },
1277
+ {
1278
+ apiKey: 'sk-test',
1279
+ timeoutMs: 1000,
1280
+ fetch: async (_url, init) => {
1281
+ const body = JSON.parse(init.body);
1282
+ if (!body.previous_response_id) return new Response(JSON.stringify({ id: 'resp_missing_selftest' }), { status: 200, headers: { 'content-type': 'application/json' } });
1283
+ return new Response(JSON.stringify({ error: { type: 'invalid_request_error', code: 'previous_response_not_found', message: 'Previous response not found.', param: 'previous_response_id' } }), { status: 400, headers: { 'content-type': 'application/json' } });
1284
+ }
1285
+ }
1286
+ );
1287
+ if (brokenChain.ok || brokenChain.status !== 'previous_response_not_found' || brokenChain.chain_unhealthy !== true) throw new Error('selftest: codex-lb response chain health check did not detect previous_response_not_found');
1113
1288
  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
1289
  if (!hasCodexUnstableFeatureWarningSuppression(codexLbConfig)) throw new Error('selftest: codex-lb setup did not suppress Codex unstable feature warning');
1115
1290
  const codexLbLaunch = codexLaunchCommand(tmp, 'codex', []);
@@ -1117,7 +1292,7 @@ export async function selftestCodexLb(tmp) {
1117
1292
  if (!codexLbLaunch.includes("'--model' 'gpt-5.5'")) throw new Error('selftest: tmux launch command without args did not force GPT-5.5');
1118
1293
  if (!codexLbLaunch.includes('SKS_TMUX_LOGO_ANIMATION') || !codexLbLaunch.includes('SNEAKOSCOPE CODEX')) throw new Error('selftest: tmux launch command does not include the animated SKS logo intro');
1119
1294
  const madLaunchSource = await safeReadText(path.join(packageRoot(), 'src', 'cli', 'maintenance-commands.mjs'));
1120
- if (!madLaunchSource.includes('const lb = await deps.maybePromptCodexLbSetupForLaunch(args)') || !madLaunchSource.includes("const launchLb = lb.status === 'present'") || !madLaunchSource.includes('codexLbImmediateLaunchOpts(cleanArgs, launchLb')) throw new Error('selftest: MAD launch does not sync codex-lb auth and fresh-session launch options');
1295
+ if (!madLaunchSource.includes('const lb = await deps.maybePromptCodexLbSetupForLaunch(args)') || !madLaunchSource.includes("const launchLb = lb.status === 'present'") || !madLaunchSource.includes('codexLbImmediateLaunchOpts(cleanArgs, launchLb') || !madLaunchSource.includes('bypass_codex_lb') || !madLaunchSource.includes('model_provider="openai"') || !madLaunchSource.includes('codexLbFreshSession: true')) throw new Error('selftest: MAD launch does not sync codex-lb auth and fresh-session launch options');
1121
1296
 
1122
1297
  }
1123
1298
 
package/src/cli/main.mjs CHANGED
@@ -18,7 +18,7 @@ import { classifySql, classifyCommand, classifyToolPayload, checkDbOperation, ha
18
18
  import { checkHarnessModification, harnessGuardStatus, isHarnessSourceProject } from '../core/harness-guard.mjs';
19
19
  import { formatHarnessConflictReport, llmHarnessCleanupPrompt, scanHarnessConflicts } from '../core/harness-conflicts.mjs';
20
20
  import { context7Docs, context7Resolve, context7Text, context7Tools } from '../core/context7-client.mjs';
21
- import { bumpProjectVersion, installVersionGitHook, runVersionPreCommit, versioningStatus } from '../core/version-manager.mjs';
21
+ import { bumpProjectVersion, disableVersionGitHook, runVersionPreCommit, versioningStatus } from '../core/version-manager.mjs';
22
22
  import { rustInfo } from '../core/rust-accelerator.mjs';
23
23
  import { renderCartridge, validateCartridge, driftCartridge, snapshotCartridge } from '../core/gx-renderer.mjs';
24
24
  import { defaultEvaluationScenario, runEvaluationBenchmark } from '../core/evaluation.mjs';
@@ -75,10 +75,10 @@ import { GOAL_WORKFLOW_ARTIFACT } from '../core/goal-workflow.mjs';
75
75
  import { CODEX_APP_DOCS_URL, codexAppIntegrationStatus, formatCodexAppStatus } from '../core/codex-app.mjs';
76
76
  import { codexAppRemoteControlCommand } from './codex-app-command.mjs';
77
77
  import { OPENCLAW_SKILL_NAME, installOpenClawSkill } from '../core/openclaw.mjs';
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';
78
+ 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';
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, ensureGlobalCodexFastModeDuringInstall, ensureGlobalCodexSkillsDuringInstall, ensureProjectContext7Config, ensureRelatedCliTools, ensureSksCommandDuringInstall, ensureTmuxCliTool, globalCodexSkillsRoot, maybePromptCodexLbSetupForLaunch, maybePromptCodexUpdateForLaunch, postinstall, postinstallBootstrapDecision, repairCodexLbAuth, selftestCodexLb, shouldAutoApproveInstall } from './install-helpers.mjs';
81
+ import { askPostinstallQuestion, checkCodexLbResponseChain, 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
 
@@ -144,13 +144,20 @@ async function defaultTmuxCommand(args = []) {
144
144
  }
145
145
 
146
146
  function codexLbImmediateLaunchOpts(args = [], lb = {}, opts = {}) {
147
- if (!lb?.ok || lb.status !== 'configured') return opts;
148
- if (readOption(args, '--session', null) || readOption(args, '--workspace', null)) return opts;
149
147
  const root = readOption(args, '--root', process.cwd());
148
+ const explicitSession = readOption(args, '--session', null) || readOption(args, '--workspace', null);
149
+ if (lb?.bypass_codex_lb) {
150
+ const session = explicitSession || sanitizeTmuxSessionName(`sks-openai-fallback-${Date.now().toString(36)}-${defaultTmuxSessionName(root)}`);
151
+ console.log(`codex-lb bypass active for this launch: ${lb.chain_health?.status || lb.status}`);
152
+ console.log(`Using fresh OpenAI fallback tmux session: ${session}`);
153
+ return { ...opts, session, codexArgs: [...(opts.codexArgs || []), '-c', 'model_provider="openai"'], codexLbBypassed: true };
154
+ }
155
+ if (!lb?.ok) return opts;
156
+ if (explicitSession) return opts;
150
157
  const session = sanitizeTmuxSessionName(`sks-codex-lb-${Date.now().toString(36)}-${defaultTmuxSessionName(root)}`);
151
- console.log(`codex-lb key loaded for this launch: ${lb.env_path}`);
158
+ console.log(`codex-lb active for this launch: ${lb.env_path || lb.base_url || 'configured'}`);
152
159
  console.log(`Using fresh tmux session: ${session}`);
153
- return { ...opts, session };
160
+ return { ...opts, session, codexLbFreshSession: true };
154
161
  }
155
162
 
156
163
  function help(args = []) {
@@ -171,8 +178,8 @@ Usage:
171
178
  sks bootstrap [--install-scope global|project] [--local-only] [--json]
172
179
  sks deps check|install [tmux|codex|context7|all] [--yes] [--json]
173
180
  sks codex-app
174
- sks codex-lb status|repair|setup --host <domain> --api-key <key>
175
- sks auth status|repair|setup --host <domain> --api-key <key>
181
+ sks codex-lb status|health|repair|setup --host <domain> --api-key <key>
182
+ sks auth status|health|repair|setup --host <domain> --api-key <key>
176
183
  sks openclaw install|path|print [--dir path] [--force] [--json]
177
184
  sks --mad [--high]
178
185
  sks auto-review status|enable|start [--high]
@@ -192,7 +199,7 @@ Usage:
192
199
  sks pipeline answer <mission-id|latest> <answers.json|--stdin|--text "...">
193
200
  sks guard check [--json]
194
201
  sks conflicts check|prompt [--json]
195
- sks versioning status|bump|pre-commit [--json]
202
+ sks versioning status|bump|disable [--json]
196
203
  sks reasoning ["prompt"] [--json]
197
204
  sks aliases
198
205
  sks setup [--bootstrap] [--install-scope global|project] [--local-only] [--force] [--json]
@@ -1002,10 +1009,19 @@ async function versioning(sub = 'status', args = []) {
1002
1009
  if (!status.ok) console.log('Run: sks doctor --fix');
1003
1010
  return;
1004
1011
  }
1005
- if (action === 'hook' || action === 'install-hook') {
1006
- const res = await installVersionGitHook(root, await globalSksCommand());
1012
+ if (action === 'hook' || action === 'install-hook' || action === 'enable') {
1013
+ const res = await disableVersionGitHook(root);
1014
+ const blocked = { ...res, ok: false, installed: false, reason: 'pre_commit_hooks_unsupported' };
1015
+ process.exitCode = 2;
1016
+ if (flag(args, '--json')) return console.log(JSON.stringify(blocked, null, 2));
1017
+ console.error('SKS no longer installs Git pre-commit hooks. Use `sks versioning bump` and release checks explicitly.');
1018
+ if (res.hook_removed) console.error(`Removed existing SKS version hook: ${res.hook_path}`);
1019
+ return;
1020
+ }
1021
+ if (action === 'disable' || action === 'off' || action === 'remove-hook' || action === 'unhook') {
1022
+ const res = await disableVersionGitHook(root);
1007
1023
  if (flag(args, '--json')) return console.log(JSON.stringify(res, null, 2));
1008
- console.log(res.installed ? `Version hook installed: ${res.hook_path}` : `Version hook skipped: ${res.reason}`);
1024
+ console.log(res.hook_removed ? `Version hook removed: ${res.hook_path}` : `Version hook disabled: ${res.reason || 'policy updated'}`);
1009
1025
  return;
1010
1026
  }
1011
1027
  if (action === 'bump') {
@@ -1032,7 +1048,7 @@ async function versioning(sub = 'status', args = []) {
1032
1048
  console.log(res.changed ? `SKS versioning synced: ${res.version}` : `SKS versioning: ${res.version} verified`);
1033
1049
  return;
1034
1050
  }
1035
- console.error('Usage: sks versioning status|bump|pre-commit [--json]');
1051
+ console.error('Usage: sks versioning status|bump|disable [--json]');
1036
1052
  process.exitCode = 1;
1037
1053
  }
1038
1054
 
@@ -1125,6 +1141,21 @@ async function codexLbCommand(action = 'status', args = []) {
1125
1141
  else console.log('\nRepair auth: sks codex-lb repair');
1126
1142
  return;
1127
1143
  }
1144
+ if (sub === 'health' || sub === 'verify-chain' || sub === 'chain') {
1145
+ const status = await codexLbStatus();
1146
+ const result = status.ok
1147
+ ? await checkCodexLbResponseChain(status, { force: true })
1148
+ : { ok: false, status: 'not_configured', codex_lb: status };
1149
+ if (json) return console.log(JSON.stringify(result, null, 2));
1150
+ if (result.ok) {
1151
+ console.log('codex-lb response chain: ok');
1152
+ return;
1153
+ }
1154
+ console.error(`codex-lb response chain: failed (${result.status})`);
1155
+ if (result.error) console.error(result.error);
1156
+ process.exitCode = 1;
1157
+ return;
1158
+ }
1128
1159
  if (sub === 'repair' || sub === 'resync' || sub === 'login') {
1129
1160
  const result = await repairCodexLbAuth();
1130
1161
  if (json) return console.log(JSON.stringify(result, null, 2));
@@ -1160,7 +1191,7 @@ async function codexLbCommand(action = 'status', args = []) {
1160
1191
  console.log(`Key env: ${result.env_path}`);
1161
1192
  return;
1162
1193
  }
1163
- console.error('Usage: sks codex-lb status|repair|setup --host <domain> --api-key <key> [--json]');
1194
+ console.error('Usage: sks codex-lb status|health|repair|setup --host <domain> --api-key <key> [--json]');
1164
1195
  process.exitCode = 1;
1165
1196
  }
1166
1197
 
@@ -1448,7 +1479,7 @@ function usage(args = []) {
1448
1479
  const topic = String(args[0] || 'overview').toLowerCase();
1449
1480
  const blocks = {
1450
1481
  overview: [sksAsciiLogo(), '', 'Usage', '', 'Discover:', ' sks commands', ' sks quickstart', ' sks root', ' sks bootstrap', ' sks deps check', ' sks codex-app check', ' sks tmux check', ' sks dollar-commands', '', `Topics: ${USAGE_TOPICS}`],
1451
- install: ['Install', '', '1. Global install:', ' npm i -g sneakoscope', '', '2. Bootstrap and check dependencies:', ' sks bootstrap', ' sks deps check', '', '3. Confirm Codex App commands:', ' sks codex-app check', ' sks dollar-commands', '', '4. Optional codex-lb key setup for CLI sks runs:', ' sks codex-lb setup --host <domain> --api-key <key>', ' sks codex-lb repair', ' sks', '', 'Fallback:', ' npx -y -p sneakoscope sks root', '', 'Project:', ' npm i -D sneakoscope', ' npx sks setup --install-scope project'],
1482
+ install: ['Install', '', '1. Global install:', ' npm i -g sneakoscope', '', '2. Bootstrap and check dependencies:', ' sks bootstrap', ' sks deps check', '', '3. Confirm Codex App commands:', ' sks codex-app check', ' sks dollar-commands', '', '4. Optional codex-lb key setup for CLI sks runs:', ' sks codex-lb setup --host <domain> --api-key <key>', ' sks codex-lb health', ' sks codex-lb repair', ' sks', '', 'Fallback:', ' npx -y -p sneakoscope sks root', '', 'Project:', ' npm i -D sneakoscope', ' npx sks setup --install-scope project'],
1452
1483
  bootstrap: ['Bootstrap', '', ' sks bootstrap', ' sks setup --bootstrap', '', 'Creates project SKS files, Codex App skills/hooks/config, state/guard files, then checks Codex App, Context7, and tmux.'],
1453
1484
  root: ['Root', '', ' sks root [--json]', '', 'Inside a project, SKS uses that project root. Outside any project marker, runtime commands use the per-user global SKS root instead of writing .sneakoscope into the current random folder.'],
1454
1485
  deps: ['Dependencies', '', ' sks deps check [--json]', ' sks deps install [tmux|codex|context7|all] [--yes]', '', 'tmux on macOS uses Homebrew after Y/n approval for missing installs or Homebrew-managed upgrades. If PATH resolves an npm-managed tmux, SKS prompts for npm i -g tmux@latest instead. Unknown non-Homebrew tmux paths are reported as conflicts.'],
@@ -1598,7 +1629,7 @@ async function setup(args) {
1598
1629
  console.log(`Install: ${install.ok ? 'ok' : 'missing'} ${install.scope} (${install.command_prefix})`);
1599
1630
  console.log(`CLI tools: Codex ${formatCodexCliToolStatus(cliTools.codex)}; tmux ${tmuxStatusKind(cliTools.tmux)} ${cliTools.tmux.version || cliTools.tmux.error || ''}`.trimEnd());
1600
1631
  console.log(`Hooks: ${path.relative(root, hooksPath)}`);
1601
- console.log(`Version: ${versioningInfo.enabled ? (versioningInfo.hook_installed ? 'auto-bump enabled' : 'auto-bump hook missing') : 'not enabled'}${versioningInfo.package_version ? ` (${versioningInfo.package_version})` : ''}`);
1632
+ console.log(`Version: explicit bump only${versioningInfo.package_version ? ` (${versioningInfo.package_version})` : ''}`);
1602
1633
  if (localOnly) console.log('Git: local-only (.git/info/exclude; user AGENTS preserved, SKS managed block refreshed)');
1603
1634
  else console.log('Git: .gitignore ignores SKS generated files');
1604
1635
  console.log(`Codex App: .codex/config.toml, .codex/hooks.json, .agents/skills, .codex/agents, .codex/SNEAKOSCOPE.md`);
@@ -1764,7 +1795,7 @@ async function doctor(args) {
1764
1795
  if (!appRuntime.ok) console.log('Codex App or first-party MCP/plugin tools missing. Run: sks codex-app check');
1765
1796
  if (!result.runtime.tmux.ok) console.log('tmux missing. Run: sks deps install tmux');
1766
1797
  if (!result.harness_guard.ok) console.log('Harness guard failed. Run: sks setup from a real terminal, then sks guard check.');
1767
- if (!result.versioning.ok) console.log('Versioning hook missing. Run: sks versioning hook, or sks doctor --fix.');
1798
+ if (!result.versioning.ok) console.log('Versioning metadata drift detected. Run: sks versioning status, then sks versioning bump if release metadata should change.');
1768
1799
  if (!result.codex_lb.ready) console.log('codex-lb config/auth drift detected. Run: sks doctor --fix, or reconfigure once with sks codex-lb reconfigure --host <domain> --api-key <key>.');
1769
1800
  if (!result.skills.ok) console.log(`Missing skills: ${result.skills.missing.join(', ')}. Run: sks setup`);
1770
1801
  if (!result.global_skills.ok) console.log(`Missing global $ skills: ${result.global_skills.missing.join(', ')}. Run: npm i -g sneakoscope, or sks setup from a non-local-only run.`);
@@ -2373,22 +2404,27 @@ async function selftest() {
2373
2404
  await writeTextAtomic(path.join(versionTmp, '.git', 'hooks', 'pre-commit'), '#!/bin/sh\nexit 0\n');
2374
2405
  await initProject(versionTmp, {});
2375
2406
  const versionStatus = await versioningStatus(versionTmp);
2376
- if (!versionStatus.ok || !versionStatus.enabled || !versionStatus.hook_installed) throw new Error('selftest: versioning hook not installed');
2377
- const versionHookText = await safeReadText(versionStatus.hook_path);
2378
- if (!versionHookText.includes('versioning pre-commit')) throw new Error('selftest: versioning hook command missing');
2379
- if (versionHookText.indexOf('versioning pre-commit') > versionHookText.indexOf('exit 0')) throw new Error('selftest: versioning hook was appended after an early exit');
2407
+ if (!versionStatus.ok || versionStatus.enabled || versionStatus.hook_installed) throw new Error('selftest: versioning hook should stay disabled after init');
2408
+ let versionHookText = await safeReadText(versionStatus.hook_path);
2409
+ if (versionHookText.includes('versioning pre-commit')) throw new Error('selftest: init installed versioning pre-commit');
2410
+ const versionHookAttempt = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'versioning', 'hook', '--json'], { cwd: versionTmp, env: { SKS_DISABLE_UPDATE_CHECK: '1' }, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2411
+ if (versionHookAttempt.code === 0 || !versionHookAttempt.stdout.includes('pre_commit_hooks_unsupported')) throw new Error('selftest: versioning hook command should be blocked');
2412
+ const versionBlockedStatus = await versioningStatus(versionTmp);
2413
+ if (versionBlockedStatus.enabled || versionBlockedStatus.hook_installed) throw new Error('selftest: blocked versioning hook changed status');
2414
+ versionHookText = await safeReadText(versionBlockedStatus.hook_path);
2415
+ if (versionHookText.includes('versioning pre-commit')) throw new Error('selftest: blocked versioning hook installed pre-commit command');
2380
2416
  await writeTextAtomic(path.join(versionTmp, 'CHANGELOG.md'), '# Changelog\n\n## [Unreleased]\n\n## [0.1.0] - 2026-05-08\n\n### Fixed\n\n- Initial version selftest fixture.\n');
2381
2417
  await writeTextAtomic(path.join(versionTmp, 'README.md'), 'version selftest\n');
2382
2418
  await runProcess('git', ['add', 'README.md', 'CHANGELOG.md'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2383
2419
  const preCommitVerify = await runVersionPreCommit(versionTmp);
2384
- if (!preCommitVerify.ok || preCommitVerify.version !== '0.1.0' || preCommitVerify.changed) throw new Error('selftest: pre-commit should verify current version without bumping');
2420
+ if (!preCommitVerify.ok || !preCommitVerify.skipped || preCommitVerify.reason !== 'disabled_by_policy') throw new Error('selftest: pre-commit path should stay disabled by policy');
2385
2421
  const firstVersionBump = await bumpProjectVersion(versionTmp);
2386
2422
  if (!firstVersionBump.ok || firstVersionBump.version !== '0.1.1' || !firstVersionBump.changed) throw new Error('selftest: first version bump did not advance patch version');
2387
2423
  const bumpedPackage = await readJson(path.join(versionTmp, 'package.json'));
2388
2424
  const bumpedLock = await readJson(path.join(versionTmp, 'package-lock.json'));
2389
2425
  const bumpedChangelog = await safeReadText(path.join(versionTmp, 'CHANGELOG.md'));
2390
2426
  if (bumpedPackage.version !== '0.1.1' || bumpedLock.version !== '0.1.1' || bumpedLock.packages[''].version !== '0.1.1') throw new Error('selftest: package lock versions not synced');
2391
- if (!bumpedChangelog.includes('## [0.1.1]') || !bumpedChangelog.includes('automatic SKS version guard')) throw new Error('selftest: version bump did not sync changelog section');
2427
+ if (!bumpedChangelog.includes('## [0.1.1]') || !bumpedChangelog.includes('explicit SKS version bump')) throw new Error('selftest: version bump did not sync changelog section');
2392
2428
  const firstCached = await runProcess('git', ['diff', '--cached', '--name-only'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2393
2429
  if (!firstCached.stdout.includes('package.json') || !firstCached.stdout.includes('package-lock.json') || !firstCached.stdout.includes('CHANGELOG.md')) throw new Error('selftest: version files not staged');
2394
2430
  await runProcess('git', ['commit', '--no-verify', '-m', 'first versioned commit'], { cwd: versionTmp, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
@@ -3305,7 +3341,7 @@ async function selftest() {
3305
3341
  await ensureDir(fakeTmuxDir);
3306
3342
  const fakeTmuxLog = path.join(fakeTmuxDir, 'tmux.log');
3307
3343
  const fakeTmuxBin = path.join(fakeTmuxDir, 'tmux');
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`);
3344
+ 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-sessions')console.log(e.SKS_FAKE_TMUX_SESSIONS||'');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`);
3309
3345
  await fsp.chmod(fakeTmuxBin, 0o755);
3310
3346
  const previousFakeTmuxLog = process.env.SKS_FAKE_TMUX_LOG;
3311
3347
  const previousPath = process.env.PATH;
@@ -3346,6 +3382,91 @@ async function selftest() {
3346
3382
  const cockpitOpenLog = await safeReadText(fakeTmuxLog);
3347
3383
  if (!cockpitOpen.ok || cockpitOpen.opened_lane_count !== 2 || cockpitOpen.main_pane_id !== '%1' || cockpitOpen.relayout?.layout_name !== 'main-vertical' || !cockpitOpenLog.includes('display-message -p') || !cockpitOpenLog.includes('split-window -h -t %1') || !cockpitOpenLog.includes('set-option -pt %80 @sks_team_managed 1') || !cockpitOpenLog.includes('select-pane -t %1') || !cockpitOpenLog.includes('select-layout -t @1 main-vertical')) throw new Error('selftest: split');
3348
3384
  await writeTextAtomic(fakeTmuxLog, '');
3385
+ process.env.SKS_FAKE_TMUX_SPLIT_ID = '%90';
3386
+ process.env.SKS_FAKE_TMUX_LIST = `%81\tscout: analysis_scout_1\tnode\t1\t${teamId}\tanalysis_scout_1\tscout\n%82\tscout: analysis_scout_1\tnode\t1\t${teamId}\tanalysis_scout_1\tscout\n%84\tscout: analysis_scout_old\tnode\t1\told-team-mission\tanalysis_scout_old\tscout`;
3387
+ const cockpitDedupe = await reconcileTmuxTeamCockpit({
3388
+ root: tmp,
3389
+ missionId: teamId,
3390
+ plan: roleTeamPlan,
3391
+ dashboard: { agents: { analysis_scout_1: { status: 'assigned' } } },
3392
+ control: { status: 'running' },
3393
+ tmux: { bin: fakeTmuxBin },
3394
+ env: { ...process.env, TMUX: '/tmp/tmux-selftest/default,1,0' }
3395
+ });
3396
+ const cockpitDedupeLog = await safeReadText(fakeTmuxLog);
3397
+ if (!cockpitDedupe.ok || cockpitDedupe.closed_lane_count !== 2 || !cockpitDedupeLog.includes('kill-pane -t %82') || !cockpitDedupeLog.includes('kill-pane -t %84') || cockpitDedupeLog.includes('kill-pane -t %81')) throw new Error('selftest: tmux cockpit did not prune duplicate or stale managed panes');
3398
+ await writeTextAtomic(fakeTmuxLog, '');
3399
+ process.env.SKS_FAKE_TMUX_LIST = `%81\tscout: analysis_scout_1\tnode\t1\t${teamId}\tanalysis_scout_1\tscout`;
3400
+ const cockpitTerminal = await reconcileTmuxTeamCockpit({
3401
+ root: tmp,
3402
+ missionId: teamId,
3403
+ plan: roleTeamPlan,
3404
+ dashboard: { agents: { analysis_scout_1: { status: 'completed' } } },
3405
+ control: { status: 'running' },
3406
+ tmux: { bin: fakeTmuxBin },
3407
+ env: { ...process.env, TMUX: '/tmp/tmux-selftest/default,1,0' }
3408
+ });
3409
+ const cockpitTerminalLog = await safeReadText(fakeTmuxLog);
3410
+ if (!cockpitTerminal.ok || cockpitTerminal.closed_lane_count !== 1 || cockpitTerminal.opened_lane_count !== 0 || !cockpitTerminalLog.includes('kill-pane -t %81')) throw new Error('selftest: tmux cockpit did not close terminal agent pane');
3411
+ await writeTextAtomic(fakeTmuxLog, '');
3412
+ const staleTeamId = 'M-20260512-000000-old1';
3413
+ const missionDirOnlyTeamId = 'M-20260512-000000-dir1';
3414
+ await ensureDir(path.join(tmp, '.sneakoscope', 'missions', missionDirOnlyTeamId));
3415
+ await writeJsonAtomic(path.join(tmp, '.sneakoscope', 'state', 'tmux-team-sessions.json'), {
3416
+ schema_version: 1,
3417
+ missions: {
3418
+ [staleTeamId]: {
3419
+ mission_id: staleTeamId,
3420
+ session: `sks-team-${staleTeamId}`,
3421
+ root: tmp,
3422
+ panes: [{ pane_id: '%201', title: 'scout: analysis_scout_1' }]
3423
+ },
3424
+ [teamId]: {
3425
+ mission_id: teamId,
3426
+ session: 'sks-existing-selftest',
3427
+ root: tmp,
3428
+ panes: [{ pane_id: '%204', title: 'scout: analysis_scout_1' }]
3429
+ }
3430
+ }
3431
+ });
3432
+ process.env.SKS_FAKE_TMUX_LIST = [
3433
+ 'sks-existing-selftest\t@1\t%1\tCodex CLI\tnode\t\t\t\t',
3434
+ `sks-team-${staleTeamId}\t@70\t%201\tscout: analysis_scout_1\tnode\t\t\t\t`,
3435
+ `sks-existing-selftest\t@1\t%202\treview: stale_review\tnode\t1\t${staleTeamId}\tstale_review\treview`,
3436
+ `sks-team-${missionDirOnlyTeamId}\t@71\t%205\treview: reviewer_1\tnode\t\t\t\t`,
3437
+ 'unrelated-session\t@9\t%203\tscout: analysis_scout_1\tnode\t\t\t\t',
3438
+ `sks-existing-selftest\t@1\t%204\tscout: analysis_scout_1\tnode\t1\t${teamId}\tanalysis_scout_1\tscout`
3439
+ ].join('\n');
3440
+ const cockpitSweep = await sweepTmuxTeamSurfaces({
3441
+ root: tmp,
3442
+ keepMissionId: teamId,
3443
+ tmux: { bin: fakeTmuxBin },
3444
+ env: { ...process.env, TMUX: '/tmp/tmux-selftest/default,1,0' }
3445
+ });
3446
+ const cockpitSweepLog = await safeReadText(fakeTmuxLog);
3447
+ if (!cockpitSweep.ok || cockpitSweep.closed_lane_count !== 3 || !cockpitSweepLog.includes('kill-pane -t %201') || !cockpitSweepLog.includes('kill-pane -t %202') || !cockpitSweepLog.includes('kill-pane -t %205') || cockpitSweepLog.includes('kill-pane -t %203') || cockpitSweepLog.includes('kill-pane -t %204') || cockpitSweepLog.includes('kill-pane -t %1')) throw new Error('selftest: tmux sweep did not close only stale recorded Team panes');
3448
+ await writeTextAtomic(fakeTmuxLog, '');
3449
+ const codexLbSuffix = defaultTmuxSessionName(tmp);
3450
+ const codexLbKeepSession = `sks-codex-lb-keep-${codexLbSuffix}`;
3451
+ const codexLbCurrentSession = `sks-codex-lb-current-${codexLbSuffix}`;
3452
+ process.env.SKS_FAKE_TMUX_DISPLAY = `${codexLbCurrentSession}\t@1\t%1`;
3453
+ process.env.SKS_FAKE_TMUX_SESSIONS = [
3454
+ `sks-codex-lb-old-${codexLbSuffix}\t0\t100\t100`,
3455
+ `sks-codex-lb-attached-${codexLbSuffix}\t1\t101\t101`,
3456
+ `${codexLbKeepSession}\t0\t102\t102`,
3457
+ `${codexLbCurrentSession}\t0\t103\t103`,
3458
+ 'sks-codex-lb-other-sks-other-00000000\t0\t104\t104'
3459
+ ].join('\n');
3460
+ const codexLbSweep = await sweepCodexLbTmuxSessions({
3461
+ root: tmp,
3462
+ keepSession: codexLbKeepSession,
3463
+ tmux: { bin: fakeTmuxBin },
3464
+ env: { ...process.env, TMUX: '/tmp/tmux-selftest/default,1,0' }
3465
+ });
3466
+ const codexLbSweepLog = await safeReadText(fakeTmuxLog);
3467
+ if (!codexLbSweep.ok || codexLbSweep.closed_session_count !== 1 || !codexLbSweepLog.includes(`kill-session -t sks-codex-lb-old-${codexLbSuffix}`) || codexLbSweepLog.includes(`kill-session -t sks-codex-lb-attached-${codexLbSuffix}`) || codexLbSweepLog.includes(`kill-session -t ${codexLbKeepSession}`) || codexLbSweepLog.includes(`kill-session -t ${codexLbCurrentSession}`) || codexLbSweepLog.includes('kill-session -t sks-codex-lb-other')) throw new Error('selftest: codex-lb tmux sweep did not close only stale detached sessions for this repo');
3468
+ await writeTextAtomic(fakeTmuxLog, '');
3469
+ process.env.SKS_FAKE_TMUX_DISPLAY = 'sks-existing-selftest\t@1\t%1';
3349
3470
  const fakePanes = `%81\tscout: analysis_scout_1\tnode\t1\t${teamId}\tanalysis_scout_1\tscout\n%82\tscout: analysis_scout_2\tnode\t1\t${teamId}\tanalysis_scout_2\tscout\n%83\tuser pane\tzsh\t\t\t\t`;
3350
3471
  process.env.SKS_FAKE_TMUX_LIST = fakePanes;
3351
3472
  const cockpitClose = await reconcileTmuxTeamCockpit({
@@ -3362,6 +3483,7 @@ async function selftest() {
3362
3483
  if (!cockpitClose.ok || cockpitClose.closed_lane_count !== 2 || !cockpitCloseLog.includes('kill-pane -t %81') || !cockpitCloseLog.includes('kill-pane -t %82') || cockpitCloseLog.includes('kill-pane -t %83')) throw new Error('selftest: cleanup');
3363
3484
  delete process.env.SKS_FAKE_TMUX_DISPLAY;
3364
3485
  delete process.env.SKS_FAKE_TMUX_LIST;
3486
+ delete process.env.SKS_FAKE_TMUX_SESSIONS;
3365
3487
  delete process.env.SKS_FAKE_TMUX_SPLIT_ID;
3366
3488
  await writeTextAtomic(fakeTmuxLog, '');
3367
3489
  const madCockpit = await launchMadTmuxUi(['--workspace', 'sks-mad-selftest-ui', '--no-attach'], { root: tmp, tmux: { ok: true, bin: fakeTmuxBin, version: '3.4' }, codex: { bin: process.execPath }, app: { ok: true, guidance: [] }, missionId: 'M-MAD-SELFTEST' });
@@ -3460,9 +3582,26 @@ async function selftest() {
3460
3582
  if (!teamLive.includes('selftest mapped options')) throw new Error('selftest: team live transcript missing event');
3461
3583
  if (!teamLive.includes('Context tracking SSOT: TriWiki')) throw new Error('selftest: team live transcript missing TriWiki context tracking');
3462
3584
  if (!(await readTeamTranscriptTail(teamDir, 1)).join('\n').includes('selftest mapped options')) throw new Error('selftest: team transcript tail missing event');
3463
- const teamLane = await renderTeamAgentLane(teamDir, { missionId: teamId, agent: 'analysis_scout_1', lines: 4 });
3585
+ const teamLane = await renderTeamAgentLane(teamDir, { missionId: teamId, agent: 'analysis_scout_1', lines: 4, color: false });
3464
3586
  if (!teamLane.includes('selftest mapped repo slice')) throw new Error('selftest: team agent lane missing event context');
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');
3587
+ const missingChatLaneParts = [
3588
+ ['codex chat heading', '## Codex Chat'],
3589
+ ['lane speaker', 'me (analysis_scout_1)'],
3590
+ ['status role metadata', '[status/scout]'],
3591
+ ['agent event body', 'selftest mapped repo slice']
3592
+ ].filter(([, needle]) => !teamLane.includes(needle)).map(([label]) => label);
3593
+ if (missingChatLaneParts.length || teamLane.includes('## Global Tail')) {
3594
+ const reason = [
3595
+ missingChatLaneParts.length ? `missing ${missingChatLaneParts.join(', ')}` : null,
3596
+ teamLane.includes('## Global Tail') ? 'unexpected global tail' : null
3597
+ ].filter(Boolean).join('; ');
3598
+ throw new Error(`selftest: chat lane (${reason})\n${teamLane.slice(0, 1600)}`);
3599
+ }
3600
+ if (!teamLane.includes('╭─') || !teamLane.includes('│ selftest mapped repo slice') || !teamLane.includes('╰─')) {
3601
+ throw new Error(`selftest: team chat lane did not render framed chat blocks\n${teamLane.slice(0, 1600)}`);
3602
+ }
3603
+ const teamLaneColor = await renderTeamAgentLane(teamDir, { missionId: teamId, agent: 'analysis_scout_1', lines: 4, color: true });
3604
+ if (!/\x1b\[[0-9;]+m/.test(teamLaneColor) || !teamLaneColor.includes('Lane color:')) throw new Error('selftest: team chat lane did not render ANSI color metadata/output');
3466
3605
  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 });
3467
3606
  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');
3468
3607
  await writeTextAtomic(path.join(teamDir, 'team-analysis.md'), '- claim: analysis scout mapped route registry | source: src/core/routes.mjs | risk: high | confidence: supported\n');