sneakoscope 0.7.55 → 0.7.57
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 +9 -7
- package/package.json +1 -1
- package/src/cli/install-helpers.mjs +220 -7
- package/src/cli/main.mjs +98 -174
- package/src/cli/maintenance-commands.mjs +181 -21
- package/src/core/auto-review.mjs +2 -0
- package/src/core/fsx.mjs +1 -1
- package/src/core/init.mjs +12 -12
- package/src/core/openclaw.mjs +1 -1
- package/src/core/pipeline.mjs +26 -8
- package/src/core/routes.mjs +53 -13
- package/src/core/team-dag.mjs +23 -0
- package/src/core/team-live.mjs +143 -25
- package/src/core/tmux-ui.mjs +99 -10
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ sks selftest --mock
|
|
|
42
42
|
|
|
43
43
|
| Area | What it does |
|
|
44
44
|
| --- | --- |
|
|
45
|
-
| CLI runtime | Bare `sks` opens or reuses the default tmux Codex CLI workspace. `sks tmux open` remains the explicit form for session/workspace flags, and `sks --mad` launches the explicit full-access high-reasoning profile. |
|
|
45
|
+
| CLI runtime | Bare `sks` opens or reuses the default tmux Codex CLI workspace. `sks tmux open` remains the explicit form for session/workspace flags, and `sks --mad` launches a multi-pane MAD tmux cockpit with the explicit full-access high-reasoning profile. |
|
|
46
46
|
| Codex App commands | Installs generated skills so `$Team`, `$From-Chat-IMG`, `$DFix`, `$QA-LOOP`, `$PPT`, `$Image-UX-Review`, `$UX-Review`, `$Goal`, `$DB`, `$Wiki`, `$Help`, and related routes are visible in prompt workflows. `sks codex-app remote-control` wraps Codex CLI 0.130.0+ headless remote control without falling back to older app-server internals. |
|
|
47
47
|
| OpenClaw agents | Generates an OpenClaw skill package so OpenClaw agents can attach `sneakoscope-codex`, enable the `shell` tool, and discover/use SKS commands from the target repo root. |
|
|
48
48
|
| Pipeline plans | Writes `pipeline-plan.json` for stateful routes so the runtime lane, kept stages, skipped stages, verification commands, and no-unrequested-fallback invariant are visible with `sks pipeline plan`. |
|
|
@@ -53,7 +53,7 @@ sks selftest --mock
|
|
|
53
53
|
| PPT pipeline | Uses `$PPT` for simple, restrained, information-first HTML/PDF presentation artifacts, first asking delivery context, audience profile, STP strategy, decision context, and 3+ pain-point to solution/aha mappings before source research, design-system work, HTML/PDF export, and render QA. Independent strategy/render/file-write phases run in parallel where inputs allow and are recorded in `ppt-parallel-report.json`; editable source HTML is preserved under `source-html/`, PPT-only temporary build files are cleaned after completion, installed skills/MCPs outside the `$PPT` allowlist are ignored, generated image assets must use real `$imagegen`/`gpt-image-2` output when sealed in the contract, and `ppt-style-tokens.json` records the design SSOT plus fused source inputs. |
|
|
54
54
|
| Image UX Review | Uses `$Image-UX-Review` / `$UX-Review` for UI/UX audits where source screenshots are first turned into generated annotated review images through Codex App `$imagegen`/`gpt-image-2`; those generated images are then read back into `image-ux-issue-ledger.json`, optional requested fixes are rechecked, and missing generated review images or text-only screenshot critique cannot pass `image-ux-review-gate.json`. |
|
|
55
55
|
| Computer Use fast lane | Uses `$Computer-Use` / `$CU` for UI/browser/visual work that needs maximum speed: skip Team debate and upfront TriWiki loops, use Codex Computer Use directly, then refresh/validate TriWiki and run Honest Mode at final closeout. |
|
|
56
|
-
| Goal | Provides a fast SKS bridge overlay for Codex native persisted `/goal` create, pause, resume, and clear controls; implementation continues through the selected SKS execution route. |
|
|
56
|
+
| Goal | Provides a fast SKS bridge overlay for Codex native persisted `/goal` create, pause, resume, and clear controls; an ambient non-disruptive Goal continuation overlay is also recorded in normal pipeline plans while implementation continues through the selected SKS execution route. |
|
|
57
57
|
| TriWiki voxels | Maintains `.sneakoscope/wiki/context-pack.json` as the context SSOT with coordinate anchors, voxel metadata, `attention.use_first`, `attention.hydrate_first`, and prompt-bound mistake recall ledgers. |
|
|
58
58
|
| Context7 | Requires current docs for external packages, APIs, MCPs, SDKs, and framework/runtime behavior when correctness depends on current guidance. |
|
|
59
59
|
| Design SSOT | Treats `design.md` as the only design decision source of truth. `docs/Design-Sys-Prompt.md` is the builder prompt; getdesign.md, official getdesign docs, and curated DESIGN.md examples from `VoltAgent/awesome-design-md` are source inputs that must be fused into `design.md` or route-local style tokens instead of becoming parallel authorities. |
|
|
@@ -80,7 +80,7 @@ The default `sks` runtime checks npm for newer `sneakoscope` and `@openai/codex`
|
|
|
80
80
|
- Checks npm for newer `sneakoscope` and `@openai/codex` versions before launch and asks whether to update when the terminal can answer y/n.
|
|
81
81
|
- Installs the latest Codex CLI with `npm i -g @openai/codex@latest` when it is missing and you approve or pass `--yes`.
|
|
82
82
|
- Requires tmux 3.x or newer before opening the session.
|
|
83
|
-
- Creates
|
|
83
|
+
- Creates a named detached tmux cockpit with multiple panes and prints only the session, gate, attach, and blocker details needed to act.
|
|
84
84
|
|
|
85
85
|
## Installation
|
|
86
86
|
|
|
@@ -167,7 +167,7 @@ sks tmux check
|
|
|
167
167
|
sks tmux status --once
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
-
Bare `sks` creates or reuses the default named tmux session for Codex CLI and attaches to it in an interactive terminal. By default it launches Codex in
|
|
170
|
+
Bare `sks` creates or reuses the default named tmux session for Codex CLI and attaches to it in an interactive terminal. By default it launches Codex in Fast service tier with `--model gpt-5.5`, `-c service_tier="fast"`, and the selected `model_reasoning_effort` with a static SKS 3D ASCII intro inside tmux; the animated intro is reserved for non-tmux unauthenticated Codex launches and can be disabled with `SKS_TMUX_LOGO_ANIMATION=0`. SKS always forces the model to `gpt-5.5`; `SKS_CODEX_MODEL` and `SKS_CODEX_FAST_HIGH=0` cannot downgrade or remove that model pin. You can still set `SKS_CODEX_REASONING` to change reasoning effort. Use `sks tmux open` when you need explicit `--workspace` / `--session` flags, `sks tmux check` for readiness without launching, and `sks help` for CLI help. Use `--no-attach` or `SKS_TMUX_NO_AUTO_ATTACH=1` when you only want SKS to create/reuse the session and print the manual attach command.
|
|
171
171
|
|
|
172
172
|
Before opening tmux, SKS checks the installed Codex CLI against npm `@openai/codex@latest`. If a newer version exists, it asks `Y/n`; answering `y` updates automatically with `npm i -g @openai/codex@latest` and then opens tmux with the updated Codex CLI.
|
|
173
173
|
|
|
@@ -211,7 +211,7 @@ sks --mad
|
|
|
211
211
|
sks --mad --yes
|
|
212
212
|
```
|
|
213
213
|
|
|
214
|
-
This syncs existing codex-lb/Codex CLI auth before launch, creates/uses the `sks-mad-high` Codex profile for a one-shot full-access, high-reasoning tmux
|
|
214
|
+
This syncs existing codex-lb/Codex CLI auth before launch, creates/uses the `sks-mad-high` Codex profile for a one-shot full-access, high-reasoning tmux cockpit with `sandbox_mode = "danger-full-access"` and `approval_policy = "never"`, opens an active MAD-SKS permission gate for that tmux run, then launches a tiled cockpit: Codex CLI, MAD permission gate status, and live guide panes. The cockpit recreates the named session on launch so stale single-pane sessions do not hide the split layout, then attaches in an interactive terminal. If codex-lb is configured and no explicit `--workspace`/`--session` was passed, SKS opens a fresh tmux session so the repaired key is loaded by the Codex process immediately. While the gate is active, live server work, Supabase MCP database writes, direct SQL, targeted DML, schema cleanup, Supabase MCP `apply_migration`, and required Supabase CLI migration application such as `supabase migration up` or `supabase db push` are allowed. Catastrophic database wipe/all-row/project-management safeguards remain active.
|
|
215
215
|
|
|
216
216
|
MAD does not disable the pipeline contract: stages, executors, reviewers, and auto-review policy still must not invent unrequested fallback implementation code. If the requested path cannot be implemented, SKS should block with evidence rather than add substitute behavior.
|
|
217
217
|
|
|
@@ -239,10 +239,12 @@ sks team log latest
|
|
|
239
239
|
|
|
240
240
|
By default, Team missions keep at least five QA/reviewer lanes active. Use explicit role counts only when you need to raise or otherwise pin the lane mix for a specific mission.
|
|
241
241
|
|
|
242
|
-
Team mode prepares the mission, records live events, compiles runtime tasks and worker inboxes, writes schema-backed effort/work-order/dashboard artifacts, and opens a named tmux Team session with split live lanes when tmux is available. The default terminal output stays compact: mission id, agent count, role count, tmux status, watch command, and artifact directory. `sks team dashboard` renders the cockpit panes for mission overview, agent lanes, task DAG, QA/dogfood, artifacts/evidence, and performance.
|
|
242
|
+
Team mode prepares the mission, records live events, compiles runtime tasks and worker inboxes, writes schema-backed effort/work-order/dashboard artifacts, and opens a named tmux Team session with split live lanes by default when tmux is available. Use `--no-open-tmux` for artifact-only mission creation. The default terminal output stays compact: mission id, agent count, role count, tmux status, watch command, and artifact directory. `sks team dashboard` renders the cockpit panes for mission overview, agent lanes, task DAG, QA/dogfood, artifacts/evidence, and performance.
|
|
243
243
|
|
|
244
244
|
The tmux Team launch is a live orchestration screen in one tmux window: the first pane follows `sks team watch <mission-id> --follow` as the mission overview, and neighboring split panes follow individual `sks team lane <mission-id> --agent <name> --follow` views. Pane headers show only mission, lane, phase, follow command, and cleanup command. SKS gives lanes role-specific colors, labels, and terminal titles, so scouts, planning/debate voices, executors, reviewers, and safety lanes are visually distinct while detailed evidence is mirrored into `team-transcript.jsonl`, `team-live.md`, and `team-dashboard.json`.
|
|
245
245
|
|
|
246
|
+
Team roster and runtime artifacts now include per-agent Fast reasoning metadata. Simple bounded Team lanes can use low reasoning, tool-heavy runtime/CLI/tmux work uses medium, and knowledge, current-docs, safety, DB, release, commit, or research-heavy lanes use high or xhigh as appropriate instead of opening every scout at high.
|
|
247
|
+
|
|
246
248
|
Agent sessions communicate through the bounded Team transcript. Use `sks team message <mission-id|latest> --from <agent> --to <agent|all> --message "..."` to add direct or broadcast messages; lane panes show messages addressed to that agent plus the fallback global tail.
|
|
247
249
|
|
|
248
250
|
When the Team route reaches `session_cleanup`, SKS marks the tmux session record complete and asks `watch --follow` / `lane --follow` panes to show a cleanup summary and stop. You can also run `sks team cleanup-tmux <mission-id|latest>` manually, or `sks team cleanup-tmux latest --close` to kill the recorded tmux session.
|
|
@@ -412,7 +414,7 @@ Use these inside Codex App or another agent prompt. They are prompt commands, no
|
|
|
412
414
|
| --- | --- |
|
|
413
415
|
| `$Team` | You want implementation, code changes, or substantial repo work. |
|
|
414
416
|
| `$From-Chat-IMG` | You have a chat screenshot plus original attachments and want each visible request mapped to work. |
|
|
415
|
-
| `$DFix` | You need
|
|
417
|
+
| `$DFix` | You need Direct Fix work: tiny copy/config/docs/labels/spacing/translation/simple mechanical edits, with broad implementation still routed to Team and UI design specifics handled by the relevant UI/design route rules. |
|
|
416
418
|
| `$Answer` | You want an answer only and no implementation should start. |
|
|
417
419
|
| `$SKS` | You need setup, status, usage, or workflow help. |
|
|
418
420
|
| `$QA-LOOP` | You want UI/API dogfooding, safe fixes, and rechecks. |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.57",
|
|
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 =
|
|
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
|
|
398
|
-
if (raw.startsWith('"
|
|
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
|
|
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
|
|
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', 'maintenance-commands.mjs'));
|
|
998
|
+
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 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
|
+
}
|