sneakoscope 10.2.0 → 10.3.0
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 +6 -1
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/cli/command-manifest-lite.js +3 -1
- package/dist/cli/command-registry.js +9 -1
- package/dist/cli/install-helpers-menubar.js +57 -0
- package/dist/cli/install-helpers.js +10 -1
- package/dist/commands/decision.js +7 -0
- package/dist/config/skills-manifest.json +1 -1
- package/dist/core/agents/native-worker-backend-router.js +7 -2
- package/dist/core/codex-app/menubar/constants.js +2 -0
- package/dist/core/decisions/cli.js +324 -0
- package/dist/core/decisions/config.js +109 -0
- package/dist/core/decisions/evaluation.js +62 -0
- package/dist/core/decisions/integration.js +262 -0
- package/dist/core/decisions/openrouter.js +270 -0
- package/dist/core/decisions/policy.js +393 -0
- package/dist/core/decisions/questions.js +215 -0
- package/dist/core/decisions/receipt.js +41 -0
- package/dist/core/decisions/recovery.js +39 -0
- package/dist/core/decisions/routing.js +58 -0
- package/dist/core/decisions/state.js +173 -0
- package/dist/core/decisions/types.js +69 -0
- package/dist/core/feature-fixtures.js +1 -0
- package/dist/core/feature-registry.js +1 -0
- package/dist/core/hooks-runtime/subagent-context.js +5 -3
- package/dist/core/hooks-runtime/subagent-spawn-policy.js +9 -3
- package/dist/core/provider/model-router.js +3 -3
- package/dist/core/routes.js +2 -1
- package/dist/core/subagents/model-policy.js +15 -0
- package/dist/core/subagents/official-subagent-lock.js +15 -0
- package/dist/core/subagents/official-subagent-preparation.js +265 -24
- package/dist/core/subagents/official-subagent-prompt.js +56 -13
- package/dist/core/update/update-migration-state/retired-local-decision.js +203 -0
- package/dist/core/update/update-migration-state.js +6 -0
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +6 -3
- package/dist/native/sks-menubar/Sources/LocalDecisionModels.swift +166 -0
- package/dist/native/sks-menubar/Sources/LocalDecisionViewController.swift +200 -0
- package/dist/native/sks-menubar/Sources/OverviewViewController.swift +4 -0
- package/dist/native/sks-menubar/Sources/SidebarItem.swift +2 -0
- package/dist/scripts/current-surface-update-e2e-check.js +1 -0
- 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.
|
|
19
|
+
Current package: **SKS 10.3.0**. 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
|
| --- | --- |
|
|
@@ -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' },
|
|
@@ -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
|
-
|
|
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
|
+
}
|
|
@@ -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
|
-
...(
|
|
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',
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fsp from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { nowIso, readJson, writeJsonAtomic } from '../fsx.js';
|
|
5
|
+
import { ensureConfinedDirectory, inspectConfinedPath, ManagedPathSafetyError } from '../managed-path-safety.js';
|
|
6
|
+
import { DESIGN_DEFAULTS } from './types.js';
|
|
7
|
+
export const DECISION_CONFIG_SCHEMA = 'sks.jev-decision-config.v1';
|
|
8
|
+
const CONFIG_DIRNAME = 'decisions';
|
|
9
|
+
export function decisionPaths(env = process.env) {
|
|
10
|
+
const sksHome = path.resolve(env.SKS_HOME || path.join(env.HOME || os.homedir(), '.sneakoscope'));
|
|
11
|
+
const configDir = path.join(sksHome, CONFIG_DIRNAME);
|
|
12
|
+
return {
|
|
13
|
+
sksHome,
|
|
14
|
+
configDir,
|
|
15
|
+
configPath: path.join(configDir, 'config.json')
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function defaultDecisionConfig() {
|
|
19
|
+
return {
|
|
20
|
+
schema: DECISION_CONFIG_SCHEMA,
|
|
21
|
+
mode: DESIGN_DEFAULTS.mode,
|
|
22
|
+
provider: 'openrouter',
|
|
23
|
+
model: DESIGN_DEFAULTS.model,
|
|
24
|
+
consentCloud: false,
|
|
25
|
+
consentAt: null,
|
|
26
|
+
capabilities: {
|
|
27
|
+
context: capability(false, false, 'not_enabled'),
|
|
28
|
+
plan: capability(false, false, 'not_enabled'),
|
|
29
|
+
recovery: capability(false, false, 'unsupported_no_sks_handler')
|
|
30
|
+
},
|
|
31
|
+
updatedAt: null
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export async function readDecisionConfig(env = process.env) {
|
|
35
|
+
const paths = decisionPaths(env);
|
|
36
|
+
const current = await readJson(paths.configPath, null).catch(() => null);
|
|
37
|
+
return current ? normalizeConfig(current) : defaultDecisionConfig();
|
|
38
|
+
}
|
|
39
|
+
export async function writeDecisionConfig(patch, env = process.env) {
|
|
40
|
+
const paths = decisionPaths(env);
|
|
41
|
+
const current = await readDecisionConfig(env);
|
|
42
|
+
const next = {
|
|
43
|
+
...current,
|
|
44
|
+
...(patch.mode === undefined ? {} : { mode: patch.mode }),
|
|
45
|
+
...(patch.model === undefined ? {} : { model: patch.model }),
|
|
46
|
+
...(patch.consentCloud === undefined ? {} : { consentCloud: patch.consentCloud }),
|
|
47
|
+
...(patch.consentAt === undefined ? {} : { consentAt: patch.consentAt }),
|
|
48
|
+
capabilities: {
|
|
49
|
+
context: patch.capabilities?.context || current.capabilities.context,
|
|
50
|
+
plan: patch.capabilities?.plan || current.capabilities.plan,
|
|
51
|
+
recovery: current.capabilities.recovery
|
|
52
|
+
},
|
|
53
|
+
updatedAt: nowIso()
|
|
54
|
+
};
|
|
55
|
+
if (next.mode !== 'off' && next.mode !== 'jev')
|
|
56
|
+
throw new Error(`invalid_decision_mode:${String(next.mode)}`);
|
|
57
|
+
if (next.mode === 'jev' && next.consentCloud !== true)
|
|
58
|
+
throw new Error('jev_cloud_consent_required');
|
|
59
|
+
if (next.model !== DESIGN_DEFAULTS.model)
|
|
60
|
+
throw new Error(`unsupported_decision_model:${next.model}`);
|
|
61
|
+
await fsp.mkdir(paths.sksHome, { recursive: true });
|
|
62
|
+
await ensureConfinedDirectory(paths.sksHome, paths.configDir);
|
|
63
|
+
const inspected = await inspectConfinedPath(paths.sksHome, paths.configPath);
|
|
64
|
+
if (inspected.leafSymlink) {
|
|
65
|
+
throw new ManagedPathSafetyError('managed_path_leaf_symlink_refused', inspected.path);
|
|
66
|
+
}
|
|
67
|
+
await writeJsonAtomic(inspected.path, next, { mode: 0o600 });
|
|
68
|
+
return next;
|
|
69
|
+
}
|
|
70
|
+
export function jevEnabled(config) {
|
|
71
|
+
return config.mode === 'jev' && config.consentCloud === true;
|
|
72
|
+
}
|
|
73
|
+
function normalizeConfig(raw) {
|
|
74
|
+
const fallback = defaultDecisionConfig();
|
|
75
|
+
if (!isRecord(raw) || raw.schema !== DECISION_CONFIG_SCHEMA)
|
|
76
|
+
return fallback;
|
|
77
|
+
const mode = raw.mode === 'jev' ? 'jev' : 'off';
|
|
78
|
+
const consentCloud = raw.consentCloud === true;
|
|
79
|
+
return {
|
|
80
|
+
schema: DECISION_CONFIG_SCHEMA,
|
|
81
|
+
mode: mode === 'jev' && consentCloud ? 'jev' : 'off',
|
|
82
|
+
provider: 'openrouter',
|
|
83
|
+
model: typeof raw.model === 'string' && raw.model.trim() ? raw.model.trim() : DESIGN_DEFAULTS.model,
|
|
84
|
+
consentCloud,
|
|
85
|
+
consentAt: typeof raw.consentAt === 'string' ? raw.consentAt : null,
|
|
86
|
+
capabilities: {
|
|
87
|
+
context: normalizeCapability(raw.capabilities, 'context', fallback.capabilities.context),
|
|
88
|
+
plan: normalizeCapability(raw.capabilities, 'plan', fallback.capabilities.plan),
|
|
89
|
+
recovery: capability(false, false, 'unsupported_no_sks_handler')
|
|
90
|
+
},
|
|
91
|
+
updatedAt: typeof raw.updatedAt === 'string' ? raw.updatedAt : null
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function normalizeCapability(raw, key, fallback) {
|
|
95
|
+
if (!isRecord(raw) || !isRecord(raw[key]))
|
|
96
|
+
return fallback;
|
|
97
|
+
const row = raw[key];
|
|
98
|
+
return {
|
|
99
|
+
ready: row.ready === true,
|
|
100
|
+
promoted: row.promoted === true,
|
|
101
|
+
reason: typeof row.reason === 'string' ? row.reason : fallback.reason
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function capability(ready, promoted, reason) {
|
|
105
|
+
return { ready, promoted, reason };
|
|
106
|
+
}
|
|
107
|
+
function isRecord(value) {
|
|
108
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
109
|
+
}
|