sneakoscope 1.21.0 → 1.21.2

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
@@ -16,7 +16,7 @@ Set up this agent project with Sneakoscope Codex. Use [[mandarange/Sneakoscope-C
16
16
 
17
17
  ## Current Release
18
18
 
19
- SKS **1.21.0** carries forward the `sks --mad` Zellij auto-attach fix and aligns the release metadata gates with explicit version bumps. Previous releases only *created* a detached background session and printed an `Attach with: ...` hint, so nothing opened in the operator's terminal. SKS now performs the follow-up foreground attach automatically when launched in an interactive TTY (using the same `ZELLIJ_SOCKET_DIR` namespace as the background session), and falls back to printing the manual attach command if attach fails. Auto-attach is skipped for `--json`, non-TTY/piped launches, when already inside a Zellij session, or with `--no-attach` / `SKS_NO_ZELLIJ_ATTACH=1`; `--attach` forces it.
19
+ SKS **1.21.2** fixes the `sks --mad` Zellij launch regression from 1.21.1: Zellij rejects `--copy-command` when it is paired with the OSC52-only `--copy-clipboard` flag, so SKS now passes only `--copy-command pbcopy` and `--copy-on-select true` on the launch CLI while keeping the generated clipboard config for attach-time behavior. It carries forward the 1.21.1 launch-speed and Codex legacy-profile fixes.
20
20
 
21
21
  SKS **1.20.4** is a targeted `sks --mad` / codex-lb Zellij usability patch: when a background MAD Zellij session launches successfully, SKS now prints the exact `Attach with: ZELLIJ_SOCKET_DIR=... zellij attach ...` command so operators can enter the fresh session without manually reconstructing the socket namespace.
22
22
 
@@ -76,7 +76,7 @@ dependencies = [
76
76
 
77
77
  [[package]]
78
78
  name = "sks-core"
79
- version = "1.21.0"
79
+ version = "1.21.2"
80
80
  dependencies = [
81
81
  "serde_json",
82
82
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "1.21.0"
3
+ version = "1.21.2"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -4,7 +4,7 @@ use std::io::{self, Read, Seek, SeekFrom};
4
4
  fn main() {
5
5
  let mut args = std::env::args().skip(1);
6
6
  match args.next().as_deref() {
7
- Some("--version") => println!("sks-rs 1.21.0"),
7
+ Some("--version") => println!("sks-rs 1.21.2"),
8
8
  Some("compact-info") => {
9
9
  let mut input = String::new();
10
10
  let _ = io::stdin().read_to_string(&mut input);
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schema": "sks.dist-build-stamp.v1",
3
3
  "package_name": "sneakoscope",
4
- "package_version": "1.21.0",
5
- "source_digest": "666db83626c95b8a8eae20389e647a2db96c4e22802cdf18ce0f7c142c9dbcaa",
6
- "source_file_count": 1750,
7
- "built_at_source_time": 1780297266324
4
+ "package_version": "1.21.2",
5
+ "source_digest": "c2de07d5136a6b0feccf10d8b647ffc2fcbc322e738bafd6cea43efdd3da1225",
6
+ "source_file_count": 1752,
7
+ "built_at_source_time": 1780308857966
8
8
  }
package/dist/bin/sks.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const FAST_PACKAGE_VERSION = '1.21.0';
2
+ const FAST_PACKAGE_VERSION = '1.21.2';
3
3
  const args = process.argv.slice(2);
4
4
  try {
5
5
  if (args[0] === '--agent' && args[1] === 'worker') {
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "schema": "sks.dist-build.v2",
3
- "version": "1.21.0",
4
- "package_version": "1.21.0",
3
+ "version": "1.21.2",
4
+ "package_version": "1.21.2",
5
5
  "typescript": true,
6
6
  "mjs_runtime_files": 0,
7
- "compiled_file_count": 1020,
8
- "compiled_js_count": 510,
9
- "compiled_dts_count": 510,
10
- "source_digest": "666db83626c95b8a8eae20389e647a2db96c4e22802cdf18ce0f7c142c9dbcaa",
11
- "source_file_count": 1750,
12
- "source_files_hash": "de93b15a72352d00481b958333ed964bf083563809e3beddfe4e64046720df75",
13
- "source_list_hash": "de93b15a72352d00481b958333ed964bf083563809e3beddfe4e64046720df75",
7
+ "compiled_file_count": 1022,
8
+ "compiled_js_count": 511,
9
+ "compiled_dts_count": 511,
10
+ "source_digest": "c2de07d5136a6b0feccf10d8b647ffc2fcbc322e738bafd6cea43efdd3da1225",
11
+ "source_file_count": 1752,
12
+ "source_files_hash": "6b8e7de7bcab2c6ddd83d31e15d7219e9d3f595813a1ec8173936208d3c877ef",
13
+ "source_list_hash": "6b8e7de7bcab2c6ddd83d31e15d7219e9d3f595813a1ec8173936208d3c877ef",
14
14
  "src_mjs_runtime_files": 0,
15
15
  "dist_stamp_schema": "sks.dist-build-stamp.v1",
16
16
  "files": [
@@ -1021,6 +1021,8 @@
1021
1021
  "core/work-order-ledger.js",
1022
1022
  "core/zellij/zellij-capability.d.ts",
1023
1023
  "core/zellij/zellij-capability.js",
1024
+ "core/zellij/zellij-clipboard-config.d.ts",
1025
+ "core/zellij/zellij-clipboard-config.js",
1024
1026
  "core/zellij/zellij-command.d.ts",
1025
1027
  "core/zellij/zellij-command.js",
1026
1028
  "core/zellij/zellij-lane-renderer.d.ts",
@@ -1525,21 +1525,18 @@ function normalizeCodexFastModeUiConfigOnce(text = '') {
1525
1525
  next = upsertTomlTableKeyIfAbsent(next, 'user.fast_mode', 'visible = true');
1526
1526
  next = upsertTomlTableKeyIfAbsent(next, 'user.fast_mode', 'enabled = true');
1527
1527
  next = upsertTomlTableKeyIfAbsent(next, 'user.fast_mode', 'default_profile = "sks-fast-high"');
1528
+ // Keep ONLY the sks-fast-high config-profile table: the Codex App fast-mode
1529
+ // (`[user.fast_mode] default_profile = "sks-fast-high"`) and the
1530
+ // codex-app:ui-preservation gate still expect it. The other SKS config profiles are
1531
+ // no longer written as `[profiles.sks-*]` tables here (Codex 0.134+ deprecates them);
1532
+ // they are managed as per-file `<name>.config.toml` overlays by
1533
+ // migrateSksProfilesToPerFile (src/core/auto-review.ts), which also writes the
1534
+ // sks-fast-high overlay for CLI `--profile` use.
1528
1535
  next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'model = "gpt-5.5"');
1529
1536
  next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'service_tier = "fast"');
1530
1537
  next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'approval_policy = "on-request"');
1531
1538
  next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'sandbox_mode = "workspace-write"');
1532
1539
  next = upsertTomlTableKey(next, 'profiles.sks-fast-high', 'model_reasoning_effort = "high"');
1533
- next = upsertTomlTableKey(next, 'profiles.sks-research-xhigh', 'model = "gpt-5.5"');
1534
- next = upsertTomlTableKey(next, 'profiles.sks-research-xhigh', 'service_tier = "fast"');
1535
- next = upsertTomlTableKey(next, 'profiles.sks-research-xhigh', 'approval_policy = "on-request"');
1536
- next = upsertTomlTableKey(next, 'profiles.sks-research-xhigh', 'sandbox_mode = "workspace-write"');
1537
- next = upsertTomlTableKey(next, 'profiles.sks-research-xhigh', 'model_reasoning_effort = "xhigh"');
1538
- next = upsertTomlTableKey(next, 'profiles.sks-research', 'model = "gpt-5.5"');
1539
- next = upsertTomlTableKey(next, 'profiles.sks-research', 'service_tier = "fast"');
1540
- next = upsertTomlTableKey(next, 'profiles.sks-research', 'approval_policy = "never"');
1541
- next = upsertTomlTableKey(next, 'profiles.sks-research', 'sandbox_mode = "workspace-write"');
1542
- next = upsertTomlTableKey(next, 'profiles.sks-research', 'model_reasoning_effort = "xhigh"');
1543
1540
  // Plugin auto-enable is OPT-IN only. Force-writing `[plugins."name@marketplace"] enabled =
1544
1541
  // true` for marketplace plugins the App may not have installed (different build/channel)
1545
1542
  // makes the App reference plugins it cannot load -> broken/blocked plugin UI. It also
@@ -27,6 +27,8 @@ export declare function run(_command: any, args?: any): Promise<void | {
27
27
  attach_requested: boolean;
28
28
  zellij_socket_dir: string | null;
29
29
  zellij_socket_dir_source: import("../core/zellij/zellij-command.js").ZellijSocketDirSource;
30
+ clipboard_config_path: string;
31
+ clipboard_copy_command: string;
30
32
  pane_proof_path: string;
31
33
  pane_proof: {
32
34
  schema: string;
@@ -119,6 +121,7 @@ export declare function run(_command: any, args?: any): Promise<void | {
119
121
  };
120
122
  moved_keys: string[];
121
123
  moved_tables: string[];
124
+ removed_legacy_profiles: string[];
122
125
  deprecated_approval_policy_fixed: boolean;
123
126
  actions: string[];
124
127
  parse_smoke: {
@@ -74,6 +74,16 @@ export declare function enableAutoReview(opts?: any): Promise<{
74
74
  dynamic_effort: "parent assigns high effort to safety/integrator lanes and medium or higher to verification lanes when proof risk is present";
75
75
  }>;
76
76
  }>;
77
+ export declare const SKS_CONFIG_PROFILES: Array<{
78
+ name: string;
79
+ stripTable: boolean;
80
+ block: string;
81
+ }>;
82
+ export declare function migrateSksProfilesToPerFile(opts?: any): Promise<{
83
+ config_path: any;
84
+ profiles_written: string[];
85
+ tables_stripped: string[];
86
+ }>;
77
87
  export declare function enableMadHighProfile(opts?: any): Promise<{
78
88
  config_path: any;
79
89
  profile_config_path: string;
@@ -92,8 +92,63 @@ export async function enableAutoReview(opts = {}) {
92
92
  launch_args: ['--profile', autoReviewProfileName({ high })]
93
93
  };
94
94
  }
95
+ // Canonical registry of every SKS config profile. Codex 0.134+ deprecated the
96
+ // `[profiles.*]` tables / top-level `profile=` selector (warns at startup) in favor of
97
+ // per-file `$CODEX_HOME/<name>.config.toml` overlays loaded by `--profile <name>`.
98
+ // `stripTable: true` => remove the legacy `[profiles.<name>]` table from the home
99
+ // config during migration. sks-fast-high keeps its table because the Codex App
100
+ // fast-mode (`[user.fast_mode] default_profile = "sks-fast-high"`) and the
101
+ // codex-app:ui-preservation gate still expect it; its per-file overlay is also written
102
+ // so CLI `--profile sks-fast-high` works too.
103
+ export const SKS_CONFIG_PROFILES = [
104
+ { name: 'sks-task-low', stripTable: true, block: sksProfileFileBlock({ effort: 'low' }) },
105
+ { name: 'sks-task-medium', stripTable: true, block: sksProfileFileBlock({ effort: 'medium' }) },
106
+ { name: 'sks-logic-high', stripTable: true, block: sksProfileFileBlock({ effort: 'high' }) },
107
+ { name: 'sks-fast-high', stripTable: false, block: sksProfileFileBlock({ effort: 'high', serviceTier: 'fast' }) },
108
+ { name: 'sks-research-xhigh', stripTable: true, block: sksProfileFileBlock({ effort: 'xhigh' }) },
109
+ { name: 'sks-research', stripTable: true, block: sksProfileFileBlock({ effort: 'xhigh', approvalPolicy: 'never' }) },
110
+ { name: 'sks-team', stripTable: true, block: sksProfileFileBlock({ effort: 'medium' }) },
111
+ { name: MAD_HIGH_PROFILE, stripTable: true, block: sksProfileFileBlock({ effort: 'high', approvalPolicy: 'never', sandboxMode: 'danger-full-access', reviewer: AUTO_REVIEW_REVIEWER }) },
112
+ { name: 'sks-default', stripTable: true, block: sksProfileFileBlock({ effort: 'high' }) }
113
+ ];
114
+ function sksProfileFileBlock(opts = {}) {
115
+ return [
116
+ 'model = "gpt-5.5"',
117
+ `service_tier = "${opts.serviceTier || 'fast'}"`,
118
+ `approval_policy = "${opts.approvalPolicy || 'on-request'}"`,
119
+ ...(opts.reviewer ? [`approvals_reviewer = "${opts.reviewer}"`] : []),
120
+ `sandbox_mode = "${opts.sandboxMode || 'workspace-write'}"`,
121
+ `model_reasoning_effort = "${opts.effort || 'medium'}"`
122
+ ].join('\n');
123
+ }
124
+ // Migrate every SKS config profile to a per-file `<name>.config.toml` overlay in
125
+ // CODEX_HOME and strip the deprecated legacy `[profiles.sks-*]` tables / `profile=`
126
+ // selectors from the home config. Idempotent (second run is a no-op). This is the
127
+ // step that clears the Codex deprecation warning on `sks --mad`.
128
+ export async function migrateSksProfilesToPerFile(opts = {}) {
129
+ const configPath = opts.configPath || codexConfigPath(opts.env || process.env);
130
+ await ensureDir(path.dirname(configPath));
131
+ const current = await readText(configPath, '');
132
+ let next = String(current || '');
133
+ for (const profile of SKS_CONFIG_PROFILES) {
134
+ if (profile.stripTable)
135
+ next = removeLegacyProfileConfig(next, profile.name);
136
+ }
137
+ if (next && !next.endsWith('\n'))
138
+ next += '\n';
139
+ if (next !== String(current || ''))
140
+ await writeTextAtomic(configPath, next);
141
+ for (const profile of SKS_CONFIG_PROFILES)
142
+ await writeProfileConfig(configPath, profile.name, profile.block);
143
+ return {
144
+ config_path: configPath,
145
+ profiles_written: SKS_CONFIG_PROFILES.map((profile) => profile.name),
146
+ tables_stripped: SKS_CONFIG_PROFILES.filter((profile) => profile.stripTable).map((profile) => profile.name)
147
+ };
148
+ }
95
149
  export async function enableMadHighProfile(opts = {}) {
96
150
  const configPath = opts.configPath || codexConfigPath(opts.env || process.env);
151
+ const env = opts.env || process.env;
97
152
  await ensureDir(path.dirname(configPath));
98
153
  const current = await readText(configPath, '');
99
154
  let next = removeLegacyProfileConfig(current, MAD_HIGH_PROFILE);
@@ -101,6 +156,9 @@ export async function enableMadHighProfile(opts = {}) {
101
156
  if (!next.endsWith('\n'))
102
157
  next += '\n';
103
158
  await writeTextAtomic(configPath, next);
159
+ // Convert all SKS profiles to per-file overlays and strip the deprecated tables /
160
+ // selectors so Codex stops warning about the legacy config profile on launch.
161
+ await migrateSksProfilesToPerFile({ configPath, env });
104
162
  await writeProfileConfig(configPath, MAD_HIGH_PROFILE, profileConfigBlock({
105
163
  effort: 'high',
106
164
  approvalPolicy: 'never',
@@ -40,6 +40,7 @@ export declare function repairCodexConfigEperm(rootInput?: string, opts?: any):
40
40
  };
41
41
  moved_keys: string[];
42
42
  moved_tables: string[];
43
+ removed_legacy_profiles: string[];
43
44
  deprecated_approval_policy_fixed: boolean;
44
45
  actions: string[];
45
46
  parse_smoke: {
@@ -32,6 +32,7 @@ export declare function splitCodexProjectConfigPolicy(rootInput?: string, opts?:
32
32
  };
33
33
  moved_keys: string[];
34
34
  moved_tables: string[];
35
+ removed_legacy_profiles: string[];
35
36
  deprecated_approval_policy_fixed: boolean;
36
37
  actions: string[];
37
38
  parse_smoke: {
@@ -4,8 +4,6 @@ import path from 'node:path';
4
4
  import { ensureDir, nowIso, readText, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
5
5
  export const CODEX_PROJECT_CONFIG_POLICY_SCHEMA = 'sks.codex-project-config-policy.v1';
6
6
  const MACHINE_LOCAL_TOP_LEVEL_KEYS = new Set([
7
- 'profile',
8
- 'profiles',
9
7
  'model_provider',
10
8
  'model_providers',
11
9
  'openai_base_url',
@@ -20,13 +18,23 @@ const MACHINE_LOCAL_TOP_LEVEL_KEYS = new Set([
20
18
  'telemetry'
21
19
  ]);
22
20
  const MACHINE_LOCAL_TABLE_PREFIXES = [
23
- 'profiles',
24
21
  'model_providers',
25
22
  'notify',
26
23
  'otel',
27
24
  'telemetry',
28
25
  'experimental_telemetry'
29
26
  ];
27
+ // Codex 0.134+ removed the legacy config-profile consumers: `--profile NAME` now
28
+ // layers `$CODEX_HOME/<name>.config.toml` over the base config and the top-level
29
+ // `profile = "..."` selector / `[profiles.*]` tables are deprecated and warned about
30
+ // at startup. So these are NO LONGER machine-local-and-moved-to-home — they are
31
+ // DROPPED from the project config entirely. The per-file profiles are owned by
32
+ // migrateSksProfilesToPerFile (src/core/auto-review.ts), which runs on `sks --mad`.
33
+ const DEPRECATED_LEGACY_PROFILE_TOP_LEVEL_KEYS = new Set(['profile', 'profiles']);
34
+ const DEPRECATED_LEGACY_PROFILE_TABLE_PREFIXES = ['profiles'];
35
+ function isDeprecatedLegacyProfileTable(table) {
36
+ return DEPRECATED_LEGACY_PROFILE_TABLE_PREFIXES.some((prefix) => table === prefix || table.startsWith(`${prefix}.`));
37
+ }
30
38
  export async function splitCodexProjectConfigPolicy(rootInput = process.cwd(), opts = {}) {
31
39
  const root = path.resolve(rootInput || process.cwd());
32
40
  const configPath = path.resolve(opts.configPath || path.join(root, '.codex', 'config.toml'));
@@ -124,6 +132,7 @@ export async function splitCodexProjectConfigPolicy(rootInput = process.cwd(), o
124
132
  },
125
133
  moved_keys: split.moved_keys,
126
134
  moved_tables: split.moved_tables,
135
+ removed_legacy_profiles: split.removed_legacy_profiles,
127
136
  deprecated_approval_policy_fixed: split.deprecated_approval_policy_fixed,
128
137
  actions,
129
138
  parse_smoke: parseSmoke,
@@ -250,10 +259,17 @@ function splitProjectToml(text) {
250
259
  const machineBlocks = [];
251
260
  const movedKeys = [];
252
261
  const movedTables = [];
262
+ const removedLegacyProfiles = [];
253
263
  const blockers = [];
254
264
  let profileName = null;
255
265
  let deprecatedFixed = false;
256
266
  for (const block of blocks) {
267
+ // Deprecated legacy config profiles are DROPPED, not moved to the home config
268
+ // (Codex 0.134+ warns about `[profiles.*]` tables and the `profile=` selector).
269
+ if (block.table && isDeprecatedLegacyProfileTable(block.table)) {
270
+ removedLegacyProfiles.push(block.table);
271
+ continue;
272
+ }
257
273
  if (block.table && isMachineLocalTable(block.table)) {
258
274
  if (block.array) {
259
275
  kept.push(block.text);
@@ -270,11 +286,15 @@ function splitProjectToml(text) {
270
286
  const moveLines = [];
271
287
  for (const line of block.text.split('\n')) {
272
288
  const key = topLevelKey(line);
289
+ if (key && DEPRECATED_LEGACY_PROFILE_TOP_LEVEL_KEYS.has(key)) {
290
+ if (key === 'profile')
291
+ profileName = tomlStringValue(line);
292
+ removedLegacyProfiles.push(`top_level:${key}`);
293
+ continue;
294
+ }
273
295
  if (key && MACHINE_LOCAL_TOP_LEVEL_KEYS.has(key)) {
274
296
  moveLines.push(line);
275
297
  movedKeys.push(key);
276
- if (key === 'profile')
277
- profileName = tomlStringValue(line);
278
298
  continue;
279
299
  }
280
300
  const fixed = fixDeprecatedApprovalPolicy(line);
@@ -302,6 +322,7 @@ function splitProjectToml(text) {
302
322
  machine_blocks: machineBlocks,
303
323
  moved_keys: [...new Set(movedKeys)],
304
324
  moved_tables: [...new Set(movedTables)],
325
+ removed_legacy_profiles: [...new Set(removedLegacyProfiles)],
305
326
  kept_keys: [],
306
327
  profile_name: profileName,
307
328
  deprecated_approval_policy_fixed: deprecatedFixed,
@@ -27,6 +27,8 @@ export declare function madHighCommand(args?: any, deps?: any): Promise<void | {
27
27
  attach_requested: boolean;
28
28
  zellij_socket_dir: string | null;
29
29
  zellij_socket_dir_source: import("../zellij/zellij-command.js").ZellijSocketDirSource;
30
+ clipboard_config_path: string;
31
+ clipboard_copy_command: string;
30
32
  pane_proof_path: string;
31
33
  pane_proof: {
32
34
  schema: string;
@@ -119,6 +121,7 @@ export declare function madHighCommand(args?: any, deps?: any): Promise<void | {
119
121
  };
120
122
  moved_keys: string[];
121
123
  moved_tables: string[];
124
+ removed_legacy_profiles: string[];
122
125
  deprecated_approval_policy_fixed: boolean;
123
126
  actions: string[];
124
127
  parse_smoke: {
@@ -57,7 +57,12 @@ export async function madHighCommand(args = [], deps = {}) {
57
57
  const launchRoot = process.cwd();
58
58
  if (!(await exists(path.join(launchRoot, '.sneakoscope'))))
59
59
  await initProject(launchRoot, {});
60
- const launchPreflight = await runCodexLaunchPreflight(launchRoot, { fix: true, profile: profile.profile_name, sandbox: 'danger-full-access', serviceTier: 'fast' });
60
+ // launchFast skips the redundant live-`codex exec` config probe (up to ~20s, run
61
+ // up to 3x via repair re-inspections): the real codex profile is exercised moments
62
+ // later when the Zellij session opens. All filesystem/permission/EPERM/symlink/ACL
63
+ // readability + repair checks still run. SKS_LAUNCH_FULL_CODEX_PROBE=1 restores the
64
+ // old behavior.
65
+ const launchPreflight = await runCodexLaunchPreflight(launchRoot, { fix: true, launchFast: process.env.SKS_LAUNCH_FULL_CODEX_PROBE !== '1', profile: profile.profile_name, sandbox: 'danger-full-access', serviceTier: 'fast' });
61
66
  if (!launchPreflight.ok) {
62
67
  console.error('SKS MAD launch blocked by config preflight.');
63
68
  for (const blocker of launchPreflight.blockers || [])
@@ -88,7 +93,7 @@ export async function madHighCommand(args = [], deps = {}) {
88
93
  // instead of leaving them to copy/paste the attach command by hand.
89
94
  if (shouldAutoAttachZellij(args)) {
90
95
  console.log(`Opening Zellij session: ${launch.session_name} (detach with Ctrl+q, re-attach later with: ${launch.attach_command_with_env})`);
91
- const attached = attachZellijSessionInteractive(launch.session_name, { cwd: process.cwd() });
96
+ const attached = attachZellijSessionInteractive(launch.session_name, { cwd: process.cwd(), configPath: launch.clipboard_config_path });
92
97
  if (!attached.ok) {
93
98
  console.log(`Could not open the Zellij session automatically${attached.error ? ` (${attached.error})` : ''}.`);
94
99
  if (launch.attach_command_with_env)
@@ -148,7 +153,12 @@ async function activateMadZellijPermissionState(cwd = process.cwd(), args = [])
148
153
  const dbWriteAllowed = has('db_write');
149
154
  const { id, dir } = await createMission(root, { mode: 'mad-sks', prompt: 'sks --mad Zellij scoped high-power maintenance session' });
150
155
  const protectedCore = resolveProtectedCore({ packageRoot: packageRoot(), targetRoot: cwd });
151
- const protectedCoreBefore = await snapshotProtectedCore(packageRoot(), 'mad-live-before');
156
+ // The interactive launch 'before' snapshot is only persisted (env + policy json)
157
+ // and is never compared against an 'after' snapshot during the session, so the
158
+ // strong full-content hash is wasted here. Use the cheap metadata digest (no file
159
+ // reads) on the launch hot path. run/apply and the release gates still take their
160
+ // own strong content snapshots where the digest is actually compared.
161
+ const protectedCoreBefore = await snapshotProtectedCore(packageRoot(), 'mad-live-before', { mode: 'metadata' });
152
162
  const protectedCorePolicyPath = path.join(dir, 'mad-sks-protected-core-policy.json');
153
163
  const protectedCoreBeforePath = path.join(dir, 'mad-sks-live-protected-core-before.json');
154
164
  await writeJsonAtomic(protectedCorePolicyPath, {
@@ -1,4 +1,4 @@
1
- export declare const PACKAGE_VERSION = "1.21.0";
1
+ export declare const PACKAGE_VERSION = "1.21.2";
2
2
  export declare const DEFAULT_PROCESS_TAIL_BYTES: number;
3
3
  export declare const DEFAULT_PROCESS_TIMEOUT_MS: number;
4
4
  export interface RunProcessOptions {
package/dist/core/fsx.js CHANGED
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import crypto from 'node:crypto';
6
6
  import { spawn } from 'node:child_process';
7
7
  import { fileURLToPath } from 'node:url';
8
- export const PACKAGE_VERSION = '1.21.0';
8
+ export const PACKAGE_VERSION = '1.21.2';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
  export function nowIso() {
package/dist/core/init.js CHANGED
@@ -803,19 +803,16 @@ export async function initProject(root, opts = {}) {
803
803
  { table: 'agents.implementation_worker', text: agentConfigBlock('implementation_worker', 'SKS bounded implementation worker.', './agents/implementation-worker.toml', ['Builder', 'Mason']) },
804
804
  { table: 'agents.db_safety_reviewer', text: agentConfigBlock('db_safety_reviewer', 'Read-only DB safety reviewer.', './agents/db-safety-reviewer.toml', ['Sentinel', 'Ledger']) },
805
805
  { table: 'agents.qa_reviewer', text: agentConfigBlock('qa_reviewer', 'Read-only QA reviewer.', './agents/qa-reviewer.toml', ['Verifier', 'Reviewer']) },
806
- { table: 'profiles.sks-task-low', text: profileConfigBlock('sks-task-low', 'low') },
807
- { table: 'profiles.sks-task-medium', text: profileConfigBlock('sks-task-medium', 'medium') },
808
- { table: 'profiles.sks-logic-high', text: profileConfigBlock('sks-logic-high', 'high') },
809
- { table: 'profiles.sks-fast-high', text: profileConfigBlock('sks-fast-high', 'high', { serviceTier: 'fast' }) },
810
- { table: 'profiles.sks-research-xhigh', text: profileConfigBlock('sks-research-xhigh', 'xhigh') },
811
- { table: 'profiles.sks-research', text: profileConfigBlock('sks-research', 'xhigh', { approval: 'never' }) },
812
- { table: 'profiles.sks-team', text: profileConfigBlock('sks-team', 'medium') },
813
- { table: 'profiles.sks-mad-high', text: profileConfigBlock('sks-mad-high', 'high', { approval: 'never', sandbox: 'danger-full-access', approvalsReviewer: 'auto_review' }) },
806
+ // NOTE: SKS config profiles are NO LONGER emitted as `[profiles.sks-*]` tables.
807
+ // Codex 0.134+ deprecated config-profile tables / the `profile=` selector (warns at
808
+ // startup) in favor of per-file `$CODEX_HOME/<name>.config.toml` overlays loaded by
809
+ // `--profile <name>`. Those per-file profiles are owned by migrateSksProfilesToPerFile
810
+ // (src/core/auto-review.ts), invoked on `sks --mad`. Emitting the tables here only got
811
+ // them relocated into the home config by the splitter, re-triggering the warning.
814
812
  {
815
813
  table: 'auto_review',
816
814
  text: '[auto_review]\npolicy = "In MAD launches, allow live-server work, normal DB writes, Supabase MCP DB writes, direct execute SQL, schema cleanup, and migration application for the active invocation. Deny only catastrophic database wipes, all-row value deletion/update, dangerous project or branch management, credential exfiltration, persistent security weakening, broad unrelated file deletion, and unrequested fallback implementation code."'
817
- },
818
- { table: 'profiles.sks-default', text: profileConfigBlock('sks-default', 'high') }
815
+ }
819
816
  ];
820
817
  }
821
818
  function agentConfigBlock(table, description, configFile, nicknames = []) {
@@ -826,17 +823,6 @@ export async function initProject(root, opts = {}) {
826
823
  `nickname_candidates = [${nicknames.map((name) => `"${name}"`).join(', ')}]`
827
824
  ].join('\n');
828
825
  }
829
- function profileConfigBlock(profile, effort, opts = {}) {
830
- return [
831
- `[profiles.${profile}]`,
832
- 'model = "gpt-5.5"',
833
- `service_tier = "${opts.serviceTier || 'fast'}"`,
834
- `approval_policy = "${opts.approval || 'on-request'}"`,
835
- ...(opts.approvalsReviewer ? [`approvals_reviewer = "${opts.approvalsReviewer}"`] : []),
836
- `sandbox_mode = "${opts.sandbox || 'workspace-write'}"`,
837
- `model_reasoning_effort = "${effort}"`
838
- ].join('\n');
839
- }
840
826
  function upsertTomlTableKey(text, table, line) {
841
827
  const key = (String(line).split('=')[0] || '').trim();
842
828
  let lines = String(text || '').split('\n');
@@ -83,9 +83,13 @@ export declare function evaluateMadSksWrite({ packageRoot: packageRootInput, tar
83
83
  };
84
84
  wrongness_kind: string | null;
85
85
  }>;
86
- export declare function snapshotProtectedCore(root?: string, label?: string): Promise<{
86
+ export interface SnapshotProtectedCoreOptions {
87
+ mode?: 'content' | 'metadata';
88
+ }
89
+ export declare function snapshotProtectedCore(root?: string, label?: string, opts?: SnapshotProtectedCoreOptions): Promise<{
87
90
  schema: string;
88
91
  label: string;
92
+ digest_mode: string;
89
93
  generated_at: string;
90
94
  package_root: string;
91
95
  engine_source_exception: boolean;
@@ -116,6 +120,7 @@ export declare function buildProtectedCoreSnapshot({ packageRoot: packageRootInp
116
120
  }): Promise<{
117
121
  schema: string;
118
122
  label: string;
123
+ digest_mode: string;
119
124
  generated_at: string;
120
125
  package_root: string;
121
126
  engine_source_exception: boolean;
@@ -122,16 +122,18 @@ export async function evaluateMadSksWrite({ packageRoot: packageRootInput = pack
122
122
  wrongness_kind: decision.wrongness_kind
123
123
  };
124
124
  }
125
- export async function snapshotProtectedCore(root = packageRoot(), label = 'snapshot') {
125
+ export async function snapshotProtectedCore(root = packageRoot(), label = 'snapshot', opts = {}) {
126
+ const mode = opts.mode === 'metadata' ? 'metadata' : 'content';
126
127
  const resolution = resolveProtectedCore(root);
127
128
  const entries = [];
128
129
  for (const entry of resolution.protected_paths) {
129
- entries.push(await hashProtectedEntry(entry));
130
+ entries.push(mode === 'metadata' ? await metadataHashProtectedEntry(entry) : await hashProtectedEntry(entry));
130
131
  }
131
132
  const digest = sha256(entries.map((entry) => `${entry.id}:${entry.hash || 'missing'}:${entry.file_count}:${entry.bytes}`).join('\n'));
132
133
  return {
133
134
  schema: MAD_SKS_PROTECTED_CORE_SNAPSHOT_SCHEMA,
134
135
  label,
136
+ digest_mode: mode,
135
137
  generated_at: nowIso(),
136
138
  package_root: resolution.package_root,
137
139
  engine_source_exception: resolution.engine_source_exception,
@@ -186,6 +188,35 @@ async function hashProtectedEntry(entry) {
186
188
  }
187
189
  return { id: entry.id, path: entry.path, relative_path: entry.path, present: true, hash: sha256(parts.join('\n')), file_count: files.length, bytes };
188
190
  }
191
+ // Cheap variant of hashProtectedEntry: hash only filesystem metadata
192
+ // (mtimeMs + size + mode) instead of reading + sha256-ing file contents. Walks the
193
+ // same file set so file_count/bytes stay comparable, but avoids the ~10MB read +
194
+ // per-file sha256 cost. Only used for the never-compared interactive launch 'before'
195
+ // snapshot; integrity-critical callers keep the default 'content' mode.
196
+ async function metadataHashProtectedEntry(entry) {
197
+ if (!(await exists(entry.absolute_path))) {
198
+ return { id: entry.id, path: entry.path, relative_path: entry.path, present: false, hash: null, file_count: 0, bytes: 0 };
199
+ }
200
+ const stat = await fsp.lstat(entry.absolute_path);
201
+ if (stat.isFile()) {
202
+ return { id: entry.id, path: entry.path, relative_path: entry.path, present: true, hash: sha256(`${stat.mtimeMs}:${stat.size}:${stat.mode}`), file_count: 1, bytes: stat.size };
203
+ }
204
+ if (!stat.isDirectory()) {
205
+ return { id: entry.id, path: entry.path, relative_path: entry.path, present: true, hash: sha256(`${stat.mode}:${stat.size}`), file_count: 1, bytes: stat.size };
206
+ }
207
+ if (entry.match === 'exact') {
208
+ return { id: entry.id, path: entry.path, relative_path: entry.path, present: true, hash: sha256(`dir:${stat.mode}:${stat.uid}:${stat.gid}`), file_count: 1, bytes: 0 };
209
+ }
210
+ const files = await walk(entry.absolute_path);
211
+ let bytes = 0;
212
+ const parts = [];
213
+ for (const file of files.sort()) {
214
+ const st = await fsp.lstat(file);
215
+ bytes += st.size;
216
+ parts.push(`${path.relative(entry.absolute_path, file).split(path.sep).join('/')}:${st.mtimeMs}:${st.size}:${st.mode}`);
217
+ }
218
+ return { id: entry.id, path: entry.path, relative_path: entry.path, present: true, hash: sha256(parts.join('\n')), file_count: files.length, bytes };
219
+ }
189
220
  async function walk(dir, out = []) {
190
221
  const entries = await fsp.readdir(dir, { withFileTypes: true }).catch(() => []);
191
222
  for (const entry of entries) {
@@ -78,6 +78,7 @@ export declare function runCodexLaunchPreflight(rootInput?: string, opts?: any):
78
78
  };
79
79
  moved_keys: string[];
80
80
  moved_tables: string[];
81
+ removed_legacy_profiles: string[];
81
82
  deprecated_approval_policy_fixed: boolean;
82
83
  actions: string[];
83
84
  parse_smoke: {
@@ -29,12 +29,19 @@ export async function runParallelPreflight(checks) {
29
29
  export async function runCodexLaunchPreflight(rootInput = process.cwd(), opts = {}) {
30
30
  const root = path.resolve(rootInput || process.cwd());
31
31
  const reportPath = opts.reportPath || path.join(root, '.sneakoscope', 'reports', 'mad-launch-preflight.json');
32
+ // On the interactive launch path the real codex profile is exercised the moment the
33
+ // Zellij session opens, so spawning `codex exec` here (up to ~20s, and again inside
34
+ // the repair re-inspections) is redundant. launchFast skips ONLY the live-codex probe;
35
+ // all filesystem/permission/symlink/ACL/EPERM readability + repair checks still run, so
36
+ // the EPERM/tcc_possible/EACCES blockers still fire for unreadable configs
37
+ // (codex_cli_config_eperm is probe-only and intentionally not exercised on this path).
38
+ const probeCodex = opts.launchFast === true ? false : opts.actualCodex !== false;
32
39
  const readonly = await runParallelPreflight([
33
- { id: 'codex_config_readability', run: () => inspectCodexConfigReadability(root, { ...opts, codexProbe: true, actualCodex: opts.actualCodex !== false, writeReport: false }) },
40
+ { id: 'codex_config_readability', run: () => inspectCodexConfigReadability(root, { ...opts, codexProbe: probeCodex, actualCodex: probeCodex, writeReport: false }) },
34
41
  { id: 'codex_project_config_policy', run: () => splitCodexProjectConfigPolicy(root, { ...opts, writeReport: false }) }
35
42
  ]);
36
43
  const repair = opts.fix === true || readonly.ok === false
37
- ? await repairCodexConfigEperm(root, { ...opts, codexProbe: true, actualCodex: opts.actualCodex !== false, fix: opts.fix !== false, writeReport: false })
44
+ ? await repairCodexConfigEperm(root, { ...opts, codexProbe: probeCodex, actualCodex: probeCodex, fix: opts.fix !== false, writeReport: false })
38
45
  : null;
39
46
  const zellijCapability = opts.zellijCapability === false
40
47
  ? null
@@ -1,2 +1,2 @@
1
- export declare const PACKAGE_VERSION = "1.21.0";
1
+ export declare const PACKAGE_VERSION = "1.21.2";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -1,2 +1,2 @@
1
- export const PACKAGE_VERSION = '1.21.0';
1
+ export const PACKAGE_VERSION = '1.21.2';
2
2
  //# sourceMappingURL=version.js.map
@@ -0,0 +1,21 @@
1
+ export declare const ZELLIJ_CLIPBOARD_CONFIG_SCHEMA = "sks.zellij-clipboard-config.v1";
2
+ export interface ZellijClipboardConfig {
3
+ copy_command: string;
4
+ copy_clipboard: 'system' | 'primary';
5
+ copy_on_select: boolean;
6
+ /** Flags to append to the `zellij ... options` subcommand for the created session. */
7
+ optionFlags: string[];
8
+ /** Path to a dedicated config.kdl for the interactive attach (delivered via ZELLIJ_CONFIG_FILE). */
9
+ config_path: string;
10
+ generated_at: string;
11
+ }
12
+ /** Pick the platform-correct clipboard command. macOS is the primary target. */
13
+ export declare function resolveCopyCommand(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): string;
14
+ /** Render a minimal, valid Zellij config.kdl that enables clipboard copy. */
15
+ export declare function buildZellijClipboardKdl(cfg: {
16
+ copy_command: string;
17
+ copy_clipboard: 'system' | 'primary';
18
+ copy_on_select: boolean;
19
+ }): string;
20
+ export declare function writeZellijClipboardConfig(root: string, missionId: string, platform?: NodeJS.Platform): Promise<ZellijClipboardConfig>;
21
+ //# sourceMappingURL=zellij-clipboard-config.d.ts.map
@@ -0,0 +1,55 @@
1
+ import path from 'node:path';
2
+ import { ensureDir, nowIso, writeTextAtomic } from '../fsx.js';
3
+ // Single source of truth for the Zellij clipboard pipeline used by `sks --mad`
4
+ // (and any other SKS-launched Zellij session). By default Zellij copies via the
5
+ // OSC 52 escape sequence, which several macOS terminals (Terminal.app always,
6
+ // some iTerm2 configs) silently drop — so text selected inside a pane never
7
+ // reaches the system clipboard. Setting copy_command="pbcopy" pipes selections
8
+ // straight to the macOS clipboard, and copy_on_select=true makes a mouse drag
9
+ // copy without entering copy mode. (Shift+drag still falls back to native
10
+ // terminal selection regardless.)
11
+ export const ZELLIJ_CLIPBOARD_CONFIG_SCHEMA = 'sks.zellij-clipboard-config.v1';
12
+ /** Pick the platform-correct clipboard command. macOS is the primary target. */
13
+ export function resolveCopyCommand(platform = process.platform, env = process.env) {
14
+ if (platform === 'darwin')
15
+ return 'pbcopy';
16
+ // Wayland sessions prefer wl-copy; fall back to xclip on X11. pbcopy is darwin-only.
17
+ if (env.WAYLAND_DISPLAY)
18
+ return 'wl-copy';
19
+ return 'xclip -selection clipboard';
20
+ }
21
+ function kdlString(value) {
22
+ return JSON.stringify(String(value || ''));
23
+ }
24
+ /** Render a minimal, valid Zellij config.kdl that enables clipboard copy. */
25
+ export function buildZellijClipboardKdl(cfg) {
26
+ return [
27
+ '// Generated by Sneakoscope (sks) so launched Zellij sessions copy to the OS clipboard.',
28
+ `copy_command ${kdlString(cfg.copy_command)}`,
29
+ `copy_clipboard ${kdlString(cfg.copy_clipboard)}`,
30
+ `copy_on_select ${cfg.copy_on_select ? 'true' : 'false'}`,
31
+ ''
32
+ ].join('\n');
33
+ }
34
+ export async function writeZellijClipboardConfig(root, missionId, platform = process.platform) {
35
+ const copy_command = resolveCopyCommand(platform);
36
+ const copy_clipboard = 'system';
37
+ const copy_on_select = true;
38
+ const dir = path.join(root, '.sneakoscope', 'missions', missionId);
39
+ await ensureDir(dir);
40
+ const config_path = path.join(dir, 'zellij-clipboard.kdl');
41
+ await writeTextAtomic(config_path, buildZellijClipboardKdl({ copy_command, copy_clipboard, copy_on_select }));
42
+ return {
43
+ copy_command,
44
+ copy_clipboard,
45
+ copy_on_select,
46
+ // Appended AFTER `--default-layout <path>` in the create command. Zellij treats
47
+ // --copy-command as mutually exclusive with --copy-clipboard (OSC52 target), so
48
+ // only pass the command and copy-on-select flags on the CLI. The generated KDL
49
+ // still records copy_clipboard for config-file consumers that use OSC52.
50
+ optionFlags: ['--copy-command', copy_command, '--copy-on-select', String(copy_on_select)],
51
+ config_path,
52
+ generated_at: nowIso()
53
+ };
54
+ }
55
+ //# sourceMappingURL=zellij-clipboard-config.js.map
@@ -44,6 +44,8 @@ export declare function launchZellijLayout(opts?: ZellijLaunchOptions): Promise<
44
44
  attach_requested: boolean;
45
45
  zellij_socket_dir: string | null;
46
46
  zellij_socket_dir_source: import("./zellij-command.js").ZellijSocketDirSource;
47
+ clipboard_config_path: string;
48
+ clipboard_copy_command: string;
47
49
  pane_proof_path: string;
48
50
  pane_proof: {
49
51
  schema: string;
@@ -105,6 +107,8 @@ export declare function launchMadZellijUi(args?: readonly unknown[], opts?: Zell
105
107
  attach_requested: boolean;
106
108
  zellij_socket_dir: string | null;
107
109
  zellij_socket_dir_source: import("./zellij-command.js").ZellijSocketDirSource;
110
+ clipboard_config_path: string;
111
+ clipboard_copy_command: string;
108
112
  pane_proof_path: string;
109
113
  pane_proof: {
110
114
  schema: string;
@@ -166,6 +170,8 @@ export declare function launchTeamZellijView(opts?: ZellijLaunchOptions): Promis
166
170
  attach_requested: boolean;
167
171
  zellij_socket_dir: string | null;
168
172
  zellij_socket_dir_source: import("./zellij-command.js").ZellijSocketDirSource;
173
+ clipboard_config_path: string;
174
+ clipboard_copy_command: string;
169
175
  pane_proof_path: string;
170
176
  pane_proof: {
171
177
  schema: string;
@@ -217,6 +223,7 @@ export interface ZellijAttachResult {
217
223
  */
218
224
  export declare function attachZellijSessionInteractive(sessionName: string, opts?: {
219
225
  cwd?: string;
226
+ configPath?: string;
220
227
  }): ZellijAttachResult;
221
228
  export declare function sanitizeZellijSessionName(value: unknown): string;
222
229
  //# sourceMappingURL=zellij-launcher.d.ts.map
@@ -4,6 +4,7 @@ import { appendJsonl, nowIso, sha256, writeJsonAtomic } from '../fsx.js';
4
4
  import { checkZellijCapability } from './zellij-capability.js';
5
5
  import { formatZellijCommand, resolveZellijProcessEnvMeta, runZellij } from './zellij-command.js';
6
6
  import { writeZellijLayout } from './zellij-layout-builder.js';
7
+ import { writeZellijClipboardConfig } from './zellij-clipboard-config.js';
7
8
  import { writeZellijPaneProof } from './zellij-pane-proof.js';
8
9
  export const ZELLIJ_SESSION_SCHEMA = 'sks.zellij-session.v1';
9
10
  export const ZELLIJ_SESSION_NAME_MAX = 64;
@@ -27,7 +28,13 @@ export async function launchZellijLayout(opts = {}) {
27
28
  layoutInput.codexBin = opts.codexBin;
28
29
  const layout = await writeZellijLayout(root, layoutInput);
29
30
  const capability = await checkZellijCapability({ root, require: opts.requireZellij === true });
30
- const createCommand = ['attach', '--create-background', sessionName, 'options', '--default-layout', layout.layout_path];
31
+ // Configure the clipboard pipeline so selections inside the session reach the OS
32
+ // clipboard (Zellij's default OSC-52 copy is dropped by Terminal.app etc.). The
33
+ // copy option flags are appended AFTER `--default-layout <path>` so the launch
34
+ // command prefix ['zellij','attach','--create-background',session,'options','--default-layout',...]
35
+ // is preserved (required by the zellij launch-command-truth gate + E2E assertions).
36
+ const clipboard = await writeZellijClipboardConfig(root, missionId);
37
+ const createCommand = ['attach', '--create-background', sessionName, 'options', '--default-layout', layout.layout_path, ...clipboard.optionFlags];
31
38
  const attachCommand = ['attach', sessionName];
32
39
  const zellijEnv = resolveZellijProcessEnvMeta();
33
40
  const launch = opts.dryRun === true || capability.status !== 'ok'
@@ -86,6 +93,8 @@ export async function launchZellijLayout(opts = {}) {
86
93
  attach_requested: opts.attach === true,
87
94
  zellij_socket_dir: zellijEnv.zellij_socket_dir,
88
95
  zellij_socket_dir_source: zellijEnv.zellij_socket_dir_source,
96
+ clipboard_config_path: clipboard.config_path,
97
+ clipboard_copy_command: clipboard.copy_command,
89
98
  pane_proof_path: path.join(root, '.sneakoscope', 'missions', missionId, 'zellij-pane-proof.json'),
90
99
  pane_proof: paneProof,
91
100
  dry_run: opts.dryRun === true,
@@ -146,6 +155,13 @@ export function attachZellijSessionInteractive(sessionName, opts = {}) {
146
155
  const env = { ...process.env };
147
156
  if (meta.zellij_socket_dir && !env.ZELLIJ_SOCKET_DIR)
148
157
  env.ZELLIJ_SOCKET_DIR = meta.zellij_socket_dir;
158
+ // Steer the foreground attach at our generated clipboard config so the interactive
159
+ // session honors copy_command=pbcopy + copy_on_select. The `options` subcommand only
160
+ // configures the *created* background session, so the attach needs its own config
161
+ // delivery; ZELLIJ_CONFIG_FILE avoids reordering CLI args. Defer to a user-exported
162
+ // ZELLIJ_CONFIG_FILE if they already set one.
163
+ if (opts.configPath && !env.ZELLIJ_CONFIG_FILE)
164
+ env.ZELLIJ_CONFIG_FILE = opts.configPath;
149
165
  try {
150
166
  const result = spawnSync('zellij', ['attach', sessionName], {
151
167
  cwd: opts.cwd || process.cwd(),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "1.21.0",
4
+ "version": "1.21.2",
5
5
  "description": "Sneakoscope Codex: fast proof-first Codex trust layer with image-based Voxel TriWiki.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",