sneakoscope 8.1.3 → 8.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,14 +22,15 @@ Proof-first orchestration for Codex CLI, ChatGPT Desktop, AI coding agents, mult
22
22
  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.
23
23
  <!-- END SKS SEARCH VISIBILITY MARKETING -->
24
24
 
25
- This README documents package **SKS 8.1.3** — its own identity, read from `package.json` and subject to release-gate verification, not advice about what to install.
25
+ This README documents package **SKS 8.2.1** — its own identity, read from `package.json` and subject to release-gate verification, not advice about what to install.
26
26
 
27
27
  Use the official latest stable SKS and Codex CLI releases. The Codex compatibility SSOT is always the **current latest stable** host; capability probes measure what that host can actually do. Product docs do not crown a fixed `0.x.y` string as SSOT (release pins and schema directories are measured artifacts for the current package, not a permanent product version claim). Menu Bar / Center induce updates to the latest stable build. Run `sks update-check` for what is installed and read the capability report for what is supported. Install SSOT is npm `sneakoscope@latest`; PATH `sks` and Menu Bar stamped generation must match that version or gates fail. It resolves managed SKS skills from the authoritative global install, preserves a runnable Naruto child slot when `max_threads=2`, and keeps Menu Bar repair transactional so stamped generations remain verifiable. Naruto uses stable opt-in multi-agent V2 when the host exposes it (Codex official multi-agent wrap-only; SKS does not reimplement a parallel runtime). Local code search is mode-separated (`sks search files|text|structure|symbol|context`); `context` is answered by the compiled TriWiki Context Graph (`context-graph.json` is exhaustive authority; `context-pack.json` and managed `AGENTS.md` are bounded projections) — see [docs/architecture/context-graph.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/docs/architecture/context-graph.md) and [docs/PRODUCT-CONTRACT.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/docs/PRODUCT-CONTRACT.md). See [CHANGELOG.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/CHANGELOG.md).
28
28
 
29
- ## What 8.1.3 Ships
29
+ ## What 8.2.0 Ships
30
30
 
31
- | Problem | 8.1.3 behavior |
31
+ | Problem | 8.2.0 behavior |
32
32
  | --- | --- |
33
+ | SKS Center could reject a valid token for a user-selected BotFather bot with only a generic verification message | Setup accepts any bot the user owns, binds the exact `getMe` identity, returns actionable verification failures, and keeps native/CLI liveness receipts schema-compatible. |
33
34
  | Overview mixed Menu Bar, installed SKS, and cached registry versions | Each value is labeled by authority, stale or unavailable probes remain explicit, and Refresh forces a bounded update-status refresh. |
34
35
  | Naruto stopped creating children after its first wave | The root parent records settled waves, recovers open-thread capacity, rescans the ready DAG, and can launch later direct-child waves under the same workflow run. |
35
36
  | Most delegated work drifted to Sol Max | Read-heavy discovery uses Terra Max, ordinary implementation uses Sol High, and Sol Max is reserved for focused high-risk or final judgment slices. |
@@ -80,10 +81,11 @@ or public webhook.
80
81
 
81
82
  #### BotFather setup
82
83
 
83
- 1. In Telegram, open **@BotFather**, send `/newbot`, and complete its name and
84
- username prompts. BotFather returns the bot token. The token authorizes the
85
- bot against the Bot API; treat it like a password. Do not place it in a
86
- shell command, config file, commit, issue, or screenshot.
84
+ 1. In Telegram, open **@BotFather** and create a bot with `/newbot`, or select
85
+ any existing bot that you own. Copy that bot's token. SKS does not assume a
86
+ fixed bot name or username: it verifies the token with `getMe` and binds the
87
+ returned bot identity. Treat the token like a password. Do not place it in
88
+ a shell command, config file, commit, issue, or screenshot.
87
89
  2. On the Mac that runs SKS, open **SKS Center → Remote Coding**, choose
88
90
  **Enter Bot Token…**, and paste the token into the native secure-input
89
91
  sheet. SKS verifies `getMe` and the bot's webhook state before it stores
@@ -190,7 +192,7 @@ remote change on the operator's behalf.
190
192
 
191
193
  ## Desktop Bridge
192
194
 
193
- SKS 8.1.3 uses a single local **Desktop Bridge** for managed Codex Desktop and
195
+ SKS 8.2.0 uses a single local **Desktop Bridge** for managed Codex Desktop and
194
196
  CLI routing. ChatGPT OAuth stays in the Codex identity plane. Codex-LB and
195
197
  OpenRouter credentials are independent profiles that can be configured and
196
198
  validated simultaneously; changing one profile does not remove the other.
@@ -259,7 +259,7 @@ dependencies = [
259
259
 
260
260
  [[package]]
261
261
  name = "sks-core"
262
- version = "8.1.3"
262
+ version = "8.2.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 = "8.1.3"
3
+ version = "8.2.1"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": "sks.skills-manifest.v1",
3
- "package_version": "8.1.3",
3
+ "package_version": "8.2.1",
4
4
  "skills": [
5
5
  {
6
6
  "canonical_name": "sks",
@@ -25,6 +25,7 @@ export const NATIVE_SOURCE_FILES = [
25
25
  'ProvidersOpenRouter.swift',
26
26
  'ProvidersBridgeCatalog.swift',
27
27
  'RemoteCodingViewController.swift',
28
+ 'RemoteCodingSettingsControls.swift',
28
29
  'DiagnosticsViewController.swift',
29
30
  'SettingsViewController.swift',
30
31
  'OperationModels.swift',
@@ -6,7 +6,7 @@ import { TELEGRAM_TOKEN_ENV_NAMES, bindTelegramBotIdentity, issueTelegramPairing
6
6
  import { spawn } from 'node:child_process';
7
7
  import fs from 'node:fs/promises';
8
8
  import path from 'node:path';
9
- import { TelegramClient } from '../telegram/client.js';
9
+ import { TelegramApiError, TelegramClient } from '../telegram/client.js';
10
10
  import { redactTelegramError } from '../telegram/redaction.js';
11
11
  import { projectRoot } from '../fsx.js';
12
12
  import { projectRootRealpath } from '../session/project-namespace.js';
@@ -63,7 +63,7 @@ export function usage() {
63
63
  ' doctor Evaluate getMe, pairing, poller, and liveness evidence.',
64
64
  ' pair Issue a short-lived code for the intended private Telegram chat.',
65
65
  ' setup --token-stdin [--remove-webhook]',
66
- ' Verify and store an @BotFather token; an existing webhook requires explicit removal consent.',
66
+ ' Verify and store the token for any @BotFather bot you own; an existing webhook requires explicit removal consent.',
67
67
  ' prepare --stdin-json Validate an authorized native request through the typed contract.',
68
68
  ' execute --stdin-json Execute a prepared request; destructive requests also need --confirmed.',
69
69
  '',
@@ -108,16 +108,22 @@ export async function telegramCommand(args = [], dependencies = {}) {
108
108
  }
109
109
  if (action === 'setup') {
110
110
  if (!args.includes('--token-stdin'))
111
- return fail('telegram_token_stdin_required', json);
111
+ return telegramSetupFailure('telegram_token_stdin_required', json);
112
112
  if (args.includes('--drop-pending-updates'))
113
- return fail('telegram_drop_pending_updates_unsupported', json);
113
+ return telegramSetupFailure('telegram_drop_pending_updates_unsupported', json);
114
114
  const environment = dependencies.environment ?? process.env;
115
115
  const envOverrideActive = args.includes('--operator-env-override-active')
116
116
  || TELEGRAM_TOKEN_ENV_NAMES.some((name) => String(environment[name] ?? '').trim().length > 0);
117
117
  if (envOverrideActive)
118
118
  return telegramSetupEnvOverrideBlocked(json);
119
119
  const token = await (dependencies.readTokenStdin ?? readBoundedTokenStdin)();
120
- const identity = await (dependencies.verifyToken ?? verifyTelegramToken)(token);
120
+ let identity;
121
+ try {
122
+ identity = await (dependencies.verifyToken ?? verifyTelegramToken)(token);
123
+ }
124
+ catch (error) {
125
+ return telegramSetupFailure(telegramIdentityFailure(error), json, 'getme');
126
+ }
121
127
  const removeWebhook = args.includes('--remove-webhook');
122
128
  let webhook;
123
129
  try {
@@ -182,6 +188,7 @@ export async function telegramCommand(args = [], dependencies = {}) {
182
188
  webhook_configured_before: webhookConfigured,
183
189
  webhook_removed: webhookRemoved, pending_updates_dropped: false,
184
190
  bot_id: identity?.id ?? null,
191
+ bot_username: publicBotUsername(identity?.username),
185
192
  bot_rotated: binding?.rotated ?? false,
186
193
  bot_state_reset: binding?.state_reset ?? false,
187
194
  restart_required: true
@@ -214,7 +221,10 @@ export async function telegramCommand(args = [], dependencies = {}) {
214
221
  return fail('telegram_unknown_action', json, ['status', 'doctor', 'pair', 'setup', 'prepare', 'execute']);
215
222
  }
216
223
  catch (error) {
217
- return fail(error instanceof Error ? error.message : String(error), json);
224
+ const message = error instanceof Error ? error.message : String(error);
225
+ return action === 'setup'
226
+ ? telegramSetupFailure(redactTelegramError(error), json)
227
+ : fail(message, json);
218
228
  }
219
229
  }
220
230
  async function verifyTelegramToken(token) {
@@ -362,6 +372,36 @@ function fail(error, json, supported) {
362
372
  ...(supported ? { supported } : {})
363
373
  }, json);
364
374
  }
375
+ function telegramSetupFailure(error, json, failureStage) {
376
+ process.exitCode = 1;
377
+ return emit({
378
+ schema: 'sks.telegram-setup-command.v1',
379
+ ok: false,
380
+ error,
381
+ token_stored: false,
382
+ ...(failureStage ? { failure_stage: failureStage } : {})
383
+ }, json);
384
+ }
385
+ function telegramIdentityFailure(error) {
386
+ const message = redactTelegramError(error);
387
+ const code = error instanceof TelegramApiError ? error.code : null;
388
+ if (code === 401 || code === 404 || /\b(?:401|404|Unauthorized|Not Found)\b/i.test(message)) {
389
+ return 'telegram_token_rejected';
390
+ }
391
+ if (/telegram_(?:request_)?timeout|timed?\s*out/i.test(message)) {
392
+ return 'telegram_identity_verification_timeout';
393
+ }
394
+ if (/fetch failed|network|ENOTFOUND|ECONN|EAI_AGAIN|offline/i.test(message)) {
395
+ return 'telegram_identity_verification_network_failed';
396
+ }
397
+ if (message === 'telegram_token_invalid')
398
+ return message;
399
+ return 'telegram_identity_verification_failed';
400
+ }
401
+ function publicBotUsername(value) {
402
+ const normalized = String(value ?? '').trim();
403
+ return /^[A-Za-z0-9_]{5,64}$/.test(normalized) ? normalized : null;
404
+ }
365
405
  function telegramSetupPartialFailure(input, json) {
366
406
  process.exitCode = 1;
367
407
  return emit({
@@ -97,6 +97,8 @@ function honestGapHeadingResolved(line, nextLine) {
97
97
  return /^(?:없음|없습니다|없다|none|no(?:ne)?|0|0개)[.!。]?$/i.test(String(nextLine || '').trim());
98
98
  }
99
99
  function honestGapLineResolved(line) {
100
+ if (honestIssueLineCompleted(line))
101
+ return true;
100
102
  if (/(?:unverified|미검증)\s*:\s*\[\s*\]/i.test(line) && /blockers?\s*:\s*\[\s*\]/i.test(line))
101
103
  return true;
102
104
  if (/(?:^|[\s*-])(?:unverified|미검증|blockers?)\s*:\s*\[\s*\](?:\s*(?:[,.;]|$).*)?$/i.test(line))
@@ -111,7 +113,7 @@ function honestGapLineResolved(line) {
111
113
  return true;
112
114
  if (/no\s+active\s+blocking\s+route\s+gate\s+detected/i.test(line))
113
115
  return true;
114
- if (/(?:blockers?|차단(?:\s*(?:항목|요소|건))?)\s*(?:[:=]\s*)?0(?:건|개)?\b/i.test(line))
116
+ if (/(?:blockers?(?:은|는|이|가)?|차단(?:\s*(?:항목|요소|건))?)\s*(?:[:=]\s*)?0(?:건|개)?\b/i.test(line))
115
117
  return true;
116
118
  if (/(non[-\s]?blocker|non[-\s]?blocking|not\s+(?:a\s+)?blocker|no\s+blocker|does\s+not\s+block|not\s+blocking|비\s*차단|blocker\s*(?:는|가)?\s*(?:아님|아닙니다|없음)|차단(?:하지|하진|하지는)\s*않|막(?:지|지는)\s*않)/i.test(line))
117
119
  return true;
@@ -123,6 +125,14 @@ function honestGapLineResolved(line) {
123
125
  return false;
124
126
  return /(없음|없습니다|없다|해당 없음|none|no unresolved|no remaining|no gaps|zero|0개|n\/a|not applicable)\.?\s*$/i.test(line);
125
127
  }
128
+ function honestIssueLineCompleted(line) {
129
+ const text = String(line || '').trim();
130
+ const unresolved = /(?:미해결|미완료|해결|수정|보완)\s*(?:필요|예정|대기|중)|(?:not|isn't|wasn't)\s+(?:fixed|resolved|addressed|implemented|completed)|(?:still|remains?|pending)\b/i;
131
+ if (unresolved.test(text))
132
+ return false;
133
+ return /(?:해결|수정|보완|정상화|완료)(?:했(?:습니다|다|음)?|됐(?:습니다|다)?|됨|함)?[.!。`*_)]*$/i.test(text)
134
+ || /(?:fixed|resolved|addressed|implemented|completed)[.!。`*_)]*$/i.test(text);
135
+ }
126
136
  export function shouldLoopBackAfterHonestMode(state = {}) {
127
137
  if (!state?.mission_id || state.implementation_allowed === false)
128
138
  return false;
@@ -9,7 +9,6 @@ const CURRENT_COMPILED_TESTS = new Set([
9
9
  'dist/core/__tests__/codex-release-manifest-current.test.js',
10
10
  'dist/core/__tests__/release-gate-hermetic-env.test.js',
11
11
  'dist/core/__tests__/update-menubar-package-local.test.js',
12
- 'dist/core/__tests__/update-simulated-upgrade.test.js',
13
12
  'dist/core/__tests__/version-manager-current-docs.test.js',
14
13
  'dist/core/install/__tests__/installed-package-smoke.test.js',
15
14
  'dist/core/perf/__tests__/release-latency-slo.test.js'
@@ -11,6 +11,8 @@ export const FORBIDDEN_RECURSIVE_GATES = new Set([
11
11
  ]);
12
12
  export const ALWAYS_ON_GATES = new Set([
13
13
  'release:metadata-current',
14
+ 'release:version-truth',
15
+ 'install-surface:ssot',
14
16
  'architecture:guard',
15
17
  'safety:mutation-callsite-coverage',
16
18
  'side-effect:runtime-report',
@@ -25,6 +27,8 @@ export const ALWAYS_ON_GATES = new Set([
25
27
  ]);
26
28
  export const REQUIRED_FOR_PUBLISH = new Set([
27
29
  'release:metadata-current',
30
+ 'release:version-truth',
31
+ 'install-surface:ssot',
28
32
  'architecture:guard',
29
33
  'safety:mutation-callsite-coverage',
30
34
  'side-effect:runtime-report',
@@ -1,6 +1,8 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  const ALWAYS_KEEP = new Set([
3
3
  'release:proof-truth',
4
+ 'release:version-truth',
5
+ 'install-surface:ssot',
4
6
  'typecheck',
5
7
  'schema:check'
6
8
  ]);
@@ -11,6 +11,7 @@ export const RELEASE_GATE_CONTRACT_IDS = Object.freeze([
11
11
  'commands:current-surface-only',
12
12
  'config:managed-merge',
13
13
  'docs:truthfulness',
14
+ 'install-surface:ssot',
14
15
  'latest-version:guidance',
15
16
  'migration:current-surface-e2e',
16
17
  'migration:upgrade-safety',
@@ -24,6 +25,7 @@ export const RELEASE_GATE_CONTRACT_IDS = Object.freeze([
24
25
  'release:metadata-current',
25
26
  'release:proof-truth',
26
27
  'release:provenance',
28
+ 'release:version-truth',
27
29
  'runtime:installed-smoke',
28
30
  'safety:mutation-callsite-coverage',
29
31
  'schema:check',
@@ -23,6 +23,7 @@ export async function probeTelegram(input) {
23
23
  blockers.push(`telegram_token_load_failed:${redactTelegramError(error)}`);
24
24
  }
25
25
  let identityValid = false;
26
+ let botId = null;
26
27
  let getMeCheckedAt = null;
27
28
  let getMeLatencyMs = null;
28
29
  if (!tokenConfigured)
@@ -30,7 +31,9 @@ export async function probeTelegram(input) {
30
31
  else {
31
32
  const started = Date.now();
32
33
  try {
33
- identityValid = (await input.client.getMe()).is_bot === true;
34
+ const identity = await input.client.getMe();
35
+ identityValid = identity.is_bot === true && Number.isSafeInteger(identity.id) && identity.id > 0;
36
+ botId = identityValid ? identity.id : null;
34
37
  if (!identityValid)
35
38
  blockers.push('telegram_getme_not_bot');
36
39
  }
@@ -72,6 +75,7 @@ export async function probeTelegram(input) {
72
75
  status: !tokenConfigured ? 'not_configured' : blockers.length ? 'degraded' : 'ready',
73
76
  token_configured: tokenConfigured,
74
77
  token_source: tokenConfigured ? (receiptProbe?.token_source ?? 'unknown') : 'none',
78
+ bot_id: botId,
75
79
  bot_identity_valid: identityValid,
76
80
  getme_checked_at: getMeCheckedAt,
77
81
  getme_latency_ms: getMeLatencyMs,
@@ -89,7 +93,7 @@ async function probeTelegramReceipt(receiptPath, now) {
89
93
  if (!liveness.ok) {
90
94
  return {
91
95
  schema: 'sks.telegram-doctor-probe.v1', ok: false, status: 'not_configured',
92
- token_configured: false, bot_identity_valid: false, paired_chat_count: 0,
96
+ token_configured: false, bot_id: null, bot_identity_valid: false, paired_chat_count: 0,
93
97
  token_source: 'none',
94
98
  audit_healthy: false, audit_last_error: null,
95
99
  getme_checked_at: null, getme_latency_ms: null,
@@ -122,9 +126,10 @@ async function probeTelegramReceipt(receiptPath, now) {
122
126
  status: !receipt.token_configured ? 'not_configured' : blockers.length ? 'degraded' : 'ready',
123
127
  token_configured: receipt.token_configured,
124
128
  token_source: receipt.token_source ?? (receipt.token_configured ? 'unknown' : 'none'),
129
+ bot_id: receipt.bot_id ?? null,
125
130
  bot_identity_valid: receipt.bot_identity_valid,
126
- getme_checked_at: receipt.getme_checked_at,
127
- getme_latency_ms: receipt.getme_latency_ms,
131
+ getme_checked_at: receipt.getme_checked_at ?? null,
132
+ getme_latency_ms: receipt.getme_latency_ms ?? null,
128
133
  getme_check_kind: 'receipt',
129
134
  paired_chat_count: receipt.paired_chat_count,
130
135
  audit_healthy: receipt.audit_healthy !== false,
@@ -89,8 +89,9 @@ function isTelegramLivenessReceipt(value) {
89
89
  && (row.token_source === undefined || ['env', 'user_secret_file', 'none', 'unknown'].includes(String(row.token_source)))
90
90
  && optionalPositiveSafeInteger(row.bot_id)
91
91
  && typeof row.bot_identity_valid === 'boolean'
92
- && nullableIso(row.getme_checked_at)
93
- && (row.getme_latency_ms === null || (Number.isFinite(row.getme_latency_ms) && Number(row.getme_latency_ms) >= 0))
92
+ && optionalNullableIso(row.getme_checked_at)
93
+ && (row.getme_latency_ms === undefined || row.getme_latency_ms === null
94
+ || (Number.isFinite(row.getme_latency_ms) && Number(row.getme_latency_ms) >= 0))
94
95
  && Number.isSafeInteger(row.paired_chat_count) && Number(row.paired_chat_count) >= 0
95
96
  && typeof row.started_at === 'string' && Number.isFinite(Date.parse(row.started_at))
96
97
  && typeof row.heartbeat_at === 'string' && Number.isFinite(Date.parse(row.heartbeat_at))
@@ -110,9 +111,14 @@ function isPollSnapshot(value) {
110
111
  && typeof row.running === 'boolean'
111
112
  && Number.isSafeInteger(row.offset) && Number(row.offset) >= 0
112
113
  && Number.isSafeInteger(row.consecutive_failures) && Number(row.consecutive_failures) >= 0
113
- && nullableIso(row.last_poll_at) && nullableIso(row.last_success_at) && nullableIso(row.last_update_at)
114
- && (row.last_error === null || typeof row.last_error === 'string');
114
+ && optionalNullableIso(row.last_poll_at)
115
+ && optionalNullableIso(row.last_success_at)
116
+ && optionalNullableIso(row.last_update_at)
117
+ && (row.last_error === undefined || row.last_error === null || typeof row.last_error === 'string');
115
118
  }
116
119
  function nullableIso(value) {
117
120
  return value === null || (typeof value === 'string' && Number.isFinite(Date.parse(value)));
118
121
  }
122
+ function optionalNullableIso(value) {
123
+ return value === undefined || nullableIso(value);
124
+ }
@@ -323,6 +323,10 @@ async function syncSourcePackageVersion(root, version) {
323
323
  rel: 'src/core/version.ts',
324
324
  replace: (text) => text.replace(/export const PACKAGE_VERSION = ['"][^'"]+['"];/, `export const PACKAGE_VERSION = '${version}';`)
325
325
  },
326
+ {
327
+ rel: 'plugins/sks/.codex-plugin/plugin.json',
328
+ replace: (text) => text.replace(/("version"\s*:\s*")[^"]+("(?=\s*[,}]))/, `$1${version}$2`)
329
+ },
326
330
  {
327
331
  rel: 'crates/sks-core/Cargo.toml',
328
332
  replace: (text) => text.replace(/^version\s*=\s*"[^"]+"/m, `version = "${version}"`)
@@ -1 +1 @@
1
- export const PACKAGE_VERSION = '8.1.3';
1
+ export const PACKAGE_VERSION = '8.2.1';
@@ -0,0 +1,160 @@
1
+ import Cocoa
2
+
3
+ final class RemoteCodingSettingsControls {
4
+ private static let tokenPattern = #"^\d{5,20}:[A-Za-z0-9_-]{20,128}$"#
5
+
6
+ let selectedBotDetail = NativeView.detail("No bot is configured.")
7
+ let pairingCommandField = NativeView.detail("Generate a code to create the /start pairing command.")
8
+ let botTokenField: NSSecureTextField
9
+ let setupButton: NSButton
10
+ let copyPairingButton: NSButton
11
+
12
+ private var pairingCommand: String?
13
+ private var selectedBotID: Int64?
14
+ private var selectedBotUsername: String?
15
+
16
+ init(target: AnyObject, submitTokenAction: Selector, copyPairingAction: Selector) {
17
+ let tokenField = NSSecureTextField(frame: NSRect(x: 0, y: 0, width: 360, height: 24))
18
+ tokenField.placeholderString = "Paste the complete BotFather HTTP API token"
19
+ tokenField.setContentHuggingPriority(.defaultLow, for: .horizontal)
20
+ tokenField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
21
+ tokenField.setAccessibilityLabel("BotFather HTTP API token")
22
+ tokenField.setAccessibilityHelp("Required. Paste the complete token for any Telegram bot you own. SKS clears this field before verification and never displays the token again.")
23
+ tokenField.setAccessibilityIdentifier("sks-center-telegram-bot-token")
24
+ tokenField.target = target
25
+ tokenField.action = submitTokenAction
26
+ botTokenField = tokenField
27
+
28
+ setupButton = NativeView.button("Verify, Save & Apply", target: target, action: submitTokenAction)
29
+ setupButton.setAccessibilityIdentifier("sks-center-telegram-verify-save-apply")
30
+ setupButton.setAccessibilityHelp("Verify the entered bot token with Telegram, save it to the private SKS user secret file, and apply it to the resident poller.")
31
+
32
+ copyPairingButton = NativeView.button("Copy Pairing Command", target: target, action: copyPairingAction)
33
+ copyPairingButton.setAccessibilityHelp("Copy only the generated /start pairing command. The bot token is never copied or displayed.")
34
+
35
+ pairingCommandField.isSelectable = true
36
+ pairingCommandField.font = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular)
37
+ pairingCommandField.setAccessibilityLabel("Telegram pairing command")
38
+ pairingCommandField.setAccessibilityIdentifier("sks-center-telegram-pairing-command")
39
+ selectedBotDetail.isSelectable = true
40
+ selectedBotDetail.setAccessibilityLabel("Selected Telegram bot")
41
+ selectedBotDetail.setAccessibilityIdentifier("sks-center-telegram-selected-bot")
42
+ }
43
+
44
+ func configure(delegate: NSTextFieldDelegate) {
45
+ botTokenField.delegate = delegate
46
+ }
47
+
48
+ func consumeTokenInput() -> String {
49
+ let value = botTokenField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
50
+ botTokenField.stringValue = ""
51
+ return value
52
+ }
53
+
54
+ func updateActionControls(operationInFlight: Bool, tokenConfigured: Bool, environmentOverride: Bool) {
55
+ let hasTokenInput = !botTokenField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
56
+ botTokenField.isEnabled = !operationInFlight && !environmentOverride
57
+ setupButton.title = tokenConfigured ? "Verify, Replace & Apply" : "Verify, Save & Apply"
58
+ setupButton.setAccessibilityLabel(setupButton.title)
59
+ setupButton.isEnabled = !operationInFlight && !environmentOverride && hasTokenInput
60
+ copyPairingButton.isEnabled = !operationInFlight && pairingCommand != nil
61
+ }
62
+
63
+ func renderSelectedBot(
64
+ botID: Int64?,
65
+ username: String? = nil,
66
+ configured: Bool,
67
+ identityValid: Bool
68
+ ) {
69
+ guard configured else {
70
+ selectedBotID = nil
71
+ selectedBotUsername = nil
72
+ selectedBotDetail.stringValue = "No bot is configured."
73
+ selectedBotDetail.setAccessibilityValue(selectedBotDetail.stringValue)
74
+ resetPairingCommand("Generate a code after connecting a bot.")
75
+ return
76
+ }
77
+ let retainedUsername = username ?? (botID == selectedBotID ? selectedBotUsername : nil)
78
+ selectedBotID = botID
79
+ selectedBotUsername = retainedUsername
80
+ if let botID, let retainedUsername, identityValid {
81
+ selectedBotDetail.stringValue = "@\(retainedUsername) · Bot ID \(botID)"
82
+ } else if let botID, identityValid {
83
+ selectedBotDetail.stringValue = "Bot ID \(botID) · verified by Telegram"
84
+ } else if let botID {
85
+ selectedBotDetail.stringValue = "Bot ID \(botID) · identity needs verification"
86
+ } else {
87
+ selectedBotDetail.stringValue = "Configured bot · identity details unavailable"
88
+ }
89
+ selectedBotDetail.setAccessibilityValue(selectedBotDetail.stringValue)
90
+ }
91
+
92
+ func setPairingCode(_ code: String) {
93
+ pairingCommand = "/start \(code)"
94
+ pairingCommandField.stringValue = pairingCommand ?? ""
95
+ pairingCommandField.setAccessibilityValue(pairingCommandField.stringValue)
96
+ }
97
+
98
+ func resetPairingCommand(_ detail: String) {
99
+ pairingCommand = nil
100
+ pairingCommandField.stringValue = detail
101
+ pairingCommandField.setAccessibilityValue(detail)
102
+ }
103
+
104
+ @discardableResult
105
+ func copyPairingCommand() -> Bool {
106
+ guard let pairingCommand else { return false }
107
+ let pasteboard = NSPasteboard.general
108
+ pasteboard.clearContents()
109
+ return pasteboard.setString(pairingCommand, forType: .string)
110
+ }
111
+
112
+ static func validTokenShape(_ value: String) -> Bool {
113
+ value.utf8.count <= 1024
114
+ && value.range(of: tokenPattern, options: .regularExpression) != nil
115
+ }
116
+
117
+ static func publicFailure(_ output: String, fallback: String) -> String {
118
+ guard let data = output.data(using: .utf8),
119
+ let row = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
120
+ let error = row["error"] as? String else { return fallback }
121
+ switch error {
122
+ case "telegram_token_invalid", "telegram_token_stdin_empty":
123
+ return "That value is not a complete BotFather HTTP API token."
124
+ case "telegram_token_rejected":
125
+ return "Telegram rejected this token. In @BotFather, select the bot you want SKS to use and copy that bot's current complete token."
126
+ case "telegram_identity_verification_timeout":
127
+ return "Telegram identity verification timed out. Check the network connection and try the selected bot token again."
128
+ case "telegram_identity_verification_network_failed":
129
+ return "Telegram identity verification could not reach the Bot API. Check the network connection and try again."
130
+ case "telegram_identity_verification_failed":
131
+ return "Telegram returned an invalid bot identity response. Try the selected bot's current token again."
132
+ case let value where value.contains("401") || value.contains("Unauthorized"):
133
+ return "Telegram rejected this token. Copy the current token from @BotFather and try again."
134
+ case let value where value.contains("timeout") || value.contains("network"):
135
+ return "Telegram identity verification could not complete. Check the network connection and try again."
136
+ default:
137
+ return fallback
138
+ }
139
+ }
140
+
141
+ static func partialSetupRecovery(_ response: TelegramCenterSetupResponse?) -> String {
142
+ var parts: [String] = []
143
+ if response?.webhook_removed == true { parts.append("The existing webhook was removed without dropping pending updates.") }
144
+ if response?.bot_state_reset == true { parts.append("Bot-scoped pairing and poll state were reset safely.") }
145
+ if response?.token_stored == false { parts.append("The token was not stored; rerun secure setup to finish.") }
146
+ if let note = response?.recovery?.note, !note.isEmpty { parts.append(note) }
147
+ return parts.isEmpty
148
+ ? "Review the secret-free recovery status, then rerun secure setup."
149
+ : parts.joined(separator: " ")
150
+ }
151
+
152
+ static func tokenSourceDescription(_ source: String) -> String {
153
+ switch source {
154
+ case "env": return "operator environment token"
155
+ case "user_secret_file": return "private token file"
156
+ case "none": return "no token source"
157
+ default: return "token source unknown"
158
+ }
159
+ }
160
+ }
@@ -1,8 +1,7 @@
1
1
  import Cocoa
2
2
 
3
- final class RemoteCodingViewController: NSViewController, ControlCenterPage {
3
+ final class RemoteCodingViewController: NSViewController, ControlCenterPage, NSTextFieldDelegate {
4
4
  private static let botFatherURL = URL(string: "https://t.me/BotFather")!
5
- private static let tokenPattern = #"^\d{5,20}:[A-Za-z0-9_-]{20,128}$"#
6
5
 
7
6
  private let processClient: ProcessClient
8
7
  private let telegramService: TelegramMenuBarService
@@ -10,7 +9,11 @@ final class RemoteCodingViewController: NSViewController, ControlCenterPage {
10
9
  private let serviceDetail = NativeView.detail("Reading secret-free Telegram liveness status…")
11
10
  private let pairingDetail = NativeView.detail("No pairing code has been issued in this session.")
12
11
  private let serviceSpinner = NativeView.spinner(label: "Telegram operation in progress")
13
- private lazy var setupButton = NativeView.button("Enter Bot Token…", target: self, action: #selector(enterToken))
12
+ private lazy var settingsControls = RemoteCodingSettingsControls(
13
+ target: self,
14
+ submitTokenAction: #selector(enterToken),
15
+ copyPairingAction: #selector(copyPairingCommand)
16
+ )
14
17
  private lazy var pairButton = NativeView.button("Generate Pairing Code", target: self, action: #selector(generatePairingCode))
15
18
  private lazy var startButton = NativeView.button("Start", target: self, action: #selector(startService))
16
19
  private lazy var stopButton = NativeView.button("Stop", target: self, action: #selector(stopService))
@@ -31,7 +34,7 @@ final class RemoteCodingViewController: NSViewController, ControlCenterPage {
31
34
  override func loadView() {
32
35
  let botFatherButton = NativeView.button("Open @BotFather", target: self, action: #selector(openBotFather))
33
36
  botFatherButton.setAccessibilityHelp("Open the official Telegram BotFather chat in your default handler.")
34
- setupButton.setAccessibilityHelp("Enter a BotFather token securely. The token is verified live and stored in the private SKS user secret file.")
37
+ settingsControls.configure(delegate: self)
35
38
  pairButton.setAccessibilityHelp("With Telegram polling running, issue a short-lived, single-use code for pairing one intended private chat.")
36
39
  startButton.setAccessibilityHelp("Start Telegram polling for the current SKS companion session after a bot token is configured.")
37
40
  stopButton.setAccessibilityHelp("Stop polling for the current companion session without deleting the token or paired chats. A companion relaunch starts it again.")
@@ -46,13 +49,15 @@ final class RemoteCodingViewController: NSViewController, ControlCenterPage {
46
49
  subtitle: "Create a Telegram bot, then let SKS validate and store its token privately.",
47
50
  views: [
48
51
  NativeView.detail(
49
- "1. Open @BotFather and send /newbot. 2. Finish the bot name and username prompts. " +
50
- "3. Copy the HTTP API token BotFather returns. 4. Choose Enter Bot Token below."
52
+ "1. Create or select any bot you own in @BotFather. 2. Copy that bot's complete HTTP API token. " +
53
+ "3. Paste it below and choose Verify, Save & Apply. The token is the only value you enter: SKS derives the bot ID from Telegram getMe, and learns chat and user IDs through private-chat pairing."
51
54
  ),
52
55
  NativeView.detail(
53
56
  "The token is sent only through process stdin to the canonical SKS setup command, verified with Telegram getMe, and stored in the private user secret file. It is never shown again or written to the SKS action log. Revoke it in BotFather if you suspect exposure."
54
57
  ),
55
- NativeView.row([botFatherButton, setupButton])
58
+ ControlKit.keyValueRow("Active Bot", settingsControls.selectedBotDetail),
59
+ ControlKit.keyValueRow("Bot Token", settingsControls.botTokenField),
60
+ NativeView.row([botFatherButton, settingsControls.setupButton])
56
61
  ]
57
62
  )
58
63
 
@@ -61,10 +66,11 @@ final class RemoteCodingViewController: NSViewController, ControlCenterPage {
61
66
  subtitle: "Authorize only the intended Telegram account and private chat.",
62
67
  views: [
63
68
  NativeView.detail(
64
- "With Telegram polling running, generate a short-lived code, open your new bot from the intended private chat, and send /start followed by that code. Codes are single-use; group chats are rejected. After pairing, try /sks status {}."
69
+ "With Telegram polling running, generate a short-lived command, open the selected bot from the intended private chat, and send that command. SKS derives the chat and user IDs from Telegram; they are never typed into this screen. Codes are single-use and group chats are rejected. After pairing, try /sks status {}."
65
70
  ),
71
+ ControlKit.keyValueRow("Pair Command", settingsControls.pairingCommandField),
66
72
  pairingDetail,
67
- pairButton
73
+ NativeView.row([pairButton, settingsControls.copyPairingButton])
68
74
  ]
69
75
  )
70
76
 
@@ -97,6 +103,7 @@ final class RemoteCodingViewController: NSViewController, ControlCenterPage {
97
103
  }
98
104
 
99
105
  @objc private func enterToken() {
106
+ guard !operationInFlight else { return }
100
107
  if TelegramPrivateFileStore.operatorEnvironmentOverrideActive() {
101
108
  showAttention(
102
109
  "Environment-managed token is active",
@@ -104,31 +111,38 @@ final class RemoteCodingViewController: NSViewController, ControlCenterPage {
104
111
  )
105
112
  return
106
113
  }
107
- promptForToken(removeWebhook: false)
114
+ let normalizedToken = settingsControls.consumeTokenInput()
115
+ updateControls()
116
+ guard RemoteCodingSettingsControls.validTokenShape(normalizedToken) else {
117
+ showAttention(
118
+ "Token not saved",
119
+ detail: "That value does not match the BotFather token format. Copy the complete HTTP API token and try again."
120
+ )
121
+ return
122
+ }
123
+ saveToken(normalizedToken, removeWebhook: false)
108
124
  }
109
125
 
110
- private func promptForToken(removeWebhook: Bool) {
126
+ private func promptForWebhookRemovalToken() {
111
127
  guard !operationInFlight, let window = view.window else { return }
112
128
  AlertFactory.textSheet(
113
129
  window: window,
114
- title: "Enter Telegram Bot Token",
115
- message: removeWebhook
116
- ? "Paste the BotFather token again. With your consent, SKS will remove this bot's existing webhook without dropping pending updates, verify the bot, and store the token privately."
117
- : "Paste the HTTP API token supplied by @BotFather. SKS will verify the bot identity before storing it privately.",
130
+ title: "Authorize Telegram Webhook Removal",
131
+ message: "Paste this selected bot's BotFather token again. With your consent, SKS will remove that bot's existing webhook without dropping pending updates, verify its live identity, and store the token privately.",
118
132
  secure: true,
119
133
  placeholder: "123456789:BotFatherToken",
120
- actionTitle: removeWebhook ? "Remove Webhook & Save" : "Verify & Save"
134
+ actionTitle: "Remove Webhook & Save"
121
135
  ) { [weak self] token in
122
136
  guard let self, let token else { return }
123
137
  let normalizedToken = token.trimmingCharacters(in: .whitespacesAndNewlines)
124
- guard self.validTokenShape(normalizedToken) else {
138
+ guard RemoteCodingSettingsControls.validTokenShape(normalizedToken) else {
125
139
  self.showAttention(
126
140
  "Token not saved",
127
141
  detail: "That value does not match the BotFather token format. Copy the complete HTTP API token and try again."
128
142
  )
129
143
  return
130
144
  }
131
- self.saveToken(normalizedToken, removeWebhook: removeWebhook)
145
+ self.saveToken(normalizedToken, removeWebhook: true)
132
146
  }
133
147
  }
134
148
 
@@ -150,7 +164,10 @@ final class RemoteCodingViewController: NSViewController, ControlCenterPage {
150
164
  code.range(of: #"^\d{6}-[A-F0-9]{4}$"#, options: .regularExpression) != nil else {
151
165
  self.showAttention(
152
166
  "Pairing code unavailable",
153
- detail: self.publicFailure(result.output, fallback: "SKS could not create a pairing code. Refresh status and try again.")
167
+ detail: RemoteCodingSettingsControls.publicFailure(
168
+ result.output,
169
+ fallback: "SKS could not create a pairing code. Refresh status and try again."
170
+ )
154
171
  )
155
172
  return
156
173
  }
@@ -160,6 +177,7 @@ final class RemoteCodingViewController: NSViewController, ControlCenterPage {
160
177
  let postPair = response.post_pair_command ?? "/sks status {}"
161
178
  let confirmation = response.confirmation_grammar ?? "/confirm <nonce>"
162
179
  let statusGuidance = instruction.contains(postPair) ? "" : " Then try \(postPair)."
180
+ self.settingsControls.setPairingCode(code)
163
181
  self.pairingDetail.stringValue = "\(instruction)\(expiration)\(statusGuidance) Confirm prompted actions with \(confirmation)."
164
182
  self.pairingDetail.setAccessibilityValue(self.pairingDetail.stringValue)
165
183
  self.showProgress("Pairing code ready", detail: "Use the displayed code once in the intended private chat.")
@@ -167,6 +185,11 @@ final class RemoteCodingViewController: NSViewController, ControlCenterPage {
167
185
  }
168
186
  }
169
187
 
188
+ @objc private func copyPairingCommand() {
189
+ guard settingsControls.copyPairingCommand() else { return }
190
+ showProgress("Pairing command copied", detail: "Paste it once into the intended private chat with the selected bot.")
191
+ }
192
+
170
193
  @objc private func startService() { runLifecycle(.start) }
171
194
  @objc private func stopService() { runLifecycle(.stop) }
172
195
  @objc private func restartService() { runLifecycle(.restart) }
@@ -217,7 +240,7 @@ final class RemoteCodingViewController: NSViewController, ControlCenterPage {
217
240
 
218
241
  private func saveToken(_ token: String, removeWebhook: Bool) {
219
242
  let normalizedToken = token.trimmingCharacters(in: .whitespacesAndNewlines)
220
- guard validTokenShape(normalizedToken) else {
243
+ guard RemoteCodingSettingsControls.validTokenShape(normalizedToken) else {
221
244
  showAttention(
222
245
  "Token not saved",
223
246
  detail: "That value does not match the BotFather token format. Copy the complete token and try again."
@@ -261,15 +284,15 @@ final class RemoteCodingViewController: NSViewController, ControlCenterPage {
261
284
  if response?.partial_success == true {
262
285
  self.showAttention(
263
286
  "Telegram setup partially completed",
264
- detail: self.partialSetupRecovery(response)
287
+ detail: RemoteCodingSettingsControls.partialSetupRecovery(response)
265
288
  )
266
289
  return
267
290
  }
268
291
  self.showAttention(
269
292
  "Token not saved",
270
- detail: self.publicFailure(
293
+ detail: RemoteCodingSettingsControls.publicFailure(
271
294
  result.output,
272
- fallback: "Telegram could not verify this bot token. Confirm it in @BotFather and try again."
295
+ fallback: "Telegram setup could not complete. Check the connection and SKS diagnostics, then try again."
273
296
  )
274
297
  )
275
298
  return
@@ -280,11 +303,21 @@ final class RemoteCodingViewController: NSViewController, ControlCenterPage {
280
303
  let receipt = try await restart.value
281
304
  self?.endOperation()
282
305
  self?.render(receipt)
306
+ self?.settingsControls.renderSelectedBot(
307
+ botID: response.bot_id ?? receipt.bot_id,
308
+ username: response.bot_username,
309
+ configured: true,
310
+ identityValid: true
311
+ )
312
+ if response.bot_state_reset == true {
313
+ self?.settingsControls.resetPairingCommand("Generate a new code because the selected bot changed.")
314
+ }
315
+ let selectedBot = response.bot_username.map { "@\($0)" } ?? "The selected bot"
283
316
  self?.showProgress(
284
317
  "Bot verified and connected",
285
318
  detail: response.bot_state_reset == true
286
- ? "The token is stored privately. Bot-scoped pairing and poll state were reset; generate a new pairing code."
287
- : "The token is stored privately. Generate a pairing code for the intended private chat."
319
+ ? "\(selectedBot) is verified and stored privately. Bot-scoped pairing and poll state were reset; generate a new pairing code."
320
+ : "\(selectedBot) is verified and stored privately. Generate a pairing code for the intended private chat."
288
321
  )
289
322
  } catch {
290
323
  self?.endOperation()
@@ -308,7 +341,7 @@ final class RemoteCodingViewController: NSViewController, ControlCenterPage {
308
341
  actionTitle: "Continue"
309
342
  ) { [weak self] confirmed in
310
343
  guard confirmed else { return }
311
- self?.promptForToken(removeWebhook: true)
344
+ self?.promptForWebhookRemovalToken()
312
345
  }
313
346
  }
314
347
 
@@ -348,8 +381,13 @@ final class RemoteCodingViewController: NSViewController, ControlCenterPage {
348
381
  private func render(_ response: TelegramCenterDoctorResponse) {
349
382
  tokenConfigured = response.token_configured
350
383
  pollerRunning = response.poller.running
384
+ settingsControls.renderSelectedBot(
385
+ botID: response.bot_id,
386
+ configured: response.token_configured,
387
+ identityValid: response.bot_identity_valid
388
+ )
351
389
  let paired = response.paired_chat_count == 1 ? "1 paired chat" : "\(response.paired_chat_count) paired chats"
352
- let source = tokenSourceDescription(response.token_source)
390
+ let source = RemoteCodingSettingsControls.tokenSourceDescription(response.token_source)
353
391
  if response.ok {
354
392
  NativeView.setBadge(serviceBadge, text: "Ready", color: .systemGreen)
355
393
  serviceDetail.stringValue = "Bot identity verified · \(source) · \(paired) · poller running · audit healthy."
@@ -383,8 +421,13 @@ final class RemoteCodingViewController: NSViewController, ControlCenterPage {
383
421
  private func render(_ receipt: TelegramLivenessReceipt) {
384
422
  tokenConfigured = receipt.token_configured
385
423
  pollerRunning = receipt.running && receipt.poller.running
424
+ settingsControls.renderSelectedBot(
425
+ botID: receipt.bot_id,
426
+ configured: receipt.token_configured,
427
+ identityValid: receipt.bot_identity_valid
428
+ )
386
429
  let paired = receipt.paired_chat_count == 1 ? "1 paired chat" : "\(receipt.paired_chat_count) paired chats"
387
- let source = tokenSourceDescription(receipt.token_source.rawValue)
430
+ let source = RemoteCodingSettingsControls.tokenSourceDescription(receipt.token_source.rawValue)
388
431
  NativeView.setBadge(
389
432
  serviceBadge,
390
433
  text: pollerRunning ? "Running" : "Stopped",
@@ -423,7 +466,12 @@ final class RemoteCodingViewController: NSViewController, ControlCenterPage {
423
466
  }
424
467
 
425
468
  private func updateControls() {
426
- setupButton.isEnabled = !operationInFlight
469
+ let environmentOverride = TelegramPrivateFileStore.operatorEnvironmentOverrideActive()
470
+ settingsControls.updateActionControls(
471
+ operationInFlight: operationInFlight,
472
+ tokenConfigured: tokenConfigured,
473
+ environmentOverride: environmentOverride
474
+ )
427
475
  pairButton.isEnabled = !operationInFlight && tokenConfigured && pollerRunning
428
476
  startButton.isEnabled = !operationInFlight && tokenConfigured && !pollerRunning
429
477
  stopButton.isEnabled = !operationInFlight && pollerRunning
@@ -431,9 +479,10 @@ final class RemoteCodingViewController: NSViewController, ControlCenterPage {
431
479
  refreshButton.isEnabled = !operationInFlight
432
480
  }
433
481
 
434
- private func validTokenShape(_ value: String) -> Bool {
435
- value.utf8.count <= 1024
436
- && value.range(of: Self.tokenPattern, options: .regularExpression) != nil
482
+ func controlTextDidChange(_ notification: Notification) {
483
+ guard let field = notification.object as? NSTextField,
484
+ field === settingsControls.botTokenField else { return }
485
+ updateControls()
437
486
  }
438
487
 
439
488
  private func decode<T: Decodable>(_ type: T.Type, _ output: String) -> T? {
@@ -441,45 +490,4 @@ final class RemoteCodingViewController: NSViewController, ControlCenterPage {
441
490
  return try? JSONDecoder().decode(type, from: data)
442
491
  }
443
492
 
444
- private func publicFailure(_ output: String, fallback: String) -> String {
445
- guard let data = output.data(using: .utf8),
446
- let row = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
447
- let error = row["error"] as? String else { return fallback }
448
- switch error {
449
- case "telegram_token_invalid", "telegram_token_stdin_empty":
450
- return "That value is not a complete BotFather HTTP API token."
451
- case let value where value.contains("401") || value.contains("Unauthorized"):
452
- return "Telegram rejected this token. Copy the current token from @BotFather and try again."
453
- case let value where value.contains("timeout") || value.contains("network"):
454
- return "Telegram identity verification could not complete. Check the network connection and try again."
455
- default:
456
- return fallback
457
- }
458
- }
459
-
460
- private func partialSetupRecovery(_ response: TelegramCenterSetupResponse?) -> String {
461
- var parts: [String] = []
462
- if response?.webhook_removed == true {
463
- parts.append("The existing webhook was removed without dropping pending updates.")
464
- }
465
- if response?.bot_state_reset == true {
466
- parts.append("Bot-scoped pairing and poll state were reset safely.")
467
- }
468
- if response?.token_stored == false {
469
- parts.append("The token was not stored; rerun secure setup to finish.")
470
- }
471
- if let note = response?.recovery?.note, !note.isEmpty { parts.append(note) }
472
- return parts.isEmpty
473
- ? "Review the secret-free recovery status, then rerun secure setup."
474
- : parts.joined(separator: " ")
475
- }
476
-
477
- private func tokenSourceDescription(_ source: String) -> String {
478
- switch source {
479
- case "env": return "operator environment token"
480
- case "user_secret_file": return "private token file"
481
- case "none": return "no token source"
482
- default: return "token source unknown"
483
- }
484
- }
485
493
  }
@@ -63,6 +63,18 @@ enum SecureProcessEnvelope {
63
63
  ["env", "user_secret_file", "none", "unchanged"].contains(source) {
64
64
  envelope["token_source"] = source
65
65
  }
66
+ if let stage = boundedString(object["failure_stage"], limit: 40),
67
+ stage == "getme" {
68
+ envelope["failure_stage"] = stage
69
+ }
70
+ if let botID = object["bot_id"] as? NSNumber,
71
+ botID.int64Value > 0 {
72
+ envelope["bot_id"] = botID
73
+ }
74
+ if let username = boundedString(object["bot_username"], limit: 64),
75
+ username.range(of: #"^[A-Za-z0-9_]{5,64}$"#, options: .regularExpression) != nil {
76
+ envelope["bot_username"] = username
77
+ }
66
78
  if let action = boundedString(object["operator_action"], limit: 480) {
67
79
  envelope["operator_action"] = action
68
80
  }
@@ -60,6 +60,27 @@ struct TelegramPollerReceipt: Codable, Sendable, Equatable {
60
60
  let last_success_at: String?
61
61
  let last_update_at: String?
62
62
  let last_error: String?
63
+
64
+ private enum CodingKeys: String, CodingKey {
65
+ case schema, running, offset, consecutive_failures
66
+ case last_poll_at, last_success_at, last_update_at, last_error
67
+ }
68
+
69
+ func encode(to encoder: Encoder) throws {
70
+ var container = encoder.container(keyedBy: CodingKeys.self)
71
+ try container.encode(schema, forKey: .schema)
72
+ try container.encode(running, forKey: .running)
73
+ try container.encode(offset, forKey: .offset)
74
+ try container.encode(consecutive_failures, forKey: .consecutive_failures)
75
+ if let last_poll_at { try container.encode(last_poll_at, forKey: .last_poll_at) }
76
+ else { try container.encodeNil(forKey: .last_poll_at) }
77
+ if let last_success_at { try container.encode(last_success_at, forKey: .last_success_at) }
78
+ else { try container.encodeNil(forKey: .last_success_at) }
79
+ if let last_update_at { try container.encode(last_update_at, forKey: .last_update_at) }
80
+ else { try container.encodeNil(forKey: .last_update_at) }
81
+ if let last_error { try container.encode(last_error, forKey: .last_error) }
82
+ else { try container.encodeNil(forKey: .last_error) }
83
+ }
63
84
  }
64
85
 
65
86
  struct TelegramLivenessReceipt: Codable, Sendable, Equatable {
@@ -80,6 +101,39 @@ struct TelegramLivenessReceipt: Codable, Sendable, Equatable {
80
101
  let audit_healthy: Bool?
81
102
  let audit_last_error: String?
82
103
  let poller: TelegramPollerReceipt
104
+
105
+ private enum CodingKeys: String, CodingKey {
106
+ case schema, generation, pid, running, token_configured, token_source
107
+ case bot_id, bot_identity_valid, getme_checked_at, getme_latency_ms
108
+ case paired_chat_count, started_at, heartbeat_at, stale_after_seconds
109
+ case audit_healthy, audit_last_error, poller
110
+ }
111
+
112
+ func encode(to encoder: Encoder) throws {
113
+ var container = encoder.container(keyedBy: CodingKeys.self)
114
+ try container.encode(schema, forKey: .schema)
115
+ try container.encode(generation, forKey: .generation)
116
+ try container.encode(pid, forKey: .pid)
117
+ try container.encode(running, forKey: .running)
118
+ try container.encode(token_configured, forKey: .token_configured)
119
+ try container.encode(token_source, forKey: .token_source)
120
+ if let bot_id { try container.encode(bot_id, forKey: .bot_id) }
121
+ else { try container.encodeNil(forKey: .bot_id) }
122
+ try container.encode(bot_identity_valid, forKey: .bot_identity_valid)
123
+ if let getme_checked_at { try container.encode(getme_checked_at, forKey: .getme_checked_at) }
124
+ else { try container.encodeNil(forKey: .getme_checked_at) }
125
+ if let getme_latency_ms { try container.encode(getme_latency_ms, forKey: .getme_latency_ms) }
126
+ else { try container.encodeNil(forKey: .getme_latency_ms) }
127
+ try container.encode(paired_chat_count, forKey: .paired_chat_count)
128
+ try container.encode(started_at, forKey: .started_at)
129
+ try container.encode(heartbeat_at, forKey: .heartbeat_at)
130
+ try container.encode(stale_after_seconds, forKey: .stale_after_seconds)
131
+ if let audit_healthy { try container.encode(audit_healthy, forKey: .audit_healthy) }
132
+ else { try container.encodeNil(forKey: .audit_healthy) }
133
+ if let audit_last_error { try container.encode(audit_last_error, forKey: .audit_last_error) }
134
+ else { try container.encodeNil(forKey: .audit_last_error) }
135
+ try container.encode(poller, forKey: .poller)
136
+ }
83
137
  }
84
138
 
85
139
  struct TelegramCenterSetupResponse: Decodable {
@@ -98,6 +152,8 @@ struct TelegramCenterSetupResponse: Decodable {
98
152
  let webhook_removed: Bool?
99
153
  let pending_updates_dropped: Bool?
100
154
  let bot_state_reset: Bool?
155
+ let bot_id: Int64?
156
+ let bot_username: String?
101
157
  let operator_action: String?
102
158
  let recovery: Recovery?
103
159
  }
@@ -123,6 +179,7 @@ struct TelegramCenterDoctorResponse: Decodable {
123
179
  let status: String
124
180
  let token_configured: Bool
125
181
  let token_source: String
182
+ let bot_id: Int64?
126
183
  let bot_identity_valid: Bool
127
184
  let paired_chat_count: Int
128
185
  let audit_healthy: Bool
@@ -24,7 +24,7 @@ if (pluginVersion && packageVersion && pluginVersion !== packageVersion) {
24
24
  blockers.push(`marketplace_plugin_version_mismatch:${pluginVersion}!=${packageVersion}`);
25
25
  }
26
26
  else if (!pluginVersion) {
27
- notes.push('marketplace_plugin_version_absent');
27
+ blockers.push('marketplace_plugin_version_absent');
28
28
  }
29
29
  else {
30
30
  notes.push(`marketplace_plugin_version_matches:${pluginVersion}`);
@@ -78,7 +78,7 @@ else {
78
78
  const report = {
79
79
  schema: 'sks.install-surface-ssot-check.v1',
80
80
  ok: blockers.length === 0,
81
- install_ssot: 'npm:sneakoscope@latest',
81
+ install_ssot: 'package.json#version',
82
82
  package_version: packageVersion || null,
83
83
  plugin_version: pluginVersion || null,
84
84
  path_sks_version: pathVersion || null,
@@ -43,6 +43,7 @@ const requiredPackageScripts = [
43
43
  'typecheck',
44
44
  'release:check',
45
45
  'release:metadata',
46
+ 'release:version-truth',
46
47
  'release:check:affected',
47
48
  'release:check:fast',
48
49
  'release:check:confidence',
@@ -70,6 +71,7 @@ const requiredReleaseGates = [
70
71
  'commands:current-surface-only',
71
72
  'config:managed-merge',
72
73
  'docs:truthfulness',
74
+ 'install-surface:ssot',
73
75
  'latest-version:guidance',
74
76
  'migration:current-surface-e2e',
75
77
  'migration:upgrade-safety',
@@ -83,6 +85,7 @@ const requiredReleaseGates = [
83
85
  'release:latency-slo',
84
86
  'release:proof-truth',
85
87
  'release:provenance',
88
+ 'release:version-truth',
86
89
  'policy:gate-audit',
87
90
  'safety:mutation-callsite-coverage',
88
91
  'schema:check',
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { assertGate, emitGate, root } from './gate-lib.js';
6
+ const pkg = readJson('package.json');
7
+ const expected = String(pkg.version || '');
8
+ const mismatches = [];
9
+ const warnings = [];
10
+ let checked = 0;
11
+ checkJson('package.json', 'version', pkg.version);
12
+ const lock = readJson('package-lock.json');
13
+ checkJson('package-lock.json', 'version', lock.version);
14
+ checkJson('package-lock.json', 'packages[""].version', lock.packages?.['']?.version);
15
+ const plugin = readJson('plugins/sks/.codex-plugin/plugin.json');
16
+ checkJson('plugins/sks/.codex-plugin/plugin.json', 'version', plugin.version);
17
+ checkRegex('src/core/version.ts', /PACKAGE_VERSION\s*=\s*['"]([^'"]+)['"]/, 'PACKAGE_VERSION');
18
+ checkReExportOrRegex('src/core/fsx.ts', /PACKAGE_VERSION\s*}\s*from\s*['"]\.\/version(?:\.js)?['"]/, /PACKAGE_VERSION\s*=\s*['"]([^'"]+)['"]/, 'PACKAGE_VERSION');
19
+ checkReExportOrRegex('src/bin/sks.ts', /PACKAGE_VERSION\s*}\s*from\s*['"]\.\.\/core\/version(?:\.js)?['"]/, /FAST_PACKAGE_VERSION\s*=\s*['"]([^'"]+)['"]/, 'FAST_PACKAGE_VERSION');
20
+ checkRegex('crates/sks-core/Cargo.toml', /^version\s*=\s*"([^"]+)"/m, 'package.version');
21
+ checkCargoLock('crates/sks-core/Cargo.lock', 'sks-core');
22
+ const dist = readJson('dist/build-manifest.json', null);
23
+ checkJson('dist/build-manifest.json', 'package_version', dist?.package_version);
24
+ checkJson('dist/build-manifest.json', 'version', dist?.version);
25
+ checkChangelog();
26
+ checkReadme();
27
+ checkReleaseMetadataScript();
28
+ checkCargoMetadata();
29
+ const ok = mismatches.length === 0;
30
+ const report = {
31
+ schema: 'sks.release-version-truth.v1',
32
+ ok,
33
+ expected,
34
+ checked,
35
+ mismatches,
36
+ warnings,
37
+ generated_at: new Date().toISOString()
38
+ };
39
+ const out = path.join(root, '.sneakoscope', 'reports', 'release-version-truth.json');
40
+ fs.mkdirSync(path.dirname(out), { recursive: true });
41
+ fs.writeFileSync(out, `${JSON.stringify(report, null, 2)}\n`);
42
+ assertGate(ok, 'release version truth mismatch', { expected, mismatches });
43
+ emitGate('release:version-truth', { version: expected, checked, warnings: warnings.length });
44
+ function checkJson(file, field, actual) {
45
+ checked += 1;
46
+ if (actual !== expected)
47
+ mismatch(file, field, actual);
48
+ }
49
+ function checkRegex(file, re, field) {
50
+ checked += 1;
51
+ const text = readText(file);
52
+ const match = text.match(re);
53
+ if (!match)
54
+ mismatch(file, field, null);
55
+ else if (match[1] !== expected)
56
+ mismatch(file, field, match[1]);
57
+ }
58
+ function checkReExportOrRegex(file, reExportRe, literalRe, field) {
59
+ const text = readText(file);
60
+ if (reExportRe.test(text)) {
61
+ checked += 1;
62
+ return;
63
+ }
64
+ checkRegex(file, literalRe, field);
65
+ }
66
+ function checkCargoLock(file, name) {
67
+ checked += 1;
68
+ const text = readText(file);
69
+ const block = text.split(/\n\[\[package\]\]\n/).find((part) => new RegExp(`name\\s*=\\s*"${escapeRe(name)}"`).test(part));
70
+ const match = block?.match(/version\s*=\s*"([^"]+)"/);
71
+ if (!match)
72
+ mismatch(file, `${name}.version`, null);
73
+ else if (match[1] !== expected)
74
+ mismatch(file, `${name}.version`, match[1]);
75
+ }
76
+ function checkChangelog() {
77
+ checked += 1;
78
+ const text = readText('CHANGELOG.md');
79
+ const latest = latestVersionedChangelogSection(text);
80
+ if (latest !== expected)
81
+ mismatch('CHANGELOG.md', 'latest release section', latest);
82
+ }
83
+ function checkReadme() {
84
+ checked += 1;
85
+ const text = readText('README.md');
86
+ const displayed = text.match(/\*\*SKS ([0-9]+\.[0-9]+\.[0-9]+)\*\*/)?.[1] || null;
87
+ if (!displayed)
88
+ mismatch('README.md', 'displayed current version', null);
89
+ else if (displayed !== expected)
90
+ mismatch('README.md', 'displayed current version', displayed);
91
+ }
92
+ function checkReleaseMetadataScript() {
93
+ checked += 1;
94
+ const script = String(pkg.scripts?.['release:metadata'] || '');
95
+ if (!script.includes('dist/scripts/release-metadata-check.js')) {
96
+ mismatch('package.json', 'scripts.release:metadata', script || null, 'node ./dist/scripts/release-metadata-check.js');
97
+ }
98
+ }
99
+ function checkCargoMetadata() {
100
+ checked += 1;
101
+ const res = spawnSync('cargo', ['metadata', '--no-deps', '--manifest-path', path.join(root, 'crates/sks-core/Cargo.toml'), '--format-version', '1'], {
102
+ cwd: root,
103
+ encoding: 'utf8',
104
+ timeout: 30000
105
+ });
106
+ if (res.status !== 0) {
107
+ warnings.push({ file: 'crates/sks-core/Cargo.toml', message: 'cargo metadata unavailable', stderr_tail: tail(res.stderr) });
108
+ return;
109
+ }
110
+ try {
111
+ const metadata = JSON.parse(res.stdout);
112
+ const crate = metadata.packages?.find((row) => row.name === 'sks-core');
113
+ if (crate?.version !== expected)
114
+ mismatch('cargo metadata', 'sks-core.version', crate?.version || null);
115
+ }
116
+ catch (err) {
117
+ warnings.push({ file: 'cargo metadata', message: `unparseable:${err instanceof Error ? err.message : String(err)}` });
118
+ }
119
+ }
120
+ function mismatch(file, field, actual, wanted = expected) {
121
+ mismatches.push({ file, field, expected: wanted, actual: actual ?? null });
122
+ }
123
+ function readJson(rel, fallback) {
124
+ try {
125
+ return JSON.parse(readText(rel));
126
+ }
127
+ catch (err) {
128
+ if (arguments.length > 1)
129
+ return fallback;
130
+ throw err;
131
+ }
132
+ }
133
+ function readText(rel) {
134
+ return fs.readFileSync(path.join(root, rel), 'utf8');
135
+ }
136
+ function escapeRe(value) {
137
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
138
+ }
139
+ function latestVersionedChangelogSection(text) {
140
+ for (const match of text.matchAll(/^## \[([^\]]+)\]/gm)) {
141
+ if (/^[0-9]+\.[0-9]+\.[0-9]+$/.test(match[1]))
142
+ return match[1];
143
+ }
144
+ return null;
145
+ }
146
+ function tail(value, limit = 1000) {
147
+ const text = String(value || '');
148
+ return text.length > limit ? text.slice(-limit) : text;
149
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "8.1.3",
4
+ "version": "8.2.1",
5
5
  "description": "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.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
@@ -40,6 +40,7 @@
40
40
  "!dist/scripts/**",
41
41
  "dist/scripts/canonical-test-runner.js",
42
42
  "dist/scripts/desktop-bridge-transport-client.js",
43
+ "dist/scripts/release-version-truth-check.js",
43
44
  "dist/scripts/{agent-guidance-dedup-check.js,agent-message-bus-reader-check.js,all-feature-deep-completion-check.js,appshots-thread-attachment-discovery-check.js,architecture-guard-check.js,certificate-sla-check.js,check-runtime-schemas.js,cli-output-consistency-check.js,codex-current-app-server-v2-check.js,codex-current-binary-identity-check.js,codex-current-capability-check.js,codex-current-core-real-probes-check.js,codex-current-manifest-check.js,codex-current-policy-check.js,codex-current-thread-store-check.js,codex-agent-role-rich-content-check.js,codex-agent-role-sync-check.js,codex-agent-type-routing-check.js,codex-app-execution-profile-check.js,codex-app-fast-ui-preservation-check.js,codex-app-handoff-check.js,codex-app-handoff-launch-check.js,codex-config-load-probe.js,context-graph-check.js,codex-control-all-pipelines-check.js,codex-control-event-stream-ledger-check.js,codex-control-tool-output-continuity-audit-check.js,desktop-bridge-transport-check.js,desktop-bridge-latency-check.js,desktop-bridge-capabilities-check.js,desktop-bridge-real-evidence-check.js,codex-native-agent-role-content-check.js,codex-native-feature-broker-blackbox.js,codex-native-feature-broker-check.js,codex-native-harness-compat-check.js,codex-native-hook-lifecycle-proof-check.js,codex-native-runtime-e2e-fixture.js,codex-plugin-app-template-policy-check.js,codex-sdk-all-pipelines-check.js,codex-sdk-backend-router-check.js,codex-sdk-capability-check.js,codex-sdk-event-stream-ledger-check.js,codex-sdk-real-smoke-check.js,concurrent-session-collision-check.js,context7-evidence-dedupe-check.js,core-skill-card-schema-check.js,core-skill-deployment-snapshot-check.js,core-skill-heldout-validation-check.js,core-skill-no-inference-optimizer-check.js,core-skill-patch-check.js,core-skill-route-runtime-integration-check.js,current-command-surface-check.js,current-surface-update-e2e-check.js,current-upgrade-matrix-check.js,desktop-bridge-unification-check.js,dfix-fixture-check.js,dfix-patch-handoff-check.js,dfix-verification-check.js,dfix-verification-recommendation-check.js,docs-truthfulness-check.js,doctor-codex-app-harness-check.js,doctor-fix-proves-codex-read-check.js,doctor-fixes-codex-app-fast-ui-check.js,evidence-flagship-coverage-check.js,fast-codex-service-tier-proof-check.js,fixtures/fake-codex-config-loader.js,gate-policy-audit-check.js,geo-claim-evidence-check.js,geo-cli-blackbox-check.js,geo-crawler-policy-check.js,git-worktree-checkpoint-check.js,git-worktree-cross-rebase-check.js,git-worktree-diff-envelope-check.js,git-worktree-dirty-lock-check.js,git-worktree-dirty-main-detection-check.js,git-worktree-integration-primary-check.js,git-worktree-manifest-append-check.js,git-worktree-untracked-diff-check.js,goal-artifact-compat-check.js,harness-benchmark-check.js,helper-dedup-check.js,hook-latency-budget-check.js,image-artifact-path-contract-check.js,imagegen-real-smoke-check.js,init-deep-backup-retention-check.js,installed-package-smoke-check.js,lib/codex-sdk-gate-lib.js,lib/ensure-dist-fresh.js,lib/git-worktree-fixture.js,lib/valid-png-fixture.js,local-collab-all-pipelines-final-gpt-check.js,loop-directive-check-lib.js,loop-integration-finalizer-check.js,loop-worker-fixture-child.js,mad-preflight-blocks-unreadable-config-check.js,mad-sks-app-ui-no-mutation-check.js}",
44
45
  "dist/scripts/{latest-version-guidance-check.js,mad-sks-launch-sql-plane-boundary-check.js,mad-sks-sql-plane-capability-check.js,mad-sks-sql-plane-lifecycle-hook-decision-check.js,mad-sks-sql-plane-result-lifecycle-check.js,managed-config-merge-check.js,mcp-plugin-inventory-check.js,model-call-concurrency-check.js,mutation-callsite-analysis.js,mutation-callsite-coverage-check.js,naruto-ssot-gate-aliases-check.js,naruto-ssot-pipeline-default-check.js,naruto-ssot-route-normalization-check.js,naruto-ssot-routing-check.js,naruto-worktree-coding-blackbox.js,native-capability-repair-matrix-check.js,native-image-generation-repair-check.js,official-subagent-workflow-check.js,package-published-contract-check.js,packlist-performance-check.js,pipeline-codex-native-doctor-mad-routing-check.js,pipeline-codex-native-doctor-mad-routing-real-blackbox.js,pipeline-codex-native-image-routing-check.js,pipeline-codex-native-image-routing-real-blackbox.js,pipeline-codex-native-loop-routing-check.js,pipeline-codex-native-loop-routing-real-blackbox.js,pipeline-codex-native-qa-routing-check.js,pipeline-codex-native-qa-routing-real-blackbox.js,pipeline-codex-native-research-routing-check.js,pipeline-codex-native-research-routing-real-blackbox.js,ppt-full-e2e-blackbox-check.js,ppt-real-export-adapter-check.js,ppt-real-imagegen-smoke-check.js,ppt-real-imagegen-wiring-check.js,ppt-reexport-rereview-check.js,prepublish-fast-check.js,publish-preflight.js,prepublish-release-check-or-fast.js,probe-memoization-check.js,product-design-plugin-routing-check.js,prompt-placeholder-guard-check.js,proof-root-cause-policy-check.js,provider-badge-context-check.js,python-codex-sdk-all-pipelines-check.js,qa-loop-app-handoff-capability-check.js,qa-loop-app-handoff-check.js,qa-loop-app-handoff-cli-check.js,qa-loop-app-handoff-confirmation-check.js,qa-loop-app-handoff-gate-lifecycle-check.js,qa-loop-app-handoff-launch-check.js,qa-loop-app-handoff-status-lifecycle-check.js,qa-loop-budget-policy-check.js,qa-loop-effort-escalation-check.js,qa-loop-execution-profile-routing-check.js,qa-loop-image-path-exposure-check.js,qa-loop-image-path-prompt-injection-check.js,release-affected-selector-check.js,release-aggressive-resource-governor-check.js,release-cache-glob-hashing-check.js,release-cache-input-classifier-check.js,release-cache-neutralization-report-check.js,release-cache-version-neutral-fixture-check.js,release-check-stamp.js,release-dynamic-presets-check.js,release-full-parallelism-blackbox.js,release-gate-batch-runner-check.js,release-gate-budget-check.js,release-gate-dag-runner-check.js,release-gate-dag-runner.js,release-latency-slo-check.js,release-metadata-check.js,release-pack-receipt.js,release-parallel-speed-budget-check.js,release-physical-gates-check.js,release-proof-truth-check.js,release-provenance-check.js,release-real-check.js,release-runtime-truth-matrix-check.js,release-speed-summary.js,research-blueprint-densifier-check.js,research-claim-builder-check.js,research-complete-package-fixture-check.js,research-execution-profile-routing-check.js,research-final-reviewer-blackbox.js,responses-retry-policy-centralized-check.js,runtime-proof-summary-check.js,runtime-script-pack-closure-check.js,scheduler-batch-dispatch-check.js,scheduler-parallel-proof-consistency-check.js,scheduler-utilization-integral-check.js,search-visibility-gate-lib.js,secret-preservation-check.js,seo-audit-fixture-check.js,seo-cli-blackbox-check.js,seo-geo-geo-disambiguation-check.js,seo-geo-route-identity-check.js,seo-geo-skill-rich-content-check.js,seo-no-mutation-by-default-check.js,shared-memory-fixture-check.js,side-effect-runtime-report-check.js,skill-name-canonicalizer-check.js,skill-surface-modernization-check.js,skills-manifest-continuity-check.js,sks-cli-gate-lib.js}",
45
46
  "dist/scripts/{real-execution-check-lib.js,gate-lib.js,runtime-domain-gate-lib.js,typed-routing-gate-lib.js,codex-native-gate-lib.js,skill-fixture-check-lib.js,sks-uninstall-regression-check.js,sksd-daemon-check.js,super-search-provider-interface-check.js,triwiki-cache-key-check.js,triwiki-proof-bank-blackbox.js,triwiki-proof-bank-check.js,triwiki-proof-card-check.js,trust-fixture-check.js,type-surface-codex-app-check.js,ultra-router-auto-router-check.js,uninstall-inventory-check.js,install-surface-ssot-check.js,ux-review-extract-wires-real-extractor-check.js,ux-review-imagegen-blackbox-check.js,ux-review-patch-diff-recheck-check.js,ux-review-real-imagegen-smoke-check.js,ux-review-run-wires-imagegen-check.js,wrongness-fixture-check.js}",
@@ -703,6 +703,43 @@
703
703
  ],
704
704
  "output_contract": "sks.gate-result.v2"
705
705
  },
706
+ {
707
+ "id": "release:version-truth",
708
+ "command": "node ./dist/scripts/release-version-truth-check.js",
709
+ "deps": [],
710
+ "resource": [
711
+ "cpu-light",
712
+ "fs-read"
713
+ ],
714
+ "side_effect": "hermetic",
715
+ "timeout_ms": 60000,
716
+ "cache": {
717
+ "enabled": false,
718
+ "inputs": [
719
+ "package.json",
720
+ "package-lock.json",
721
+ "plugins/sks/.codex-plugin/plugin.json",
722
+ "src/core/version.ts",
723
+ "src/core/fsx.ts",
724
+ "src/bin/sks.ts",
725
+ "crates/sks-core/Cargo.toml",
726
+ "crates/sks-core/Cargo.lock",
727
+ "dist/build-manifest.json",
728
+ "CHANGELOG.md",
729
+ "README.md",
730
+ "src/scripts/release-version-truth-check.ts"
731
+ ]
732
+ },
733
+ "isolation": {
734
+ "home": "temp",
735
+ "codex_home": "temp",
736
+ "report_dir": "per-gate"
737
+ },
738
+ "preset": [
739
+ "release"
740
+ ],
741
+ "output_contract": "sks.gate-result.v2"
742
+ },
706
743
  {
707
744
  "id": "install-surface:ssot",
708
745
  "command": "node ./dist/scripts/install-surface-ssot-check.js",
@@ -714,7 +751,7 @@
714
751
  "side_effect": "hermetic",
715
752
  "timeout_ms": 60000,
716
753
  "cache": {
717
- "enabled": true,
754
+ "enabled": false,
718
755
  "inputs": [
719
756
  "package.json",
720
757
  "plugins/sks/.codex-plugin/plugin.json",
@@ -727,7 +764,7 @@
727
764
  "report_dir": "per-gate"
728
765
  },
729
766
  "preset": [
730
- "confidence"
767
+ "release"
731
768
  ],
732
769
  "output_contract": "sks.gate-result.v2"
733
770
  },