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
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import fsp from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { runProcess } from '../../fsx.js';
|
|
5
|
+
import { inspectConfinedPath, ManagedPathSafetyError } from '../../managed-path-safety.js';
|
|
6
|
+
const RUNTIME_DIRNAME = 'local-decision';
|
|
7
|
+
const MAX_MARKER_BYTES = 256 * 1024;
|
|
8
|
+
export async function runRetiredLocalDecisionStage(env = process.env) {
|
|
9
|
+
const cleanup = await removeRetiredLocalDecisionRuntime(env);
|
|
10
|
+
return {
|
|
11
|
+
ok: cleanup.ok,
|
|
12
|
+
status: cleanup.ok ? 'ok' : 'failed',
|
|
13
|
+
actions: cleanup.actions,
|
|
14
|
+
blockers: cleanup.blockers,
|
|
15
|
+
warnings: cleanup.warnings,
|
|
16
|
+
detail: cleanup.detail
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export async function removeRetiredLocalDecisionRuntime(env = process.env, opts = {}) {
|
|
20
|
+
const sksHome = path.resolve(env.SKS_HOME || path.join(env.HOME || os.homedir(), '.sneakoscope'));
|
|
21
|
+
const runtimeRoot = path.join(sksHome, RUNTIME_DIRNAME);
|
|
22
|
+
const actions = [];
|
|
23
|
+
const blockers = [];
|
|
24
|
+
const warnings = [];
|
|
25
|
+
const detail = {
|
|
26
|
+
runtime_root: runtimeRoot,
|
|
27
|
+
removed_runtime: false,
|
|
28
|
+
removed_socket_dir: false,
|
|
29
|
+
stopped_worker: false
|
|
30
|
+
};
|
|
31
|
+
if (sksHome === path.parse(sksHome).root) {
|
|
32
|
+
blockers.push('retired_local_decision_home_root_refused');
|
|
33
|
+
return { ok: false, actions, blockers, warnings, detail };
|
|
34
|
+
}
|
|
35
|
+
const stopped = await stopRecordedWorker(runtimeRoot).catch(() => 'unreadable');
|
|
36
|
+
if (stopped === 'stopped') {
|
|
37
|
+
detail.stopped_worker = true;
|
|
38
|
+
actions.push('stopped_retired_local_decision_worker');
|
|
39
|
+
}
|
|
40
|
+
else if (stopped === 'not_ours') {
|
|
41
|
+
warnings.push('retired_local_decision_worker_pid_not_owned');
|
|
42
|
+
}
|
|
43
|
+
const removedDefault = await removeConfinedTree(sksHome, runtimeRoot);
|
|
44
|
+
if (removedDefault === 'removed') {
|
|
45
|
+
detail.removed_runtime = true;
|
|
46
|
+
actions.push('removed_retired_local_decision_runtime');
|
|
47
|
+
}
|
|
48
|
+
else if (removedDefault === 'absent') {
|
|
49
|
+
actions.push('retired_local_decision_runtime_absent');
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
blockers.push(`retired_local_decision_runtime_${removedDefault}`);
|
|
53
|
+
}
|
|
54
|
+
const explicit = explicitRuntimeRoot(env, runtimeRoot);
|
|
55
|
+
if (explicit) {
|
|
56
|
+
const removedExplicit = await removeMarkedExplicitRoot(explicit);
|
|
57
|
+
if (removedExplicit === 'removed')
|
|
58
|
+
actions.push('removed_explicit_retired_local_decision_root');
|
|
59
|
+
else if (removedExplicit === 'preserved')
|
|
60
|
+
warnings.push('retired_local_decision_explicit_root_preserved');
|
|
61
|
+
else if (removedExplicit !== 'absent')
|
|
62
|
+
blockers.push(`retired_local_decision_explicit_root_${removedExplicit}`);
|
|
63
|
+
}
|
|
64
|
+
const socketDir = opts.socketDir || defaultSocketDir();
|
|
65
|
+
const removedSocket = await removeSocketDir(socketDir, Boolean(opts.socketDir));
|
|
66
|
+
if (removedSocket === 'removed') {
|
|
67
|
+
detail.removed_socket_dir = true;
|
|
68
|
+
actions.push('removed_retired_local_decision_socket_dir');
|
|
69
|
+
}
|
|
70
|
+
else if (removedSocket !== 'absent') {
|
|
71
|
+
blockers.push(`retired_local_decision_socket_${removedSocket}`);
|
|
72
|
+
}
|
|
73
|
+
return { ok: blockers.length === 0, actions, blockers, warnings, detail };
|
|
74
|
+
}
|
|
75
|
+
function explicitRuntimeRoot(env, defaultRoot) {
|
|
76
|
+
const raw = String(env.SKS_LOCAL_DECISION_ROOT || '').trim();
|
|
77
|
+
if (!raw)
|
|
78
|
+
return null;
|
|
79
|
+
const resolved = path.resolve(raw);
|
|
80
|
+
if (resolved === defaultRoot)
|
|
81
|
+
return null;
|
|
82
|
+
return resolved;
|
|
83
|
+
}
|
|
84
|
+
async function removeMarkedExplicitRoot(target) {
|
|
85
|
+
const parent = path.dirname(target);
|
|
86
|
+
if (parent === target || parent === path.parse(parent).root)
|
|
87
|
+
return 'preserved';
|
|
88
|
+
const marked = await isRetiredRuntimeMarker(target);
|
|
89
|
+
if (marked === 'absent')
|
|
90
|
+
return 'absent';
|
|
91
|
+
if (marked !== 'marked')
|
|
92
|
+
return 'preserved';
|
|
93
|
+
return removeConfinedTree(parent, target);
|
|
94
|
+
}
|
|
95
|
+
async function isRetiredRuntimeMarker(root) {
|
|
96
|
+
const configPath = path.join(root, 'config.json');
|
|
97
|
+
const receiptPath = path.join(root, 'install-receipt.json');
|
|
98
|
+
const configStat = await fsp.lstat(configPath).catch(() => null);
|
|
99
|
+
const receiptStat = await fsp.lstat(receiptPath).catch(() => null);
|
|
100
|
+
if (!configStat && !receiptStat)
|
|
101
|
+
return 'absent';
|
|
102
|
+
if (!configStat?.isFile() || configStat.isSymbolicLink() || !receiptStat?.isFile() || receiptStat.isSymbolicLink())
|
|
103
|
+
return 'unmarked';
|
|
104
|
+
if (configStat.size > MAX_MARKER_BYTES || receiptStat.size > MAX_MARKER_BYTES)
|
|
105
|
+
return 'unmarked';
|
|
106
|
+
try {
|
|
107
|
+
const config = JSON.parse(await fsp.readFile(configPath, 'utf8'));
|
|
108
|
+
if (config.schemaVersion !== 1)
|
|
109
|
+
return 'unmarked';
|
|
110
|
+
return 'marked';
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return 'unmarked';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function removeConfinedTree(boundary, target) {
|
|
117
|
+
try {
|
|
118
|
+
const inspected = await inspectConfinedPath(boundary, target);
|
|
119
|
+
if (!inspected.exists)
|
|
120
|
+
return 'absent';
|
|
121
|
+
await removeExistingTree(inspected.path);
|
|
122
|
+
const after = await inspectConfinedPath(boundary, target);
|
|
123
|
+
return after.exists ? 'refused' : 'removed';
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (error instanceof ManagedPathSafetyError)
|
|
127
|
+
return 'refused';
|
|
128
|
+
return 'refused';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function removeSocketDir(socketDir, trustedParent) {
|
|
132
|
+
const resolved = path.resolve(socketDir);
|
|
133
|
+
const parent = path.dirname(resolved);
|
|
134
|
+
const name = path.basename(resolved);
|
|
135
|
+
if (!/^sks-ld-\d+$/.test(name))
|
|
136
|
+
return 'refused';
|
|
137
|
+
const stat = await fsp.lstat(resolved).catch((error) => error.code === 'ENOENT' ? null : Promise.reject(error));
|
|
138
|
+
if (!stat)
|
|
139
|
+
return 'absent';
|
|
140
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
141
|
+
return 'refused';
|
|
142
|
+
if (!trustedParent && !(await parentIsTemp(parent)))
|
|
143
|
+
return 'refused';
|
|
144
|
+
await removeExistingTree(resolved);
|
|
145
|
+
return 'removed';
|
|
146
|
+
}
|
|
147
|
+
async function parentIsTemp(parent) {
|
|
148
|
+
const parentReal = await fsp.realpath(parent).catch(() => null);
|
|
149
|
+
const tempReal = await fsp.realpath('/tmp').catch(() => null);
|
|
150
|
+
return Boolean(parentReal && tempReal && parentReal === tempReal);
|
|
151
|
+
}
|
|
152
|
+
async function removeExistingTree(target) {
|
|
153
|
+
await fsp.rm(target, { recursive: true, force: false });
|
|
154
|
+
}
|
|
155
|
+
function defaultSocketDir() {
|
|
156
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : 0;
|
|
157
|
+
const base = process.platform === 'win32' ? os.tmpdir() : '/tmp';
|
|
158
|
+
return path.join(base, `sks-ld-${uid}`);
|
|
159
|
+
}
|
|
160
|
+
async function stopRecordedWorker(runtimeRoot) {
|
|
161
|
+
const file = path.join(runtimeRoot, 'runtime', 'service.json');
|
|
162
|
+
const stat = await fsp.lstat(file).catch(() => null);
|
|
163
|
+
if (!stat)
|
|
164
|
+
return 'absent';
|
|
165
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_MARKER_BYTES)
|
|
166
|
+
return 'unreadable';
|
|
167
|
+
let pid = null;
|
|
168
|
+
try {
|
|
169
|
+
const parsed = JSON.parse(await fsp.readFile(file, 'utf8'));
|
|
170
|
+
pid = typeof parsed.pid === 'number' && Number.isInteger(parsed.pid) ? parsed.pid : null;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return 'unreadable';
|
|
174
|
+
}
|
|
175
|
+
if (!pid || pid === process.pid || pid <= 1)
|
|
176
|
+
return 'unreadable';
|
|
177
|
+
const command = await processCommand(pid);
|
|
178
|
+
if (!command)
|
|
179
|
+
return 'absent';
|
|
180
|
+
if (!command.includes('sks_local_decision'))
|
|
181
|
+
return 'not_ours';
|
|
182
|
+
if (!signal(pid, 'SIGTERM'))
|
|
183
|
+
return 'absent';
|
|
184
|
+
return 'stopped';
|
|
185
|
+
}
|
|
186
|
+
async function processCommand(pid) {
|
|
187
|
+
const result = await runProcess('ps', ['-p', String(pid), '-o', 'command='], {
|
|
188
|
+
timeoutMs: 2_000,
|
|
189
|
+
maxOutputBytes: 16 * 1024
|
|
190
|
+
}).catch(() => null);
|
|
191
|
+
if (!result || result.code !== 0)
|
|
192
|
+
return null;
|
|
193
|
+
return String(result.stdout || '').trim() || null;
|
|
194
|
+
}
|
|
195
|
+
function signal(pid, sig) {
|
|
196
|
+
try {
|
|
197
|
+
process.kill(pid, sig);
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -14,6 +14,7 @@ import { runCurrentPublicSurfaceReconcileStage } from './update-migration-state/
|
|
|
14
14
|
import { runDesktopBridgeCatalogRepairStage } from './update-migration-state/desktop-bridge-catalog-repair-stage.js';
|
|
15
15
|
import { runDesktopBridgeRestageStage } from './update-migration-state/desktop-bridge-restage.js';
|
|
16
16
|
import { runSessionStateSplitStage } from './update-migration-state/session-state-split.js';
|
|
17
|
+
import { runRetiredLocalDecisionStage } from './update-migration-state/retired-local-decision.js';
|
|
17
18
|
import { runHookTrustRefreshStage, runOtherHarnessCleanupStage } from './update-migration-state/simple-stages.js';
|
|
18
19
|
import { compareSemVer } from './semver.js';
|
|
19
20
|
export const UPDATE_MIGRATION_SCHEMA = 'sks.project-migration-receipt.v2';
|
|
@@ -227,6 +228,11 @@ const UPDATE_MIGRATION_STAGES = [
|
|
|
227
228
|
min_from_version: '0.0.0',
|
|
228
229
|
run: runOtherHarnessCleanupStage
|
|
229
230
|
},
|
|
231
|
+
{
|
|
232
|
+
id: 'retired-local-decision',
|
|
233
|
+
min_from_version: '0.0.0',
|
|
234
|
+
run: () => runRetiredLocalDecisionStage()
|
|
235
|
+
},
|
|
230
236
|
{
|
|
231
237
|
id: 'skills-reconcile',
|
|
232
238
|
min_from_version: '0.0.0',
|
package/dist/core/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '10.
|
|
1
|
+
export const PACKAGE_VERSION = '10.3.0';
|
|
@@ -16,12 +16,14 @@ final class ControlCenterWindowController: NSWindowController, NSTableViewDataSo
|
|
|
16
16
|
let overview = OverviewViewController(processClient: processClient, operations: operations)
|
|
17
17
|
overviewController = overview
|
|
18
18
|
defer {
|
|
19
|
-
// Overview quick navigation: section names only, so the
|
|
20
|
-
// Overview compile harness never needs the SidebarItem type.
|
|
21
|
-
|
|
19
|
+
// Overview / Decisions quick navigation: section names only, so the
|
|
20
|
+
// standalone Overview compile harness never needs the SidebarItem type.
|
|
21
|
+
let open: (String) -> Void = { [weak self] name in
|
|
22
22
|
guard let item = SidebarItem.allCases.first(where: { $0.rawValue == name }) else { return }
|
|
23
23
|
self?.show(section: item)
|
|
24
24
|
}
|
|
25
|
+
overview.openSection = open
|
|
26
|
+
(controllers[.localDecision] as? LocalDecisionViewController)?.openSection = open
|
|
25
27
|
}
|
|
26
28
|
controllers = [
|
|
27
29
|
.overview: overview,
|
|
@@ -29,6 +31,7 @@ final class ControlCenterWindowController: NSWindowController, NSTableViewDataSo
|
|
|
29
31
|
.mcpServers: MCPServersViewController(processClient: processClient, operations: operations, notifications: notifications),
|
|
30
32
|
.providers: ProvidersViewController(processClient: processClient, operations: operations),
|
|
31
33
|
.remoteCoding: RemoteCodingViewController(),
|
|
34
|
+
.localDecision: LocalDecisionViewController(processClient: processClient, operations: operations),
|
|
32
35
|
.diagnostics: DiagnosticsViewController(processClient: processClient, operations: operations),
|
|
33
36
|
.settings: SettingsViewController(processClient: processClient, operations: operations, notifications: notifications)
|
|
34
37
|
]
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Exact `sks decision` argv the Control Center page runs. Tests lock this
|
|
4
|
+
/// against the CLI contract so enable/disable cannot silently drift.
|
|
5
|
+
enum LocalDecisionCommand {
|
|
6
|
+
static let provider = "openrouter"
|
|
7
|
+
static let pinnedModel = "typesafe/jev-1.13"
|
|
8
|
+
static let statusSchema = "sks.jev-decision-status.v1"
|
|
9
|
+
static let enableSchema = "sks.jev-decision-enable.v1"
|
|
10
|
+
static let disableSchema = "sks.jev-decision-disable.v1"
|
|
11
|
+
|
|
12
|
+
static var status: [String] { ["decision", "status", "--json"] }
|
|
13
|
+
static var enable: [String] {
|
|
14
|
+
[
|
|
15
|
+
"decision", "enable",
|
|
16
|
+
"--provider", provider,
|
|
17
|
+
"--model", pinnedModel,
|
|
18
|
+
"--consent-cloud",
|
|
19
|
+
"--json"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
static var disable: [String] { ["decision", "disable", "--json"] }
|
|
23
|
+
|
|
24
|
+
static func mutationSucceeded(_ payload: [String: Any]?, schema: String) -> Bool {
|
|
25
|
+
payload?["ok"] as? Bool == true && payload?["schema"] as? String == schema
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// stdout+stderr are merged by ProcessClient. Prefer the first complete object
|
|
30
|
+
/// whose schema is a Jev decision payload, not the last nested `{`.
|
|
31
|
+
enum LocalDecisionJSON {
|
|
32
|
+
static func object(from text: String) -> [String: Any]? {
|
|
33
|
+
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
34
|
+
if let object = parseEntire(trimmed) { return object }
|
|
35
|
+
var search = trimmed.startIndex
|
|
36
|
+
var fallback: [String: Any]?
|
|
37
|
+
while search < trimmed.endIndex, let start = trimmed[search...].firstIndex(of: "{") {
|
|
38
|
+
if let slice = completeObject(in: trimmed, from: start), let object = parseEntire(slice) {
|
|
39
|
+
if let schema = object["schema"] as? String, schema.hasPrefix("sks.jev-decision") {
|
|
40
|
+
return object
|
|
41
|
+
}
|
|
42
|
+
if fallback == nil, object["ok"] != nil { fallback = object }
|
|
43
|
+
}
|
|
44
|
+
search = trimmed.index(after: start)
|
|
45
|
+
}
|
|
46
|
+
return fallback
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static func statusFailureReason(code: Int32, output: String) -> String {
|
|
50
|
+
let text = output.lowercased()
|
|
51
|
+
if code == 127 || text.contains("sks command not found") {
|
|
52
|
+
return "SKS is not on this Menu Bar path. Update SKS, then reopen Decisions."
|
|
53
|
+
}
|
|
54
|
+
if text.contains("unknown subcommand") || text.contains("unknown command")
|
|
55
|
+
|| (text.contains("usage: sks") && !text.contains("jev-decision") && !text.contains("sks decision")) {
|
|
56
|
+
return "This SKS build does not include `decision`. Update SKS, then reopen Decisions."
|
|
57
|
+
}
|
|
58
|
+
return "Status unavailable · update SKS, then reopen this page."
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private static func parseEntire(_ text: String) -> [String: Any]? {
|
|
62
|
+
guard let data = text.data(using: .utf8) else { return nil }
|
|
63
|
+
return try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private static func completeObject(in text: String, from start: String.Index) -> String? {
|
|
67
|
+
var depth = 0
|
|
68
|
+
var inString = false
|
|
69
|
+
var escape = false
|
|
70
|
+
var index = start
|
|
71
|
+
while index < text.endIndex {
|
|
72
|
+
let ch = text[index]
|
|
73
|
+
if inString {
|
|
74
|
+
if escape { escape = false }
|
|
75
|
+
else if ch == "\\" { escape = true }
|
|
76
|
+
else if ch == "\"" { inString = false }
|
|
77
|
+
} else if ch == "\"" {
|
|
78
|
+
inString = true
|
|
79
|
+
} else if ch == "{" {
|
|
80
|
+
depth += 1
|
|
81
|
+
} else if ch == "}" {
|
|
82
|
+
depth -= 1
|
|
83
|
+
if depth == 0 { return String(text[start...index]) }
|
|
84
|
+
}
|
|
85
|
+
index = text.index(after: index)
|
|
86
|
+
}
|
|
87
|
+
return nil
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Public projection of `sks decision status --json`. Pure parsing so the
|
|
92
|
+
/// Control Center page and its tests share one reading of the CLI contract.
|
|
93
|
+
struct LocalDecisionStatus: Equatable {
|
|
94
|
+
enum NextStep: String { case missingKey = "missing_key", enable, ready }
|
|
95
|
+
|
|
96
|
+
let mode: String
|
|
97
|
+
let provider: String
|
|
98
|
+
let model: String
|
|
99
|
+
let consentCloud: Bool
|
|
100
|
+
let credentialPresent: Bool
|
|
101
|
+
let credentialSource: String?
|
|
102
|
+
let recoverySupported: Bool
|
|
103
|
+
let recoveryReason: String?
|
|
104
|
+
let nextStep: NextStep
|
|
105
|
+
|
|
106
|
+
static let schema = LocalDecisionCommand.statusSchema
|
|
107
|
+
|
|
108
|
+
static func decode(from payload: [String: Any]) -> LocalDecisionStatus? {
|
|
109
|
+
guard payload["schema"] as? String == schema, payload["ok"] as? Bool == true,
|
|
110
|
+
let mode = payload["mode"] as? String,
|
|
111
|
+
let provider = payload["provider"] as? String,
|
|
112
|
+
let model = payload["model"] as? String,
|
|
113
|
+
let consentCloud = payload["consentCloud"] as? Bool,
|
|
114
|
+
let credential = payload["credential"] as? [String: Any],
|
|
115
|
+
let present = credential["present"] as? Bool else { return nil }
|
|
116
|
+
let recovery = payload["recovery"] as? [String: Any]
|
|
117
|
+
let step = NextStep(rawValue: payload["nextStep"] as? String ?? "") ?? .enable
|
|
118
|
+
return LocalDecisionStatus(
|
|
119
|
+
mode: mode,
|
|
120
|
+
provider: provider,
|
|
121
|
+
model: model,
|
|
122
|
+
consentCloud: consentCloud,
|
|
123
|
+
credentialPresent: present,
|
|
124
|
+
credentialSource: credential["source"] as? String,
|
|
125
|
+
recoverySupported: recovery?["supported"] as? Bool == true,
|
|
126
|
+
recoveryReason: recovery?["reason"] as? String,
|
|
127
|
+
nextStep: step
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
var modeTitle: String { mode == "jev" ? "Jev" : "Off" }
|
|
132
|
+
var canEnable: Bool { mode != "jev" }
|
|
133
|
+
var canDisable: Bool { mode == "jev" }
|
|
134
|
+
|
|
135
|
+
var badgeText: String {
|
|
136
|
+
if mode == "jev", consentCloud {
|
|
137
|
+
return credentialPresent ? "Ready · Jev via OpenRouter" : "Jev on · add OpenRouter key"
|
|
138
|
+
}
|
|
139
|
+
return "Off · deterministic baseline"
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
var badgeReady: Bool { mode == "jev" && consentCloud && credentialPresent }
|
|
143
|
+
|
|
144
|
+
var modelLabel: String {
|
|
145
|
+
let credential = credentialPresent ? (credentialSource ?? "present") : "missing"
|
|
146
|
+
return "\(model) via \(provider) · credential \(credential)"
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
var guidance: String {
|
|
150
|
+
switch nextStep {
|
|
151
|
+
case .missingKey:
|
|
152
|
+
return "Add an OpenRouter key in Connections first. SKS reuses that credential; there is no Jev-specific secret. You can still turn the mode on from this page; preparation stays on the deterministic baseline until a key is present."
|
|
153
|
+
case .enable:
|
|
154
|
+
return "Turn Jev on here to use it in official-subagent preparation. Off makes no network call. Jev may choose an automatic plan variant or optional context; recovery is unsupported."
|
|
155
|
+
case .ready:
|
|
156
|
+
return "Jev is on. Eligible Naruto/official preparation may send one bounded Decisions request and compile the answer into the SKS plan. Recovery remains unsupported, and a saved plan is not proof that Codex spawned a child."
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
static let enableConsentBody = "SKS will send bounded task evidence to OpenRouter's Decisions endpoint using your existing OpenRouter credential. A valid answer is compiled into an existing plan or context selection. This is not a measured performance claim, and recovery stays unsupported."
|
|
161
|
+
|
|
162
|
+
var enableConsentMessage: String {
|
|
163
|
+
if credentialPresent { return Self.enableConsentBody }
|
|
164
|
+
return "\(Self.enableConsentBody) No OpenRouter key is saved yet — add one in Connections or Jev stays on the deterministic baseline."
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import Cocoa
|
|
2
|
+
|
|
3
|
+
/// Control Center page for optional Jev decisions. Every action is an explicit
|
|
4
|
+
/// `sks decision …` call. Status never contacts OpenRouter.
|
|
5
|
+
final class LocalDecisionViewController: NSViewController, ControlCenterPage {
|
|
6
|
+
private let processClient: ProcessClient
|
|
7
|
+
private let operations: OperationCoordinator
|
|
8
|
+
private let badge = NativeView.badge("Checking…", color: .secondaryLabelColor)
|
|
9
|
+
private let modelDetail = NativeView.detail("Checking configuration…")
|
|
10
|
+
private let guidance = NativeView.detail("")
|
|
11
|
+
private let actionStatus = NativeView.detail("")
|
|
12
|
+
private let modePopup = NSPopUpButton()
|
|
13
|
+
private let modeStatus = NativeView.detail("Jev compiles a typed Choice into SKS-owned plan or context selection.")
|
|
14
|
+
private var enableButton: NSButton!
|
|
15
|
+
private var disableButton: NSButton!
|
|
16
|
+
private var refreshButton: NSButton!
|
|
17
|
+
private var connectionsButton: NSButton!
|
|
18
|
+
private var status: LocalDecisionStatus?
|
|
19
|
+
private var pendingMode: String?
|
|
20
|
+
private var busy = false
|
|
21
|
+
private var generation = 0
|
|
22
|
+
/// Section navigation by sidebar title, wired by ControlCenterWindowController.
|
|
23
|
+
var openSection: ((String) -> Void)?
|
|
24
|
+
|
|
25
|
+
init(processClient: ProcessClient, operations: OperationCoordinator) {
|
|
26
|
+
self.processClient = processClient
|
|
27
|
+
self.operations = operations
|
|
28
|
+
super.init(nibName: nil, bundle: nil)
|
|
29
|
+
}
|
|
30
|
+
required init?(coder: NSCoder) { nil }
|
|
31
|
+
|
|
32
|
+
override func loadView() {
|
|
33
|
+
enableButton = NativeView.button("Enable Jev…", target: self, action: #selector(enableJev))
|
|
34
|
+
disableButton = NativeView.button("Disable", target: self, action: #selector(disableJev))
|
|
35
|
+
refreshButton = NativeView.button("Refresh", target: self, action: #selector(refresh))
|
|
36
|
+
connectionsButton = NativeView.button("Connections…", target: self, action: #selector(openConnections))
|
|
37
|
+
connectionsButton.setAccessibilityIdentifier("sks-decision-connections")
|
|
38
|
+
enableButton.isEnabled = false
|
|
39
|
+
disableButton.isEnabled = false
|
|
40
|
+
modePopup.addItems(withTitles: ["Off", "Jev"])
|
|
41
|
+
modePopup.target = self
|
|
42
|
+
modePopup.action = #selector(applyMode)
|
|
43
|
+
modePopup.isEnabled = false
|
|
44
|
+
modePopup.setAccessibilityLabel("Decision mode")
|
|
45
|
+
modePopup.setAccessibilityIdentifier("sks-decision-mode")
|
|
46
|
+
badge.setAccessibilityIdentifier("sks-decision-badge")
|
|
47
|
+
actionStatus.setAccessibilityIdentifier("sks-decision-action-status")
|
|
48
|
+
let statusCard = NativeView.card(
|
|
49
|
+
title: "Decisions",
|
|
50
|
+
subtitle: "Optional Jev judgments through the existing OpenRouter credential. Off by default. A valid answer is compiled into an existing SKS candidate; there is no advisory text and no second LLM judge.",
|
|
51
|
+
views: [badge, modelDetail, guidance, NativeView.row([refreshButton, connectionsButton])]
|
|
52
|
+
)
|
|
53
|
+
let modeCard = NativeView.card(
|
|
54
|
+
title: "Mode",
|
|
55
|
+
subtitle: "Off: no network call. Jev: send bounded evidence to OpenRouter after explicit cloud consent. Recovery is unsupported.",
|
|
56
|
+
views: [NativeView.row([modePopup, enableButton, disableButton]), modeStatus, actionStatus]
|
|
57
|
+
)
|
|
58
|
+
view = NativeView.page([
|
|
59
|
+
ControlKit.header("Decisions", "Optional Jev selection through OpenRouter."),
|
|
60
|
+
statusCard, modeCard
|
|
61
|
+
])
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func refreshOnAppear() { refresh() }
|
|
65
|
+
|
|
66
|
+
@objc private func openConnections() { openSection?("Providers") }
|
|
67
|
+
|
|
68
|
+
@objc private func refresh() {
|
|
69
|
+
guard !busy else { return }
|
|
70
|
+
generation += 1
|
|
71
|
+
let requestGeneration = generation
|
|
72
|
+
processClient.run(LocalDecisionCommand.status, timeout: NativeView.statusTimeout) { [weak self] result in
|
|
73
|
+
guard let self, requestGeneration == self.generation, !self.busy else { return }
|
|
74
|
+
guard result.code == 0, let payload = LocalDecisionJSON.object(from: result.output),
|
|
75
|
+
let status = LocalDecisionStatus.decode(from: payload) else {
|
|
76
|
+
self.status = nil
|
|
77
|
+
self.render(failure: LocalDecisionJSON.statusFailureReason(code: result.code, output: result.output))
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
self.status = status
|
|
81
|
+
self.pendingMode = nil
|
|
82
|
+
self.render(failure: nil)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private func render(failure: String?) {
|
|
87
|
+
let mode = pendingMode ?? status?.mode
|
|
88
|
+
if let status {
|
|
89
|
+
let color: NSColor = status.badgeReady ? .systemGreen : (status.mode == "jev" ? .systemOrange : .secondaryLabelColor)
|
|
90
|
+
NativeView.setBadge(badge, text: status.badgeText, color: color)
|
|
91
|
+
modelDetail.stringValue = status.modelLabel
|
|
92
|
+
guidance.stringValue = status.guidance
|
|
93
|
+
if !status.recoverySupported {
|
|
94
|
+
modeStatus.stringValue = "Recovery is unsupported: no SKS-owned ambiguous-failure handler exists."
|
|
95
|
+
modeStatus.textColor = .secondaryLabelColor
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
NativeView.setBadge(badge, text: failure ?? "Unavailable", color: .systemOrange)
|
|
99
|
+
modelDetail.stringValue = ""
|
|
100
|
+
guidance.stringValue = "Enable and Disable still call `sks decision` so a stale status read cannot trap this page."
|
|
101
|
+
}
|
|
102
|
+
enableButton.isEnabled = mode != "jev" || mode == nil
|
|
103
|
+
disableButton.isEnabled = mode == "jev" || mode == nil
|
|
104
|
+
connectionsButton.isEnabled = true
|
|
105
|
+
modePopup.isEnabled = true
|
|
106
|
+
if let mode { modePopup.selectItem(withTitle: mode == "jev" ? "Jev" : "Off") }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@objc private func applyMode() {
|
|
110
|
+
guard !busy else { return }
|
|
111
|
+
let desired = modePopup.titleOfSelectedItem == "Jev" ? "jev" : "off"
|
|
112
|
+
let current = pendingMode ?? status?.mode
|
|
113
|
+
guard desired != current else { return }
|
|
114
|
+
if desired == "jev" {
|
|
115
|
+
enableJev()
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
disableJev()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@objc private func enableJev() {
|
|
122
|
+
guard !busy else { return }
|
|
123
|
+
guard let window = hostWindow() else {
|
|
124
|
+
revertModePopup()
|
|
125
|
+
actionStatus.stringValue = "Open SKS Control Center to confirm cloud consent."
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
AlertFactory.confirmSheet(
|
|
129
|
+
window: window,
|
|
130
|
+
title: "Enable Jev through OpenRouter?",
|
|
131
|
+
message: status?.enableConsentMessage ?? LocalDecisionStatus.enableConsentBody,
|
|
132
|
+
destructive: false,
|
|
133
|
+
actionTitle: "Consent and Enable"
|
|
134
|
+
) { [weak self] confirmed in
|
|
135
|
+
guard let self, confirmed else {
|
|
136
|
+
self?.revertModePopup()
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
guard let operation = self.operations.begin(kind: "decision-enable", mutationGroup: "decision", summary: "Enable Jev via OpenRouter") else {
|
|
140
|
+
self.revertModePopup()
|
|
141
|
+
self.actionStatus.stringValue = "Another configuration change is running. Try again when it finishes."
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
self.setBusy(true, message: "Enabling Jev…")
|
|
145
|
+
self.processClient.run(LocalDecisionCommand.enable, timeout: NativeView.mutationTimeout) { [weak self] result in
|
|
146
|
+
guard let self else { return }
|
|
147
|
+
let payload = LocalDecisionJSON.object(from: result.output)
|
|
148
|
+
let ok = result.code == 0 && LocalDecisionCommand.mutationSucceeded(payload, schema: LocalDecisionCommand.enableSchema)
|
|
149
|
+
if ok { self.pendingMode = "jev" }
|
|
150
|
+
else { self.revertModePopup() }
|
|
151
|
+
let summary = ok ? "Jev enabled through OpenRouter." : "Enable failed · \(NativeView.redactPreview(result.output))"
|
|
152
|
+
_ = self.operations.update(operation, state: ok ? .succeeded : .failed, stage: "complete", progress: 1, summary: summary)
|
|
153
|
+
self.setBusy(false, message: summary)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@objc private func disableJev() {
|
|
159
|
+
guard !busy else { return }
|
|
160
|
+
guard let operation = operations.begin(kind: "decision-disable", mutationGroup: "decision", summary: "Disable Jev") else {
|
|
161
|
+
revertModePopup()
|
|
162
|
+
actionStatus.stringValue = "Another configuration change is running. Try again when it finishes."
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
setBusy(true, message: "Returning to the deterministic baseline…")
|
|
166
|
+
processClient.run(LocalDecisionCommand.disable, timeout: NativeView.mutationTimeout) { [weak self] result in
|
|
167
|
+
guard let self else { return }
|
|
168
|
+
let payload = LocalDecisionJSON.object(from: result.output)
|
|
169
|
+
let ok = result.code == 0 && LocalDecisionCommand.mutationSucceeded(payload, schema: LocalDecisionCommand.disableSchema)
|
|
170
|
+
if ok { self.pendingMode = "off" }
|
|
171
|
+
else { self.revertModePopup() }
|
|
172
|
+
let summary = ok ? "Mode set to off." : "Disable could not be confirmed · \(NativeView.redactPreview(result.output))"
|
|
173
|
+
_ = self.operations.update(operation, state: ok ? .succeeded : .failed, stage: "complete", progress: 1, summary: summary)
|
|
174
|
+
self.setBusy(false, message: summary)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private func setBusy(_ value: Bool, message: String) {
|
|
179
|
+
busy = value
|
|
180
|
+
actionStatus.stringValue = message
|
|
181
|
+
actionStatus.textColor = value ? .secondaryLabelColor : .labelColor
|
|
182
|
+
if value {
|
|
183
|
+
for button in [enableButton, disableButton, refreshButton, connectionsButton] { button?.isEnabled = false }
|
|
184
|
+
modePopup.isEnabled = false
|
|
185
|
+
} else {
|
|
186
|
+
refreshButton.isEnabled = true
|
|
187
|
+
connectionsButton.isEnabled = true
|
|
188
|
+
refresh()
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private func revertModePopup() {
|
|
193
|
+
let mode = pendingMode ?? status?.mode
|
|
194
|
+
modePopup.selectItem(withTitle: mode == "jev" ? "Jev" : "Off")
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private func hostWindow() -> NSWindow? {
|
|
198
|
+
view.window ?? NSApp.keyWindow ?? NSApp.windows.first(where: { $0.title == "SKS Control Center" })
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -38,6 +38,7 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
|
|
|
38
38
|
let shortcuts = NativeView.row([
|
|
39
39
|
NativeView.button("Remote Coding…", target: self, action: #selector(openRemoteCoding)),
|
|
40
40
|
connectionsShortcut(),
|
|
41
|
+
NativeView.button("Decisions…", target: self, action: #selector(openDecisions)),
|
|
41
42
|
NativeView.button("Updates…", target: self, action: #selector(openUpdates)),
|
|
42
43
|
NativeView.button("Diagnostics…", target: self, action: #selector(openDiagnostics))
|
|
43
44
|
])
|
|
@@ -73,6 +74,7 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
|
|
|
73
74
|
|
|
74
75
|
@objc private func openRemoteCoding() { openSection?("Remote Coding") }
|
|
75
76
|
@objc private func openProviders() { openSection?("Providers") }
|
|
77
|
+
@objc private func openDecisions() { openSection?("Decisions") }
|
|
76
78
|
@objc private func openUpdates() { openSection?("Updates") }
|
|
77
79
|
@objc private func openDiagnostics() { openSection?("Diagnostics") }
|
|
78
80
|
|
|
@@ -89,6 +91,8 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
|
|
|
89
91
|
|| operation.kind.localizedCaseInsensitiveContains("openrouter")
|
|
90
92
|
|| operation.kind.localizedCaseInsensitiveContains("codex-lb") {
|
|
91
93
|
section = "Providers"
|
|
94
|
+
} else if operation.kind.localizedCaseInsensitiveContains("decision") {
|
|
95
|
+
section = "Decisions"
|
|
92
96
|
} else if operation.kind.localizedCaseInsensitiveContains("update") {
|
|
93
97
|
section = "Updates"
|
|
94
98
|
} else {
|
|
@@ -8,6 +8,7 @@ enum SidebarItem: String, CaseIterable {
|
|
|
8
8
|
case remoteCoding = "Remote Coding"
|
|
9
9
|
case updates = "Updates"
|
|
10
10
|
case mcpServers = "MCP Servers"
|
|
11
|
+
case localDecision = "Decisions"
|
|
11
12
|
case diagnostics = "Diagnostics"
|
|
12
13
|
case settings = "Settings"
|
|
13
14
|
|
|
@@ -19,6 +20,7 @@ enum SidebarItem: String, CaseIterable {
|
|
|
19
20
|
case .providers: return "network"
|
|
20
21
|
case .updates: return "arrow.down.circle"
|
|
21
22
|
case .mcpServers: return "server.rack"
|
|
23
|
+
case .localDecision: return "cpu"
|
|
22
24
|
case .remoteCoding: return "laptopcomputer.and.iphone"
|
|
23
25
|
case .diagnostics: return "stethoscope"
|
|
24
26
|
case .settings: return "gearshape"
|
|
@@ -9,6 +9,7 @@ import { runSksUpdateNow } from '../core/update-check.js';
|
|
|
9
9
|
import { ensureCurrentMigrationBeforeCommand, projectUpdateMigrationReceiptPath, runPackageLocalDoctor } from '../core/update/update-migration-state.js';
|
|
10
10
|
const REQUIRED_MIGRATION_STAGES = [
|
|
11
11
|
'other-harness-cleanup',
|
|
12
|
+
'retired-local-decision',
|
|
12
13
|
'current-public-surface-reconcile',
|
|
13
14
|
'session-state-split',
|
|
14
15
|
'skills-reconcile',
|