sneakoscope 8.1.3 → 8.2.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 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.0** — 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.0"
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.0"
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.0",
4
4
  "skills": [
5
5
  {
6
6
  "canonical_name": "sks",
@@ -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;
@@ -123,8 +123,8 @@ async function probeTelegramReceipt(receiptPath, now) {
123
123
  token_configured: receipt.token_configured,
124
124
  token_source: receipt.token_source ?? (receipt.token_configured ? 'unknown' : 'none'),
125
125
  bot_identity_valid: receipt.bot_identity_valid,
126
- getme_checked_at: receipt.getme_checked_at,
127
- getme_latency_ms: receipt.getme_latency_ms,
126
+ getme_checked_at: receipt.getme_checked_at ?? null,
127
+ getme_latency_ms: receipt.getme_latency_ms ?? null,
128
128
  getme_check_kind: 'receipt',
129
129
  paired_chat_count: receipt.paired_chat_count,
130
130
  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
+ }
@@ -1 +1 @@
1
- export const PACKAGE_VERSION = '8.1.3';
1
+ export const PACKAGE_VERSION = '8.2.0';
@@ -46,8 +46,8 @@ final class RemoteCodingViewController: NSViewController, ControlCenterPage {
46
46
  subtitle: "Create a Telegram bot, then let SKS validate and store its token privately.",
47
47
  views: [
48
48
  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."
49
+ "1. Create or select any bot you own in @BotFather. 2. Copy that bot's complete HTTP API token. " +
50
+ "3. Choose Enter Bot Token below. SKS binds the identity returned by Telegram getMe; it never assumes a bot name or username."
51
51
  ),
52
52
  NativeView.detail(
53
53
  "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."
@@ -113,8 +113,8 @@ final class RemoteCodingViewController: NSViewController, ControlCenterPage {
113
113
  window: window,
114
114
  title: "Enter Telegram Bot Token",
115
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.",
116
+ ? "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."
117
+ : "Paste the complete HTTP API token for any bot you own in @BotFather. SKS will accept the identity returned by Telegram getMe without assuming a bot name, then store the token privately.",
118
118
  secure: true,
119
119
  placeholder: "123456789:BotFatherToken",
120
120
  actionTitle: removeWebhook ? "Remove Webhook & Save" : "Verify & Save"
@@ -269,7 +269,7 @@ final class RemoteCodingViewController: NSViewController, ControlCenterPage {
269
269
  "Token not saved",
270
270
  detail: self.publicFailure(
271
271
  result.output,
272
- fallback: "Telegram could not verify this bot token. Confirm it in @BotFather and try again."
272
+ fallback: "Telegram setup could not complete. Check the connection and SKS diagnostics, then try again."
273
273
  )
274
274
  )
275
275
  return
@@ -280,11 +280,12 @@ final class RemoteCodingViewController: NSViewController, ControlCenterPage {
280
280
  let receipt = try await restart.value
281
281
  self?.endOperation()
282
282
  self?.render(receipt)
283
+ let selectedBot = response.bot_username.map { "@\($0)" } ?? "The selected bot"
283
284
  self?.showProgress(
284
285
  "Bot verified and connected",
285
286
  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."
287
+ ? "\(selectedBot) is verified and stored privately. Bot-scoped pairing and poll state were reset; generate a new pairing code."
288
+ : "\(selectedBot) is verified and stored privately. Generate a pairing code for the intended private chat."
288
289
  )
289
290
  } catch {
290
291
  self?.endOperation()
@@ -448,6 +449,14 @@ final class RemoteCodingViewController: NSViewController, ControlCenterPage {
448
449
  switch error {
449
450
  case "telegram_token_invalid", "telegram_token_stdin_empty":
450
451
  return "That value is not a complete BotFather HTTP API token."
452
+ case "telegram_token_rejected":
453
+ return "Telegram rejected this token. In @BotFather, select the bot you want SKS to use and copy that bot's current complete token."
454
+ case "telegram_identity_verification_timeout":
455
+ return "Telegram identity verification timed out. Check the network connection and try the selected bot token again."
456
+ case "telegram_identity_verification_network_failed":
457
+ return "Telegram identity verification could not reach the Bot API. Check the network connection and try again."
458
+ case "telegram_identity_verification_failed":
459
+ return "Telegram returned an invalid bot identity response. Try the selected bot's current token again."
451
460
  case let value where value.contains("401") || value.contains("Unauthorized"):
452
461
  return "Telegram rejected this token. Copy the current token from @BotFather and try again."
453
462
  case let value where value.contains("timeout") || value.contains("network"):
@@ -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
  }
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.0",
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",