sneakoscope 0.8.6 → 0.9.1

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
@@ -59,6 +59,19 @@ Research scouts now use named persona-inspired cognitive lenses: Einstein Scout,
59
59
 
60
60
  For existing 0.7.x users, the visible change is new report-only evidence, not a route personality rewrite. Team still feels like Team, DFix stays ultralight, DB remains conservative, QA-LOOP still dogfoods, PPT stays information-first, imagegen still requires real raster evidence, and Honest Mode remains the final truth pass. The original strong reminder idea became neutral RecallPulse so user-facing prompts stay short, professional, and non-repetitive; hook messages can point at status, but `mission-status-ledger.json` is the durable source when app-visible text disappears. The planning source is `docs/RECALLPULSE_0_8_0_TASKS.md`, and implementation is designed to land in safe task-sized slices before any enforcement promotion.
61
61
 
62
+ ## 0.9.0 Report-Only Decision Lattice
63
+
64
+ Sneakoscope 0.9.0 adds a report-only Decision Lattice planner that uses A* over proof-debt signals to explain which route or verification path the pipeline would prefer. It is an evidence and planning surface, not a runtime shortcut: SKS must not claim speedup, fast-lane accuracy, or reduced verification cost from the lattice until replay or scored eval evidence demonstrates those outcomes.
65
+
66
+ The lattice integrates with the existing proof-field and `sks pipeline plan` surfaces. Its reports are expected to show the explored frontier, the selected path, and rejected paths with their proof-debt reasons, so reviewers can audit why a route stayed on the full Team/Honest path or why a smaller verification plan was only proposed. Like RecallPulse, this is designed to land as report-only evidence first; route enforcement and performance claims remain gated by later validation.
67
+
68
+ Quick checks:
69
+
70
+ ```bash
71
+ sks proof-field scan --json --intent "small CLI change"
72
+ sks pipeline plan latest --proof-field --json
73
+ ```
74
+
62
75
  ## Requirements
63
76
 
64
77
  - Node.js `>=20.11`
@@ -174,7 +187,7 @@ sks codex-lb repair
174
187
  sks
175
188
  ```
176
189
 
177
- Bare `sks` can also prompt for codex-lb auth; SKS stores the base URL/key in `~/.codex/sks-codex-lb.env`, writes `model_provider = "codex-lb"` into `~/.codex/config.toml` for Codex App routing, loads the provider env key for tmux launches, and syncs the macOS user launch environment so the Codex App can see `CODEX_LB_API_KEY` after restart. npm postinstall upgrades resync that stored env file and restore the Codex App provider selection when postinstall is not stopped by a hard harness conflict, and `sks doctor --fix` does the same during repair. If an older SKS release left the codex-lb dashboard key only in the shared Codex `auth.json` login cache, SKS migrates that key back into `~/.codex/sks-codex-lb.env` when a codex-lb provider or env base URL is already recoverable. It does not rewrite the shared Codex `auth.json` login cache by default; set `SKS_CODEX_LB_SYNC_CODEX_LOGIN=1` only if you intentionally want the old API-key login-cache behavior. When codex-lb is active, SKS opens a fresh `sks-codex-lb-*` tmux session and sweeps older detached codex-lb sessions for the same repo before launch so stale Responses API chains are not reused. Configured launch paths, including non-interactive runs, verify that codex-lb can continue a Responses API chain with `previous_response_id`; if that check fails, SKS bypasses codex-lb for that launch with `model_provider="openai"` instead of letting the Codex session fail mid-work.
190
+ Bare `sks` can also prompt for codex-lb auth; SKS stores the base URL/key in `~/.codex/sks-codex-lb.env`, writes the upstream codex-lb Codex CLI / IDE Extension provider block into `~/.codex/config.toml` for Codex App routing, loads the provider env key for tmux launches, and syncs the macOS user launch environment so the Codex App can see `CODEX_LB_API_KEY` after restart. If the provider block disappears but the stored env file is still recoverable, bare `sks`, npm postinstall upgrades, `sks doctor --fix`, and `sks codex-lb repair` restore it with `env_key = "CODEX_LB_API_KEY"`, `supports_websockets = true`, and `requires_openai_auth = true` as documented by codex-lb. If an older SKS release left the codex-lb dashboard key only in the shared Codex `auth.json` login cache, SKS migrates that key back into `~/.codex/sks-codex-lb.env` when a codex-lb provider or env base URL is already recoverable. It does not rewrite the shared Codex `auth.json` login cache by default; set `SKS_CODEX_LB_SYNC_CODEX_LOGIN=1` only if you intentionally want the old API-key login-cache behavior. When codex-lb is active, SKS opens a fresh `sks-codex-lb-*` tmux session and sweeps older detached codex-lb sessions for the same repo before launch so stale Responses API chains are not reused. Configured launch paths, including non-interactive runs, verify that codex-lb can continue a Responses API chain with `previous_response_id`; if that check fails, SKS bypasses codex-lb for that launch with `model_provider="openai"` instead of letting the Codex session fail mid-work.
178
191
 
179
192
  If codex-lb provider 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>`.
180
193
 
@@ -239,7 +252,7 @@ sks code-structure scan --json
239
252
 
240
253
  `sks recallpulse` is the 0.8.0 report-only RecallPulse utility. It writes `recallpulse-decision.json`, `mission-status-ledger.json`, `route-proof-capsule.json`, `evidence-envelope.json`, `recallpulse-governance-report.json`, `recallpulse-task-goal-ledger.json`, and `recallpulse-eval-report.json` for the current mission. RecallPulse does not replace route gates, Honest Mode, DB safety, imagegen evidence, or TriWiki validation; it records cache hits, hydration needs, duplicate suppression, route-governance risks, and final-summary-ready durable status so later releases can promote only measured improvements. Checklist updates are sequential: every `Txxx` row is treated as a child `$Goal` checkpoint, and `sks recallpulse checklist ... --task T001 --apply` refuses out-of-order checks unless explicitly overridden.
241
254
 
242
- `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.
255
+ `sks pipeline plan` shows the active route lane, kept/skipped stages, verification commands, and no-unrequested-fallback invariant. The 0.9.0 Decision Lattice augments this planning surface with report-only A*/proof-debt evidence: frontier paths considered, the selected path, and rejected paths with rejection reasons. `sks proof-field scan` remains the lightweight rubric for small changes; risky or broad signals return to the full Team/Honest path, and no speedup claim is valid without replay or eval evidence.
243
256
 
244
257
  ### Ambiguity Questions
245
258
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.8.6",
4
+ "version": "0.9.1",
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",
@@ -230,11 +230,13 @@ export async function codexLbStatus(opts = {}) {
230
230
  const providerConfigured = /\[model_providers\.codex-lb\]/.test(config);
231
231
  const selected = hasTopLevelCodexLbSelected(config);
232
232
  const baseUrl = codexLbProviderBaseUrl(config) || parseCodexLbEnvBaseUrl(envText) || null;
233
+ const providerRequiresOpenAiAuth = codexLbProviderRequiresOpenAiAuth(config);
233
234
  return {
234
- ok: providerConfigured && envKeyConfigured && Boolean(baseUrl),
235
+ ok: providerConfigured && envKeyConfigured && Boolean(baseUrl) && providerRequiresOpenAiAuth,
235
236
  config_path: configPath,
236
237
  env_path: envPath,
237
238
  provider_configured: providerConfigured,
239
+ provider_requires_openai_auth: providerRequiresOpenAiAuth,
238
240
  selected,
239
241
  env_file: envExists,
240
242
  env_key_configured: envKeyConfigured,
@@ -373,6 +375,11 @@ function codexLbProviderBaseUrl(text = '') {
373
375
  return block.match(/(^|\n)\s*base_url\s*=\s*"([^"]+)"/)?.[2] || '';
374
376
  }
375
377
 
378
+ function codexLbProviderRequiresOpenAiAuth(text = '') {
379
+ const block = String(text || '').match(/(^|\n)\[model_providers\.codex-lb\]([\s\S]*?)(?=\n\[[^\]]+\]|\s*$)/)?.[2] || '';
380
+ return /(^|\n)\s*requires_openai_auth\s*=\s*true\s*(?:#.*)?(?=\n|$)/.test(block);
381
+ }
382
+
376
383
  export async function repairCodexLbAuth(opts = {}) {
377
384
  let status = await codexLbStatus(opts);
378
385
  let configRepaired = false;
@@ -387,7 +394,7 @@ export async function repairCodexLbAuth(opts = {}) {
387
394
  status = await codexLbStatus(opts);
388
395
  }
389
396
  }
390
- if (status.env_key_configured && status.base_url && (!status.ok || !status.selected || legacyAuthMigrated || hasTopLevelCodexModeLock(currentConfig))) {
397
+ if (status.env_key_configured && status.base_url && (!status.ok || !status.selected || !status.provider_requires_openai_auth || legacyAuthMigrated || hasTopLevelCodexModeLock(currentConfig))) {
391
398
  await ensureDir(path.dirname(status.config_path));
392
399
  const next = normalizeCodexFastModeUiConfig(upsertCodexLbConfig(currentConfig, status.base_url));
393
400
  await writeTextAtomic(status.config_path, next);
@@ -426,7 +433,7 @@ export async function ensureCodexLbAuthDuringInstall(opts = {}) {
426
433
  if (process.env.SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH === '1' && !opts.force) return { status: 'skipped', reason: 'SKS_SKIP_POSTINSTALL_CODEX_LB_AUTH=1' };
427
434
  const status = await codexLbStatus(opts);
428
435
  if (!status.selected && !status.provider_configured && !status.env_file) return { status: 'not_configured', codex_lb: status };
429
- if (status.ok && !status.selected) return repairCodexLbAuth(opts);
436
+ if (status.ok && (!status.selected || !status.provider_requires_openai_auth)) return repairCodexLbAuth(opts);
430
437
  if (!status.ok) {
431
438
  if (status.base_url && (status.env_key_configured || status.provider_configured || status.selected || status.env_base_url_configured)) return repairCodexLbAuth(opts);
432
439
  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 };
@@ -465,7 +472,21 @@ async function restoreCodexLbEnvFromSharedLogin(status = {}, opts = {}) {
465
472
 
466
473
  export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
467
474
  if (args.includes('--json') || args.includes('--skip-codex-lb') || process.env.SKS_SKIP_CODEX_LB_PROMPT === '1') return { status: 'skipped' };
468
- const status = await codexLbStatus(opts);
475
+ let status = await codexLbStatus(opts);
476
+ if (status.env_key_configured && status.base_url && (!status.provider_configured || !status.selected || !status.provider_requires_openai_auth)) {
477
+ let promptedRestore = false;
478
+ if (!status.provider_configured && canAskYesNo()) {
479
+ promptedRestore = true;
480
+ const restore = (await askPostinstallQuestion('\ncodex-lb provider section is missing, but stored auth exists. Restore and route Codex through codex-lb? [Y/n] ')).trim();
481
+ if (/^(n|no|아니|아니요|ㄴ)$/i.test(restore)) return { status: 'continued_to_codex', codex_lb: status };
482
+ }
483
+ const repaired = await repairCodexLbAuth(opts);
484
+ status = await codexLbStatus(opts);
485
+ if (!status.ok) return { status: 'repair_failed', ok: false, repair: repaired, codex_lb: status };
486
+ if (!repaired.ok && repaired.error && promptedRestore) console.log(`codex-lb provider restored, but launch environment sync reported: ${repaired.error}`);
487
+ else if (!repaired.ok && promptedRestore) console.log(`codex-lb provider restored, but provider auth sync reported: ${repaired.status}`);
488
+ else if (repaired.config_repaired && promptedRestore) console.log(`codex-lb provider restored: ${status.base_url}`);
489
+ }
469
490
  if (status.ok) {
470
491
  const codexEnvironment = await syncCodexLbProviderEnvironment(status, opts);
471
492
  if (codexEnvironment.status === 'synced') console.log('codex-lb provider auth synced for this user session.');
@@ -480,7 +501,7 @@ export async function maybePromptCodexLbSetupForLaunch(args = [], opts = {}) {
480
501
  return { status: 'present', ...status, codex_environment: codexEnvironment, codex_login: codexLogin, chain_health: chainHealth };
481
502
  }
482
503
  if (!canAskYesNo()) return { status: 'non_interactive', codex_lb: status };
483
- const useCodexLb = (await askPostinstallQuestion('\nAuthenticate and route Codex through codex-lb? [y/N] ')).trim();
504
+ const useCodexLb = (await askPostinstallQuestion('\ncodex-lb is not configured for this Codex App profile. Configure and route Codex through codex-lb now? [y/N] ')).trim();
484
505
  if (!/^(y|yes|예|네|응)$/i.test(useCodexLb)) return { status: 'continued_to_codex' };
485
506
  const host = (await askPostinstallQuestion('codex-lb host domain [http://127.0.0.1:2455]: ')).trim() || 'http://127.0.0.1:2455';
486
507
  const apiKey = (await askPostinstallQuestion('codex-lb API key: ')).trim();
@@ -1235,6 +1256,7 @@ export async function selftestCodexLb(tmp) {
1235
1256
  const codexLbEnv = await safeReadText(path.join(codexLbHome, '.codex', 'sks-codex-lb.env'));
1236
1257
  const codexLbAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1237
1258
  if (!codexLbSetupJson.ok || codexLbSetupJson.base_url !== 'https://lb.example.test/backend-api/codex' || !hasTopLevelCodexLbSelected(codexLbConfig) || !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'") || codexLbSetupJson.codex_environment?.ok !== true || codexLbSetupJson.codex_login?.status !== 'skipped' || codexLbAuth.trim()) throw new Error('selftest: codex-lb setup');
1259
+ if (!codexLbConfig.includes('requires_openai_auth = true')) throw new Error('selftest: codex-lb setup did not write upstream-required requires_openai_auth');
1238
1260
  const codexLbFailLaunchctl = path.join(codexLbFakeBin, 'launchctl-fail');
1239
1261
  await writeTextAtomic(codexLbFailLaunchctl, '#!/bin/sh\necho "launchctl denied" >&2\nexit 7\n');
1240
1262
  await fsp.chmod(codexLbFailLaunchctl, 0o755);
@@ -1244,6 +1266,7 @@ export async function selftestCodexLb(tmp) {
1244
1266
  await initProject(codexLbHome, { installScope: 'global', force: true, repair: true });
1245
1267
  const codexLbRepairSetupConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1246
1268
  if (!hasTopLevelCodexLbSelected(codexLbRepairSetupConfig) || !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');
1269
+ if (!codexLbRepairSetupConfig.includes('requires_openai_auth = true')) throw new Error('selftest: init codex-lb did not preserve upstream-required requires_openai_auth');
1247
1270
  if (!hasCodexUnstableFeatureWarningSuppression(codexLbRepairSetupConfig)) throw new Error('selftest: init codex-lb did not suppress Codex unstable feature warning');
1248
1271
  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`);
1249
1272
  const ptmp = path.join(tmp, 'codex-lb-project-config'), prevHome = process.env.HOME;
@@ -1251,6 +1274,7 @@ export async function selftestCodexLb(tmp) {
1251
1274
  finally { if (prevHome === undefined) delete process.env.HOME; else process.env.HOME = prevHome; }
1252
1275
  const pcfg = await safeReadText(path.join(ptmp, '.codex', 'config.toml'));
1253
1276
  if (!hasTopLevelCodexLbSelected(pcfg) || !pcfg.includes('[model_providers.codex-lb]') || !pcfg.includes('[mcp_servers.supabase]') || !pcfg.includes('read_only=true')) throw new Error('selftest: project codex-lb');
1277
+ if (!pcfg.includes('requires_openai_auth = true')) throw new Error('selftest: project codex-lb did not copy upstream-required requires_openai_auth');
1254
1278
  if (!hasCodexUnstableFeatureWarningSuppression(pcfg)) throw new Error('selftest: project codex-lb config did not suppress Codex unstable feature warning');
1255
1279
  await writeTextAtomic(path.join(codexLbHome, '.codex', 'auth.json'), '{"auth_mode":"browser"}\n');
1256
1280
  const codexLbRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'auth', 'repair', '--json'], { cwd: tmp, env: codexLbEnvForSelftest, timeoutMs: 15000, maxOutputBytes: 64 * 1024 });
@@ -1310,6 +1334,7 @@ export async function selftestCodexLb(tmp) {
1310
1334
  const codexLbLoginCallsAfterBootstrap = await codexLbLoginCallCount(codexLbHome);
1311
1335
  if (!codexLbPostBootstrapAuth.includes('"auth_mode":"browser"') || codexLbPostBootstrapAuth.includes('sk-test') || codexLbLoginCallsAfterBootstrap !== codexLbLoginCallsBeforeBootstrap) throw new Error('selftest: postinstall drift auth');
1312
1336
  if (!hasTopLevelCodexLbSelected(codexLbPostBootstrapConfig) || !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');
1337
+ if (!codexLbPostBootstrapConfig.includes('requires_openai_auth = true')) throw new Error('selftest: postinstall drift config did not restore upstream-required requires_openai_auth');
1313
1338
  const doctorProject = tmpdir();
1314
1339
  await ensureDir(path.join(doctorProject, '.git'));
1315
1340
  await writeTextAtomic(path.join(doctorProject, 'package.json'), '{"name":"codex-lb-doctor-project","version":"0.0.0"}\n');
@@ -1327,6 +1352,7 @@ export async function selftestCodexLb(tmp) {
1327
1352
  const codexLbDoctorAuth = await safeReadText(path.join(codexLbHome, '.codex', 'auth.json'));
1328
1353
  const codexLbDoctorConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1329
1354
  if (!codexLbDoctorJson.repair?.codex_lb?.ok || !codexLbDoctorJson.repair.codex_lb.config_repaired || !codexLbDoctorJson.codex_lb?.ok || !codexLbDoctorAuth.includes('"auth_mode":"browser"') || codexLbDoctorAuth.includes('sk-test') || !hasTopLevelCodexLbSelected(codexLbDoctorConfig) || !codexLbDoctorConfig.includes('https://lb.example.test/backend-api/codex') || !hasCodexUnstableFeatureWarningSuppression(codexLbDoctorConfig)) throw new Error('selftest: doctor codex-lb');
1355
+ if (!codexLbDoctorConfig.includes('requires_openai_auth = true')) throw new Error('selftest: doctor codex-lb did not restore upstream-required requires_openai_auth');
1330
1356
  const codexLbContext7Bin = path.join(tmp, 'codex-lb-context7-bin');
1331
1357
  await ensureDir(codexLbContext7Bin);
1332
1358
  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');
@@ -1468,6 +1494,7 @@ export async function selftestCodexLb(tmp) {
1468
1494
  home: codexLbHome,
1469
1495
  apiKey: 'sk-test',
1470
1496
  codexBin: path.join(codexLbFakeBin, 'codex'),
1497
+ syncLaunchEnv: false,
1471
1498
  timeoutMs: 1000,
1472
1499
  fetch: async (url, init) => {
1473
1500
  nonInteractiveLaunchChainCalls.push({ url, body: JSON.parse(init.body) });
@@ -1479,6 +1506,7 @@ export async function selftestCodexLb(tmp) {
1479
1506
  home: codexLbHome,
1480
1507
  apiKey: 'sk-test',
1481
1508
  codexBin: path.join(codexLbFakeBin, 'codex'),
1509
+ syncLaunchEnv: false,
1482
1510
  timeoutMs: 1000,
1483
1511
  fetch: async (_url, init) => {
1484
1512
  const body = JSON.parse(init.body);
@@ -1487,6 +1515,22 @@ export async function selftestCodexLb(tmp) {
1487
1515
  }
1488
1516
  });
1489
1517
  if (nonInteractiveBrokenLaunch.status !== 'chain_unhealthy' || nonInteractiveBrokenLaunch.bypass_codex_lb !== true || nonInteractiveBrokenLaunch.chain_health?.status !== 'previous_response_not_found') throw new Error('selftest: non-interactive codex-lb launch path did not bypass on previous_response_not_found');
1518
+ await writeTextAtomic(path.join(codexLbHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nservice_tier = "fast"\n');
1519
+ 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");
1520
+ const missingProviderLaunchCalls = [];
1521
+ const missingProviderLaunch = await maybePromptCodexLbSetupForLaunch([], {
1522
+ home: codexLbHome,
1523
+ apiKey: 'sk-test',
1524
+ codexBin: path.join(codexLbFakeBin, 'codex'),
1525
+ syncLaunchEnv: false,
1526
+ timeoutMs: 1000,
1527
+ fetch: async (url, init) => {
1528
+ missingProviderLaunchCalls.push({ url, body: JSON.parse(init.body) });
1529
+ return new Response(JSON.stringify({ id: missingProviderLaunchCalls.length === 1 ? 'resp_missing_provider_1' : 'resp_missing_provider_2' }), { status: 200, headers: { 'content-type': 'application/json' } });
1530
+ }
1531
+ });
1532
+ const missingProviderRepairedConfig = await safeReadText(path.join(codexLbHome, '.codex', 'config.toml'));
1533
+ if (!missingProviderLaunch.ok || missingProviderLaunch.status !== 'present' || missingProviderLaunch.chain_health?.status !== 'chain_ok' || missingProviderLaunchCalls.length !== 2 || !hasTopLevelCodexLbSelected(missingProviderRepairedConfig) || !missingProviderRepairedConfig.includes('[model_providers.codex-lb]') || !missingProviderRepairedConfig.includes('env_key = "CODEX_LB_API_KEY"') || !missingProviderRepairedConfig.includes('supports_websockets = true') || !missingProviderRepairedConfig.includes('requires_openai_auth = true')) throw new Error('selftest: bare sks launch did not restore missing upstream codex-lb provider block from stored env');
1490
1534
  const chainCalls = [];
1491
1535
  const okChain = await checkCodexLbResponseChain(
1492
1536
  { base_url: 'https://lb.example.test/backend-api/codex', env_path: path.join(codexLbHome, '.codex', 'sks-codex-lb.env') },
package/src/cli/main.mjs CHANGED
@@ -1146,9 +1146,11 @@ async function codexLbCommand(action = 'status', args = []) {
1146
1146
  console.log(`Configured: ${status.ok ? 'yes' : 'no'}`);
1147
1147
  console.log(`Selected: ${status.selected ? 'yes' : 'no'}`);
1148
1148
  console.log(`Provider: ${status.provider_configured ? 'yes' : 'no'}`);
1149
+ console.log(`Codex App auth: ${status.provider_requires_openai_auth ? 'yes' : 'missing'}`);
1149
1150
  console.log(`Env file: ${status.env_file ? status.env_path : 'missing'}`);
1150
1151
  if (status.base_url) console.log(`Base URL: ${status.base_url}`);
1151
1152
  if (status.ok && !status.selected) console.log('\nRun: sks codex-lb repair to activate codex-lb for Codex App.');
1153
+ else if (!status.ok && status.base_url && status.env_key_configured) console.log('\nRun: sks codex-lb repair to restore the upstream codex-lb provider block.');
1152
1154
  else if (!status.ok) console.log('\nRun: sks codex-lb setup --host <domain> --api-key <key>');
1153
1155
  else console.log('\nRepair provider auth: sks codex-lb repair');
1154
1156
  return;
@@ -2103,14 +2105,18 @@ async function selftest() {
2103
2105
  await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', 'agent-team', 'SKILL.md'), '---\nname: agent-team\ndescription: Fallback Codex App picker alias for $Team.\n---\n');
2104
2106
  await ensureDir(path.join(repairTmp, '.agents', 'skills', 'stale-sks-generated'));
2105
2107
  await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', 'stale-sks-generated', 'SKILL.md'), '---\nname: stale-sks-generated\ndescription: Old SKS generated skill that should disappear on update.\n---\n');
2106
- await ensureDir(path.join(repairTmp, '.agents', 'skills', 'computer-use'));
2107
- await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', 'computer-use', 'SKILL.md'), '---\nname: computer-use\ndescription: Maximum-speed $Computer-Use/$CU lane for Codex Computer Use UI/browser/visual tasks.\n---\n');
2108
+ const stalePluginSkillNames = ['browser', 'browser-use', 'computer-use', 'chrome', 'documents', 'presentations', 'spreadsheets', 'latex'];
2109
+ const stalePluginSkillContent = (name) => `---\nname: ${name}\ndescription: Sneakoscope generated stale plugin collision for selftest.\n---\n\nCodex App pipeline activation:\n- stale selftest marker\n`;
2110
+ for (const name of stalePluginSkillNames) {
2111
+ await ensureDir(path.join(repairTmp, '.agents', 'skills', name));
2112
+ await writeTextAtomic(path.join(repairTmp, '.agents', 'skills', name, 'SKILL.md'), stalePluginSkillContent(name));
2113
+ }
2108
2114
  await writeJsonAtomic(path.join(repairTmp, '.agents', 'skills', '.sks-generated.json'), {
2109
2115
  schema_version: 1,
2110
2116
  generated_by: 'sneakoscope',
2111
2117
  version: '0.0.1',
2112
- skills: ['team', 'stale-sks-generated', 'computer-use'],
2113
- files: ['.agents/skills/team/SKILL.md', '.agents/skills/stale-sks-generated/SKILL.md', '.agents/skills/computer-use/SKILL.md']
2118
+ skills: ['team', 'stale-sks-generated', ...stalePluginSkillNames],
2119
+ files: ['.agents/skills/team/SKILL.md', '.agents/skills/stale-sks-generated/SKILL.md', ...stalePluginSkillNames.map((name) => `.agents/skills/${name}/SKILL.md`)]
2114
2120
  });
2115
2121
  const staleCodexAgentRel = '.codex/agents/stale-generated.toml';
2116
2122
  await writeTextAtomic(path.join(repairTmp, staleCodexAgentRel), 'name = "stale_generated"\n');
@@ -2131,8 +2137,6 @@ async function selftest() {
2131
2137
  await writeJsonAtomic(path.join(repairTmp, '.sneakoscope', 'policy.json'), { broken: true });
2132
2138
  const existingAgentsMd = await safeReadText(path.join(repairTmp, 'AGENTS.md'));
2133
2139
  await writeTextAtomic(path.join(repairTmp, 'AGENTS.md'), existingAgentsMd.replace(/<!-- BEGIN Sneakoscope Codex GX MANAGED BLOCK -->[\s\S]*?<!-- END Sneakoscope Codex GX MANAGED BLOCK -->\n?/, '<!-- BEGIN Sneakoscope Codex GX MANAGED BLOCK -->\ntampered managed block\n<!-- END Sneakoscope Codex GX MANAGED BLOCK -->\n'));
2134
- const stalePluginSkillNames = ['computer-use', 'browser-use', 'browser'];
2135
- const stalePluginSkillContent = (name) => `---\nname: ${name}\ndescription: Sneakoscope generated stale plugin collision for selftest.\n---\n\nCodex App pipeline activation:\n- stale selftest marker\n`;
2136
2140
  const doctorRepair = await runProcess(process.execPath, [path.join(packageRoot(), 'bin', 'sks.mjs'), 'doctor', '--fix', '--local-only', '--json'], {
2137
2141
  cwd: repairTmp,
2138
2142
  env: { HOME: path.join(repairTmp, 'home'), SKS_DISABLE_UPDATE_CHECK: '1' },
@@ -2150,7 +2154,9 @@ async function selftest() {
2150
2154
  if (!repairedTeamSkill.includes('SKS Team orchestration') || repairedTeamSkill.includes('tampered')) throw new Error('selftest: doctor repair did not regenerate team skill');
2151
2155
  if (await exists(path.join(repairTmp, '.agents', 'skills', 'agent-team', 'SKILL.md'))) throw new Error('selftest: doctor repair did not remove deprecated agent-team alias skill');
2152
2156
  if (await exists(path.join(repairTmp, '.agents', 'skills', 'stale-sks-generated', 'SKILL.md'))) throw new Error('selftest: doctor repair did not prune stale generated skill from previous SKS manifest');
2153
- if (await exists(path.join(repairTmp, '.agents', 'skills', 'computer-use', 'SKILL.md'))) throw new Error('selftest: computer-use shadow');
2157
+ for (const name of stalePluginSkillNames) {
2158
+ if (await exists(path.join(repairTmp, '.agents', 'skills', name, 'SKILL.md'))) throw new Error(`selftest: doctor repair left stale generated ${name} plugin shadow skill`);
2159
+ }
2154
2160
  if (await exists(path.join(repairTmp, staleCodexAgentRel))) throw new Error('selftest: doctor repair did not prune stale generated agent file from previous SKS manifest');
2155
2161
  if (!doctorRepairJson.repair?.project?.skill_install?.removed_stale_generated_skills?.includes('.agents/skills/stale-sks-generated')) throw new Error('selftest: stale skill report');
2156
2162
  const generatedCleanupReport = doctorRepairJson.repair?.project?.generated_cleanup || {};
@@ -2169,6 +2175,8 @@ async function selftest() {
2169
2175
  await writeJsonAtomic(path.join(doctorGlobalTmp, 'package.json'), { name: 'doctor-global-skill-repair-smoke', version: '0.0.0' });
2170
2176
  await initProject(doctorGlobalTmp, { installScope: 'global' });
2171
2177
  const doctorGlobalHome = path.join(doctorGlobalTmp, 'home');
2178
+ await ensureDir(path.join(doctorGlobalHome, '.codex'));
2179
+ await writeTextAtomic(path.join(doctorGlobalHome, '.codex', 'config.toml'), 'model = "gpt-5.5"\nmodel_reasoning_effort = "high"\nservice_tier = "fast"\n\n[features]\nplugins = false\napps = false\n');
2172
2180
  for (const name of stalePluginSkillNames) {
2173
2181
  await ensureDir(path.join(doctorGlobalHome, '.agents', 'skills', name));
2174
2182
  await writeTextAtomic(path.join(doctorGlobalHome, '.agents', 'skills', name, 'SKILL.md'), stalePluginSkillContent(name));
@@ -2184,6 +2192,7 @@ async function selftest() {
2184
2192
  const doctorGlobalCodexConfig = await safeReadText(path.join(doctorGlobalHome, '.codex', 'config.toml'));
2185
2193
  if (!doctorGlobalRepairJson.repair?.global_codex_config) throw new Error('selftest: doctor global config repair missing');
2186
2194
  assertCodexWarn(doctorGlobalCodexConfig, 'doctor global config');
2195
+ if (hasTopLevelCodexModeLock(doctorGlobalCodexConfig)) throw new Error('selftest: doctor global config repair left top-level model_reasoning_effort lock that can hide Codex App plugin UI');
2187
2196
  if (missingGeneratedCodexAppFeatureFlags(doctorGlobalCodexConfig).length || hasDeprecatedCodexHooksFeatureFlag(doctorGlobalCodexConfig) || !hasResearchProfileConfig(doctorGlobalCodexConfig)) throw new Error('selftest: doctor global config repair did not restore Codex App feature flags and Research xhigh profiles');
2188
2197
  for (const name of stalePluginSkillNames) {
2189
2198
  if (await exists(path.join(doctorGlobalHome, '.agents', 'skills', name, 'SKILL.md'))) throw new Error(`selftest: doctor --fix did not remove global generated ${name} plugin shadow skill`);
@@ -3068,12 +3077,32 @@ async function selftest() {
3068
3077
  await ensureDir(fakeCodexApp);
3069
3078
  await ensureDir(fakeCodexBinDir);
3070
3079
  await ensureDir(path.join(appFeatureTmp, '.codex'));
3071
- await writeTextAtomic(path.join(appFeatureTmp, '.codex', 'config.toml'), codexConfigText);
3080
+ const codexAppFixtureConfigText = codexConfigText.replace(/(?:^|\n)\[marketplaces\.[^\]\r\n]+\][\s\S]*?(?=\n\[[^\]]+\]|\s*$)/g, '\n').replace(/\n{3,}/g, '\n\n');
3081
+ await writeTextAtomic(path.join(appFeatureTmp, '.codex', 'config.toml'), codexAppFixtureConfigText);
3082
+ const fakeDefaultPluginCacheNames = ['browser', 'chrome', 'computer-use', 'latex', 'documents', 'presentations', 'spreadsheets'];
3083
+ for (const name of fakeDefaultPluginCacheNames) await ensureDir(path.join(appFeatureTmp, '.codex', 'plugins', 'cache', name));
3072
3084
  const fakeCodex = path.join(fakeCodexBinDir, 'codex');
3073
3085
  await writeTextAtomic(fakeCodex, '#!/bin/sh\nif [ "$1" = "mcp" ] && [ "$2" = "list" ]; then printf "%s\\n" "computer-use enabled" "browser-use enabled"; exit 0; fi\nif [ "$1" = "features" ] && [ "$2" = "list" ]; then cat <<EOF\napps stable true\nbrowser_use stable true\nbrowser_use_external stable true\ncodex_git_commit under development true\ncomputer_use stable true\nfast_mode stable true\nguardian_approval stable true\nhooks stable true\nimage_generation stable true\nin_app_browser stable true\nplugins stable true\nremote_control under development true\ntool_suggest stable true\nEOF\nexit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
3074
3086
  await fsp.chmod(fakeCodex, 0o755);
3075
- const codexAppFeatureStatus = await codexAppIntegrationStatus({ codex: { bin: fakeCodex, version: 'codex-cli 99.0.0' }, home: appFeatureTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } });
3087
+ const codexAppFixtureOpts = { codex: { bin: fakeCodex, version: 'codex-cli 99.0.0' }, home: appFeatureTmp, cwd: appFeatureTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } };
3088
+ const codexAppFeatureStatus = await codexAppIntegrationStatus(codexAppFixtureOpts);
3076
3089
  if (!codexAppFeatureStatus.ok || !codexAppFeatureStatus.features?.required_flags_ok || !codexAppFeatureStatus.features?.codex_git_commit || !codexAppFeatureStatus.features?.remote_control || !codexAppFeatureStatus.features?.fast_mode_config?.ok) throw new Error('selftest: codex-app check did not accept required app feature flags, remote_control, and unlocked Fast UI config');
3090
+ const missingDefaultPluginTmp = tmpdir();
3091
+ await ensureDir(path.join(missingDefaultPluginTmp, '.codex'));
3092
+ const codexConfigWithoutMarketplaceSources = codexConfigText.replace(/(?:^|\n)\[marketplaces\.[^\]\r\n]+\][\s\S]*?(?=\n\[[^\]]+\]|\s*$)/g, '').trim();
3093
+ await writeTextAtomic(path.join(missingDefaultPluginTmp, '.codex', 'config.toml'), `${codexConfigWithoutMarketplaceSources}\n`);
3094
+ const codexAppMissingDefaultPluginStatus = await codexAppIntegrationStatus({ codex: { bin: fakeCodex, version: 'codex-cli 99.0.0' }, home: missingDefaultPluginTmp, cwd: missingDefaultPluginTmp, env: { SKS_CODEX_APP_PATH: fakeCodexApp } });
3095
+ if (codexAppMissingDefaultPluginStatus.ok || codexAppMissingDefaultPluginStatus.plugins?.default_plugins?.ok || codexAppMissingDefaultPluginStatus.plugins?.picker?.ok || !codexAppMissingDefaultPluginStatus.plugins?.default_plugins?.missing_installed?.includes('browser@openai-bundled') || !codexAppMissingDefaultPluginStatus.guidance.some((line) => line.includes('default plugin source'))) throw new Error('selftest: codex-app check did not block missing default plugin source');
3096
+ await ensureDir(path.join(appFeatureTmp, '.agents', 'skills', 'browser'));
3097
+ await writeTextAtomic(path.join(appFeatureTmp, '.agents', 'skills', 'browser', 'SKILL.md'), stalePluginSkillContent('browser'));
3098
+ const codexAppShadowStatus = await codexAppIntegrationStatus(codexAppFixtureOpts);
3099
+ if (codexAppShadowStatus.ok || codexAppShadowStatus.plugins?.picker?.ok || codexAppShadowStatus.plugins?.skill_shadows?.blocking?.[0]?.name !== 'browser' || codexAppShadowStatus.plugins?.skill_shadows?.generated?.[0]?.name !== 'browser' || !codexAppShadowStatus.guidance.some((line) => line.includes('plugin picker generated skill shadow'))) throw new Error('selftest: codex-app check did not block generated skill shadow that can hide @ plugin picker entries');
3100
+ await fsp.rm(path.join(appFeatureTmp, '.agents', 'skills', 'browser'), { recursive: true, force: true });
3101
+ await ensureDir(path.join(appFeatureTmp, '.agents', 'skills', 'browser'));
3102
+ await writeTextAtomic(path.join(appFeatureTmp, '.agents', 'skills', 'browser', 'SKILL.md'), '---\nname: browser\ndescription: User custom skill, not generated by SKS.\n---\n');
3103
+ const codexAppCustomShadowStatus = await codexAppIntegrationStatus(codexAppFixtureOpts);
3104
+ if (codexAppCustomShadowStatus.ok || codexAppCustomShadowStatus.plugins?.picker?.ok || codexAppCustomShadowStatus.plugins?.skill_shadows?.custom?.[0]?.name !== 'browser' || codexAppCustomShadowStatus.plugins?.skill_shadows?.generated?.length || !codexAppCustomShadowStatus.guidance.some((line) => line.includes('user-owned reserved skill name')) || codexAppCustomShadowStatus.guidance.some((line) => line.includes('plugin picker generated skill shadow'))) throw new Error('selftest: codex-app check did not distinguish user-owned reserved plugin skill names from generated shadows');
3105
+ await fsp.rm(path.join(appFeatureTmp, '.agents', 'skills', 'browser'), { recursive: true, force: true });
3077
3106
  const fakeCodexMissing = path.join(fakeCodexBinDir, 'codex-missing-git-commit');
3078
3107
  await writeTextAtomic(fakeCodexMissing, '#!/bin/sh\nif [ "$1" = "mcp" ] && [ "$2" = "list" ]; then printf "%s\\n" "computer-use enabled" "browser-use enabled"; exit 0; fi\nif [ "$1" = "features" ] && [ "$2" = "list" ]; then cat <<EOF\napps stable true\nbrowser_use stable true\nbrowser_use_external stable true\ncodex_git_commit under development false\ncomputer_use stable true\nfast_mode stable true\nguardian_approval stable true\nhooks stable true\nimage_generation stable true\nin_app_browser stable true\nplugins stable true\nremote_control under development true\ntool_suggest stable true\nEOF\nexit 0; fi\necho "unexpected codex $*" >&2\nexit 2\n');
3079
3108
  await fsp.chmod(fakeCodexMissing, 0o755);
@@ -3828,20 +3857,20 @@ async function selftest() {
3828
3857
  if (!harnessReport.forgetting.fixture.passed || !harnessReport.tmux.views.includes('Harness Experiments View') || !harnessReport.reliability.tool_error_taxonomy.includes('Unknown')) throw new Error('selftest: harness growth fixture incomplete');
3829
3858
  const proofField = await proofFieldFixture();
3830
3859
  if (!proofField.validation.ok || !validateProofFieldReport(proofField.report).ok) throw new Error('selftest: proof field report invalid');
3831
- if (!proofField.checks.route_cone_selected || !proofField.checks.cli_cone_selected || !proofField.checks.catastrophic_guard_present || !proofField.checks.negative_release_work_recorded || !proofField.checks.outcome_rubric_present || !proofField.checks.adversarial_lenses_present || !proofField.checks.route_economy_present || !proofField.checks.simplicity_score_usable || !proofField.checks.execution_fast_lane_selected) throw new Error('selftest: proof field fixture checks incomplete');
3860
+ if (!proofField.checks.route_cone_selected || !proofField.checks.cli_cone_selected || !proofField.checks.catastrophic_guard_present || !proofField.checks.negative_release_work_recorded || !proofField.checks.outcome_rubric_present || !proofField.checks.adversarial_lenses_present || !proofField.checks.route_economy_present || !proofField.checks.decision_lattice_present || !proofField.checks.decision_lattice_report_only || !proofField.checks.decision_lattice_selected_path || !proofField.checks.decision_lattice_frontier_present || !proofField.checks.decision_lattice_rejections_present || !proofField.checks.decision_lattice_scoring_formula_present || !proofField.checks.simplicity_score_usable || !proofField.checks.execution_fast_lane_selected) throw new Error('selftest: proof field fixture checks incomplete');
3832
3861
  if (!speedLanePolicyText().includes('proof_field_fast_lane') || !proofField.report.execution_lane?.skip_when_fast?.includes('planning_debate')) throw new Error('selftest: Proof Field speed lane policy missing');
3833
3862
  const fastPipelinePlan = buildPipelinePlan({ route: routePrompt('$Team small CLI help update'), task: 'small CLI help surface update', proofField: proofField.report });
3834
3863
  if (!validatePipelinePlan(fastPipelinePlan).ok || fastPipelinePlan.runtime_lane?.lane !== 'proof_field_fast_lane' || !fastPipelinePlan.skipped_stages.includes('planning_debate') || !fastPipelinePlan.invariants.includes('no_unrequested_fallback_code')) throw new Error('selftest: pipeline plan did not encode fast lane stage skips and fallback guard');
3835
3864
  const broadProofField = await buildProofField(tmp, { intent: 'database security route refactor', changedFiles: ['src/core/db-safety.mjs', 'src/core/routes.mjs', 'src/cli/main.mjs', 'README.md'] });
3836
3865
  const broadPipelinePlan = buildPipelinePlan({ route: routePrompt('$Team database security route refactor'), task: 'database security route refactor', proofField: broadProofField });
3837
3866
  if (!validatePipelinePlan(broadPipelinePlan).ok || broadPipelinePlan.runtime_lane?.lane === 'proof_field_fast_lane' || broadPipelinePlan.skipped_stages.includes('planning_debate')) throw new Error('selftest: pipeline plan did not fail closed for broad/security work');
3838
- if (broadPipelinePlan.route_economy?.mode !== 'report_only' || !broadPipelinePlan.route_economy.active_team_triggers?.includes('broad_change_set') || !broadPipelinePlan.route_economy.verification_stage_cache_key) throw new Error('selftest: route economy projection missing from pipeline plan');
3867
+ if (broadPipelinePlan.route_economy?.mode !== 'report_only' || !broadPipelinePlan.route_economy.active_team_triggers?.includes('broad_change_set') || !broadPipelinePlan.route_economy.verification_stage_cache_key || !broadPipelinePlan.route_economy.decision_lattice?.report_only) throw new Error('selftest: route economy projection missing from pipeline plan');
3839
3868
  const workflowPerf = await runWorkflowPerfBench(tmp, {
3840
3869
  iterations: 2,
3841
3870
  intent: 'small CLI help surface update',
3842
3871
  changedFiles: ['src/cli/maintenance-commands.mjs', 'src/core/routes.mjs']
3843
3872
  });
3844
- if (!validateWorkflowPerfReport(workflowPerf).ok || workflowPerf.metrics.decision_mode !== 'fast_lane' || workflowPerf.metrics.execution_lane !== 'proof_field_fast_lane' || workflowPerf.metrics.pipeline_lane !== 'proof_field_fast_lane' || !workflowPerf.metrics.fast_lane_eligible || !workflowPerf.metrics.fast_lane_allowed || Number(workflowPerf.metrics.simplicity_score) < 0.75 || Number(workflowPerf.metrics.outcome_criteria_passed) < 3) throw new Error('selftest: workflow perf proof field did not produce a valid outcome-scored fast lane report');
3873
+ if (!validateWorkflowPerfReport(workflowPerf).ok || workflowPerf.metrics.decision_mode !== 'fast_lane' || workflowPerf.metrics.execution_lane !== 'proof_field_fast_lane' || workflowPerf.metrics.pipeline_lane !== 'proof_field_fast_lane' || !workflowPerf.metrics.fast_lane_eligible || !workflowPerf.metrics.fast_lane_allowed || !workflowPerf.metrics.decision_lattice_valid || Number(workflowPerf.metrics.decision_lattice_frontier_count) < 1 || Number(workflowPerf.metrics.simplicity_score) < 0.75 || Number(workflowPerf.metrics.outcome_criteria_passed) < 3) throw new Error('selftest: workflow perf proof field did not produce a valid outcome-scored fast lane report');
3845
3874
  if (classifyToolError({ message: 'operation timed out' }) !== 'Timeout' || classifyToolError({ message: 'unclassified weirdness' }) !== 'Unknown') throw new Error('selftest: tool error taxonomy classification');
3846
3875
  const coord = rgbaToWikiCoord({ r: 12, g: 34, b: 56, a: 255 });
3847
3876
  if (coord.schema !== 'sks.wiki-coordinate.v1' || coord.xyzw.length !== 4) throw new Error('selftest: RGBA wiki coordinate conversion');
@@ -969,6 +969,9 @@ export async function proofFieldCommand(sub, args = []) {
969
969
  console.log(`Workflow complexity: ${report.workflow_complexity.band} (${report.workflow_complexity.score})`);
970
970
  if (report.team_trigger_matrix.active_triggers.length) console.log(`Team triggers: ${report.team_trigger_matrix.active_triggers.join(', ')}`);
971
971
  console.log(`Proof cones: ${report.proof_cones.map((cone) => cone.id).join(', ')}`);
972
+ if (report.decision_lattice?.selected_path?.id) {
973
+ console.log(`Decision lattice: ${report.decision_lattice.selected_path.id} f=${report.decision_lattice.selected_path.cost?.f ?? 'n/a'} frontier=${report.decision_lattice.frontier?.expanded_order?.length || 0} rejected=${report.decision_lattice.rejected_alternatives?.length || 0}`);
974
+ }
972
975
  console.log(`Verification: ${report.fast_lane_decision.verification.join('; ')}`);
973
976
  console.log(`Report: ${path.relative(root, report.report_path)}`);
974
977
  }
@@ -3,6 +3,7 @@ import os from 'node:os';
3
3
  import fsp from 'node:fs/promises';
4
4
  import { exists, runProcess } from './fsx.mjs';
5
5
  import { getCodexInfo } from './codex-adapter.mjs';
6
+ import { DEFAULT_CODEX_APP_PLUGINS as DEFAULT_CODEX_APP_PLUGIN_TUPLES, RESERVED_CODEX_PLUGIN_SKILL_NAMES } from './routes.mjs';
6
7
 
7
8
  export const CODEX_APP_DOCS_URL = 'https://developers.openai.com/codex/app/features';
8
9
  export const CODEX_CHANGELOG_URL = 'https://developers.openai.com/codex/changelog';
@@ -22,15 +23,7 @@ const REQUIRED_CODEX_APP_FEATURE_FLAGS = [
22
23
  'apps',
23
24
  'plugins'
24
25
  ];
25
- const DEFAULT_CODEX_APP_PLUGINS = [
26
- { name: 'browser', marketplace: 'openai-bundled' },
27
- { name: 'chrome', marketplace: 'openai-bundled' },
28
- { name: 'computer-use', marketplace: 'openai-bundled' },
29
- { name: 'latex', marketplace: 'openai-bundled' },
30
- { name: 'documents', marketplace: 'openai-primary-runtime' },
31
- { name: 'presentations', marketplace: 'openai-primary-runtime' },
32
- { name: 'spreadsheets', marketplace: 'openai-primary-runtime' }
33
- ];
26
+ const DEFAULT_CODEX_APP_PLUGINS = DEFAULT_CODEX_APP_PLUGIN_TUPLES.map(([name, marketplace]) => ({ name, marketplace }));
34
27
 
35
28
  export function codexAppCandidatePaths(home = os.homedir(), env = process.env) {
36
29
  const candidates = [];
@@ -128,6 +121,7 @@ export async function codexAppIntegrationStatus(opts = {}) {
128
121
  const browserUsePath = await findPluginCache('browser-use', opts);
129
122
  const computerUsePath = await findPluginCache('computer-use', opts);
130
123
  const defaultPlugins = await codexDefaultPluginStatus(opts);
124
+ const pluginSkillShadows = await codexPluginSkillShadowStatus(opts);
131
125
  const fastModeConfig = await codexFastModeConfigStatus(opts);
132
126
  const computerUseMcpListed = /computer[-_ ]?use/i.test(mcpText);
133
127
  const browserUseMcpListed = /browser[-_ ]?use/i.test(mcpText);
@@ -140,7 +134,8 @@ export async function codexAppIntegrationStatus(opts = {}) {
140
134
  const browserUseReady = browserUseMcpListed || Boolean(browserUsePath);
141
135
  const browserToolReady = inAppBrowserReady || browserUseFeatureReady || browserUseReady;
142
136
  const appInstalled = Boolean(appPath);
143
- const ready = appInstalled && Boolean(codex.bin) && mcpList.ok && featureList.ok && requiredFeatureFlagsOk && defaultPlugins.ok && fastModeConfig.ok && imageGenerationReady && computerUseReady && browserToolReady;
137
+ const pluginPickerReady = requiredFeatureFlags.tool_suggest && requiredFeatureFlags.plugins && requiredFeatureFlags.apps && defaultPlugins.ok && pluginSkillShadows.ok && fastModeConfig.ok;
138
+ const ready = appInstalled && Boolean(codex.bin) && mcpList.ok && featureList.ok && requiredFeatureFlagsOk && pluginPickerReady && fastModeConfig.ok && imageGenerationReady && computerUseReady && browserToolReady;
144
139
  return {
145
140
  ok: ready,
146
141
  app: {
@@ -191,9 +186,17 @@ export async function codexAppIntegrationStatus(opts = {}) {
191
186
  plugins: {
192
187
  computer_use_cache: computerUsePath,
193
188
  browser_use_cache: browserUsePath,
194
- default_plugins: defaultPlugins
189
+ default_plugins: defaultPlugins,
190
+ skill_shadows: pluginSkillShadows,
191
+ picker: {
192
+ ok: pluginPickerReady,
193
+ required_flags_ok: Boolean(requiredFeatureFlags.tool_suggest && requiredFeatureFlags.plugins && requiredFeatureFlags.apps),
194
+ default_plugins_ok: defaultPlugins.ok,
195
+ skill_shadows_ok: pluginSkillShadows.ok,
196
+ fast_mode_config_ok: fastModeConfig.ok
197
+ }
195
198
  },
196
- guidance: codexAppGuidance({ appInstalled, codex, mcpList, featureList, requiredFeatureFlags, requiredFeatureFlagsOk, defaultPlugins, fastModeConfig, imageGenerationReady, inAppBrowserReady, browserUseFeatureReady, computerUseReady, browserUseReady, browserToolReady, computerUseMcpListed, browserUseMcpListed, remoteControl })
199
+ guidance: codexAppGuidance({ appInstalled, codex, mcpList, featureList, requiredFeatureFlags, requiredFeatureFlagsOk, defaultPlugins, pluginSkillShadows, fastModeConfig, imageGenerationReady, inAppBrowserReady, browserUseFeatureReady, computerUseReady, browserUseReady, browserToolReady, computerUseMcpListed, browserUseMcpListed, remoteControl })
197
200
  };
198
201
  }
199
202
 
@@ -248,7 +251,7 @@ export function formatCodexRemoteControlStatus(status) {
248
251
  return lines.filter(Boolean).join('\n');
249
252
  }
250
253
 
251
- export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, requiredFeatureFlags = {}, requiredFeatureFlagsOk = true, defaultPlugins = { ok: true, missing_enabled: [] }, fastModeConfig = { ok: true, blockers: [] }, imageGenerationReady, inAppBrowserReady, browserUseFeatureReady, computerUseReady, browserUseReady, browserToolReady, computerUseMcpListed, browserUseMcpListed, remoteControl }) {
254
+ export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, requiredFeatureFlags = {}, requiredFeatureFlagsOk = true, defaultPlugins = { ok: true, missing_enabled: [] }, pluginSkillShadows = { ok: true, blocking: [] }, fastModeConfig = { ok: true, blockers: [] }, imageGenerationReady, inAppBrowserReady, browserUseFeatureReady, computerUseReady, browserUseReady, browserToolReady, computerUseMcpListed, browserUseMcpListed, remoteControl }) {
252
255
  const lines = [];
253
256
  if (!appInstalled) {
254
257
  lines.push('Install and open Codex App for first-party MCP/plugin tools. SKS tmux launch can still run with Codex CLI alone, but Codex Computer Use and imagegen/gpt-image-2 evidence will be unavailable until Codex App is ready.');
@@ -278,6 +281,19 @@ export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, re
278
281
  lines.push(`Codex default plugin(s) installed but not enabled: ${defaultPlugins.missing_enabled.join(', ')}. Composer/tool UI can hide built-in surfaces even while feature flags look green.`);
279
282
  lines.push('Run: sks doctor --fix');
280
283
  }
284
+ if (defaultPlugins?.missing_installed?.length) {
285
+ lines.push(`Codex default plugin source(s) missing: ${defaultPlugins.missing_installed.join(', ')}. The @ plugin picker can hide built-in surfaces when plugin files are absent even if config says enabled.`);
286
+ lines.push('Run: sks doctor --fix, then restart Codex App if the plugin cache was just restored.');
287
+ }
288
+ if (pluginSkillShadows?.generated?.length) {
289
+ const names = pluginSkillShadows.generated.map((entry) => `${entry.name}:${entry.scope}`).join(', ');
290
+ lines.push(`Codex plugin picker generated skill shadow(s) detected: ${names}. Generated SKS skills with first-party plugin names can hide @ plugin entries after upgrades.`);
291
+ lines.push('Run: sks doctor --fix');
292
+ }
293
+ if (pluginSkillShadows?.custom?.length) {
294
+ const names = pluginSkillShadows.custom.map((entry) => `${entry.name}:${entry.scope}`).join(', ');
295
+ lines.push(`Codex plugin picker user-owned reserved skill name(s) detected: ${names}. Rename or remove these custom skills to avoid hiding first-party @ plugin entries; SKS doctor will not delete user-owned skills.`);
296
+ }
281
297
  if (fastModeConfig?.blockers?.length) {
282
298
  lines.push(`Codex App speed selector can be hidden or locked by config: ${fastModeConfig.blockers.join(', ')}.`);
283
299
  lines.push('Run: sks doctor --fix');
@@ -315,7 +331,8 @@ export function formatCodexAppStatus(status, { includeRaw = false } = {}) {
315
331
  `Remote Ctrl: ${status.remote_control?.ok ? 'ok' : 'missing'}${status.remote_control?.codex_cli?.version_number ? ` min ${status.remote_control.min_version}` : ''}`,
316
332
  `App Flags: ${status.features?.required_flags_ok ? 'ok' : `missing ${missingRequiredFeatureFlags(status.features?.required_flags).join(', ') || 'required flags'}`}`,
317
333
  `Fast UI: ${status.features?.fast_mode_config?.ok ? 'ok' : `locked ${(status.features?.fast_mode_config?.blockers || []).join(', ') || 'config'}`}`,
318
- `Default Plugins:${status.plugins?.default_plugins?.ok ? ' ok' : ` missing ${(status.plugins?.default_plugins?.missing_enabled || []).join(', ') || 'enabled plugin config'}`}`,
334
+ `Default Plugins:${status.plugins?.default_plugins?.ok ? ' ok' : ` missing ${defaultPluginMissingSummary(status.plugins?.default_plugins) || 'plugin install/config'}`}`,
335
+ `Plugin Picker:${status.plugins?.picker?.ok ? ' ok' : ` blocked ${pluginPickerBlockers(status).join(', ') || 'config'}`}`,
319
336
  `Computer Use:${status.mcp.has_computer_use ? status.mcp.computer_use_source === 'plugin_cache' ? ' installed (verify @Computer in thread)' : ' ok' : ' missing'}`,
320
337
  `Browser: ${status.features?.browser_tool_ready ? `ok (${status.features.browser_tool_source})` : status.mcp.has_browser_use ? status.mcp.browser_use_source === 'plugin_cache' ? 'installed (plugin scoped)' : 'ok' : 'missing'}`,
321
338
  `Image Gen: ${status.features?.image_generation ? 'ok ($imagegen/gpt-image-2)' : status.features?.checked ? 'missing' : 'not checked'}`,
@@ -376,15 +393,56 @@ async function codexDefaultPluginStatus(opts = {}) {
376
393
  });
377
394
  }
378
395
  const installed = entries.filter((entry) => entry.installed);
396
+ const missingInstalled = entries.filter((entry) => !entry.installed).map((entry) => entry.id);
379
397
  const missingEnabled = installed.filter((entry) => !entry.enabled).map((entry) => entry.id);
380
398
  return {
381
- ok: missingEnabled.length === 0,
399
+ ok: missingInstalled.length === 0 && missingEnabled.length === 0,
382
400
  checked: true,
383
401
  entries,
402
+ missing_installed: missingInstalled,
384
403
  missing_enabled: missingEnabled
385
404
  };
386
405
  }
387
406
 
407
+ async function codexPluginSkillShadowStatus(opts = {}) {
408
+ const home = opts.home || os.homedir();
409
+ const cwd = opts.cwd || process.cwd();
410
+ const roots = [
411
+ { scope: 'global', root: path.join(home || '', '.agents', 'skills') }
412
+ ];
413
+ const projectRoot = path.join(cwd || '', '.agents', 'skills');
414
+ if (path.resolve(projectRoot) !== path.resolve(roots[0].root)) roots.push({ scope: 'project', root: projectRoot });
415
+ const entries = [];
416
+ for (const root of roots) {
417
+ for (const name of RESERVED_CODEX_PLUGIN_SKILL_NAMES) {
418
+ const skillPath = path.join(root.root, name, 'SKILL.md');
419
+ if (!(await exists(skillPath))) continue;
420
+ const text = await readTextIfExists(skillPath);
421
+ entries.push({
422
+ name,
423
+ scope: root.scope,
424
+ path: skillPath,
425
+ generated: isGeneratedSksPluginShadow(text, name)
426
+ });
427
+ }
428
+ }
429
+ return {
430
+ ok: entries.length === 0,
431
+ checked: true,
432
+ reserved_names: RESERVED_CODEX_PLUGIN_SKILL_NAMES,
433
+ blocking: entries,
434
+ generated: entries.filter((entry) => entry.generated),
435
+ custom: entries.filter((entry) => !entry.generated)
436
+ };
437
+ }
438
+
439
+ function isGeneratedSksPluginShadow(text = '', name = '') {
440
+ const s = String(text || '');
441
+ if (!new RegExp(`^name:\\s*${escapeRegExp(name)}\\s*$`, 'm').test(s)) return false;
442
+ if (/\bnot generated by SKS\b/i.test(s)) return false;
443
+ return /Sneakoscope generated|Codex App pipeline activation:|Dollar-command route generated by SKS|stale plugin collision/i.test(s);
444
+ }
445
+
388
446
  async function codexFastModeConfigStatus(opts = {}) {
389
447
  const home = opts.home || os.homedir();
390
448
  const cwd = opts.cwd || process.cwd();
@@ -451,6 +509,22 @@ function codexPluginEnabled(configText = '', plugin = {}) {
451
509
  return /(?:^|\n)\s*enabled\s*=\s*true\s*(?:#.*)?(?=\n|$)/.test(block);
452
510
  }
453
511
 
512
+ function pluginPickerBlockers(status = {}) {
513
+ const out = [];
514
+ if (!status.plugins?.picker?.required_flags_ok) out.push('tool_suggest/plugins/apps');
515
+ if (!status.plugins?.picker?.default_plugins_ok) out.push('default_plugins');
516
+ if (!status.plugins?.picker?.skill_shadows_ok) out.push('skill_shadows');
517
+ if (!status.plugins?.picker?.fast_mode_config_ok) out.push('fast_mode_config');
518
+ return out;
519
+ }
520
+
521
+ function defaultPluginMissingSummary(defaultPlugins = {}) {
522
+ return [
523
+ ...(defaultPlugins?.missing_installed || []),
524
+ ...(defaultPlugins?.missing_enabled || [])
525
+ ].join(', ');
526
+ }
527
+
454
528
  function topLevelToml(text = '') {
455
529
  const lines = String(text || '').split('\n');
456
530
  const firstTable = lines.findIndex((line) => /^\s*\[.+\]\s*$/.test(line));
@@ -0,0 +1,481 @@
1
+ export const DECISION_LATTICE_SCHEMA_VERSION = 1;
2
+
3
+ export const DEFAULT_LATTICE_WEIGHTS = Object.freeze({
4
+ step: 1,
5
+ proof_debt: 3,
6
+ risk: 1,
7
+ friction: 1,
8
+ info_gain: 1
9
+ });
10
+
11
+ const AXES = Object.freeze(['contract', 'context', 'implementation', 'verification', 'review']);
12
+
13
+ const DEFAULT_START = Object.freeze({
14
+ contract: 0,
15
+ context: 0,
16
+ implementation: 0,
17
+ verification: 0,
18
+ review: 0
19
+ });
20
+
21
+ const DEFAULT_GOAL = Object.freeze({
22
+ contract: 2,
23
+ context: 2,
24
+ implementation: 2,
25
+ verification: 2,
26
+ review: 1
27
+ });
28
+
29
+ const DEFAULT_ACTIONS = Object.freeze([
30
+ {
31
+ id: 'seal_contract',
32
+ label: 'Seal decision contract',
33
+ delta: { contract: 2 },
34
+ risk: 0.05,
35
+ friction: 0.25,
36
+ info_gain: 0.9,
37
+ notes: ['Removes ambiguity before route selection.']
38
+ },
39
+ {
40
+ id: 'read_triwiki',
41
+ label: 'Read bounded TriWiki context',
42
+ delta: { context: 1 },
43
+ risk: 0.05,
44
+ friction: 0.2,
45
+ info_gain: 0.7,
46
+ notes: ['Uses compact high-trust recall before editing.']
47
+ },
48
+ {
49
+ id: 'proof_field_scan',
50
+ label: 'Run proof-field scan',
51
+ delta: { context: 2, verification: 1 },
52
+ risk: 0.1,
53
+ friction: 0.35,
54
+ info_gain: 0.95,
55
+ notes: ['Scores route surface and escalation triggers.']
56
+ },
57
+ {
58
+ id: 'minimal_patch',
59
+ label: 'Implement smallest scoped change',
60
+ delta: { implementation: 2 },
61
+ risk: 0.35,
62
+ friction: 0.35,
63
+ info_gain: 0.4,
64
+ notes: ['Touches only the selected proof cone.']
65
+ },
66
+ {
67
+ id: 'focused_verification',
68
+ label: 'Run focused verification',
69
+ delta: { verification: 1 },
70
+ risk: 0.12,
71
+ friction: 0.45,
72
+ info_gain: 0.85,
73
+ notes: ['Checks syntax and behavior for the changed module.']
74
+ },
75
+ {
76
+ id: 'five_lane_review',
77
+ label: 'Collect five-lane Team review',
78
+ delta: { review: 1 },
79
+ risk: 0.2,
80
+ friction: 1.1,
81
+ info_gain: 1,
82
+ notes: ['Satisfies Team review gate for broad missions.']
83
+ },
84
+ {
85
+ id: 'honest_mode',
86
+ label: 'Run Honest Mode closeout',
87
+ delta: { verification: 1 },
88
+ risk: 0.05,
89
+ friction: 0.2,
90
+ info_gain: 0.65,
91
+ notes: ['Binds final claims to evidence and gaps.']
92
+ }
93
+ ]);
94
+
95
+ const DEFAULT_ROUTE_PATHS = Object.freeze([
96
+ {
97
+ id: 'proof_field_fast_lane',
98
+ label: 'Proof Field Fast Lane',
99
+ action_ids: ['seal_contract', 'read_triwiki', 'proof_field_scan', 'minimal_patch', 'focused_verification', 'honest_mode'],
100
+ notes: ['Lowest friction when scope is narrow and risk flags stay low.']
101
+ },
102
+ {
103
+ id: 'balanced_team_lane',
104
+ label: 'Balanced Team Lane',
105
+ action_ids: ['seal_contract', 'read_triwiki', 'proof_field_scan', 'minimal_patch', 'focused_verification', 'five_lane_review', 'honest_mode'],
106
+ notes: ['Adds review evidence while preserving a compact change surface.']
107
+ },
108
+ {
109
+ id: 'full_team_honest_path',
110
+ label: 'Full Team Honest Path',
111
+ action_ids: ['seal_contract', 'read_triwiki', 'proof_field_scan', 'five_lane_review', 'minimal_patch', 'focused_verification', 'five_lane_review', 'honest_mode'],
112
+ notes: ['Heaviest default for broad or release-sensitive missions.']
113
+ }
114
+ ]);
115
+
116
+ export function buildDecisionLatticeReport(input = {}) {
117
+ const weights = normalizeWeights(input.weights);
118
+ const start = normalizeState(input.start_state || input.start || DEFAULT_START);
119
+ const goal = normalizeState(input.goal_state || input.target_state || input.target || inferredGoal(input));
120
+ const actions = normalizeActions(input.actions || DEFAULT_ACTIONS);
121
+ const routePaths = normalizeRoutePaths(input.route_paths || input.candidate_route_paths || DEFAULT_ROUTE_PATHS, actions);
122
+ const grid = buildConceptualGrid(start, goal, actions);
123
+ const search = runAStar({ start, goal, actions, weights });
124
+ const routeCandidates = routePaths.map((routePath, index) => evaluateRoutePath(routePath, index, { start, goal, actions, weights }));
125
+ const candidates = routeCandidates.concat([{ ...search.selected_path, rank_hint: routeCandidates.length }]).sort(compareCandidates);
126
+ const selected = selectPath(candidates, search.selected_path);
127
+ const rejected = candidates
128
+ .filter((candidate) => candidate.id !== selected.id)
129
+ .map((candidate) => ({
130
+ id: candidate.id,
131
+ label: candidate.label,
132
+ f: candidate.cost.f,
133
+ delta_from_selected: round(candidate.cost.f - selected.cost.f),
134
+ rejection_reasons: rejectionReasons(candidate, selected)
135
+ }));
136
+ const report = {
137
+ schema_version: DECISION_LATTICE_SCHEMA_VERSION,
138
+ report_only: true,
139
+ deterministic: true,
140
+ module: 'decision-lattice',
141
+ scoring_formula: String(input.scoring_formula || 'f = g + h + risk + friction - info_gain'),
142
+ research_basis: {
143
+ model: 'Decision Lattice A* planner',
144
+ scoring_formula: 'f = g + h + risk + friction - info_gain',
145
+ proof_debt_heuristic: 'h is weighted remaining lattice debt across contract, context, implementation, verification, and review axes.'
146
+ },
147
+ input_summary: {
148
+ intent: String(input.intent || input.goal || '').trim() || null,
149
+ weights,
150
+ start_state: start,
151
+ goal_state: goal,
152
+ action_count: actions.length,
153
+ route_path_count: routePaths.length
154
+ },
155
+ heuristic: {
156
+ id: 'proof_debt',
157
+ h_start: proofDebt(start, goal, weights),
158
+ axes: AXES.map((axis) => ({
159
+ axis,
160
+ start: start[axis],
161
+ goal: goal[axis],
162
+ debt: debtForAxis(start, goal, axis),
163
+ weighted_debt: round(debtForAxis(start, goal, axis) * weights.proof_debt)
164
+ }))
165
+ },
166
+ conceptual_grid: grid,
167
+ frontier: search.frontier,
168
+ candidate_paths: candidates,
169
+ selected_path: selected,
170
+ rejected_alternatives: rejected,
171
+ validation: null
172
+ };
173
+ report.validation = validateDecisionLatticeReport(report);
174
+ return report;
175
+ }
176
+
177
+ function inferredGoal(input = {}) {
178
+ const goal = { ...DEFAULT_GOAL };
179
+ if (input.execution_lane?.fast_lane_allowed === true && !(input.team_trigger_matrix?.active_triggers || []).length) {
180
+ goal.review = 0;
181
+ }
182
+ return goal;
183
+ }
184
+
185
+ export function validateDecisionLatticeReport(report = {}) {
186
+ const issues = [];
187
+ if (report.schema_version !== DECISION_LATTICE_SCHEMA_VERSION) issues.push('schema_version');
188
+ if (report.report_only !== true) issues.push('report_only');
189
+ if (report.deterministic !== true) issues.push('deterministic');
190
+ if (report.research_basis?.scoring_formula !== 'f = g + h + risk + friction - info_gain') issues.push('scoring_formula');
191
+ if (!Array.isArray(report.heuristic?.axes) || report.heuristic.axes.length !== AXES.length) issues.push('heuristic_axes');
192
+ if (!Number.isFinite(Number(report.heuristic?.h_start))) issues.push('heuristic_h_start');
193
+ if (!Array.isArray(report.conceptual_grid?.cells) || report.conceptual_grid.cells.length < 1) issues.push('conceptual_grid');
194
+ if (!Array.isArray(report.frontier?.expanded_order) || report.frontier.expanded_order.length < 1) issues.push('frontier_expanded_order');
195
+ if (!Array.isArray(report.candidate_paths) || report.candidate_paths.length < 1) issues.push('candidate_paths');
196
+ if (!report.selected_path?.id || !Array.isArray(report.selected_path?.steps)) issues.push('selected_path');
197
+ if (!Array.isArray(report.rejected_alternatives)) issues.push('rejected_alternatives');
198
+ if (report.candidate_paths?.some((candidate) => !Number.isFinite(Number(candidate?.cost?.f)))) issues.push('candidate_costs');
199
+ if (report.selected_path?.cost?.f !== Math.min(...(report.candidate_paths || []).map((candidate) => candidate.cost.f))) issues.push('selected_path_not_min_f');
200
+ return { ok: issues.length === 0, issues };
201
+ }
202
+
203
+ function normalizeWeights(input = {}) {
204
+ return {
205
+ step: positiveNumber(input.step, DEFAULT_LATTICE_WEIGHTS.step),
206
+ proof_debt: positiveNumber(input.proof_debt, DEFAULT_LATTICE_WEIGHTS.proof_debt),
207
+ risk: positiveNumber(input.risk, DEFAULT_LATTICE_WEIGHTS.risk),
208
+ friction: positiveNumber(input.friction, DEFAULT_LATTICE_WEIGHTS.friction),
209
+ info_gain: positiveNumber(input.info_gain, DEFAULT_LATTICE_WEIGHTS.info_gain)
210
+ };
211
+ }
212
+
213
+ function normalizeState(input = {}) {
214
+ const state = {};
215
+ for (const axis of AXES) state[axis] = clampInt(input[axis], 0, 3);
216
+ return state;
217
+ }
218
+
219
+ function normalizeActions(input = []) {
220
+ return input
221
+ .map((action, index) => ({
222
+ id: safeId(action.id || `action_${index + 1}`),
223
+ label: String(action.label || action.id || `Action ${index + 1}`),
224
+ delta: normalizeDelta(action.delta || {}),
225
+ risk: nonNegativeNumber(action.risk, 0),
226
+ friction: nonNegativeNumber(action.friction, 0),
227
+ info_gain: nonNegativeNumber(action.info_gain, 0),
228
+ notes: arrayOfStrings(action.notes)
229
+ }))
230
+ .filter((action) => AXES.some((axis) => action.delta[axis] > 0))
231
+ .sort(compareById);
232
+ }
233
+
234
+ function normalizeRoutePaths(input = [], actions = []) {
235
+ const actionIds = new Set(actions.map((action) => action.id));
236
+ return input
237
+ .map((routePath, index) => ({
238
+ id: safeId(routePath.id || `route_path_${index + 1}`),
239
+ label: String(routePath.label || routePath.id || `Route Path ${index + 1}`),
240
+ action_ids: arrayOfStrings(routePath.action_ids || routePath.actions).map(safeId).filter((id) => actionIds.has(id)),
241
+ notes: arrayOfStrings(routePath.notes)
242
+ }))
243
+ .filter((routePath) => routePath.action_ids.length > 0)
244
+ .sort(compareById);
245
+ }
246
+
247
+ function normalizeDelta(delta = {}) {
248
+ const out = {};
249
+ for (const axis of AXES) out[axis] = clampInt(delta[axis], 0, 3);
250
+ return out;
251
+ }
252
+
253
+ function runAStar({ start, goal, actions, weights }) {
254
+ const open = [nodeForState(start, { g: 0, h: proofDebt(start, goal, weights), risk: 0, friction: 0, info_gain: 0, steps: [] })];
255
+ const best = new Map([[stateKey(start), 0]]);
256
+ const closed = [];
257
+ const snapshots = [];
258
+ let selected = open[0];
259
+
260
+ while (open.length > 0 && closed.length < 64) {
261
+ open.sort(compareNodes);
262
+ const current = open.shift();
263
+ closed.push(current);
264
+ snapshots.push({ step: closed.length, current: current.key, f: current.f, open: open.map((node) => node.key).sort() });
265
+ if (isGoal(current.state, goal)) {
266
+ selected = current;
267
+ break;
268
+ }
269
+ for (const action of actions) {
270
+ const nextState = applyAction(current.state, action, goal);
271
+ const key = stateKey(nextState);
272
+ const g = round(current.g + weights.step);
273
+ if (best.has(key) && best.get(key) <= g) continue;
274
+ best.set(key, g);
275
+ const risk = round(current.risk + action.risk * weights.risk);
276
+ const friction = round(current.friction + action.friction * weights.friction);
277
+ const infoGain = round(current.info_gain + action.info_gain * weights.info_gain);
278
+ const h = proofDebt(nextState, goal, weights);
279
+ open.push(nodeForState(nextState, {
280
+ g,
281
+ h,
282
+ risk,
283
+ friction,
284
+ info_gain: infoGain,
285
+ steps: current.steps.concat([stepFromAction(action, nextState)])
286
+ }));
287
+ }
288
+ }
289
+
290
+ return {
291
+ selected_path: pathFromNode('astar_frontier_path', 'A* Frontier Path', selected),
292
+ frontier: {
293
+ expanded_order: closed.map((node, index) => ({ index, key: node.key, f: node.f, h: node.h, steps: node.steps.map((step) => step.id) })),
294
+ open_nodes: open.sort(compareNodes).slice(0, 12).map((node) => ({ key: node.key, f: node.f, h: node.h })),
295
+ closed_nodes: closed.map((node) => node.key),
296
+ snapshots
297
+ }
298
+ };
299
+ }
300
+
301
+ function evaluateRoutePath(routePath, index, { start, goal, actions, weights }) {
302
+ const actionById = new Map(actions.map((action) => [action.id, action]));
303
+ let state = { ...start };
304
+ let g = 0;
305
+ let risk = 0;
306
+ let friction = 0;
307
+ let infoGain = 0;
308
+ const steps = [];
309
+ for (const id of routePath.action_ids) {
310
+ const action = actionById.get(id);
311
+ if (!action) continue;
312
+ g = round(g + weights.step);
313
+ risk = round(risk + action.risk * weights.risk);
314
+ friction = round(friction + action.friction * weights.friction);
315
+ infoGain = round(infoGain + action.info_gain * weights.info_gain);
316
+ state = applyAction(state, action, goal);
317
+ steps.push(stepFromAction(action, state));
318
+ }
319
+ const h = proofDebt(state, goal, weights);
320
+ const f = round(g + h + risk + friction - infoGain);
321
+ return {
322
+ id: routePath.id,
323
+ label: routePath.label,
324
+ rank_hint: index,
325
+ route: routePath.action_ids,
326
+ steps,
327
+ final_state: state,
328
+ proof_debt: h,
329
+ complete: isGoal(state, goal),
330
+ cost: { g, h, risk, friction, info_gain: infoGain, f },
331
+ notes: routePath.notes
332
+ };
333
+ }
334
+
335
+ function selectPath(candidates, astarPath) {
336
+ const complete = candidates.filter((candidate) => candidate.complete);
337
+ const pool = complete.length ? complete : candidates;
338
+ const selected = pool.slice().sort(compareCandidates)[0] || astarPath;
339
+ return selected.cost.f <= astarPath.cost.f ? selected : astarPath;
340
+ }
341
+
342
+ function pathFromNode(id, label, node) {
343
+ return {
344
+ id,
345
+ label,
346
+ route: node.steps.map((step) => step.id),
347
+ steps: node.steps,
348
+ final_state: node.state,
349
+ proof_debt: node.h,
350
+ complete: node.h === 0,
351
+ cost: {
352
+ g: node.g,
353
+ h: node.h,
354
+ risk: node.risk,
355
+ friction: node.friction,
356
+ info_gain: node.info_gain,
357
+ f: node.f
358
+ },
359
+ notes: ['Generated by A* frontier expansion over the conceptual lattice.']
360
+ };
361
+ }
362
+
363
+ function nodeForState(state, input) {
364
+ const f = round(input.g + input.h + input.risk + input.friction - input.info_gain);
365
+ return { ...input, state, key: stateKey(state), f };
366
+ }
367
+
368
+ function applyAction(state, action, goal) {
369
+ const next = {};
370
+ for (const axis of AXES) next[axis] = Math.min(goal[axis], state[axis] + action.delta[axis]);
371
+ return next;
372
+ }
373
+
374
+ function proofDebt(state, goal, weights) {
375
+ return round(AXES.reduce((sum, axis) => sum + debtForAxis(state, goal, axis), 0) * weights.proof_debt);
376
+ }
377
+
378
+ function debtForAxis(state, goal, axis) {
379
+ return Math.max(0, Number(goal[axis] || 0) - Number(state[axis] || 0));
380
+ }
381
+
382
+ function buildConceptualGrid(start, goal, actions) {
383
+ return {
384
+ axes: AXES.map((axis) => ({ axis, start: start[axis], goal: goal[axis], span: Math.max(0, goal[axis] - start[axis]) })),
385
+ cells: AXES.map((axis) => ({
386
+ id: `axis_${axis}`,
387
+ axis,
388
+ start: start[axis],
389
+ goal: goal[axis],
390
+ candidate_actions: actions.filter((action) => action.delta[axis] > 0).map((action) => action.id)
391
+ })),
392
+ legend: {
393
+ g: 'path steps already paid',
394
+ h: 'remaining proof debt',
395
+ risk: 'expected safety and integration exposure',
396
+ friction: 'coordination and verification drag',
397
+ info_gain: 'uncertainty removed by the step'
398
+ }
399
+ };
400
+ }
401
+
402
+ function rejectionReasons(candidate, selected) {
403
+ const reasons = [];
404
+ if (!candidate.complete) reasons.push('remaining_proof_debt');
405
+ if (candidate.cost.risk > selected.cost.risk) reasons.push('higher_risk');
406
+ if (candidate.cost.friction > selected.cost.friction) reasons.push('higher_friction');
407
+ if (candidate.cost.info_gain < selected.cost.info_gain) reasons.push('lower_info_gain');
408
+ if (candidate.cost.f > selected.cost.f) reasons.push('higher_total_f');
409
+ return reasons.length ? reasons : ['tie_broken_by_deterministic_order'];
410
+ }
411
+
412
+ function compareCandidates(a, b) {
413
+ return (a.cost.f - b.cost.f)
414
+ || (a.cost.h - b.cost.h)
415
+ || (a.cost.risk - b.cost.risk)
416
+ || (a.cost.friction - b.cost.friction)
417
+ || (b.cost.info_gain - a.cost.info_gain)
418
+ || a.id.localeCompare(b.id);
419
+ }
420
+
421
+ function compareNodes(a, b) {
422
+ return (a.f - b.f)
423
+ || (a.h - b.h)
424
+ || (a.risk - b.risk)
425
+ || (a.friction - b.friction)
426
+ || (b.info_gain - a.info_gain)
427
+ || a.key.localeCompare(b.key);
428
+ }
429
+
430
+ function compareById(a, b) {
431
+ return a.id.localeCompare(b.id);
432
+ }
433
+
434
+ function stateKey(state) {
435
+ return AXES.map((axis) => `${axis}:${state[axis]}`).join('|');
436
+ }
437
+
438
+ function isGoal(state, goal) {
439
+ return AXES.every((axis) => state[axis] >= goal[axis]);
440
+ }
441
+
442
+ function stepFromAction(action, state) {
443
+ return {
444
+ id: action.id,
445
+ label: action.label,
446
+ state_after: state,
447
+ risk: action.risk,
448
+ friction: action.friction,
449
+ info_gain: action.info_gain,
450
+ notes: action.notes
451
+ };
452
+ }
453
+
454
+ function arrayOfStrings(value) {
455
+ if (!Array.isArray(value)) return [];
456
+ return value.map((item) => String(item || '').trim()).filter(Boolean);
457
+ }
458
+
459
+ function safeId(value) {
460
+ return String(value || 'item').trim().toLowerCase().replace(/[^a-z0-9_./-]+/g, '_').replace(/^_+|_+$/g, '') || 'item';
461
+ }
462
+
463
+ function clampInt(value, min, max) {
464
+ const number = Number(value);
465
+ if (!Number.isFinite(number)) return min;
466
+ return Math.max(min, Math.min(max, Math.floor(number)));
467
+ }
468
+
469
+ function positiveNumber(value, fallback) {
470
+ const number = Number(value);
471
+ return Number.isFinite(number) && number > 0 ? number : fallback;
472
+ }
473
+
474
+ function nonNegativeNumber(value, fallback) {
475
+ const number = Number(value);
476
+ return Number.isFinite(number) && number >= 0 ? number : fallback;
477
+ }
478
+
479
+ function round(value) {
480
+ return Math.round(Number(value || 0) * 1000) / 1000;
481
+ }
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.8.6';
8
+ export const PACKAGE_VERSION = '0.9.1';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
package/src/core/init.mjs CHANGED
@@ -7,7 +7,7 @@ import { isHarnessSourceProject, writeHarnessGuardPolicy } from './harness-guard
7
7
  import { repairSksGeneratedArtifacts } from './harness-conflicts.mjs';
8
8
  import { disableVersionGitHook } from './version-manager.mjs';
9
9
  import { MIN_TEAM_REVIEWER_LANES, MIN_TEAM_REVIEW_POLICY_TEXT } from './team-review-policy.mjs';
10
- import { AWESOME_DESIGN_MD_REFERENCE, CODEX_APP_IMAGE_GENERATION_DOC_URL, CODEX_COMPUTER_USE_ONLY_POLICY, CODEX_IMAGEGEN_REQUIRED_POLICY, DESIGN_SYSTEM_SSOT, DOLLAR_COMMANDS, DOLLAR_COMMAND_ALIASES, DOLLAR_SKILL_NAMES, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, GETDESIGN_REFERENCE, PPT_CONDITIONAL_SKILL_ALLOWLIST, PPT_PIPELINE_MCP_ALLOWLIST, PPT_PIPELINE_SKILL_ALLOWLIST, RECOMMENDED_DESIGN_REFERENCES, RECOMMENDED_MCP_SERVERS, RECOMMENDED_SKILLS, RESERVED_CODEX_PLUGIN_SKILL_NAMES, SOLUTION_SCOUT_SKILL_NAME, chatCaptureIntakeText, context7ConfigToml, getdesignReferencePolicyText, imageUxReviewPipelinePolicyText, outcomeRubricPolicyText, pptPipelineAllowlistPolicyText, solutionScoutPolicyText, speedLanePolicyText, stackCurrentDocsPolicyText, triwikiContextTracking, triwikiContextTrackingText, triwikiStagePolicyText } from './routes.mjs';
10
+ import { AWESOME_DESIGN_MD_REFERENCE, CODEX_APP_IMAGE_GENERATION_DOC_URL, CODEX_COMPUTER_USE_ONLY_POLICY, CODEX_IMAGEGEN_REQUIRED_POLICY, DEFAULT_CODEX_APP_PLUGINS, DESIGN_SYSTEM_SSOT, DOLLAR_COMMANDS, DOLLAR_COMMAND_ALIASES, DOLLAR_SKILL_NAMES, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, GETDESIGN_REFERENCE, PPT_CONDITIONAL_SKILL_ALLOWLIST, PPT_PIPELINE_MCP_ALLOWLIST, PPT_PIPELINE_SKILL_ALLOWLIST, RECOMMENDED_DESIGN_REFERENCES, RECOMMENDED_MCP_SERVERS, RECOMMENDED_SKILLS, RESERVED_CODEX_PLUGIN_SKILL_NAMES, SOLUTION_SCOUT_SKILL_NAME, chatCaptureIntakeText, context7ConfigToml, getdesignReferencePolicyText, imageUxReviewPipelinePolicyText, outcomeRubricPolicyText, pptPipelineAllowlistPolicyText, solutionScoutPolicyText, speedLanePolicyText, stackCurrentDocsPolicyText, triwikiContextTracking, triwikiContextTrackingText, triwikiStagePolicyText } from './routes.mjs';
11
11
  import { SKILL_DREAM_POLICY, skillDreamPolicyText } from './skill-forge.mjs';
12
12
 
13
13
  const REFLECTION_MEMORY_PATH = '.sneakoscope/memory/q2_facts/post-route-reflection.md';
@@ -33,16 +33,6 @@ export const REQUIRED_GENERATED_CODEX_APP_FEATURE_FLAGS = [
33
33
  'plugins'
34
34
  ];
35
35
 
36
- const DEFAULT_CODEX_APP_PLUGINS = [
37
- ['browser', 'openai-bundled'],
38
- ['chrome', 'openai-bundled'],
39
- ['computer-use', 'openai-bundled'],
40
- ['latex', 'openai-bundled'],
41
- ['documents', 'openai-primary-runtime'],
42
- ['presentations', 'openai-primary-runtime'],
43
- ['spreadsheets', 'openai-primary-runtime']
44
- ];
45
-
46
36
  export function hasTopLevelCodexModeLock(text = '') {
47
37
  const lines = String(text || '').split('\n');
48
38
  const firstTable = lines.findIndex((x) => /^\s*\[.+\]\s*$/.test(x));
@@ -110,6 +110,11 @@ export async function runWorkflowPerfBench(root, opts = {}) {
110
110
  workflow_complexity_band: proofField?.workflow_complexity?.band || null,
111
111
  team_trigger_count: proofField?.team_trigger_matrix?.active_triggers?.length || 0,
112
112
  verification_stage_cache_key: proofField?.verification_stage_cache?.cache_key || null,
113
+ decision_lattice_selected_path: proofField?.decision_lattice?.selected_path?.id || null,
114
+ decision_lattice_frontier_count: proofField?.decision_lattice?.frontier?.expanded_order?.length || 0,
115
+ decision_lattice_rejected_alternative_count: proofField?.decision_lattice?.rejected_alternatives?.length || 0,
116
+ decision_lattice_scoring_formula: proofField?.decision_lattice?.scoring_formula || null,
117
+ decision_lattice_valid: Boolean(proofField?.decision_lattice?.report_only) && proofValidation.ok,
113
118
  outcome_criteria_passed: (proofField?.simplicity_scorecard?.criteria || []).filter((item) => item.passed).length,
114
119
  proof_field_valid: proofValidation.ok,
115
120
  pipeline_plan_valid: planValidation.ok
@@ -138,7 +143,19 @@ export function validateWorkflowPerfReport(report = {}) {
138
143
  if (!Number.isFinite(Number(report.metrics?.workflow_complexity_score))) issues.push('workflow_complexity_score');
139
144
  if (!report.metrics?.workflow_complexity_band) issues.push('workflow_complexity_band');
140
145
  if (!report.metrics?.verification_stage_cache_key) issues.push('verification_stage_cache_key');
146
+ if (!report.metrics?.decision_lattice_selected_path) issues.push('decision_lattice_selected_path');
147
+ if (!Number.isFinite(Number(report.metrics?.decision_lattice_frontier_count))) issues.push('decision_lattice_frontier_count');
148
+ if (!Number.isFinite(Number(report.metrics?.decision_lattice_rejected_alternative_count))) issues.push('decision_lattice_rejected_alternative_count');
149
+ if (!report.metrics?.decision_lattice_scoring_formula) issues.push('decision_lattice_scoring_formula');
150
+ if (report.metrics?.decision_lattice_valid !== true) issues.push('decision_lattice_valid');
141
151
  if (!report.proof_field || !validateProofFieldReport(report.proof_field).ok) issues.push('proof_field');
152
+ else {
153
+ const lattice = report.proof_field.decision_lattice;
154
+ if (report.metrics.decision_lattice_selected_path !== lattice?.selected_path?.id) issues.push('decision_lattice_selected_path_mismatch');
155
+ if (Number(report.metrics.decision_lattice_frontier_count) !== Number(lattice?.frontier?.expanded_order?.length || 0)) issues.push('decision_lattice_frontier_count_mismatch');
156
+ if (Number(report.metrics.decision_lattice_rejected_alternative_count) !== Number(lattice?.rejected_alternatives?.length || 0)) issues.push('decision_lattice_rejected_alternative_count_mismatch');
157
+ if (report.metrics.decision_lattice_scoring_formula !== lattice?.scoring_formula) issues.push('decision_lattice_scoring_formula_mismatch');
158
+ }
142
159
  if (!report.pipeline_plan || !validatePipelinePlan(report.pipeline_plan).ok) issues.push('pipeline_plan');
143
160
  if (!report.recommendation?.mode) issues.push('recommendation');
144
161
  return { ok: issues.length === 0, issues };
@@ -135,11 +135,32 @@ export function validatePipelinePlan(plan = {}) {
135
135
  if (!Array.isArray(plan.stages) || !plan.stages.length) issues.push('stages');
136
136
  if (!Array.isArray(plan.verification) || !plan.verification.length) issues.push('verification');
137
137
  if (!plan.route_economy?.mode) issues.push('route_economy');
138
+ const routeEconomyLatticeIssues = validateRouteEconomyDecisionLattice(plan.route_economy, plan.proof_field);
139
+ if (routeEconomyLatticeIssues.length) issues.push(...routeEconomyLatticeIssues.map((issue) => `route_economy.decision_lattice:${issue}`));
138
140
  if (plan.no_unrequested_fallback_code !== true || !plan.invariants?.includes('no_unrequested_fallback_code')) issues.push('fallback_guard');
139
141
  if (!plan.next_actions?.length) issues.push('next_actions');
140
142
  return { ok: issues.length === 0, issues };
141
143
  }
142
144
 
145
+ function validateRouteEconomyDecisionLattice(routeEconomy = {}, proof = {}) {
146
+ const lattice = routeEconomy.decision_lattice;
147
+ if (!lattice) return [];
148
+ const issues = [];
149
+ if (routeEconomy.report_only !== true || routeEconomy.mode !== 'report_only') issues.push('requires_report_only_route_economy');
150
+ if (lattice.report_only !== true) issues.push('report_only');
151
+ if (!lattice.selected_path) issues.push('selected_path');
152
+ if (!Number.isFinite(Number(lattice.selected_f_score))) issues.push('selected_f_score');
153
+ if (!Number.isFinite(Number(lattice.frontier_count)) || Number(lattice.frontier_count) < 1) issues.push('frontier_count');
154
+ if (!Number.isFinite(Number(lattice.rejected_alternatives_count))) issues.push('rejected_alternatives_count');
155
+ if (proof?.attached && proof.decision_lattice) {
156
+ const source = proof.decision_lattice;
157
+ if (lattice.selected_path !== source.selected_path?.id) issues.push('selected_path_mismatch');
158
+ if (Number(lattice.frontier_count) !== Number(source.frontier?.expanded_order?.length || 0)) issues.push('frontier_count_mismatch');
159
+ if (Number(lattice.rejected_alternatives_count) !== Number(source.rejected_alternatives?.length || 0)) issues.push('rejected_alternatives_count_mismatch');
160
+ }
161
+ return issues;
162
+ }
163
+
143
164
  function normalizeAmbiguity(value = {}, route) {
144
165
  const required = value.required ?? !CLARIFICATION_BYPASS_ROUTES.has(route?.id);
145
166
  const slots = Number(value.slots || 0);
@@ -173,7 +194,8 @@ function normalizeProofField(report) {
173
194
  contract_clarity: report.contract_clarity || null,
174
195
  workflow_complexity: report.workflow_complexity || null,
175
196
  team_trigger_matrix: report.team_trigger_matrix || null,
176
- verification_stage_cache: report.verification_stage_cache || null
197
+ verification_stage_cache: report.verification_stage_cache || null,
198
+ decision_lattice: report.decision_lattice || null
177
199
  };
178
200
  }
179
201
 
@@ -199,6 +221,13 @@ function routeEconomyPlan(proof = {}) {
199
221
  team_trigger_count: triggers.length,
200
222
  active_team_triggers: triggers,
201
223
  verification_stage_cache_key: proof.verification_stage_cache?.cache_key || null,
224
+ decision_lattice: proof.decision_lattice ? {
225
+ selected_path: proof.decision_lattice.selected_path?.id || null,
226
+ selected_f_score: proof.decision_lattice.selected_path?.cost?.f ?? null,
227
+ frontier_count: proof.decision_lattice.frontier?.expanded_order?.length || 0,
228
+ rejected_alternatives_count: proof.decision_lattice.rejected_alternatives?.length || 0,
229
+ report_only: proof.decision_lattice.report_only === true
230
+ } : null,
202
231
  deletion_policy: 'do_not_delete_or_skip_pipeline_stages_until_report_only_metrics_are_calibrated'
203
232
  };
204
233
  }
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { nowIso, readText, rel, runProcess, sha256, writeJsonAtomic } from './fsx.mjs';
3
+ import { buildDecisionLatticeReport, validateDecisionLatticeReport } from './decision-lattice.mjs';
3
4
 
4
5
  export const PROOF_FIELD_SCHEMA_VERSION = 1;
5
6
  export const FAST_LANE_MIN_SCORE = 0.75;
@@ -88,6 +89,20 @@ export async function buildProofField(root, opts = {}) {
88
89
  const verificationStageCache = verificationStageCachePlan({ sourceHash, changedFiles, verification: fastLane.verification });
89
90
  const simplicity = outcomeScorecard({ intent, changedFiles, selectedCones, risk, negativeWork, fastLane, workflowComplexity });
90
91
  const executionLane = executionLaneDecision({ fastLane, simplicity, workflowComplexity, teamTriggerMatrix });
92
+ const decisionLattice = normalizeDecisionLatticeReport(await buildDecisionLatticeReport({
93
+ intent,
94
+ changed_files: changedFiles,
95
+ proof_cones: selectedCones,
96
+ risk,
97
+ contract_clarity: contractClarity,
98
+ workflow_complexity: workflowComplexity,
99
+ team_trigger_matrix: teamTriggerMatrix,
100
+ verification_stage_cache: verificationStageCache,
101
+ simplicity_scorecard: simplicity,
102
+ fast_lane_decision: fastLane,
103
+ execution_lane: executionLane,
104
+ scoring_formula: 'simplicity_scorecard.score + contract_clarity.score - workflow_complexity.score - active_team_trigger_penalty'
105
+ }));
91
106
  return {
92
107
  schema_version: PROOF_FIELD_SCHEMA_VERSION,
93
108
  generated_at: nowIso(),
@@ -102,6 +117,7 @@ export async function buildProofField(root, opts = {}) {
102
117
  workflow_complexity: workflowComplexity,
103
118
  team_trigger_matrix: teamTriggerMatrix,
104
119
  verification_stage_cache: verificationStageCache,
120
+ decision_lattice: decisionLattice,
105
121
  simplicity_scorecard: simplicity,
106
122
  execution_lane: executionLane,
107
123
  proof_cones: selectedCones,
@@ -133,6 +149,8 @@ export function validateProofFieldReport(report = {}) {
133
149
  if (!Number.isFinite(Number(report.workflow_complexity?.score))) issues.push('workflow_complexity');
134
150
  if (!Array.isArray(report.team_trigger_matrix?.triggers)) issues.push('team_trigger_matrix');
135
151
  if (report.verification_stage_cache?.report_only !== true || !report.verification_stage_cache?.cache_key) issues.push('verification_stage_cache');
152
+ const latticeValidation = validateDecisionLatticeReport(report.decision_lattice);
153
+ if (!latticeValidation.ok) issues.push(`decision_lattice:${latticeValidation.issues.join('|')}`);
136
154
  if (!report.execution_lane?.lane) issues.push('execution_lane');
137
155
  if (report.execution_lane?.lane === SPEED_LANE_POLICY.fast_lane && report.execution_lane?.score < FAST_LANE_MIN_SCORE) issues.push('execution_lane_score');
138
156
  if (!Array.isArray(report.proof_cones)) issues.push('proof_cones');
@@ -158,12 +176,25 @@ export async function proofFieldFixture() {
158
176
  outcome_rubric_present: report.outcome_rubric.length === OUTCOME_RUBRIC.length,
159
177
  adversarial_lenses_present: report.outcome_rubric.every((item) => item.adversarial_lens) && report.simplicity_scorecard.criteria.every((item) => item.adversarial_lens),
160
178
  route_economy_present: report.contract_clarity?.report_only === true && report.workflow_complexity?.report_only === true && report.team_trigger_matrix?.report_only === true && report.verification_stage_cache?.report_only === true,
179
+ decision_lattice_present: validateDecisionLatticeReport(report.decision_lattice).ok,
180
+ decision_lattice_report_only: report.decision_lattice?.report_only === true,
181
+ decision_lattice_selected_path: Boolean(report.decision_lattice?.selected_path?.id),
182
+ decision_lattice_frontier_present: Array.isArray(report.decision_lattice?.frontier?.expanded_order) && report.decision_lattice.frontier.expanded_order.length > 0,
183
+ decision_lattice_rejections_present: Array.isArray(report.decision_lattice?.rejected_alternatives),
184
+ decision_lattice_scoring_formula_present: Boolean(report.decision_lattice?.scoring_formula),
161
185
  simplicity_score_usable: Number(report.simplicity_scorecard?.score) >= FAST_LANE_MIN_SCORE,
162
186
  execution_fast_lane_selected: report.execution_lane?.lane === SPEED_LANE_POLICY.fast_lane
163
187
  }
164
188
  };
165
189
  }
166
190
 
191
+ function normalizeDecisionLatticeReport(report = {}) {
192
+ return {
193
+ ...report,
194
+ scoring_formula: report.scoring_formula || report.research_basis?.scoring_formula || null
195
+ };
196
+ }
197
+
167
198
  async function gitChangedFiles(root) {
168
199
  const result = await runProcess('git', ['diff', '--name-only', 'HEAD', '--'], { cwd: root, timeoutMs: 10000, maxOutputBytes: 128 * 1024 });
169
200
  if (result.code !== 0) return [];
@@ -35,7 +35,19 @@ export const CODEX_APP_IMAGE_GENERATION_DOC_URL = 'https://developers.openai.com
35
35
  export const OPENAI_IMAGE_GENERATION_DOC_URL = 'https://developers.openai.com/api/docs/guides/image-generation';
36
36
  export const CODEX_COMPUTER_USE_ONLY_POLICY = 'Pipeline UI/browser verification and visual inspection must use Codex Computer Use only. Do not use or install Playwright packages, Chrome MCP, Browser Use, Selenium, Puppeteer, or any other browser automation substitute; if Codex Computer Use is unavailable for the target UI, mark the UI/browser evidence unverified instead of substituting another tool. Codex App readiness/config verification is not target-UI evidence: use the Codex-provided control surfaces `codex features list`, `codex mcp list`, `sks codex-app check`, remote-control status, and plugin/tool exposure, not direct OS Accessibility control of the Codex App bundle. In Codex App prompts, invoke @Computer or @AppName in a new thread when live Computer Use tools are needed for the actual target app or screen; SKS hooks and skills can require the policy but cannot attach missing host tools to an already-started turn.';
37
37
  export const CODEX_IMAGEGEN_REQUIRED_POLICY = 'Pipeline image generation, raster asset creation/editing, and generated image-review evidence must use real Codex App imagegen/$imagegen with gpt-image-2 when that evidence is required. Do not substitute placeholder SVG/HTML/CSS, prose-only critique, stock-like stand-ins, manually fabricated files, or missing-output ledgers for requested/generated raster assets or required generated review images. If imagegen/gpt-image-2 is unavailable, record the blocker and mark the image asset or review evidence unverified instead of passing the gate. In Codex App prompts, invoke $imagegen when live image generation is needed; SKS hooks and skills can require the policy but cannot attach missing host image-generation tools to an already-started turn.';
38
- export const RESERVED_CODEX_PLUGIN_SKILL_NAMES = Object.freeze(['computer-use', 'browser', 'browser-use']);
38
+ export const DEFAULT_CODEX_APP_PLUGINS = Object.freeze([
39
+ ['browser', 'openai-bundled'],
40
+ ['chrome', 'openai-bundled'],
41
+ ['computer-use', 'openai-bundled'],
42
+ ['latex', 'openai-bundled'],
43
+ ['documents', 'openai-primary-runtime'],
44
+ ['presentations', 'openai-primary-runtime'],
45
+ ['spreadsheets', 'openai-primary-runtime']
46
+ ]);
47
+ export const RESERVED_CODEX_PLUGIN_SKILL_NAMES = Object.freeze([
48
+ 'browser-use',
49
+ ...DEFAULT_CODEX_APP_PLUGINS.map(([name]) => name)
50
+ ].sort());
39
51
  export const FORBIDDEN_BROWSER_AUTOMATION_RE = /\b(playwright|chrome\s+mcp|browser\s+use|selenium|puppeteer)\b/i;
40
52
 
41
53
  export function evidenceMentionsForbiddenBrowserAutomation(value, seen = new Set()) {
@@ -499,10 +511,8 @@ export const DOLLAR_COMMANDS = ROUTES.flatMap(({ command, route, description, do
499
511
  ]);
500
512
  export function routeAppSkillNames(route) {
501
513
  const canonical = dollarSkillName(route.command);
502
- return [
503
- ...(RESERVED_CODEX_PLUGIN_SKILL_NAMES.includes(canonical) ? [] : [canonical]),
504
- ...(route.appSkillAliases || [])
505
- ];
514
+ const reserved = new Set(RESERVED_CODEX_PLUGIN_SKILL_NAMES);
515
+ return [canonical, ...(route.appSkillAliases || [])].filter((name) => !reserved.has(name));
506
516
  }
507
517
 
508
518
  export const DOLLAR_SKILL_NAMES = ROUTES.flatMap((route) => routeAppSkillNames(route));