sneakoscope 0.7.67 → 0.7.69

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
@@ -38,25 +38,7 @@ sks selftest --mock
38
38
 
39
39
  ## What Sneakoscope Adds
40
40
 
41
- | Area | What it does |
42
- | --- | --- |
43
- | CLI runtime | Bare `sks` opens/reuses the default tmux Codex CLI workspace. `sks tmux open` handles explicit session flags, and `sks --mad` launches a single-pane MAD session. |
44
- | Codex App commands | Installs generated skills for `$Team`, `$DFix`, `$QA-LOOP`, `$PPT`, `$UX-Review`, `$Goal`, `$DB`, `$Wiki`, `$Help`, and related routes. |
45
- | OpenClaw agents | Generates an OpenClaw skill package so agents can attach `sneakoscope-codex` and discover SKS commands from the repo root. |
46
- | Pipeline plans | Writes `pipeline-plan.json` so runtime lanes, stages, verification, and no-fallback invariants are visible with `sks pipeline plan`. |
47
- | Team orchestration | Runs substantial work through ambiguity handling, scouts, TriWiki, debate, runtime tasks, implementation, review, cleanup, reflection, and Honest Mode; narrow work should use Proof Field evidence. |
48
- | Skill dreaming | Records cheap generated-skill usage counters in JSON and only periodically scans `.agents/skills` for keep, merge, prune, and improvement candidates. Reports are recommendation-only and never delete skills automatically. |
49
- | From-Chat-IMG | Turns chat screenshots plus original attachments into source-bound work orders, then requires scoped QA evidence before completion. |
50
- | QA loop | Dogfoods UI/API behavior with safety gates, Codex Computer Use-only UI evidence, safe fixes, and rechecks. |
51
- | PPT pipeline | Uses `$PPT` for restrained HTML/PDF decks with sealed context, design SSOT, export QA, editable source HTML, and `$imagegen` assets when required. |
52
- | Image UX Review | Uses `$Image-UX-Review` / `$UX-Review` for UI/UX audits that require generated annotated review images before issue extraction. |
53
- | Computer Use fast lane | Uses `$Computer-Use` / `$CU` for fast UI/browser/visual work, then refreshes/validates TriWiki and runs Honest Mode. |
54
- | Goal | Bridges Codex native `/goal` create, pause, resume, and clear controls while implementation continues through the selected SKS route. |
55
- | TriWiki voxels | Maintains `.sneakoscope/wiki/context-pack.json` as the context SSOT with coordinate anchors, voxel metadata, attention hints, and mistake recall. |
56
- | Context7 | Requires current docs for external packages, APIs, MCPs, SDKs, and framework/runtime behavior when correctness depends on current guidance. |
57
- | Design SSOT | Treats `design.md` as the only design decision source of truth; getdesign and curated DESIGN.md examples are inputs, not parallel authorities. |
58
- | DB safety | Treats SQL, migrations, Supabase, RLS, and destructive operations as high risk. |
59
- | Release hygiene | Checks versioning, changelog, size, syntax. |
41
+ `sks` adds a tmux Codex CLI runtime, Codex App `$` commands, Team/QA/PPT/Research/DB/GX/Wiki routes, OpenClaw skill generation, Context7-gated current docs, TriWiki context packs, DB safety, design SSOT policy, skill dreaming, release checks, and Honest Mode.
60
42
 
61
43
  ## Requirements
62
44
 
@@ -172,9 +154,9 @@ sks codex-lb repair
172
154
  sks
173
155
  ```
174
156
 
175
- Bare `sks` can also prompt for codex-lb auth before launch; SKS stores the key in `~/.codex/sks-codex-lb.env`, syncs `codex login --with-api-key`, and loads it into a fresh tmux session.
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.
176
158
 
177
- If Codex CLI auth drifts after a launch or reinstall, run `sks codex-lb repair`; to replace it, run `sks codex-lb reconfigure --host <domain> --api-key <key>`.
159
+ 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>`.
178
160
 
179
161
  ### MAD tmux Launch
180
162
 
@@ -210,6 +192,8 @@ sks qa-loop prepare "http://localhost:3000"
210
192
  sks qa-loop run latest --max-cycles 2
211
193
  sks goal create "persist this migration workflow"
212
194
  sks research prepare "evaluate this approach"
195
+ sks research run latest --max-cycles 3
196
+ sks research status latest
213
197
  sks db scan --json
214
198
  sks wiki refresh
215
199
  sks wiki sweep latest --json
@@ -227,6 +211,8 @@ sks skill-dream run --json
227
211
  sks code-structure scan --json
228
212
  ```
229
213
 
214
+ `sks research` prepares a genius-lens scout council, requires every scout to run at `xhigh`, records one literal `Eureka!` idea per scout, runs an evidence-bound debate, maximizes available web/source retrieval before synthesis, and requires `research-report.md`, `research-paper.md`, `source-ledger.json`, `scout-ledger.json`, `debate-ledger.json`, `novelty-ledger.json`, `falsification-ledger.json`, and `research-gate.json` so research runs stay source-backed, adversarially checked, falsifiable, and paper-ready. `research status` reports source entries, counterevidence, xhigh scout count, Eureka moments, debate exchanges, paper presence/sections, scout findings, and falsification cases alongside the gate.
215
+
230
216
  `sks pipeline plan` shows the active route lane, kept/skipped stages, verification commands, and no-unrequested-fallback invariant. `sks proof-field scan` is the lightweight rubric for small changes; risky or broad signals return to the full Team/Honest path.
231
217
 
232
218
  ### Ambiguity Questions
@@ -273,6 +259,7 @@ $DFix change this label and spacing only
273
259
  $QA-LOOP dogfood localhost:3000 and fix safe issues
274
260
  $PPT create an investor deck as HTML/PDF
275
261
  $Goal persist this migration workflow with native /goal continuation
262
+ $Research investigate this mechanism with source-backed scout lenses
276
263
  $Wiki refresh and validate the context pack
277
264
  $DB inspect this migration for destructive risk
278
265
  ```
@@ -302,41 +289,7 @@ sks openclaw install
302
289
  sks openclaw path
303
290
  ```
304
291
 
305
- By default this writes:
306
-
307
- ```text
308
- ~/.openclaw/skills/sneakoscope-codex/
309
- ```
310
-
311
- The generated skill contains `manifest.yaml`, `SKILL.md`, a skill README, and `openclaw-agent-config.example.yaml`. If you use a custom OpenClaw home, set `OPENCLAW_HOME` or pass `--dir`:
312
-
313
- ```sh
314
- OPENCLAW_HOME=/opt/openclaw sks openclaw install
315
- sks openclaw install --dir /opt/openclaw/skills/sneakoscope-codex
316
- ```
317
-
318
- Attach the skill to an OpenClaw agent with the built-in `shell` tool enabled:
319
-
320
- ```yaml
321
- agents:
322
- coding-agent:
323
- tools:
324
- - shell
325
- env:
326
- SKS_OPENCLAW: "1"
327
- skills:
328
- - sneakoscope-codex
329
- ```
330
-
331
- `SKS_OPENCLAW=1` tells SKS that commands are running from OpenClaw. In that mode, SKS auto-approves update/install prompts such as the Codex CLI update check before tmux launch, instead of waiting for a human `Y/n` response.
332
-
333
- Then prompt the OpenClaw agent from the target repo root:
334
-
335
- ```text
336
- Run sks root, inspect AGENTS.md, then use the SKS Team route to implement this fix and verify it.
337
- ```
338
-
339
- Useful commands for OpenClaw agents:
292
+ By default this writes `~/.openclaw/skills/sneakoscope-codex/` with `manifest.yaml`, `SKILL.md`, a README, and `openclaw-agent-config.example.yaml`. Set `OPENCLAW_HOME` or pass `--dir` for a custom location. Attach the skill with the built-in `shell` tool enabled and set `SKS_OPENCLAW=1` so SKS can auto-approve update/install prompts that would otherwise wait for `Y/n`.
340
293
 
341
294
  ```sh
342
295
  SKS_OPENCLAW=1 sks root
@@ -346,114 +299,43 @@ SKS_OPENCLAW=1 sks deps check
346
299
  SKS_OPENCLAW=1 sks proof-field scan --intent "small CLI change" --changed src/cli/main.mjs
347
300
  ```
348
301
 
349
- If OpenClaw runs the skill inside a sandbox, grant shell execution only for the trusted local workspace. Database, Supabase, migration, and destructive filesystem work should still follow the repo's SKS safety route and require explicit write scope.
302
+ If OpenClaw runs in a sandbox, grant shell execution only for trusted workspaces. Database, migration, and destructive work still follows SKS safety routes.
350
303
 
351
304
  ## Prompt `$` Commands
352
305
 
353
306
  Use these inside Codex App or another agent prompt. They are prompt commands, not terminal commands.
354
307
 
355
- | Prompt | Use when |
356
- | --- | --- |
357
- | `$Team` | You want implementation, code changes, or substantial repo work. |
358
- | `$From-Chat-IMG` | You have a chat screenshot plus original attachments and want each visible request mapped to work. |
359
- | `$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. |
360
- | `$Answer` | You want an answer only and no implementation should start. |
361
- | `$SKS` | You need setup, status, usage, or workflow help. |
362
- | `$QA-LOOP` | You want UI/API dogfooding, safe fixes, and rechecks. |
363
- | `$PPT` | You want a restrained HTML/PDF presentation with sealed delivery context, audience profile, STP strategy, decision context, and 3+ pain-point/solution/aha mappings. |
364
- | `$Computer-Use` / `$CU` | You want the fastest Codex Computer Use lane for UI/browser/visual inspection or small safe fixes. |
365
- | `$Goal` | You want a fast SKS bridge overlay for Codex native persisted `/goal` continuation. |
366
- | `$Research` | You need frontier-style research with hypotheses and falsification. |
367
- | `$AutoResearch` | You want iterative improve/test/keep-or-discard optimization. |
368
- | `$DB` | You need database, Supabase, migration, SQL, or MCP safety checks. |
369
- | `$MAD-SKS` | You explicitly authorize scoped Supabase MCP DB cleanup/write permissions for the active invocation only, while keeping catastrophic wipe safeguards. |
370
- | `$GX` | You need deterministic visual context cartridges. |
371
- | `$Wiki` | You want TriWiki refresh, pack, prune, validate, or maintenance. |
372
- | `$Help` | You want installed command and workflow explanation. |
308
+ Common prompts: `$Team`, `$From-Chat-IMG`, `$DFix`, `$Answer`, `$SKS`, `$QA-LOOP`, `$PPT`, `$Computer-Use`/`$CU`, `$Goal`, `$Research`, `$AutoResearch`, `$DB`, `$MAD-SKS`, `$GX`, `$Wiki`, and `$Help`.
373
309
 
374
310
  ## Common Workflows
375
311
 
376
- ### First Install Checklist
377
-
378
- 1. Install SKS.
312
+ First install:
379
313
 
380
314
  ```sh
381
315
  npm i -g sneakoscope
382
- ```
383
-
384
- 2. Bootstrap and check dependencies.
385
-
386
- ```sh
387
316
  sks bootstrap
388
317
  sks deps check
389
- ```
390
-
391
- On macOS, missing tmux installs and Homebrew-managed tmux upgrades ask `Y/n` before running `brew install tmux` or `brew upgrade tmux`. If PATH resolves an npm-managed `tmux`, SKS prompts for `npm i -g tmux@latest` instead of using Homebrew. Unknown non-Homebrew `tmux` paths are reported as conflicts so the user can remove, upgrade with the owning package manager, or reorder PATH first.
392
-
393
- 3. Confirm Codex App command surfaces.
394
-
395
- ```sh
396
318
  sks codex-app check
397
- sks dollar-commands
398
- ```
399
-
400
- 4. Optional codex-lb key setup for CLI `sks` runs.
401
-
402
- ```sh
403
- sks codex-lb setup --host <domain> --api-key <key>
404
- sks codex-lb repair
405
- sks
406
- ```
407
-
408
- 5. Run a local smoke test.
409
-
410
- ```sh
411
319
  sks selftest --mock
412
320
  ```
413
321
 
414
- ### Start A CLI Workspace
322
+ Start a CLI workspace:
415
323
 
416
324
  ```sh
417
325
  sks tmux check
418
326
  sks
327
+ # or: sks --mad
419
328
  ```
420
329
 
421
- `sks tmux open` is the equivalent explicit launch form when you want to pass tmux session flags.
422
-
423
- For the high-reasoning full-access profile:
424
-
425
- ```sh
426
- sks --mad
427
- ```
428
-
429
- ### Use Codex App `$Team`
430
-
431
- ```text
432
- $Team implement the requested change, update docs if needed, and verify with the relevant tests
433
- ```
434
-
435
- Team mode records a mission under `.sneakoscope/missions/`, keeps a live transcript, uses TriWiki context, and finishes with evidence and Honest Mode.
436
- Every new Team mission now also writes `work-order-ledger.json`, `effort-decision.json`, and `team-dashboard-state.json`. Run `sks validate-artifacts latest` to check the schema gates before treating mission artifacts as completion evidence.
437
-
438
- ### Dogfood A UI Or API
439
-
440
- ```sh
441
- sks qa-loop prepare "http://localhost:3000"
442
- sks qa-loop run latest --max-cycles 2
443
- sks qa-loop status latest
444
- ```
445
-
446
- Use `$QA-LOOP` in Codex App when UI-level E2E needs verification. UI verification must use Codex Computer Use evidence only; Chrome MCP, Browser Use, Playwright, Selenium, Puppeteer, and other browser automation do not satisfy UI-level E2E verification.
330
+ Use Codex App routes with `$Team`, `$DFix`, `$QA-LOOP`, `$PPT`, `$Goal`, `$Wiki`, and `$DB`. Team missions write artifacts under `.sneakoscope/missions/`; validate them with `sks validate-artifacts latest`.
447
331
 
448
- ### Refresh Context Before Risky Work
332
+ Refresh context before risky work:
449
333
 
450
334
  ```sh
451
335
  sks wiki refresh
452
336
  sks wiki validate .sneakoscope/wiki/context-pack.json
453
337
  ```
454
338
 
455
- TriWiki is the long-running context source of truth. It keeps compact high-trust recall in `attention.use_first`, source-hydration targets in `attention.hydrate_first`, and binds relevant prior-mistake claims into the current decision contract when they match the prompt.
456
-
457
339
  ## Safety Model
458
340
 
459
341
  Sneakoscope intentionally treats these as high-risk:
@@ -527,10 +409,12 @@ npm run changelog:check
527
409
  npm run packcheck
528
410
  npm run selftest
529
411
  npm run sizecheck
412
+ npm run registry:check
530
413
  npm run release:check
414
+ npm run publish:dry
531
415
  ```
532
416
 
533
- `release:check` runs audit, changelog, syntax, selftest, size, and registry checks.
417
+ `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.
534
418
 
535
419
  ## License
536
420
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.7.67",
4
+ "version": "0.7.69",
5
5
  "description": "Sneakoscope Codex: database-safe Codex CLI/App harness with Team, Goal, AutoResearch, TriWiki, and Honest Mode.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
@@ -3,7 +3,7 @@ import os from 'node:os';
3
3
  import fsp from 'node:fs/promises';
4
4
  import readline from 'node:readline/promises';
5
5
  import { stdin as input, stdout as output } from 'node:process';
6
- import { ensureDir, exists, globalSksRoot, packageRoot, readText, runProcess, which, writeTextAtomic } from '../core/fsx.mjs';
6
+ import { ensureDir, exists, globalSksRoot, packageRoot, readText, runProcess, tmpdir, which, writeTextAtomic } from '../core/fsx.mjs';
7
7
  import { getCodexInfo } from '../core/codex-adapter.mjs';
8
8
  import { formatHarnessConflictReport, llmHarnessCleanupPrompt, scanHarnessConflicts } from '../core/harness-conflicts.mjs';
9
9
  import { initProject, installSkills } from '../core/init.mjs';
@@ -71,9 +71,10 @@ export async function postinstall({ bootstrap }) {
71
71
 
72
72
  async function reportPostinstallCodexLbAuth() {
73
73
  const codexLbAuth = await ensureCodexLbAuthDuringInstall();
74
- if (codexLbAuth.status === 'synced' || codexLbAuth.status === 'present') console.log(`codex-lb auth: preserved from ${codexLbAuth.env_path}.`);
74
+ if (codexLbAuth.status === 'synced' || codexLbAuth.status === 'present' || codexLbAuth.status === 'repaired') console.log(`codex-lb auth: preserved from ${codexLbAuth.env_path}.`);
75
75
  else if (codexLbAuth.status === 'skipped') console.log(`codex-lb auth: skipped (${codexLbAuth.reason}).`);
76
76
  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.');
77
+ else if (codexLbAuth.status === 'missing_base_url') console.log('codex-lb auth: stored key has no recoverable base URL. Run `sks codex-lb reconfigure --host <domain> --api-key <key>` once.');
77
78
  else if (codexLbAuth.status && codexLbAuth.status !== 'not_configured') console.log(`codex-lb auth: repair skipped (${codexLbAuth.status}${codexLbAuth.error ? `: ${codexLbAuth.error}` : ''}).`);
78
79
  return codexLbAuth;
79
80
  }
@@ -148,11 +149,11 @@ async function capturePostinstallCodexLbConfigSnapshot(home = process.env.HOME |
148
149
  const configPath = codexLbConfigPath(home);
149
150
  const envPath = codexLbEnvPath(home);
150
151
  const config = await readText(configPath, '');
151
- if (!hasTopLevelCodexLbSelected(config)) return null;
152
- const baseUrl = codexLbProviderBaseUrl(config);
152
+ const envText = await readText(envPath, '');
153
+ if (!parseCodexLbEnvKey(envText)) return null;
154
+ const baseUrl = codexLbProviderBaseUrl(config) || parseCodexLbEnvBaseUrl(envText);
153
155
  if (!baseUrl) return null;
154
- if (!parseCodexLbEnvKey(await readText(envPath, ''))) return null;
155
- return { config_path: configPath, env_path: envPath, base_url: baseUrl };
156
+ return { config_path: configPath, env_path: envPath, base_url: normalizeCodexLbBaseUrl(baseUrl) };
156
157
  }
157
158
 
158
159
  async function restorePostinstallCodexLbConfigSnapshot(snapshot) {
@@ -185,7 +186,7 @@ export async function configureCodexLb(opts = {}) {
185
186
  const current = await readText(configPath, '');
186
187
  const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(current, baseUrl));
187
188
  await writeTextAtomic(configPath, next);
188
- await writeTextAtomic(envPath, `export CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`);
189
+ await writeTextAtomic(envPath, `export CODEX_LB_BASE_URL=${shellSingleQuote(baseUrl)}\nexport CODEX_LB_API_KEY=${shellSingleQuote(apiKey)}\n`);
189
190
  await fsp.chmod(envPath, 0o600).catch(() => {});
190
191
  process.env.CODEX_LB_API_KEY = apiKey;
191
192
  const codexLogin = await syncCodexApiKeyLogin(apiKey, { home, force: true });
@@ -202,15 +203,17 @@ export async function codexLbStatus(opts = {}) {
202
203
  const envKeyConfigured = Boolean(parseCodexLbEnvKey(envText));
203
204
  const providerConfigured = /\[model_providers\.codex-lb\]/.test(config);
204
205
  const selected = /model_provider\s*=\s*"codex-lb"/.test(config);
206
+ const baseUrl = codexLbProviderBaseUrl(config) || parseCodexLbEnvBaseUrl(envText) || null;
205
207
  return {
206
- ok: selected && providerConfigured && envKeyConfigured,
208
+ ok: selected && providerConfigured && envKeyConfigured && Boolean(baseUrl),
207
209
  config_path: configPath,
208
210
  env_path: envPath,
209
211
  provider_configured: providerConfigured,
210
212
  selected,
211
213
  env_file: envExists,
212
214
  env_key_configured: envKeyConfigured,
213
- base_url: config.match(/base_url\s*=\s*"([^"]+)"/)?.[1] || null
215
+ env_base_url_configured: Boolean(parseCodexLbEnvBaseUrl(envText)),
216
+ base_url: baseUrl
214
217
  };
215
218
  }
216
219
 
@@ -225,11 +228,20 @@ function codexLbProviderBaseUrl(text = '') {
225
228
  }
226
229
 
227
230
  export async function repairCodexLbAuth(opts = {}) {
228
- const status = await codexLbStatus(opts);
231
+ let status = await codexLbStatus(opts);
232
+ let configRepaired = false;
233
+ if (!status.ok && status.env_key_configured && status.base_url) {
234
+ await ensureDir(path.dirname(status.config_path));
235
+ const current = await readText(status.config_path, '');
236
+ const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(current, status.base_url));
237
+ await writeTextAtomic(status.config_path, next);
238
+ configRepaired = true;
239
+ status = await codexLbStatus(opts);
240
+ }
229
241
  if (!status.ok) {
230
242
  return {
231
243
  ok: false,
232
- status: 'not_configured',
244
+ status: !status.env_key_configured ? 'missing_env_key' : !status.base_url ? 'missing_base_url' : 'not_configured',
233
245
  config_path: status.config_path,
234
246
  env_path: status.env_path,
235
247
  codex_lb: status
@@ -242,6 +254,7 @@ export async function repairCodexLbAuth(opts = {}) {
242
254
  config_path: status.config_path,
243
255
  env_path: status.env_path,
244
256
  base_url: status.base_url,
257
+ config_repaired: configRepaired,
245
258
  codex_lb: status,
246
259
  codex_login: codexLogin
247
260
  };
@@ -251,7 +264,10 @@ export async function ensureCodexLbAuthDuringInstall(opts = {}) {
251
264
  if (process.env.SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH === '1' && !opts.force) return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH=1' };
252
265
  const status = await codexLbStatus(opts);
253
266
  if (!status.selected && !status.provider_configured && !status.env_file) return { status: 'not_configured', codex_lb: status };
254
- 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 };
267
+ if (!status.ok) {
268
+ if (status.env_key_configured && status.base_url) return repairCodexLbAuth(opts);
269
+ return { status: status.env_key_configured ? 'missing_base_url' : 'missing_env_key', codex_lb: status, config_path: status.config_path, env_path: status.env_path };
270
+ }
255
271
  const codexLogin = await ensureCodexLbLoginFromEnv(status, { ...opts, force: true });
256
272
  return {
257
273
  ok: Boolean(codexLogin.ok),
@@ -464,9 +480,18 @@ function shellSingleQuote(value) {
464
480
  }
465
481
 
466
482
  function parseCodexLbEnvKey(text = '') {
467
- const match = String(text || '').match(/^\s*(?:export\s+)?CODEX_LB_API_KEY\s*=\s*(.+?)\s*$/m);
468
- if (!match) return '';
469
- const raw = match[1].trim();
483
+ return parseShellEnvValue(text, 'CODEX_LB_API_KEY');
484
+ }
485
+
486
+ function parseCodexLbEnvBaseUrl(text = '') {
487
+ const value = parseShellEnvValue(text, 'CODEX_LB_BASE_URL');
488
+ return value ? normalizeCodexLbBaseUrl(value) : '';
489
+ }
490
+
491
+ function parseShellEnvValue(text = '', key = '') {
492
+ const re = new RegExp(`^\\s*(?:export\\s+)?${escapeRegExp(key)}\\s*=\\s*(.+?)\\s*$`, 'm');
493
+ const envMatch = String(text || '').match(re);
494
+ const raw = envMatch?.[1]?.trim() || '';
470
495
  if (!raw) return '';
471
496
  if (raw.startsWith("'")) return raw.endsWith("'") && raw.length > 1 ? raw.slice(1, -1).replace(/'\\''/g, "'") : '';
472
497
  if (raw.startsWith('"')) return raw.endsWith('"') && raw.length > 1 ? raw.slice(1, -1).replace(/\\"/g, '"') : '';
@@ -898,27 +923,27 @@ export async function selftestCodexLb(tmp) {
898
923
  timeoutMs: 15000,
899
924
  maxOutputBytes: 64 * 1024
900
925
  });
901
- if (codexLbSetup.code !== 0) throw new Error(`selftest failed: codex-lb setup exited ${codexLbSetup.code}: ${codexLbSetup.stderr}`);
926
+ if (codexLbSetup.code !== 0) throw new Error(`selftest: codex-lb setup exited ${codexLbSetup.code}: ${codexLbSetup.stderr}`);
902
927
  const codexLbSetupJson = JSON.parse(codexLbSetup.stdout);
903
928
  const codexLbConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
904
929
  const codexLbEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
905
930
  const codexLbAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
906
- 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');
931
+ if (!codexLbSetupJson.ok || codexLbSetupJson.base_url !== 'https://lb.example.test/backend-api/codex' || !codexLbConfig.includes('model_provider = "codex-lb"') || !codexLbConfig.includes('[model_providers.codex-lb]') || !codexLbEnv.includes("CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'") || !codexLbEnv.includes("CODEX_LB_API_KEY='sk-test'") || !/(\"auth_mode\"\s*:\s*\"apikey\")/.test(codexLbAuth)) throw new Error('selftest: codex-lb setup');
907
932
  await initProject(codexLbHome, { installScope: 'global', force: true, repair: true });
908
933
  const codexLbRepairSetupConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
909
- if (!codexLbRepairSetupConfig.includes('model_provider = "codex-lb"') || !codexLbRepairSetupConfig.includes('[model_providers.codex-lb]') || !codexLbRepairSetupConfig.includes('https://lb.example.test/backend-api/codex') || codexLbRepairSetupConfig.includes('sk-test')) throw new Error('selftest failed: initProject repair lost codex-lb provider config or exposed the stored key');
934
+ if (!codexLbRepairSetupConfig.includes('model_provider = "codex-lb"') || !codexLbRepairSetupConfig.includes('[model_providers.codex-lb]') || !codexLbRepairSetupConfig.includes('https://lb.example.test/backend-api/codex') || codexLbRepairSetupConfig.includes('sk-test')) throw new Error('selftest: init codex-lb');
910
935
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), `${codexLbConfig}\n[mcp_servers.supabase]\nurl = "https://mcp.supabase.com/mcp?project_ref=ref&read_only=true&features=database,docs"\n`);
911
936
  const ptmp = path.join(tmp, 'codex-lb-project-config'), prevHome = process.env.HOME;
912
937
  try { process.env.HOME = codexLbHome; await initProject(ptmp, { installScope: 'global' }); }
913
938
  finally { if (prevHome === undefined) delete process.env.HOME; else process.env.HOME = prevHome; }
914
939
  const pcfg = await safeReadText(path.join(ptmp, '.codex', 'config.toml'));
915
- if (!pcfg.includes('model_provider = "codex-lb"') || !pcfg.includes('[model_providers.codex-lb]') || !pcfg.includes('[mcp_servers.supabase]') || !pcfg.includes('read_only=true')) throw new Error('selftest failed: project bootstrap lost global codex-lb or MCP config');
940
+ if (!pcfg.includes('model_provider = "codex-lb"') || !pcfg.includes('[model_providers.codex-lb]') || !pcfg.includes('[mcp_servers.supabase]') || !pcfg.includes('read_only=true')) throw new Error('selftest: project codex-lb');
916
941
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n');
917
942
  const codexLbRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'auth', 'repair', '--json'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
918
- if (codexLbRepair.code !== 0) throw new Error(`selftest failed: codex-lb repair exited ${codexLbRepair.code}: ${codexLbRepair.stderr}`);
943
+ if (codexLbRepair.code !== 0) throw new Error(`selftest: codex-lb repair exited ${codexLbRepair.code}: ${codexLbRepair.stderr}`);
919
944
  const codexLbRepairJson = JSON.parse(codexLbRepair.stdout);
920
945
  const codexLbRepairedAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
921
- 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');
946
+ if (!codexLbRepairJson.ok || codexLbRepairJson.status !== 'repaired' || !codexLbRepairedAuth.includes('"auth_mode":"apikey"') || !codexLbRepairedAuth.includes('sk-test')) throw new Error('selftest: codex-lb repair');
922
947
  const codexLbLoginCallsBeforePostinstall = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
923
948
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n');
924
949
  const codexLbPostinstall = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
@@ -935,10 +960,10 @@ export async function selftestCodexLb(tmp) {
935
960
  timeoutMs: 15000,
936
961
  maxOutputBytes: 128 * 1024
937
962
  });
938
- if (codexLbPostinstall.code !== 0) throw new Error(`selftest failed: codex-lb postinstall auth preservation exited ${codexLbPostinstall.code}: ${codexLbPostinstall.stderr}`);
963
+ if (codexLbPostinstall.code !== 0) throw new Error(`selftest: codex-lb postinstall auth preservation exited ${codexLbPostinstall.code}: ${codexLbPostinstall.stderr}`);
939
964
  const codexLbPostinstallAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
940
965
  const codexLbLoginCallsAfterPostinstall = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
941
- 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');
966
+ if (!String(codexLbPostinstall.stdout || '').includes('codex-lb auth: preserved') || !codexLbPostinstallAuth.includes('"auth_mode":"apikey"') || !codexLbPostinstallAuth.includes('sk-test') || codexLbLoginCallsAfterPostinstall <= codexLbLoginCallsBeforePostinstall) throw new Error('selftest: postinstall auth');
942
967
  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'];
943
968
  const postinstallEnvBefore = Object.fromEntries(postinstallEnvKeys.map((key) => [key, process.env[key]]));
944
969
  const codexLbLoginCallsBeforeBootstrap = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
@@ -971,8 +996,25 @@ export async function selftestCodexLb(tmp) {
971
996
  const codexLbPostBootstrapAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
972
997
  const codexLbPostBootstrapConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
973
998
  const codexLbLoginCallsAfterBootstrap = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
974
- 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');
975
- if (!codexLbPostBootstrapConfig.includes('model_provider = "codex-lb"') || !codexLbPostBootstrapConfig.includes('[model_providers.codex-lb]') || !codexLbPostBootstrapConfig.includes('https://lb.example.test/backend-api/codex') || codexLbPostBootstrapConfig.includes('sk-test')) throw new Error('selftest failed: postinstall did not restore codex-lb provider config after bootstrap drift');
999
+ if (!codexLbPostBootstrapAuth.includes('"auth_mode":"apikey"') || !codexLbPostBootstrapAuth.includes('sk-test') || codexLbLoginCallsAfterBootstrap <= codexLbLoginCallsBeforeBootstrap) throw new Error('selftest: postinstall drift auth');
1000
+ if (!codexLbPostBootstrapConfig.includes('model_provider = "codex-lb"') || !codexLbPostBootstrapConfig.includes('[model_providers.codex-lb]') || !codexLbPostBootstrapConfig.includes('https://lb.example.test/backend-api/codex') || codexLbPostBootstrapConfig.includes('sk-test')) throw new Error('selftest: postinstall drift config');
1001
+ const doctorProject = tmpdir();
1002
+ await ensureDir(path.join(doctorProject, '.git'));
1003
+ await writeTextAtomic(path.join(doctorProject, 'package.json'), '{"name":"codex-lb-doctor-project","version":"0.0.0"}\n');
1004
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_BASE_URL='https://lb.example.test/backend-api/codex'\nexport CODEX_LB_API_KEY='sk-test'\n");
1005
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n');
1006
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nservice_tier = "fast"\n\n[features]\nhooks = true\n');
1007
+ const codexLbDoctorRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'doctor', '--fix', '--json'], {
1008
+ cwd: doctorProject,
1009
+ env: { ...codexLbEnvForSelftest, SKS_GLOBAL_ROOT: path.join(tmp, 'codex-lb-doctor-global') },
1010
+ timeoutMs: 30000,
1011
+ maxOutputBytes: 256 * 1024
1012
+ });
1013
+ if (codexLbDoctorRepair.code !== 0) throw new Error(`selftest: doctor --fix codex-lb repair exited ${codexLbDoctorRepair.code}: ${codexLbDoctorRepair.stderr}`);
1014
+ const codexLbDoctorJson = JSON.parse(codexLbDoctorRepair.stdout);
1015
+ const codexLbDoctorAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1016
+ const codexLbDoctorConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1017
+ if (!codexLbDoctorJson.repair?.codex_lb?.ok || !codexLbDoctorJson.repair.codex_lb.config_repaired || !codexLbDoctorJson.codex_lb?.ok || !codexLbDoctorAuth.includes('"auth_mode":"apikey"') || !codexLbDoctorAuth.includes('sk-test') || !codexLbDoctorConfig.includes('model_provider = "codex-lb"') || !codexLbDoctorConfig.includes('https://lb.example.test/backend-api/codex')) throw new Error('selftest: doctor codex-lb');
976
1018
  const codexLbContext7Bin = path.join(tmp, 'codex-lb-context7-bin');
977
1019
  await ensureDir(codexLbContext7Bin);
978
1020
  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');
@@ -992,7 +1034,7 @@ export async function selftestCodexLb(tmp) {
992
1034
  timeoutMs: 15000,
993
1035
  maxOutputBytes: 128 * 1024
994
1036
  });
995
- 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');
1037
+ if (codexLbContext7Postinstall.code !== 0 || String(`${codexLbContext7Postinstall.stdout}\n${codexLbContext7Postinstall.stderr}`).includes('leaked CODEX_LB_API_KEY')) throw new Error('selftest: Context7 key leak');
996
1038
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_API_KEY='unterminated\n");
997
1039
  const codexLbLoginCallsBeforeMalformed = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
998
1040
  const codexLbMalformedPostinstall = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
@@ -1010,7 +1052,7 @@ export async function selftestCodexLb(tmp) {
1010
1052
  maxOutputBytes: 128 * 1024
1011
1053
  });
1012
1054
  const codexLbLoginCallsAfterMalformed = (await safeReadText(path.join(codexLbHome, '.codex', 'login-calls.log'))).trim().split(/\r?\n/).filter(Boolean).length;
1013
- 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');
1055
+ if (codexLbMalformedPostinstall.code !== 0 || !String(codexLbMalformedPostinstall.stdout || '').includes('codex-lb auth: stored key missing') || codexLbLoginCallsAfterMalformed !== codexLbLoginCallsBeforeMalformed) throw new Error('selftest: bad codex-lb env');
1014
1056
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'), "export CODEX_LB_API_KEY='sk-test'\n");
1015
1057
  const codexLbMissingCli = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
1016
1058
  cwd: tmp,
@@ -1028,7 +1070,7 @@ export async function selftestCodexLb(tmp) {
1028
1070
  timeoutMs: 15000,
1029
1071
  maxOutputBytes: 128 * 1024
1030
1072
  });
1031
- 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');
1073
+ if (codexLbMissingCli.code !== 0 || !String(codexLbMissingCli.stdout || '').includes('codex-lb auth: repair skipped (codex_missing')) throw new Error('selftest: codex missing');
1032
1074
  const codexLbNotConfiguredHome = path.join(tmp, 'codex-lb-not-configured-home');
1033
1075
  const codexLbNotConfigured = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'postinstall'], {
1034
1076
  cwd: tmp,
@@ -1046,16 +1088,16 @@ export async function selftestCodexLb(tmp) {
1046
1088
  timeoutMs: 15000,
1047
1089
  maxOutputBytes: 128 * 1024
1048
1090
  });
1049
- 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');
1091
+ 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');
1050
1092
  const codexLbStatusText = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'codex-lb', 'status'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
1051
- if (!String(codexLbStatusText.stdout || '').includes('Repair auth: sks codex-lb repair')) throw new Error('selftest failed: codex-lb status did not advertise repair command');
1052
- 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 failed: 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');
1093
+ if (!String(codexLbStatusText.stdout || '').includes('Repair auth: sks codex-lb repair')) throw new Error('selftest: codex-lb status did not advertise repair command');
1094
+ 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');
1053
1095
  const codexLbLaunch = codexLaunchCommand(tmp, 'codex', []);
1054
- if (!codexLbLaunch.includes('sks-codex-lb.env')) throw new Error('selftest failed: tmux launch command does not source codex-lb env file');
1055
- if (!codexLbLaunch.includes("'--model' 'gpt-5.5'")) throw new Error('selftest failed: tmux launch command without args did not force GPT-5.5');
1056
- 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');
1096
+ if (!codexLbLaunch.includes('sks-codex-lb.env')) throw new Error('selftest: tmux launch command does not source codex-lb env file');
1097
+ if (!codexLbLaunch.includes("'--model' 'gpt-5.5'")) throw new Error('selftest: tmux launch command without args did not force GPT-5.5');
1098
+ 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');
1057
1099
  const madLaunchSource = await safeReadText(path.join(packageRoot(), 'src', 'cli', 'maintenance-commands.mjs'));
1058
- 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');
1100
+ 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');
1059
1101
 
1060
1102
  }
1061
1103