sneakoscope 10.2.0 → 10.3.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.
Files changed (46) hide show
  1. package/README.md +6 -1
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/cli/command-manifest-lite.js +3 -1
  5. package/dist/cli/command-registry.js +9 -1
  6. package/dist/cli/install-helpers-codex-lb-config.js +14 -4
  7. package/dist/cli/install-helpers-menubar.js +57 -0
  8. package/dist/cli/install-helpers.js +10 -1
  9. package/dist/commands/decision.js +7 -0
  10. package/dist/config/skills-manifest.json +1 -1
  11. package/dist/core/agents/native-worker-backend-router.js +7 -2
  12. package/dist/core/codex-app/menubar/constants.js +2 -0
  13. package/dist/core/codex-lb/desktop-controller-v3/provider-commands.js +9 -0
  14. package/dist/core/decisions/cli.js +324 -0
  15. package/dist/core/decisions/config.js +109 -0
  16. package/dist/core/decisions/evaluation.js +62 -0
  17. package/dist/core/decisions/integration.js +297 -0
  18. package/dist/core/decisions/openrouter.js +270 -0
  19. package/dist/core/decisions/policy.js +393 -0
  20. package/dist/core/decisions/questions.js +215 -0
  21. package/dist/core/decisions/receipt.js +41 -0
  22. package/dist/core/decisions/recovery.js +39 -0
  23. package/dist/core/decisions/routing.js +58 -0
  24. package/dist/core/decisions/state.js +173 -0
  25. package/dist/core/decisions/types.js +69 -0
  26. package/dist/core/feature-fixtures.js +1 -0
  27. package/dist/core/feature-registry.js +1 -0
  28. package/dist/core/hooks-runtime/subagent-context.js +5 -3
  29. package/dist/core/hooks-runtime/subagent-spawn-policy.js +9 -3
  30. package/dist/core/hooks-runtime.js +25 -1
  31. package/dist/core/provider/model-router.js +3 -3
  32. package/dist/core/routes.js +2 -1
  33. package/dist/core/subagents/model-policy.js +15 -0
  34. package/dist/core/subagents/official-subagent-lock.js +15 -0
  35. package/dist/core/subagents/official-subagent-preparation.js +265 -24
  36. package/dist/core/subagents/official-subagent-prompt.js +56 -13
  37. package/dist/core/update/update-migration-state/retired-local-decision.js +203 -0
  38. package/dist/core/update/update-migration-state.js +6 -0
  39. package/dist/core/version.js +1 -1
  40. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +6 -3
  41. package/dist/native/sks-menubar/Sources/LocalDecisionModels.swift +166 -0
  42. package/dist/native/sks-menubar/Sources/LocalDecisionViewController.swift +200 -0
  43. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +4 -0
  44. package/dist/native/sks-menubar/Sources/SidebarItem.swift +2 -0
  45. package/dist/scripts/current-surface-update-e2e-check.js +1 -0
  46. package/package.json +1 -1
package/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
  Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.
17
17
  <!-- END SKS SEARCH VISIBILITY MARKETING -->
18
18
 
19
- Current package: **SKS 10.2.0**. Install the latest stable release from npm.
19
+ Current package: **SKS 10.3.1**. Install the latest stable release from npm.
20
20
 
21
21
  [Quick start](#install-in-one-command) · [Commands](#everyday-commands) · [SKS Center](#sks-center-macos) · [Documentation](#documentation) · [Changelog](CHANGELOG.md)
22
22
 
@@ -92,6 +92,11 @@ in your terminal.
92
92
  ## SKS Center (macOS)
93
93
 
94
94
  Open **SKS Control Center** from the SKS menu bar.
95
+ The Menu Bar app (with the Control Center) is built from source on macOS when you run
96
+ `npm i -g sneakoscope` interactively in a terminal, and by `sks install`, `sks doctor --fix`,
97
+ `sks update`, or `sks menubar install`. Dependency, CI, and piped installs never touch your
98
+ home directory; set `SKS_POSTINSTALL_MENUBAR=1` to force the build in a script, or
99
+ `SKS_POSTINSTALL_NO_MENUBAR=1` to skip it.
95
100
 
96
101
  | Page | Use it to |
97
102
  | --- | --- |
@@ -259,7 +259,7 @@ dependencies = [
259
259
 
260
260
  [[package]]
261
261
  name = "sks-core"
262
- version = "10.2.0"
262
+ version = "10.3.1"
263
263
  dependencies = [
264
264
  "globset",
265
265
  "grep-matcher",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "10.2.0"
3
+ version = "10.3.1"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -95,7 +95,8 @@ const COMMAND_MANIFEST_LITE_BASE = [
95
95
  { name: 'perf', summary: 'Run performance checks', maturity: 'beta' },
96
96
  { name: 'bench', summary: 'Run core trust-kernel benchmark budgets', maturity: 'beta' },
97
97
  { name: 'mcp-server', summary: 'Run a stdio MCP server exposing SKS commands as tools for MCP-capable agent hosts', maturity: 'beta', skipMigrationGate: true, allowedDuringActiveRoute: true },
98
- { name: 'agent-bridge', summary: 'Register SKS tools or run read-only tools with native Astra async calling', maturity: 'beta', readonly: true, diagnostic: true }
98
+ { name: 'agent-bridge', summary: 'Register SKS tools or run read-only tools with native Astra async calling', maturity: 'beta', readonly: true, diagnostic: true },
99
+ { name: 'decision', summary: 'Manage optional Jev decisions through OpenRouter: status, enable, disable, probe, evaluate', maturity: 'labs', skipMigrationGate: true, allowedDuringActiveRoute: true }
99
100
  ];
100
101
  const SAFE_COMMAND_CONTRACT_LITE = {
101
102
  risk: 'R2',
@@ -107,6 +108,7 @@ const SAFE_COMMAND_CONTRACT_LITE = {
107
108
  };
108
109
  const COMMAND_CONTRACT_OVERRIDES_LITE = {
109
110
  align: { latency: 'long', supportsJson: true, inputProfile: 'json-only' },
111
+ decision: { risk: 'R2', latency: 'long', supportsJson: true, remoteAllowed: false, inputProfile: 'json-only' },
110
112
  cleanup: { risk: 'R3', latency: 'long', supportsJson: true, remoteAllowed: false, inputProfile: 'json-only' },
111
113
  autoresearch: { latency: 'long' },
112
114
  bench: { latency: 'long' },
@@ -247,10 +247,18 @@ const COMMAND_DEFINITIONS = {
247
247
  allowedDuringActiveRoute: true,
248
248
  activeRoutePolicy: 'always'
249
249
  }),
250
- 'agent-bridge': readOnly(entry('beta', 'Register SKS tools or run read-only tools with native Astra async calling', 'dist/core/commands/agent-bridge-command.js', subcommand(() => import('../core/commands/agent-bridge-command.js'), 'agentBridgeCommand', 'dist/core/commands/agent-bridge-command.js', 'setup')))
250
+ 'agent-bridge': readOnly(entry('beta', 'Register SKS tools or run read-only tools with native Astra async calling', 'dist/core/commands/agent-bridge-command.js', subcommand(() => import('../core/commands/agent-bridge-command.js'), 'agentBridgeCommand', 'dist/core/commands/agent-bridge-command.js', 'setup'))),
251
+ decision: {
252
+ ...skipMigrationGate(entry('labs', 'Manage optional Jev decisions through OpenRouter: status, enable, disable, probe, evaluate', 'dist/commands/decision.js', directCommand(() => import('../commands/decision.js'), 'dist/commands/decision.js'), {
253
+ allowedDuringActiveRoute: true,
254
+ activeRoutePolicy: 'always'
255
+ })),
256
+ packageRequiredFiles: ['dist/commands/decision.js']
257
+ }
251
258
  };
252
259
  const COMMANDS_WITH_LEGACY_CONTRACT_OVERRIDES = applyCommandContractOverrides(COMMAND_DEFINITIONS, {
253
260
  align: { latency: 'long', supportsJson: true, inputProfile: 'json-only' },
261
+ decision: { risk: 'R2', latency: 'long', supportsJson: true, remoteAllowed: false, inputProfile: 'json-only' },
254
262
  cleanup: { risk: 'R3', latency: 'long', supportsJson: true, remoteAllowed: false, inputProfile: 'json-only' },
255
263
  autoresearch: { latency: 'long' },
256
264
  bench: { latency: 'long' },
@@ -4,6 +4,14 @@ import { escapeRegExp } from '../core/text/regex.js';
4
4
  export const DESKTOP_BRIDGE_MANAGED_MARKER = '# sks-desktop-bridge-managed';
5
5
  export const DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER = '# sks-desktop-bridge-managed-base-url';
6
6
  export const DESKTOP_BRIDGE_MANAGED_MODEL_CATALOG_MARKER = '# sks-desktop-bridge-managed-model-catalog';
7
+ export function bindCodexClientProvider(text, provider) {
8
+ const selected = topLevelTomlString(text, 'model_provider');
9
+ if (selected && selected !== 'openai' && selected !== 'codex-lb')
10
+ return text;
11
+ if (selected === provider)
12
+ return text;
13
+ return upsertTopLevelTomlString(String(text || ''), 'model_provider', provider);
14
+ }
7
15
  export function upsertDesktopBridgeManagedConfig(text, input) {
8
16
  const bridgeBaseUrl = normalizeManagedBridgeBaseUrl(input.bridgeBaseUrl);
9
17
  const combinedCatalogPath = String(input.combinedCatalogPath || '').trim();
@@ -16,7 +24,8 @@ export function upsertDesktopBridgeManagedConfig(text, input) {
16
24
  const orphanCleanup = removeDesktopBridgeOrphanManagedMarkers(String(text || ''));
17
25
  let next = orphanCleanup.text;
18
26
  const selectedProvider = topLevelTomlString(next, 'model_provider');
19
- if (selectedProvider && (selectedProvider !== 'openai'
27
+ const managedProvider = selectedProvider === 'codex-lb' || selectedProvider === 'openai';
28
+ if (selectedProvider && (!managedProvider
20
29
  || !topLevelHasLine(next, DESKTOP_BRIDGE_MANAGED_MARKER))) {
21
30
  throw new Error('historical_user_owned_config_conflict:model_provider');
22
31
  }
@@ -30,8 +39,9 @@ export function upsertDesktopBridgeManagedConfig(text, input) {
30
39
  if (existingCatalog && !catalogOwned) {
31
40
  throw new Error('historical_user_owned_config_conflict:model_catalog_json');
32
41
  }
42
+ const keepCodexLb = selectedProvider === 'codex-lb';
33
43
  next = removeManagedBridgeTopLevelBindings(next);
34
- next = upsertTopLevelTomlString(next, 'model_provider', 'openai');
44
+ next = upsertTopLevelTomlString(next, 'model_provider', keepCodexLb ? 'codex-lb' : 'openai');
35
45
  next = addTopLevelMarkerBeforeKey(next, 'model_provider', DESKTOP_BRIDGE_MANAGED_MARKER);
36
46
  next = upsertTopLevelTomlString(next, 'openai_base_url', bridgeBaseUrl);
37
47
  next = addTopLevelMarkerBeforeKey(next, 'openai_base_url', DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER);
@@ -45,7 +55,7 @@ export function removeDesktopBridgeManagedConfig(text) {
45
55
  const baseUrl = topLevelTomlString(next, 'openai_base_url');
46
56
  const catalog = topLevelTomlString(next, 'model_catalog_json');
47
57
  const owned = [
48
- [DESKTOP_BRIDGE_MANAGED_MARKER, 'model_provider', selectedProvider === 'openai'],
58
+ [DESKTOP_BRIDGE_MANAGED_MARKER, 'model_provider', selectedProvider === 'openai' || selectedProvider === 'codex-lb'],
49
59
  [DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER, 'openai_base_url', Boolean(baseUrl)],
50
60
  [DESKTOP_BRIDGE_MANAGED_MODEL_CATALOG_MARKER, 'model_catalog_json', Boolean(catalog)]
51
61
  ];
@@ -67,7 +77,7 @@ export function removeDesktopBridgeOrphanManagedMarkers(text) {
67
77
  const source = String(text || '');
68
78
  const selectedProvider = topLevelTomlString(source, 'model_provider');
69
79
  const markerTargets = [
70
- [DESKTOP_BRIDGE_MANAGED_MARKER, selectedProvider === 'openai'],
80
+ [DESKTOP_BRIDGE_MANAGED_MARKER, selectedProvider === 'openai' || selectedProvider === 'codex-lb'],
71
81
  [DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER, Boolean(topLevelTomlString(source, 'openai_base_url'))],
72
82
  [DESKTOP_BRIDGE_MANAGED_MODEL_CATALOG_MARKER, Boolean(topLevelTomlString(source, 'model_catalog_json'))]
73
83
  ];
@@ -0,0 +1,57 @@
1
+ import { stdin as input, stdout as output } from 'node:process';
2
+ import { globalSksRoot } from '../core/fsx.js';
3
+ import { installSksMenuBar, sksMenuBarRestartDeferred } from '../core/codex-app/menubar/installer.js';
4
+ export function postinstallMenuBarPolicy(env, io, platform = process.platform) {
5
+ const launch = !sksMenuBarRestartDeferred(env);
6
+ if (env.SKS_POSTINSTALL_NO_BOOTSTRAP === '1')
7
+ return { install: false, launch: false, reason: 'SKS_POSTINSTALL_NO_BOOTSTRAP=1' };
8
+ if (env.SKS_POSTINSTALL_NO_MENUBAR === '1')
9
+ return { install: false, launch: false, reason: 'SKS_POSTINSTALL_NO_MENUBAR=1' };
10
+ if (platform !== 'darwin')
11
+ return { install: false, launch: false, reason: `platform:${platform}` };
12
+ if (env.SKS_POSTINSTALL_MENUBAR === '1')
13
+ return { install: true, launch, reason: 'forced by SKS_POSTINSTALL_MENUBAR=1' };
14
+ if (env.npm_config_global !== 'true')
15
+ return { install: false, launch: false, reason: 'not a global npm install' };
16
+ if (env.CI === 'true' || env.CI === '1')
17
+ return { install: false, launch: false, reason: 'CI' };
18
+ if (!io.stdinTTY || !io.stdoutTTY)
19
+ return { install: false, launch: false, reason: 'non-interactive install' };
20
+ return { install: true, launch, reason: 'interactive global npm install on macOS' };
21
+ }
22
+ export async function ensureSksMenuBarDuringPostinstall(env = process.env, io = { stdinTTY: Boolean(input.isTTY), stdoutTTY: Boolean(output.isTTY) }, installer = installSksMenuBar) {
23
+ const policy = postinstallMenuBarPolicy(env, io);
24
+ if (!policy.install)
25
+ return { status: 'skipped', reason: policy.reason, app_path: null, launched: false, blockers: [] };
26
+ try {
27
+ const result = await installer({ root: globalSksRoot(), apply: true, launch: policy.launch, env, quiet: true });
28
+ const blockers = Array.isArray(result.blockers) ? result.blockers.map(String) : [];
29
+ if (!result.ok)
30
+ return { status: 'blocked', reason: policy.reason, app_path: result.app_path ?? null, launched: false, blockers };
31
+ const upToDate = Array.isArray(result.actions) && result.actions.includes('menubar_up_to_date');
32
+ return {
33
+ status: upToDate ? 'up_to_date' : 'installed',
34
+ reason: policy.reason,
35
+ app_path: result.app_path ?? null,
36
+ launched: result.launch?.ok === true && result.launch?.requested === true,
37
+ blockers
38
+ };
39
+ }
40
+ catch (error) {
41
+ return { status: 'failed', reason: policy.reason, app_path: null, launched: false, blockers: [error instanceof Error ? error.message : String(error)] };
42
+ }
43
+ }
44
+ export function describePostinstallMenuBar(outcome) {
45
+ switch (outcome.status) {
46
+ case 'installed':
47
+ return `SKS Menu Bar + Control Center: installed at ${outcome.app_path ?? 'unknown path'}${outcome.launched ? ' and started' : ' (start it with `sks menubar restart`)'} (${outcome.reason}).`;
48
+ case 'up_to_date':
49
+ return `SKS Menu Bar + Control Center: already current at ${outcome.app_path ?? 'unknown path'}.`;
50
+ case 'blocked':
51
+ return `SKS Menu Bar + Control Center: not installed (${outcome.blockers.join(', ') || 'blocked'}). Run \`sks menubar install\` after fixing the blocker.`;
52
+ case 'failed':
53
+ return `SKS Menu Bar + Control Center: install failed (${outcome.blockers.join(', ')}). Run \`sks menubar install\`.`;
54
+ default:
55
+ return `SKS Menu Bar + Control Center: not installed during npm install (${outcome.reason}). Run \`sks install\` or \`sks menubar install\` to add it.`;
56
+ }
57
+ }
@@ -7,14 +7,23 @@ import { formatHarnessConflictReport, llmHarnessCleanupPrompt, scanHarnessConfli
7
7
  import { reconcileCodexAppUpgradeProcesses } from '../core/codex-app.js';
8
8
  import { runPostinstallGlobalDoctorAndMarkPending } from '../core/update/update-migration-state.js';
9
9
  import { isProjectSetupCandidate } from './install-tool-helpers.js';
10
+ import { describePostinstallMenuBar, ensureSksMenuBarDuringPostinstall } from './install-helpers-menubar.js';
10
11
  import { ensureCodexImagegenDuringInstall, ensureGlobalCodexSkillsDuringInstall, ensureGlobalContext7DuringInstall, ensureGlobalGetdesignSkillDuringInstall, ensureSksCommandDuringInstall } from './install-helpers-install-support.js';
11
12
  const doctorFixCommand = ['sks doctor', '--fix'].join(' ');
12
13
  export async function postinstall({ bootstrap }) {
13
14
  const installRoot = path.resolve(process.env.INIT_CWD || process.cwd());
14
15
  console.log('\nSKS installed.');
15
16
  await restoreInstalledPackageBuildStamp();
17
+ const menuBar = await ensureSksMenuBarDuringPostinstall(process.env);
18
+ if (menuBar.status !== 'skipped')
19
+ console.log(describePostinstallMenuBar(menuBar));
16
20
  if (!postinstallExternalMutationsAllowed(process.env)) {
17
- console.log('Automatic bootstrap was not run; npm install leaves project, HOME, Codex, and global SKS state unchanged by default.');
21
+ const menuBarTouchedHome = menuBar.status === 'installed' || menuBar.status === 'up_to_date';
22
+ console.log(menuBarTouchedHome
23
+ ? 'Automatic bootstrap was not run; apart from the SKS Menu Bar app above, npm install leaves project, HOME, Codex, and global SKS state unchanged.'
24
+ : 'Automatic bootstrap was not run; npm install leaves project, HOME, Codex, and global SKS state unchanged by default.');
25
+ if (menuBar.status === 'skipped')
26
+ console.log(describePostinstallMenuBar(menuBar));
18
27
  console.log('Next: run `sks bootstrap` when you are ready to initialize SKS.');
19
28
  console.log('Dependency diagnostics remain explicit: sks deps check');
20
29
  console.log('Explicit lifecycle opt-in: SKS_POSTINSTALL_BOOTSTRAP=1 npm i -g sneakoscope');
@@ -0,0 +1,7 @@
1
+ import { runDecisionCommand, usage, UsageError } from '../core/decisions/cli.js';
2
+ export { UsageError, parseDecisionArgs, runDecisionCommand, usage } from '../core/decisions/cli.js';
3
+ export async function run(_command, args = []) {
4
+ const code = await runDecisionCommand(args);
5
+ if (code !== 0)
6
+ process.exitCode = code;
7
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": "sks.skills-manifest.v1",
3
- "package_version": "10.2.0",
3
+ "package_version": "10.3.1",
4
4
  "skills": [
5
5
  {
6
6
  "canonical_name": "sks",
@@ -245,6 +245,11 @@ export async function resolveWorkerModelRouting(input, deps = {}) {
245
245
  && input.agent?.routed_model === ASTRA_SUBAGENT_MODEL
246
246
  ? normalizeModelReasoning(input.agent?.routed_model_reasoning_effort)
247
247
  : null;
248
+ const jevModel = !explicitModel && input.agent?.routed_model_policy === 'jev_sealed_routing'
249
+ ? String(input.agent.routed_model || '').trim()
250
+ : '';
251
+ const jevEffort = jevModel ? normalizeModelReasoning(input.agent?.routed_model_reasoning_effort) : null;
252
+ const selectedModel = explicitModel || jevModel;
248
253
  const taskPolicy = decideSubagentModel({ title: taskKindText, description: riskText, role: input.agent?.role });
249
254
  const routed = narutoOnly
250
255
  ? await routeModel(category, {
@@ -252,10 +257,10 @@ export async function resolveWorkerModelRouting(input, deps = {}) {
252
257
  narutoOnly: true,
253
258
  taskText: taskKindText,
254
259
  riskText,
255
- reasoningEffort: savedAstraEffort,
260
+ reasoningEffort: jevEffort || savedAstraEffort,
256
261
  availableModels: lbCatalog?.models || [],
257
262
  availableModelEfforts: lbCatalog?.model_efforts || {},
258
- ...(explicitModel ? { model: explicitModel } : {})
263
+ ...(selectedModel ? { model: selectedModel } : {})
259
264
  })
260
265
  : {
261
266
  model: ASTRA_SUBAGENT_MODEL,
@@ -32,6 +32,8 @@ export const NATIVE_SOURCE_FILES = [
32
32
  'RemoteCodingViewController.swift',
33
33
  'DiagnosticsViewController.swift',
34
34
  'SettingsViewController.swift',
35
+ 'LocalDecisionModels.swift',
36
+ 'LocalDecisionViewController.swift',
35
37
  'OperationModels.swift',
36
38
  'ProviderRouteExplanation.swift',
37
39
  'OperationCoordinator.swift',
@@ -1,3 +1,6 @@
1
+ import { bindCodexClientProvider } from '../../../cli/install-helpers-codex-lb-config.js';
2
+ import { safeWriteCodexConfigToml } from '../../codex-runtime/codex-desktop-config-policy.js';
3
+ import { readText } from '../../fsx.js';
1
4
  import { testOpenRouterConnection } from '../../providers/openrouter/openrouter-account.js';
2
5
  import { openRouterSecretPaths } from '../../providers/openrouter/openrouter-secret-store.js';
3
6
  import { codexLbEnvPath, codexLbMetadataPath, readCodexLbModelCatalog } from '../codex-lb-env.js';
@@ -108,6 +111,12 @@ export async function setProviderState(providerId, enabled, operation, options)
108
111
  });
109
112
  }
110
113
  }
114
+ if (providerId === 'codex-lb') {
115
+ const current = await readText(paths.configPath, '');
116
+ const next = bindCodexClientProvider(current, enabled ? 'codex-lb' : 'openai');
117
+ if (next !== current)
118
+ await safeWriteCodexConfigToml(paths.configPath, current, next, 'codex-lb-client-provider');
119
+ }
111
120
  const status = await desktopBridgeStatusV3(options);
112
121
  return commandResult(operation, true, status, { provider_id: providerId, enabled }, [], options);
113
122
  }
@@ -0,0 +1,324 @@
1
+ import fsp from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { printJson } from '../../cli/output.js';
4
+ import { nowIso, writeJsonAtomic } from '../fsx.js';
5
+ import { resolveOpenRouterApiKey } from '../providers/openrouter/openrouter-secret-store.js';
6
+ import { jevEnabled, readDecisionConfig, writeDecisionConfig } from './config.js';
7
+ import { buildEvaluationReport } from './evaluation.js';
8
+ import { OPENROUTER_DECISIONS_ENDPOINT, OPENROUTER_DECISIONS_MODEL, requestOpenRouterDecision } from './openrouter.js';
9
+ import { buildDecisionBundle } from './questions.js';
10
+ import { RECOVERY_CAPABILITY } from './recovery.js';
11
+ import { DESIGN_DEFAULTS } from './types.js';
12
+ export const EXIT_USAGE = 2;
13
+ export const EXIT_FAILURE = 1;
14
+ export class UsageError extends Error {
15
+ }
16
+ export const SUBCOMMANDS = ['status', 'enable', 'disable', 'probe', 'evaluate'];
17
+ const OPTION_SPEC = {
18
+ status: { booleans: ['--json'], values: [] },
19
+ enable: { booleans: ['--json', '--consent-cloud'], values: ['--provider', '--model'] },
20
+ disable: { booleans: ['--json'], values: [] },
21
+ probe: { booleans: ['--json'], values: [] },
22
+ evaluate: { booleans: ['--json'], values: ['--dataset', '--output'] }
23
+ };
24
+ export function parseDecisionArgs(args) {
25
+ const [first, ...rest] = args.map(String);
26
+ if (!first || first === '--help' || first === '-h' || first === 'help')
27
+ throw new UsageError('help');
28
+ if (!SUBCOMMANDS.includes(first))
29
+ throw new UsageError(`unknown subcommand: ${first}`);
30
+ const subcommand = first;
31
+ const spec = OPTION_SPEC[subcommand];
32
+ const flags = new Set();
33
+ const values = new Map();
34
+ for (let index = 0; index < rest.length; index += 1) {
35
+ const arg = rest[index];
36
+ if (!arg.startsWith('--'))
37
+ throw new UsageError(`unexpected argument: ${arg}`);
38
+ const [name, inline] = arg.includes('=') ? [arg.slice(0, arg.indexOf('=')), arg.slice(arg.indexOf('=') + 1)] : [arg, undefined];
39
+ if (spec.booleans.includes(name)) {
40
+ if (inline !== undefined)
41
+ throw new UsageError(`flag takes no value: ${name}`);
42
+ flags.add(name);
43
+ continue;
44
+ }
45
+ if (spec.values.includes(name)) {
46
+ const value = inline !== undefined ? inline : rest[index + 1];
47
+ if (value === undefined || value.startsWith('--'))
48
+ throw new UsageError(`missing value for ${name}`);
49
+ if (values.has(name))
50
+ throw new UsageError(`duplicate option: ${name}`);
51
+ values.set(name, value);
52
+ if (inline === undefined)
53
+ index += 1;
54
+ continue;
55
+ }
56
+ throw new UsageError(`unknown option for ${subcommand}: ${name}`);
57
+ }
58
+ return { subcommand, flags, values };
59
+ }
60
+ export function usage() {
61
+ return [
62
+ 'Usage: sks decision <subcommand> [options]',
63
+ '',
64
+ 'Optional Jev decisions through OpenRouter. Default mode is off: no network',
65
+ 'call, no model load, and no source hydration solely for Jev. A valid Jev',
66
+ 'answer is compiled into an existing SKS plan or context selection. There is',
67
+ 'no advisory mode and no second LLM judge.',
68
+ '',
69
+ ' status [--json] local, non-billable readiness',
70
+ ' enable --provider openrouter --model typesafe/jev-1.13 --consent-cloud [--json]',
71
+ ' disable [--json] return to the deterministic baseline',
72
+ ' probe [--json] explicit tiny synthetic Decisions request',
73
+ ' evaluate --dataset <path> --output <path> [--json]',
74
+ '',
75
+ 'Exit codes: 0 ok, 1 unavailable or failed, 2 usage error.'
76
+ ].join('\n');
77
+ }
78
+ function output(result, json, textLines) {
79
+ if (json) {
80
+ printJson(result, { failureExitCode: false });
81
+ return;
82
+ }
83
+ for (const line of textLines())
84
+ console.log(line);
85
+ }
86
+ export async function runDecisionCommand(args, env = process.env) {
87
+ let parsed;
88
+ try {
89
+ parsed = parseDecisionArgs(args);
90
+ }
91
+ catch (error) {
92
+ const message = error instanceof UsageError ? error.message : String(error);
93
+ if (message !== 'help')
94
+ console.error(`error: ${message}\n`);
95
+ console.log(usage());
96
+ return message === 'help' ? 0 : EXIT_USAGE;
97
+ }
98
+ const json = parsed.flags.has('--json');
99
+ try {
100
+ switch (parsed.subcommand) {
101
+ case 'status': {
102
+ const report = await statusReport(env);
103
+ output(report, json, () => [
104
+ `mode: ${report.mode} consent: ${report.consentCloud} credential: ${report.credential.present ? report.credential.source : 'missing'}`,
105
+ `model: ${report.model} endpoint: ${report.endpoint}`,
106
+ `capabilities: context=${report.capabilities.context.ready ? 'ready' : 'off'} plan=${report.capabilities.plan.ready ? 'ready' : 'off'} recovery=${report.capabilities.recovery.reason}`
107
+ ]);
108
+ return 0;
109
+ }
110
+ case 'enable':
111
+ return await runEnable(parsed, env);
112
+ case 'disable':
113
+ return await runDisable(env, json);
114
+ case 'probe':
115
+ return await runProbe(env, json);
116
+ case 'evaluate':
117
+ return await runEvaluate(parsed, env);
118
+ }
119
+ }
120
+ catch (error) {
121
+ if (error instanceof UsageError) {
122
+ console.error(`error: ${error.message}\n`);
123
+ console.log(usage());
124
+ return EXIT_USAGE;
125
+ }
126
+ const message = error instanceof Error ? error.message : String(error);
127
+ if (json)
128
+ printJson({ ok: false, error: message }, { failureExitCode: false });
129
+ else
130
+ console.error(`error: ${message}`);
131
+ return EXIT_FAILURE;
132
+ }
133
+ }
134
+ export async function statusReport(env) {
135
+ const config = await readDecisionConfig(env);
136
+ const resolved = await resolveOpenRouterApiKey({ env });
137
+ const nextStep = !resolved.key
138
+ ? 'missing_key'
139
+ : !jevEnabled(config)
140
+ ? 'enable'
141
+ : 'ready';
142
+ return {
143
+ schema: 'sks.jev-decision-status.v1',
144
+ ok: true,
145
+ mode: config.mode,
146
+ provider: config.provider,
147
+ model: config.model,
148
+ endpoint: OPENROUTER_DECISIONS_ENDPOINT,
149
+ consentCloud: config.consentCloud,
150
+ credential: {
151
+ present: Boolean(resolved.key),
152
+ source: resolved.source,
153
+ preview: resolved.key_preview
154
+ },
155
+ capabilities: config.capabilities,
156
+ recovery: RECOVERY_CAPABILITY,
157
+ nextStep,
158
+ notes: [
159
+ 'status never calls OpenRouter and never starts a service',
160
+ 'enabled Jev is consumed by official-subagent preparation: automatic plan variants and optional context',
161
+ 'recovery is unsupported: no SKS-owned ambiguous-failure handler exists'
162
+ ],
163
+ utilization: {
164
+ officialSubagentPreparation: jevEnabled(config),
165
+ planSelection: 'automatic_only',
166
+ recovery: false
167
+ }
168
+ };
169
+ }
170
+ async function runEnable(parsed, env) {
171
+ const provider = parsed.values.get('--provider');
172
+ const model = parsed.values.get('--model');
173
+ if (provider !== 'openrouter')
174
+ throw new UsageError('--provider must be openrouter');
175
+ if (model !== OPENROUTER_DECISIONS_MODEL)
176
+ throw new UsageError(`--model must be ${OPENROUTER_DECISIONS_MODEL}`);
177
+ if (!parsed.flags.has('--consent-cloud'))
178
+ throw new UsageError('--consent-cloud is required to send bounded evidence to OpenRouter');
179
+ const json = parsed.flags.has('--json');
180
+ const config = await writeDecisionConfig({
181
+ mode: 'jev',
182
+ model,
183
+ consentCloud: true,
184
+ consentAt: nowIso(),
185
+ capabilities: {
186
+ context: { ready: true, promoted: false, reason: 'implementation_ready_not_workload_promoted' },
187
+ plan: { ready: true, promoted: false, reason: 'implementation_ready_host_dispatch_unverified' },
188
+ recovery: { ready: false, promoted: false, reason: RECOVERY_CAPABILITY.reason }
189
+ }
190
+ }, env);
191
+ output({ schema: 'sks.jev-decision-enable.v1', ok: true, config }, json, () => [
192
+ `enabled Jev via OpenRouter (${config.model})`,
193
+ 'context and SKS-owned plan selection are ready; recovery remains unsupported',
194
+ 'this is not a measured performance claim'
195
+ ]);
196
+ return 0;
197
+ }
198
+ async function runDisable(env, json) {
199
+ const config = await writeDecisionConfig({
200
+ mode: 'off',
201
+ consentCloud: false,
202
+ consentAt: null,
203
+ capabilities: {
204
+ context: { ready: false, promoted: false, reason: 'not_enabled' },
205
+ plan: { ready: false, promoted: false, reason: 'not_enabled' },
206
+ recovery: { ready: false, promoted: false, reason: RECOVERY_CAPABILITY.reason }
207
+ }
208
+ }, env);
209
+ output({ schema: 'sks.jev-decision-disable.v1', ok: true, config }, json, () => [
210
+ 'mode: off'
211
+ ]);
212
+ return 0;
213
+ }
214
+ async function runProbe(env, json) {
215
+ const resolved = await resolveOpenRouterApiKey({ env });
216
+ if (!resolved.key) {
217
+ output({ schema: 'sks.jev-decision-probe.v1', ok: false, error: 'missing_key', live: false }, json, () => [
218
+ 'probe skipped: OpenRouter key is missing'
219
+ ]);
220
+ return EXIT_FAILURE;
221
+ }
222
+ const bundle = buildDecisionBundle({
223
+ projectId: 'probe',
224
+ workflowRunId: 'probe',
225
+ workflowRevision: 'probe',
226
+ sourceDigest: 'probe',
227
+ graphDigest: null,
228
+ goal: 'Synthetic connectivity probe only. No repository or personal data.',
229
+ facts: { notice: 'synthetic_probe' },
230
+ recoveryCandidates: [{
231
+ id: 'failed',
232
+ summary: 'The recorded status is failed.',
233
+ handlerId: 'none',
234
+ authorized: true,
235
+ readOnly: true
236
+ }, {
237
+ id: 'passed',
238
+ summary: 'The recorded status is passed.',
239
+ handlerId: 'none',
240
+ authorized: true,
241
+ readOnly: true
242
+ }],
243
+ recoveryDiagnostic: 'A synthetic test failed.'
244
+ });
245
+ bundle.request.state = {
246
+ notice: 'Synthetic connectivity probe only. No repository or personal data.',
247
+ status: 'failed',
248
+ diagnostic: 'A synthetic test failed.'
249
+ };
250
+ bundle.request.questions = {
251
+ status: {
252
+ type: 'choice',
253
+ instructions: 'Which status is explicitly recorded in state.status?',
254
+ criteria: {
255
+ passed: 'The recorded status is passed.',
256
+ failed: 'The recorded status is failed.'
257
+ }
258
+ },
259
+ has_failure: {
260
+ type: 'noul',
261
+ instructions: 'Does state.diagnostic explicitly describe a failure?'
262
+ },
263
+ severity: {
264
+ type: 'score',
265
+ instructions: 'How directly does state.diagnostic state a failure?',
266
+ criteria: [
267
+ 'No failure is stated.',
268
+ 'A failure is implied but not explicit.',
269
+ 'A failure is explicitly stated.'
270
+ ]
271
+ }
272
+ };
273
+ const started = Date.now();
274
+ const result = await requestOpenRouterDecision(bundle, { env, deadlineMs: DESIGN_DEFAULTS.deadlineMs });
275
+ const receipt = {
276
+ schema: 'sks.jev-decision-probe.v1',
277
+ live: result.ok,
278
+ ok: result.ok,
279
+ endpoint: OPENROUTER_DECISIONS_ENDPOINT,
280
+ requestedModel: OPENROUTER_DECISIONS_MODEL,
281
+ responseModel: result.ok ? result.resolvedModel : null,
282
+ timestamp: nowIso(),
283
+ httpStatus: result.ok ? 200 : result.status,
284
+ reason: result.ok ? 'connected' : result.reason,
285
+ usage: result.ok ? result.usage : result.usage,
286
+ elapsedMs: Date.now() - started,
287
+ evidenceLevel: result.ok ? 'live_protocol' : 'failed_probe',
288
+ note: 'A 200 response is connectivity evidence, not SKS task accuracy or a privacy audit.'
289
+ };
290
+ output(receipt, json, () => [
291
+ result.ok
292
+ ? `probe ok · model ${receipt.responseModel} · ${receipt.elapsedMs}ms`
293
+ : `probe failed · ${receipt.reason}`
294
+ ]);
295
+ return result.ok ? 0 : EXIT_FAILURE;
296
+ }
297
+ async function runEvaluate(parsed, env) {
298
+ const dataset = parsed.values.get('--dataset');
299
+ const outputPath = parsed.values.get('--output');
300
+ if (!dataset || !outputPath)
301
+ throw new UsageError('--dataset and --output are required');
302
+ const json = parsed.flags.has('--json');
303
+ let rows = [];
304
+ try {
305
+ const raw = JSON.parse(await fsp.readFile(dataset, 'utf8'));
306
+ rows = Array.isArray(raw) ? raw : Array.isArray(raw.rows) ? raw.rows : [];
307
+ }
308
+ catch {
309
+ throw new UsageError('invalid JSON dataset');
310
+ }
311
+ const report = buildEvaluationReport({
312
+ rows,
313
+ evidenceLevel: 'synthetic',
314
+ live: false
315
+ });
316
+ await fsp.mkdir(path.dirname(path.resolve(outputPath)), { recursive: true });
317
+ await writeJsonAtomic(path.resolve(outputPath), report, { mode: 0o600 });
318
+ output({ schema: 'sks.jev-decision-evaluate.v1', ok: true, reportPath: outputPath, report }, json, () => [
319
+ `wrote ${outputPath}`,
320
+ `tasks: ${report.taskCount} live: ${report.live}`,
321
+ ...(report.unavailableReasons.length ? [`unavailable: ${report.unavailableReasons.join(', ')}`] : [])
322
+ ]);
323
+ return 0;
324
+ }