sneakoscope 0.7.55 → 0.7.56

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.55",
4
+ "version": "0.7.56",
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",
@@ -8,7 +8,7 @@ import { getCodexInfo } from '../core/codex-adapter.mjs';
8
8
  import { formatHarnessConflictReport, llmHarnessCleanupPrompt, scanHarnessConflicts } from '../core/harness-conflicts.mjs';
9
9
  import { installSkills } from '../core/init.mjs';
10
10
  import { context7ConfigToml, DOLLAR_SKILL_NAMES, GETDESIGN_REFERENCE, hasContext7ConfigText, RECOMMENDED_SKILLS } from '../core/routes.mjs';
11
- import { platformTmuxInstallHint, tmuxReadiness } from '../core/tmux-ui.mjs';
11
+ import { codexLaunchCommand, platformTmuxInstallHint, tmuxReadiness } from '../core/tmux-ui.mjs';
12
12
 
13
13
  export async function postinstall({ bootstrap }) {
14
14
  const installRoot = path.resolve(process.env.INIT_CWD || process.cwd());
@@ -50,8 +50,10 @@ export async function postinstall({ bootstrap }) {
50
50
  if (bootstrapDecision.run) {
51
51
  console.log(`SKS bootstrap: ${bootstrapDecision.reason}.`);
52
52
  await runPostinstallBootstrap(installRoot, bootstrap);
53
+ await reportPostinstallCodexLbAuth();
53
54
  return;
54
55
  }
56
+ await reportPostinstallCodexLbAuth();
55
57
  console.log('\nNext:');
56
58
  console.log(' sks bootstrap');
57
59
  console.log(`\nSKS bootstrap was not run automatically: ${bootstrapDecision.reason}.`);
@@ -60,6 +62,15 @@ export async function postinstall({ bootstrap }) {
60
62
  console.log('Open runtime after readiness is green: sks\n');
61
63
  }
62
64
 
65
+ async function reportPostinstallCodexLbAuth() {
66
+ const codexLbAuth = await ensureCodexLbAuthDuringInstall();
67
+ if (codexLbAuth.status === 'synced' || codexLbAuth.status === 'present') console.log(`codex-lb auth: preserved from ${codexLbAuth.env_path}.`);
68
+ else if (codexLbAuth.status === 'skipped') console.log(`codex-lb auth: skipped (${codexLbAuth.reason}).`);
69
+ else if (codexLbAuth.status === 'missing_env_key') console.log('codex-lb auth: stored key missing. Run `sks codex-lb setup --host <domain> --api-key <key>` to repair.');
70
+ else if (codexLbAuth.status && codexLbAuth.status !== 'not_configured') console.log(`codex-lb auth: repair skipped (${codexLbAuth.status}${codexLbAuth.error ? `: ${codexLbAuth.error}` : ''}).`);
71
+ return codexLbAuth;
72
+ }
73
+
63
74
  async function postinstallHarnessConflictNotice(conflictScan) {
64
75
  console.log('\nSneakoscope Codex package installed, but SKS setup is blocked.');
65
76
  console.log(formatHarnessConflictReport(conflictScan, { includePrompt: false }));
@@ -159,7 +170,7 @@ export async function codexLbStatus(opts = {}) {
159
170
  const config = await readText(configPath, '');
160
171
  const envExists = await exists(envPath);
161
172
  const envText = envExists ? await readText(envPath, '') : '';
162
- const envKeyConfigured = /^(\s*export\s+)?CODEX_LB_API_KEY\s*=.+$/m.test(envText);
173
+ const envKeyConfigured = Boolean(parseCodexLbEnvKey(envText));
163
174
  const providerConfigured = /\[model_providers\.codex-lb\]/.test(config);
164
175
  const selected = /model_provider\s*=\s*"codex-lb"/.test(config);
165
176
  return {
@@ -197,6 +208,24 @@ export async function repairCodexLbAuth(opts = {}) {
197
208
  };
198
209
  }
199
210
 
211
+ export async function ensureCodexLbAuthDuringInstall(opts = {}) {
212
+ if (process.env.SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH === '1' && !opts.force) return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH=1' };
213
+ const status = await codexLbStatus(opts);
214
+ if (!status.selected && !status.provider_configured && !status.env_file) return { status: 'not_configured', codex_lb: status };
215
+ if (!status.ok) return { status: status.env_key_configured ? 'not_configured' : 'missing_env_key', codex_lb: status, config_path: status.config_path, env_path: status.env_path };
216
+ const codexLogin = await ensureCodexLbLoginFromEnv(status, { ...opts, force: true });
217
+ return {
218
+ ok: Boolean(codexLogin.ok),
219
+ status: codexLogin.status,
220
+ config_path: status.config_path,
221
+ env_path: status.env_path,
222
+ base_url: status.base_url,
223
+ codex_lb: status,
224
+ codex_login: codexLogin,
225
+ error: codexLogin.error || null
226
+ };
227
+ }
228
+
200
229
  export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
201
230
  if (args.includes('--json') || args.includes('--skip-codex-lb') || process.env.SKS_SKIP_CODEX_LB_PROMPT === '1') return { status: 'skipped' };
202
231
  if (!canAskYesNo()) return { status: 'non_interactive' };
@@ -237,7 +266,7 @@ async function syncCodexApiKeyLogin(apiKey, opts = {}) {
237
266
  }
238
267
  const login = await runProcess(codexBin, ['login', '--with-api-key'], { input: `${apiKey}\n`, env, timeoutMs: 15000, maxOutputBytes: 8192 });
239
268
  if (login.code === 0) return { ok: true, status: 'synced' };
240
- return { ok: false, status: 'login_failed', error: (login.stderr || login.stdout || 'codex login failed').trim() };
269
+ return { ok: false, status: 'login_failed', error: redactSecretText(login.stderr || login.stdout || 'codex login failed', [apiKey]).trim() };
241
270
  }
242
271
 
243
272
  function upsertCodexLbConfig(text = '', baseUrl) {
@@ -394,11 +423,23 @@ function parseCodexLbEnvKey(text = '') {
394
423
  const match = String(text || '').match(/^\s*(?:export\s+)?CODEX_LB_API_KEY\s*=\s*(.+?)\s*$/m);
395
424
  if (!match) return '';
396
425
  const raw = match[1].trim();
397
- if (raw.startsWith("'") && raw.endsWith("'")) return raw.slice(1, -1).replace(/'\\''/g, "'");
398
- if (raw.startsWith('"') && raw.endsWith('"')) return raw.slice(1, -1).replace(/\\"/g, '"');
426
+ if (!raw) return '';
427
+ if (raw.startsWith("'")) return raw.endsWith("'") && raw.length > 1 ? raw.slice(1, -1).replace(/'\\''/g, "'") : '';
428
+ if (raw.startsWith('"')) return raw.endsWith('"') && raw.length > 1 ? raw.slice(1, -1).replace(/\\"/g, '"') : '';
429
+ if (raw.includes("'") || raw.includes('"') || /\s/.test(raw)) return '';
399
430
  return raw;
400
431
  }
401
432
 
433
+ function redactSecretText(text = '', secrets = []) {
434
+ let out = String(text || '');
435
+ for (const secret of secrets) {
436
+ const value = String(secret || '');
437
+ if (!value) continue;
438
+ out = out.split(value).join('[redacted]');
439
+ }
440
+ return out;
441
+ }
442
+
402
443
  function escapeRegExp(value) {
403
444
  return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
404
445
  }
@@ -466,13 +507,20 @@ async function ensureGlobalContext7DuringInstall() {
466
507
  if (process.env.SKS_SKIP_POSTINSTALL_CONTEXT7 === '1') return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_CONTEXT7=1' };
467
508
  const codex = await getCodexInfo().catch(() => ({}));
468
509
  if (!codex.bin) return { status: 'codex_missing' };
469
- const list = await runProcess(codex.bin, ['mcp', 'list'], { timeoutMs: 8000, maxOutputBytes: 32 * 1024 }).catch((err) => ({ code: 1, stderr: err.message, stdout: '' }));
510
+ const env = withoutSecretEnv(['CODEX_LB_API_KEY']);
511
+ const list = await runProcess(codex.bin, ['mcp', 'list'], { env, timeoutMs: 8000, maxOutputBytes: 32 * 1024 }).catch((err) => ({ code: 1, stderr: err.message, stdout: '' }));
470
512
  if (list.code === 0 && /context7/i.test(`${list.stdout}\n${list.stderr}`)) return { status: 'present' };
471
- const add = await runProcess(codex.bin, ['mcp', 'add', 'context7', '--', 'npx', '-y', '@upstash/context7-mcp@latest'], { timeoutMs: 30000, maxOutputBytes: 64 * 1024 }).catch((err) => ({ code: 1, stderr: err.message, stdout: '' }));
513
+ const add = await runProcess(codex.bin, ['mcp', 'add', 'context7', '--', 'npx', '-y', '@upstash/context7-mcp@latest'], { env, timeoutMs: 30000, maxOutputBytes: 64 * 1024 }).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
472
514
  if (add.code === 0) return { status: 'installed' };
473
515
  return { status: 'failed', error: `${add.stderr || add.stdout || 'codex mcp add failed'}`.trim() };
474
516
  }
475
517
 
518
+ function withoutSecretEnv(keys = []) {
519
+ const env = { ...process.env };
520
+ for (const key of keys) env[key] = '';
521
+ return env;
522
+ }
523
+
476
524
  export async function ensureGlobalCodexSkillsDuringInstall(opts = {}) {
477
525
  if (process.env.SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS === '1' && !opts.force) return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS=1' };
478
526
  const home = opts.home || process.env.HOME || os.homedir();
@@ -789,3 +837,168 @@ async function safeReadText(file, fallback = '') {
789
837
  return fallback;
790
838
  }
791
839
  }
840
+
841
+ export async function selftestCodexLb(tmp) {
842
+ const codexLbHome = path.join(tmp, 'codex-lb-home');
843
+ await ensureDir(path.join(codexLbHome, '.codex'));
844
+ const codexLbFakeBin = path.join(tmp, 'codex-lb-fake-bin');
845
+ await ensureDir(codexLbFakeBin);
846
+ const codexLbFakeCodex = path.join(codexLbFakeBin, 'codex');
847
+ await writeTextAtomic(codexLbFakeCodex, "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo \"codex-cli 99.0.0\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"status\" ]; then echo \"logged in with browser auth\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"--with-api-key\" ]; then read key; mkdir -p \"$HOME/.codex\"; printf '{\\\"auth_mode\\\":\\\"apikey\\\",\\\"key\\\":\\\"%s\\\"}\\n' \"$key\" > \"$HOME/.codex/auth.json\"; printf '%s\\n' \"$key\" >> \"$HOME/.codex/login-calls.log\"; exit 0; fi\necho \"fake codex unsupported\" >&2\nexit 1\n");
848
+ await fsp.chmod(codexLbFakeCodex, 0o755);
849
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "high"\nservice_tier = "fast"\n\n[notice]\nfast_default_opt_out = true\n\n[features]\ncodex_hooks = true\n');
850
+ const codexLbEnvForSelftest = { HOME: codexLbHome, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-global'), PATH: `${codexLbFakeBin}${path.delimiter}${process.env.PATH || ''}` };
851
+ const codexLbSetup = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'setup', '--host', 'lb.example.test', '--api-key', 'sk-test', '--json'], {
852
+ cwd: tmp,
853
+ env: codexLbEnvForSelftest,
854
+ timeoutMs: 15000,
855
+ maxOutputBytes: 64 * 1024
856
+ });
857
+ if (codexLbSetup.code !== 0) throw new Error(`selftest failed: codex-lb setup exited ${codexLbSetup.code}: ${codexLbSetup.stderr}`);
858
+ const codexLbSetupJson = JSON.parse(codexLbSetup.stdout);
859
+ const codexLbConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
860
+ const codexLbEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
861
+ const codexLbAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
862
+ 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_API_KEY='sk-test'") || !/(\"auth_mode\"\s*:\s*\"apikey\")/.test(codexLbAuth)) throw new Error('selftest failed: codex-lb setup did not write provider config, env key, and Codex API-key auth');
863
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n');
864
+ const codexLbRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'auth', 'repair', '--json'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
865
+ if (codexLbRepair.code !== 0) throw new Error(`selftest failed: codex-lb repair exited ${codexLbRepair.code}: ${codexLbRepair.stderr}`);
866
+ const codexLbRepairJson = JSON.parse(codexLbRepair.stdout);
867
+ const codexLbRepairedAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
868
+ if (!codexLbRepairJson.ok || codexLbRepairJson.status !== 'repaired' || !codexLbRepairedAuth.includes('"auth_mode":"apikey"') || !codexLbRepairedAuth.includes('sk-test')) throw new Error('selftest failed: codex-lb repair did not force API-key auth from stored env key');
869
+ const codexLbLoginCallsBeforePostinstall = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
870
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n');
871
+ const codexLbPostinstall = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
872
+ cwd: tmp,
873
+ env: {
874
+ ...codexLbEnvForSelftest,
875
+ SKS_POSTINSTALL_NO_BOOTSTRAP: '1',
876
+ SKS_SKIP_POSTINSTALL_SHIM: '1',
877
+ SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
878
+ SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
879
+ SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
880
+ SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0'
881
+ },
882
+ timeoutMs: 15000,
883
+ maxOutputBytes: 128 * 1024
884
+ });
885
+ if (codexLbPostinstall.code !== 0) throw new Error(`selftest failed: codex-lb postinstall auth preservation exited ${codexLbPostinstall.code}: ${codexLbPostinstall.stderr}`);
886
+ const codexLbPostinstallAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
887
+ const codexLbLoginCallsAfterPostinstall = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
888
+ if (!String(codexLbPostinstall.stdout || '').includes('codex-lb auth: preserved') || !codexLbPostinstallAuth.includes('"auth_mode":"apikey"') || !codexLbPostinstallAuth.includes('sk-test') || codexLbLoginCallsAfterPostinstall <= codexLbLoginCallsBeforePostinstall) throw new Error('selftest failed: postinstall did not preserve codex-lb Codex CLI API-key auth from stored env key');
889
+ 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'];
890
+ const postinstallEnvBefore = Object.fromEntries(postinstallEnvKeys.map((key) => [key, process.env[key]]));
891
+ const codexLbLoginCallsBeforeBootstrap = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
892
+ try {
893
+ for (const key of postinstallEnvKeys) delete process.env[key];
894
+ Object.assign(process.env, {
895
+ HOME: codexLbHome,
896
+ PATH: `${codexLbFakeBin}${path.delimiter}${postinstallEnvBefore.PATH || ''}`,
897
+ INIT_CWD: tmp,
898
+ SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-postinstall-global'),
899
+ SKS_POSTINSTALL_BOOTSTRAP: '1',
900
+ SKS_SKIP_POSTINSTALL_SHIM: '1',
901
+ SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
902
+ SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
903
+ SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
904
+ SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0'
905
+ });
906
+ await postinstall({ bootstrap: async () => writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n') });
907
+ } finally {
908
+ for (const key of postinstallEnvKeys) {
909
+ if (postinstallEnvBefore[key] === undefined) delete process.env[key];
910
+ else process.env[key] = postinstallEnvBefore[key];
911
+ }
912
+ }
913
+ const codexLbPostBootstrapAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
914
+ const codexLbLoginCallsAfterBootstrap = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
915
+ if (!codexLbPostBootstrapAuth.includes('"auth_mode":"apikey"') || !codexLbPostBootstrapAuth.includes('sk-test') || codexLbLoginCallsAfterBootstrap <= codexLbLoginCallsBeforeBootstrap) throw new Error('selftest failed: postinstall did not repair codex-lb auth after bootstrap drift');
916
+ const codexLbContext7Bin = path.join(tmp, 'codex-lb-context7-bin');
917
+ await ensureDir(codexLbContext7Bin);
918
+ 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');
919
+ await fsp.chmod(path.join(codexLbContext7Bin, 'codex'), 0o755);
920
+ const codexLbContext7Postinstall = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
921
+ cwd: tmp,
922
+ env: {
923
+ ...codexLbEnvForSelftest,
924
+ PATH: `${codexLbContext7Bin}${path.delimiter}${process.env.PATH || ''}`,
925
+ CODEX_LB_API_KEY: 'sk-test',
926
+ SKS_POSTINSTALL_NO_BOOTSTRAP: '1',
927
+ SKS_SKIP_POSTINSTALL_SHIM: '1',
928
+ SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
929
+ SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
930
+ SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '1'
931
+ },
932
+ timeoutMs: 15000,
933
+ maxOutputBytes: 128 * 1024
934
+ });
935
+ if (codexLbContext7Postinstall.code !== 0 || String(`${codexLbContext7Postinstall.stdout}\n${codexLbContext7Postinstall.stderr}`).includes('leaked CODEX_LB_API_KEY')) throw new Error('selftest failed: postinstall Context7 setup leaked CODEX_LB_API_KEY to unrelated Codex subprocesses');
936
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_API_KEY='unterminated\n");
937
+ const codexLbLoginCallsBeforeMalformed = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
938
+ const codexLbMalformedPostinstall = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
939
+ cwd: tmp,
940
+ env: {
941
+ ...codexLbEnvForSelftest,
942
+ SKS_POSTINSTALL_NO_BOOTSTRAP: '1',
943
+ SKS_SKIP_POSTINSTALL_SHIM: '1',
944
+ SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
945
+ SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
946
+ SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
947
+ SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0'
948
+ },
949
+ timeoutMs: 15000,
950
+ maxOutputBytes: 128 * 1024
951
+ });
952
+ const codexLbLoginCallsAfterMalformed = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
953
+ if (codexLbMalformedPostinstall.code !== 0 || !String(codexLbMalformedPostinstall.stdout || '').includes('codex-lb auth: stored key missing') || codexLbLoginCallsAfterMalformed !== codexLbLoginCallsBeforeMalformed) throw new Error('selftest failed: malformed codex-lb env should not be passed to Codex login during postinstall');
954
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_API_KEY='sk-test'\n");
955
+ const codexLbMissingCli = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
956
+ cwd: tmp,
957
+ env: {
958
+ HOME: codexLbHome,
959
+ SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-missing-cli-global'),
960
+ PATH: '',
961
+ SKS_POSTINSTALL_NO_BOOTSTRAP: '1',
962
+ SKS_SKIP_POSTINSTALL_SHIM: '1',
963
+ SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
964
+ SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
965
+ SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
966
+ SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0'
967
+ },
968
+ timeoutMs: 15000,
969
+ maxOutputBytes: 128 * 1024
970
+ });
971
+ if (codexLbMissingCli.code !== 0 || !String(codexLbMissingCli.stdout || '').includes('codex-lb auth: repair skipped (codex_missing')) throw new Error('selftest failed: postinstall should handle configured codex-lb when Codex CLI is missing');
972
+ const codexLbNotConfiguredHome = path.join(tmp, 'codex-lb-not-configured-home');
973
+ const codexLbNotConfigured = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
974
+ cwd: tmp,
975
+ env: {
976
+ HOME: codexLbNotConfiguredHome,
977
+ SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-not-configured-global'),
978
+ PATH: '',
979
+ SKS_POSTINSTALL_NO_BOOTSTRAP: '1',
980
+ SKS_SKIP_POSTINSTALL_SHIM: '1',
981
+ SKS_SKIP_POSTINSTALL_CONTEXT7: '1',
982
+ SKS_SKIP_POSTINSTALL_GETDESIGN: '1',
983
+ SKS_SKIP_POSTINSTALL_GLOBAL_SKILLS: '1',
984
+ SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH: '0'
985
+ },
986
+ timeoutMs: 15000,
987
+ maxOutputBytes: 128 * 1024
988
+ });
989
+ if (codexLbNotConfigured.code !== 0 || String(codexLbNotConfigured.stdout || '').includes('codex-lb auth:')) throw new Error('selftest failed: postinstall should stay quiet when codex-lb is not configured');
990
+ const codexLbStatusText = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'status'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
991
+ if (!String(codexLbStatusText.stdout || '').includes('Repair auth: sks codex-lb repair')) throw new Error('selftest failed: codex-lb status did not advertise repair command');
992
+ if (!/^model = "gpt-5\.5"/m.test(codexLbConfig) || !codexLbConfig.includes('service_tier = "fast"') || !codexLbConfig.includes('hooks = true') || codexLbConfig.includes('codex_hooks = true') || !codexLbConfig.includes('fast_mode = true') || !codexLbConfig.includes('fast_mode_ui = 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 failed: codex-lb setup did not preserve Codex App Fast mode defaults, force GPT-5.5, or migrate the hooks feature flag');
993
+ const codexLbLaunch = codexLaunchCommand(tmp, 'codex', []);
994
+ if (!codexLbLaunch.includes('sks-codex-lb.env')) throw new Error('selftest failed: tmux launch command does not source codex-lb env file');
995
+ if (!codexLbLaunch.includes("'--model' 'gpt-5.5'")) throw new Error('selftest failed: tmux launch command without args did not force GPT-5.5');
996
+ if (!codexLbLaunch.includes('SKS_TMUX_LOGO_ANIMATION') || !codexLbLaunch.includes('SNEAKOSCOPE CODEX')) throw new Error('selftest failed: tmux launch command does not include the animated SKS logo intro');
997
+ const madLaunchSource = await safeReadText(path.join(packageRoot(), 'src', 'cli', 'main.mjs'));
998
+ if (!madLaunchSource.includes('const lb = await maybePromptCodexLbSetupForLaunch(args)') || !madLaunchSource.includes("const launchLb = lb.status === 'present'") || !madLaunchSource.includes('codexLbImmediateLaunchOpts(cleanArgs, launchLb')) throw new Error('selftest failed: MAD launch does not sync codex-lb auth and fresh-session launch options');
999
+
1000
+ }
1001
+
1002
+ function hasTopLevelCodexModeLock(text = '') {
1003
+ return /(^|\n)\s*model\s*=\s*"codex-lb"\s*(\n|$)/.test(text) || /(^|\n)\s*model_provider\s*=\s*"openai"\s*(\n|$)/.test(text);
1004
+ }
package/src/cli/main.mjs CHANGED
@@ -55,7 +55,7 @@ import { rgbaKey, rgbaToWikiCoord, validateWikiCoordinateIndex } from '../core/w
55
55
  import { ALLOWED_REASONING_EFFORTS, AWESOME_DESIGN_MD_REFERENCE, CODEX_APP_IMAGE_GENERATION_DOC_URL, CODEX_COMPUTER_USE_EVIDENCE_SOURCE, CODEX_COMPUTER_USE_ONLY_POLICY, CODEX_IMAGEGEN_REQUIRED_POLICY, COMMAND_CATALOG, DESIGN_SYSTEM_SSOT, DOLLAR_COMMAND_ALIASES, DOLLAR_COMMANDS, DOLLAR_SKILL_NAMES, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_SOURCE_INVENTORY_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, FROM_CHAT_IMG_VISUAL_MAP_ARTIFACT, FROM_CHAT_IMG_WORK_ORDER_ARTIFACT, GETDESIGN_REFERENCE, PPT_PIPELINE_SKILL_ALLOWLIST, RECOMMENDED_SKILLS, ROUTES, USAGE_TOPICS, context7ConfigToml, hasContext7ConfigText, hasFromChatImgSignal, looksLikeAnswerOnlyRequest, noUnrequestedFallbackCodePolicyText, reflectionRequiredForRoute, reasoningInstruction, routePrompt, routeReasoning, routeRequiresSubagents, speedLanePolicyText, stackCurrentDocsPolicy, triwikiContextTracking } from '../core/routes.mjs';
56
56
  import { PIPELINE_PLAN_ARTIFACT, buildPipelinePlan, context7Evidence, evaluateStop, projectGateStatus, recordContext7Evidence, recordSubagentEvidence, validatePipelinePlan, writePipelinePlan } from '../core/pipeline.mjs';
57
57
  import { TEAM_DECOMPOSITION_ARTIFACT, TEAM_GRAPH_ARTIFACT, TEAM_INBOX_DIR, TEAM_RUNTIME_TASKS_ARTIFACT, validateTeamRuntimeArtifacts, writeTeamRuntimeArtifacts } from '../core/team-dag.mjs';
58
- import { appendTeamEvent, initTeamLive, parseTeamSpecText, readTeamDashboard, readTeamLive, readTeamTranscriptTail, renderTeamAgentLane } from '../core/team-live.mjs';
58
+ import { appendTeamEvent, initTeamLive, parseTeamSpecText, readTeamDashboard, readTeamLive, readTeamTranscriptTail, renderTeamAgentLane, renderTeamWatch } from '../core/team-live.mjs';
59
59
  import { evaluateTeamReviewPolicyGate } from '../core/team-review-policy.mjs';
60
60
  import { ARTIFACT_FILES, validateDogfoodReport, validateEffortDecision, validateFromChatImgVisualMap, validateSkillCandidate, validateSkillInjectionDecision, validateTeamDashboardState, validateWorkOrderLedger } from '../core/artifact-schemas.mjs';
61
61
  import { selectEffort, writeEffortDecision } from '../core/effort-orchestrator.mjs';
@@ -78,7 +78,7 @@ import { OPENCLAW_SKILL_NAME, installOpenClawSkill } from '../core/openclaw.mjs'
78
78
  import { buildTmuxLaunchPlan, buildTmuxOpenArgs, codexLaunchCommand, createTmuxSession, isTmuxShellSession, runTmuxLaunchPlanSyntaxCheck, shouldAutoAttachTmux, tmuxReadiness, tmuxStatusKind, defaultTmuxSessionName, formatTmuxBanner, launchTmuxTeamView, launchTmuxUi, platformTmuxInstallHint, 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, shouldAutoApproveInstall } from './install-helpers.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';
82
82
  import { buildTeamPlan, codeStructureCommand, dbCommand, defaultBeta, defaultVGraph, evalCommand, gcCommand, goalCommand, gxCommand, harnessCommand, hproofCommand, 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
 
@@ -2281,42 +2281,7 @@ async function selftest() {
2281
2281
  if (explicitBadModelPlan.codexArgs.join(' ').includes('gpt-5.4') || explicitBadModelPlan.codexArgs.join(' ') !== '--model gpt-5.5 --profile legacy-5.4 -c model_reasoning_effort="low"') throw new Error('selftest failed: explicit tmux model override was not forced back to GPT-5.5');
2282
2282
  const codexExecArgs = buildCodexExecArgs({ root: tmp, prompt: 'model guard selftest', profile: 'legacy-5.4', extraArgs: ['--model=gpt-5.4-mini', '--config', 'model = "gpt-5.4"', '-c', 'model_reasoning_effort="medium"'] });
2283
2283
  if (codexExecArgs.join(' ').includes('gpt-5.4') || !codexExecArgs.includes('gpt-5.5') || codexExecArgs.includes('--model=gpt-5.4-mini')) throw new Error('selftest failed: codex exec args allowed a non-GPT-5.5 model override');
2284
- const codexLbHome = path.join(tmp, 'codex-lb-home');
2285
- await ensureDir(path.join(codexLbHome, '.codex'));
2286
- const codexLbFakeBin = path.join(tmp, 'codex-lb-fake-bin');
2287
- await ensureDir(codexLbFakeBin);
2288
- const codexLbFakeCodex = path.join(codexLbFakeBin, 'codex');
2289
- await writeTextAtomic(codexLbFakeCodex, "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo \"codex-cli 99.0.0\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"status\" ]; then echo \"logged in with browser auth\"; exit 0; fi\nif [ \"$1\" = \"login\" ] && [ \"$2\" = \"--with-api-key\" ]; then read key; mkdir -p \"$HOME/.codex\"; printf '{\\\"auth_mode\\\":\\\"apikey\\\",\\\"key\\\":\\\"%s\\\"}\\n' \"$key\" > \"$HOME/.codex/auth.json\"; printf '%s\\n' \"$key\" >> \"$HOME/.codex/login-calls.log\"; exit 0; fi\necho \"fake codex unsupported\" >&2\nexit 1\n");
2290
- await fsp.chmod(codexLbFakeCodex, 0o755);
2291
- await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "high"\nservice_tier = "fast"\n\n[notice]\nfast_default_opt_out = true\n\n[features]\ncodex_hooks = true\n');
2292
- const codexLbEnvForSelftest = { HOME: codexLbHome, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-global'), PATH: `${codexLbFakeBin}${path.delimiter}${process.env.PATH || ''}` };
2293
- const codexLbSetup = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'setup', '--host', 'lb.example.test', '--api-key', 'sk-test', '--json'], {
2294
- cwd: tmp,
2295
- env: codexLbEnvForSelftest,
2296
- timeoutMs: 15000,
2297
- maxOutputBytes: 64 * 1024
2298
- });
2299
- if (codexLbSetup.code !== 0) throw new Error(`selftest failed: codex-lb setup exited ${codexLbSetup.code}: ${codexLbSetup.stderr}`);
2300
- const codexLbSetupJson = JSON.parse(codexLbSetup.stdout);
2301
- const codexLbConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
2302
- const codexLbEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
2303
- const codexLbAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
2304
- 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_API_KEY='sk-test'") || !/(\"auth_mode\"\s*:\s*\"apikey\")/.test(codexLbAuth)) throw new Error('selftest failed: codex-lb setup did not write provider config, env key, and Codex API-key auth');
2305
- await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n');
2306
- const codexLbRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'auth', 'repair', '--json'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2307
- if (codexLbRepair.code !== 0) throw new Error(`selftest failed: codex-lb repair exited ${codexLbRepair.code}: ${codexLbRepair.stderr}`);
2308
- const codexLbRepairJson = JSON.parse(codexLbRepair.stdout);
2309
- const codexLbRepairedAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
2310
- if (!codexLbRepairJson.ok || codexLbRepairJson.status !== 'repaired' || !codexLbRepairedAuth.includes('"auth_mode":"apikey"') || !codexLbRepairedAuth.includes('sk-test')) throw new Error('selftest failed: codex-lb repair did not force API-key auth from stored env key');
2311
- const codexLbStatusText = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'status'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
2312
- if (!String(codexLbStatusText.stdout || '').includes('Repair auth: sks codex-lb repair')) throw new Error('selftest failed: codex-lb status did not advertise repair command');
2313
- if (!/^model = "gpt-5\.5"/m.test(codexLbConfig) || !codexLbConfig.includes('service_tier = "fast"') || !codexLbConfig.includes('hooks = true') || codexLbConfig.includes('codex_hooks = true') || !codexLbConfig.includes('fast_mode = true') || !codexLbConfig.includes('fast_mode_ui = 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 failed: codex-lb setup did not preserve Codex App Fast mode defaults, force GPT-5.5, or migrate the hooks feature flag');
2314
- const codexLbLaunch = codexLaunchCommand(tmp, 'codex', []);
2315
- if (!codexLbLaunch.includes('sks-codex-lb.env')) throw new Error('selftest failed: tmux launch command does not source codex-lb env file');
2316
- if (!codexLbLaunch.includes("'--model' 'gpt-5.5'")) throw new Error('selftest failed: tmux launch command without args did not force GPT-5.5');
2317
- if (!codexLbLaunch.includes('SKS_TMUX_LOGO_ANIMATION') || !codexLbLaunch.includes('SNEAKOSCOPE CODEX')) throw new Error('selftest failed: tmux launch command does not include the animated SKS logo intro');
2318
- const madLaunchSource = await safeReadText(path.join(packageRoot(), 'src', 'cli', 'main.mjs'));
2319
- if (!madLaunchSource.includes('const lb = await maybePromptCodexLbSetupForLaunch(args)') || !madLaunchSource.includes("const launchLb = lb.status === 'present'") || !madLaunchSource.includes('codexLbImmediateLaunchOpts(cleanArgs, launchLb')) throw new Error('selftest failed: MAD launch does not sync codex-lb auth and fresh-session launch options');
2284
+ await selftestCodexLb(tmp);
2320
2285
  if (!shouldAutoAttachTmux(['--mad'], {}, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest failed: MAD tmux launch does not auto-attach in an interactive terminal');
2321
2286
  if (shouldAutoAttachTmux(['--mad', '--json'], {}, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest failed: MAD tmux json mode should not auto-attach');
2322
2287
  if (shouldAutoAttachTmux(['--mad', '--no-attach'], {}, { stdin: { isTTY: true }, stdout: { isTTY: true } })) throw new Error('selftest failed: MAD tmux --no-attach should remain print-only');
@@ -3279,12 +3244,16 @@ async function selftest() {
3279
3244
  if (!roleTeamPlan.roster.debate_team.some((agent) => /inconvenience/.test(agent.persona))) throw new Error('selftest failed: user friction persona missing from debate team');
3280
3245
  const tmuxTeam = await launchTmuxTeamView({ root: tmp, missionId: teamId, plan: roleTeamPlan, json: true });
3281
3246
  if (!tmuxTeam.agents?.length || !tmuxTeam.agents.some((entry) => entry.agent === 'analysis_scout_1') || !tmuxTeam.agents.every((entry) => String(entry.command || '').includes('team lane') && String(entry.command || '').includes('--agent'))) throw new Error('selftest failed: Team tmux view did not expose agent live lanes');
3247
+ if (!roleTeamPlan.roster.analysis_team.every((agent) => tmuxTeam.agents.some((entry) => entry.agent === agent.id))) throw new Error('selftest failed: Team tmux view collapsed numbered analysis scout lanes');
3282
3248
  if (!tmuxTeam.overview?.command?.includes('team watch') || !tmuxTeam.lanes?.some((entry) => entry.role === 'overview') || !tmuxTeam.lanes?.some((entry) => entry.agent === 'analysis_scout_1')) throw new Error('selftest failed: Team tmux view did not expose orchestration overview plus agent lanes');
3283
3249
  if (tmuxTeam.split_ui?.mode !== 'single_window_split_panes' || tmuxTeam.split_ui?.layout !== 'tiled' || tmuxTeam.split_ui?.live_updates !== true) throw new Error('selftest failed: Team tmux view did not expose single-window split UI metadata');
3284
3250
  if (String(tmuxTeam.overview?.command || '').includes('SNEAKOSCOPE CODEX') || !String(tmuxTeam.overview?.command || '').includes('Follow: team watch')) throw new Error('selftest failed: Team tmux pane banner is too noisy or missing compact follow hint');
3285
3251
  if (teamLaneStyle('analysis_scout_1').role !== 'scout' || teamLaneStyle('executor_1').role !== 'execution' || teamLaneStyle('reviewer_1').role !== 'review') throw new Error('selftest failed: Team tmux role palette did not classify lane roles');
3286
3252
  if (!String(tmuxTeam.cleanup_policy || '').includes('mark-complete') || !tmuxTeam.lanes.every((entry) => entry.style?.color && entry.title)) throw new Error('selftest failed: Team tmux view did not expose color/title metadata and cleanup policy');
3287
3253
  if (tmuxTeam.session !== `sks-team-${teamId}` || !tmuxTeam.attach_command?.includes(`sks-team-${teamId}`)) throw new Error('selftest failed: Team tmux session is not named for visibility');
3254
+ await initTeamLive(teamId, teamDir, '역할 팀 테스트', { agentSessions: roleTeamPlan.agent_session_count, roleCounts: roleTeamPlan.role_counts, roster: roleTeamPlan.roster });
3255
+ const teamWatch = await renderTeamWatch(teamDir, { missionId: teamId });
3256
+ if (!roleTeamPlan.roster.analysis_team.every((agent) => teamWatch.includes(`- ${agent.id}:`))) throw new Error('selftest failed: Team watch overview collapsed numbered analysis scout lanes');
3288
3257
  if (routeReasoning(routePrompt('$Research frontier idea'), '$Research frontier idea').effort !== 'xhigh') throw new Error('selftest failed: research reasoning not xhigh');
3289
3258
  if (routeReasoning(routePrompt('$From-Chat-IMG 채팅 이미지 작업'), '$From-Chat-IMG 채팅 이미지 작업').effort !== 'xhigh') throw new Error('selftest failed: From-Chat-IMG reasoning not xhigh');
3290
3259
  if (routeReasoning(routePrompt('$Computer-Use localhost UI smoke'), '$Computer-Use localhost UI smoke').effort !== 'low') throw new Error('selftest failed: Computer Use fast lane reasoning not low');
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.55';
8
+ export const PACKAGE_VERSION = '0.7.56';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
@@ -457,10 +457,7 @@ export async function renderTeamWatch(dir, opts = {}) {
457
457
  const control = await readTeamControl(dir);
458
458
  const runtime = await readJson(path.join(dir, TEAM_RUNTIME_TASKS_ARTIFACT), null);
459
459
  const missionId = opts.missionId || dashboard?.mission_id || runtime?.mission_id || path.basename(dir);
460
- const agents = Object.entries(dashboard?.agents || {});
461
- const visibleAgents = agents
462
- .filter(([name]) => name !== 'parent_orchestrator')
463
- .slice(0, Math.max(3, Number(dashboard?.agent_session_count) || 3));
460
+ const visibleAgents = visibleDashboardAgentEntries(dashboard);
464
461
  const events = (await readTeamTranscriptTail(dir, lines)).map(parseTranscriptLine).filter(Boolean);
465
462
  const runtimeTasks = Array.isArray(runtime?.tasks) ? runtime.tasks : Array.isArray(runtime) ? runtime : [];
466
463
  return [
@@ -497,6 +494,26 @@ export async function renderTeamWatch(dir, opts = {}) {
497
494
  ].filter((line) => line !== null).join('\n');
498
495
  }
499
496
 
497
+ function visibleDashboardAgentEntries(dashboard = {}) {
498
+ const agents = dashboard?.agents || {};
499
+ const roster = dashboard?.roster || {};
500
+ const analysis = uniqueAgentIds(roster.analysis_team || []);
501
+ const debate = uniqueAgentIds(roster.debate_team || []);
502
+ const development = uniqueAgentIds(roster.development_team || []);
503
+ const validation = uniqueAgentIds(roster.validation_team || []);
504
+ const reviewers = validation.filter((id) => /review|qa|validation/i.test(id));
505
+ const reviewerTarget = Math.max(MIN_TEAM_REVIEWER_LANES, Number(dashboard?.role_counts?.reviewer) || 0);
506
+ const reviewLanes = reviewers.slice(0, reviewerTarget);
507
+ const phaseRepresentatives = [development[0], debate[0]].filter(Boolean);
508
+ const requiredVisible = [...analysis, ...reviewLanes, ...phaseRepresentatives];
509
+ const concreteAgentIds = Object.keys(agents).filter((name) => name !== 'parent_orchestrator' && !DEFAULT_AGENTS.includes(name));
510
+ const fallbackAgentIds = Object.keys(agents).filter((name) => name !== 'parent_orchestrator');
511
+ const limit = Math.max(3, Number(dashboard?.agent_session_count) || 3, requiredVisible.length);
512
+ return uniqueAgentIds([...requiredVisible, ...concreteAgentIds, ...debate, ...development, ...validation, ...fallbackAgentIds])
513
+ .slice(0, limit)
514
+ .map((id) => [id, agents[id] || { status: 'pending', phase: null, last_seen: null }]);
515
+ }
516
+
500
517
  function normalizeEvent(event = {}) {
501
518
  return {
502
519
  ts: event.ts || nowIso(),
@@ -509,6 +526,18 @@ function normalizeEvent(event = {}) {
509
526
  };
510
527
  }
511
528
 
529
+ function uniqueAgentIds(agents = []) {
530
+ const ids = [];
531
+ const seen = new Set();
532
+ for (const agent of agents) {
533
+ const id = agent?.id || String(agent || '');
534
+ if (!id || seen.has(id)) continue;
535
+ seen.add(id);
536
+ ids.push(id);
537
+ }
538
+ return ids;
539
+ }
540
+
512
541
  function parseTranscriptLine(line) {
513
542
  try {
514
543
  return JSON.parse(line);
@@ -427,13 +427,16 @@ export async function createTmuxSession(plan = {}, panes = [], opts = {}) {
427
427
  return { ok: true, reused: true, session, panes: [], attach_command: `tmux attach-session -t ${session}` };
428
428
  }
429
429
  const first = normalizedPanes[0] || { cwd: root, command: 'pwd' };
430
- const create = await tmuxRun(tmuxBin, ['new-session', '-d', '-s', session, '-c', path.resolve(first.cwd || root), '-n', 'sks', '-P', '-F', '#{pane_id}', first.command || 'pwd']);
430
+ const detachedWidth = String(Math.max(120, Number(opts.width || opts.detachedWidth) || 180));
431
+ const detachedHeight = String(Math.max(36, Number(opts.height || opts.detachedHeight) || 48));
432
+ const create = await tmuxRun(tmuxBin, ['new-session', '-d', '-x', detachedWidth, '-y', detachedHeight, '-s', session, '-c', path.resolve(first.cwd || root), '-n', 'sks', '-P', '-F', '#{pane_id}', first.command || 'pwd']);
431
433
  if (create.code !== 0) return { ok: false, session, panes: [], stderr: create.stderr || create.stdout || 'tmux new-session failed' };
432
434
  const created = [{ pane_id: paneId(create.stdout), role: first.role || 'overview', title: first.title || 'overview' }];
433
435
  for (const pane of normalizedPanes.slice(1)) {
434
436
  const split = await tmuxRun(tmuxBin, ['split-window', '-t', session, pane.vertical ? '-v' : '-h', '-d', '-P', '-F', '#{pane_id}', '-c', path.resolve(pane.cwd || root), pane.command || 'pwd']);
435
437
  if (split.code !== 0) return { ok: false, session, panes: created, stderr: split.stderr || split.stdout || 'tmux split-window failed' };
436
438
  created.push({ pane_id: paneId(split.stdout), role: pane.role || 'lane', title: pane.title || null });
439
+ await tmuxRun(tmuxBin, ['select-layout', '-t', session, opts.layout || 'tiled']).catch(() => null);
437
440
  }
438
441
  await tmuxRun(tmuxBin, ['select-layout', '-t', session, opts.layout || 'tiled']).catch(() => null);
439
442
  return { ok: true, reused: false, session, panes: created, attach_command: `tmux attach-session -t ${session}` };
@@ -505,9 +508,10 @@ function teamViewAgentIds(plan = {}) {
505
508
  const reviewers = validation.filter((id) => teamLaneStyle(id).role === 'review');
506
509
  const reviewerTarget = Math.max(MIN_TEAM_REVIEWER_LANES, Number(plan.review_policy?.minimum_reviewer_lanes) || 0, Number(plan.role_counts?.reviewer) || 0);
507
510
  const reviewLanes = reviewers.slice(0, reviewerTarget);
508
- const representative = [analysis[0], development[0], debate[0]].filter(Boolean);
509
- const ordered = [...reviewLanes, ...representative, ...analysis, ...debate, ...development, ...validation];
510
- const limit = Math.max(Number(plan.agent_session_count) || MIN_TEAM_REVIEWER_LANES, reviewLanes.length + representative.length);
511
+ const phaseRepresentatives = [development[0], debate[0]].filter(Boolean);
512
+ const requiredVisible = [...analysis, ...reviewLanes, ...phaseRepresentatives];
513
+ const ordered = [...requiredVisible, ...debate, ...development, ...validation];
514
+ const limit = Math.max(Number(plan.agent_session_count) || MIN_TEAM_REVIEWER_LANES, requiredVisible.length);
511
515
  return uniqueAgentIds(ordered).slice(0, Math.max(1, limit));
512
516
  }
513
517