sneakoscope 1.18.13 → 1.18.14

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.18.13** is the Zellij-only interactive runtime release: actual Codex CLI config-load truth, fake Codex EPERM fixtures, doctor readiness matrix proof, `sks mad repair-config`, safer project-local config splitting, Codex 0.135 compatibility gates, and official Fast mode CLI override proof are wired into release checks. Doctor now refuses Ready yes without Codex config-load evidence, MAD blocks launch before unreadable config can crash Codex, and interactive MAD/lane UI requires Zellij with no removed-runtime fallback.
19
+ SKS **1.18.14** is the Zellij-only interactive runtime release: actual Codex CLI config-load truth, fake Codex EPERM fixtures, doctor readiness matrix proof, `sks mad repair-config`, safer project-local config splitting, Codex 0.135 compatibility gates, and official Fast mode CLI override proof are wired into release checks. Doctor now refuses Ready yes without Codex config-load evidence, MAD blocks launch before unreadable config can crash Codex, and interactive MAD/lane UI requires Zellij with no removed-runtime fallback.
20
20
 
21
21
  ```bash
22
22
  sks mad-sks plan --target-root <path> --json
@@ -699,7 +699,7 @@ npm run release:check
699
699
  npm run publish:dry
700
700
  ```
701
701
 
702
- `release:check` runs the 1.18.13 Zellij-only closure DAG, writes a source digest stamp under `.sneakoscope/reports/`, then refreshes release readiness so publish commands can verify the same stamp. The DAG preserves the 1.18 baseline gates and adds patch swarm runtime truth, transaction journaling, serial conflict rebase, strict strategy-to-patch proof, rollback command proof, Native CLI Session Swarm 5/10/20-process proof, Real Worker Backend Router proof, Codex child overlap proof, model-authored patch-envelope separation, Zellij layout/pane/screen proof, no-subagent-scaling proof, Fast mode default/worker/Codex/MAD propagation proof, Appshots attachment provenance, MCP runtime overlap evidence, Codex 0.134/0.135 runner truth, task graph expansion, schema-bound follow-up work, actual Agent/Team/Research/QA route blackboxes, scheduler proof hardening, Source Intelligence propagation, and Goal mode propagation checks. Broader live gates remain explicit scripts such as `release:real-check`; real Codex patch smoke, real Codex parallel worker proof, and real Zellij proof are optional unless their `SKS_REQUIRE_REAL_*` or `SKS_REQUIRE_ZELLIJ=1` environment variables are set. Generate the human-readable registry with `sks features inventory --write-docs`. Plain `npm publish` uses the `latest` dist-tag. npm's `prepublishOnly` verifies the fresh release stamp instead of rerunning the full gate, and `prepack` only rebuilds `dist`; publish no longer repeats the expensive release suite during packaging. `npm run publish:dry` remains the explicit dry-run helper.
702
+ `release:check` runs the 1.18.14 Zellij-only closure DAG, writes a source digest stamp under `.sneakoscope/reports/`, then refreshes release readiness so publish commands can verify the same stamp. The DAG preserves the 1.18 baseline gates and adds patch swarm runtime truth, transaction journaling, serial conflict rebase, strict strategy-to-patch proof, rollback command proof, Native CLI Session Swarm 5/10/20-process proof, Real Worker Backend Router proof, Codex child overlap proof, model-authored patch-envelope separation, Zellij layout/pane/screen proof, no-subagent-scaling proof, Fast mode default/worker/Codex/MAD propagation proof, Appshots attachment provenance, MCP runtime overlap evidence, Codex 0.134/0.135 runner truth, task graph expansion, schema-bound follow-up work, actual Agent/Team/Research/QA route blackboxes, scheduler proof hardening, Source Intelligence propagation, and Goal mode propagation checks. Broader live gates remain explicit scripts such as `release:real-check`; real Codex patch smoke, real Codex parallel worker proof, and real Zellij proof are optional unless their `SKS_REQUIRE_REAL_*` or `SKS_REQUIRE_ZELLIJ=1` environment variables are set. Generate the human-readable registry with `sks features inventory --write-docs`. Plain `npm publish` uses the `latest` dist-tag. npm's `prepublishOnly` verifies the fresh release stamp instead of rerunning the full gate, and `prepack` only rebuilds `dist`; publish no longer repeats the expensive release suite during packaging. `npm run publish:dry` remains the explicit dry-run helper.
703
703
 
704
704
  Version bumps are manual. Run `sks versioning bump` only when preparing release metadata; SKS will not create `.git/hooks/pre-commit` or auto-bump during ordinary commits.
705
705
 
@@ -76,7 +76,7 @@ dependencies = [
76
76
 
77
77
  [[package]]
78
78
  name = "sks-core"
79
- version = "1.18.13"
79
+ version = "1.18.14"
80
80
  dependencies = [
81
81
  "serde_json",
82
82
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "1.18.13"
3
+ version = "1.18.14"
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.18.13"),
7
+ Some("--version") => println!("sks-rs 1.18.14"),
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.18.13",
5
- "source_digest": "6474b08fceec48bfd4915e09945f17e3ac1ccb4c9e01918d6a533f1a3d6bb30e",
6
- "source_file_count": 1677,
7
- "built_at_source_time": 1780041475580
4
+ "package_version": "1.18.14",
5
+ "source_digest": "b13a96880fa5b57ba4b3649ac5bedbcca941d871f92f9a4bf396ab20fd9c8b4b",
6
+ "source_file_count": 1679,
7
+ "built_at_source_time": 1780051095178
8
8
  }
package/dist/bin/sks.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const FAST_PACKAGE_VERSION = '1.18.13';
2
+ const FAST_PACKAGE_VERSION = '1.18.14';
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.18.13",
4
- "package_version": "1.18.13",
3
+ "version": "1.18.14",
4
+ "package_version": "1.18.14",
5
5
  "typescript": true,
6
- "mjs_runtime_files": 0,
6
+ "mjs_runtime_files": 1,
7
7
  "compiled_file_count": 972,
8
8
  "compiled_js_count": 486,
9
9
  "compiled_dts_count": 486,
10
- "source_digest": "6474b08fceec48bfd4915e09945f17e3ac1ccb4c9e01918d6a533f1a3d6bb30e",
11
- "source_file_count": 1677,
12
- "source_files_hash": "fb8f5ab51d8a8c9809fa26ee6688932a083f83741916a19c2b331dad0d4b69e7",
13
- "source_list_hash": "fb8f5ab51d8a8c9809fa26ee6688932a083f83741916a19c2b331dad0d4b69e7",
10
+ "source_digest": "b13a96880fa5b57ba4b3649ac5bedbcca941d871f92f9a4bf396ab20fd9c8b4b",
11
+ "source_file_count": 1679,
12
+ "source_files_hash": "737317372a19030a70f432942ce650900c05270a2a1d1c49281d135a1b15e66c",
13
+ "source_list_hash": "737317372a19030a70f432942ce650900c05270a2a1d1c49281d135a1b15e66c",
14
14
  "src_mjs_runtime_files": 0,
15
15
  "dist_stamp_schema": "sks.dist-build-stamp.v1",
16
16
  "files": [
@@ -985,6 +985,7 @@
985
985
  "core/zellij/zellij-pane-proof.js",
986
986
  "core/zellij/zellij-screen-proof.d.ts",
987
987
  "core/zellij/zellij-screen-proof.js",
988
+ "scripts/codex-config-load-probe.mjs",
988
989
  "scripts/release-parallel-check.d.ts",
989
990
  "scripts/release-parallel-check.js",
990
991
  "vendor/openai-codex/latest/hooks/permission-request.command.input.schema.json",
@@ -113,6 +113,7 @@ export declare function run(_command: any, args?: any): Promise<void | {
113
113
  };
114
114
  blockers: string[];
115
115
  };
116
+ structure_repairs: any[];
116
117
  repair_actions: any[];
117
118
  after: import("../core/codex/codex-config-readability.js").CodexConfigReadabilityReport;
118
119
  tcc_risk: boolean;
@@ -49,6 +49,7 @@ export declare function repairCodexConfigEperm(rootInput?: string, opts?: any):
49
49
  };
50
50
  blockers: string[];
51
51
  };
52
+ structure_repairs: any[];
52
53
  repair_actions: any[];
53
54
  after: import("./codex-config-readability.js").CodexConfigReadabilityReport;
54
55
  tcc_risk: boolean;
@@ -1,15 +1,29 @@
1
1
  import fsp from 'node:fs/promises';
2
+ import os from 'node:os';
2
3
  import path from 'node:path';
3
4
  import { nowIso, readText, runProcess, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
4
5
  import { inspectCodexConfigReadability } from './codex-config-readability.js';
5
- import { splitCodexProjectConfigPolicy } from './codex-project-config-policy.js';
6
+ import { repairCodexConfigStructure, splitCodexProjectConfigPolicy } from './codex-project-config-policy.js';
6
7
  export const CODEX_CONFIG_EPERM_REPAIR_SCHEMA = 'sks.codex-config-eperm-repair.v1';
7
8
  export async function repairCodexConfigEperm(rootInput = process.cwd(), opts = {}) {
8
9
  const root = path.resolve(rootInput || process.cwd());
9
10
  const reportPath = opts.reportPath || path.join(root, '.sneakoscope', 'reports', 'codex-config-eperm-repair.json');
10
11
  const configPath = path.resolve(opts.configPath || path.join(root, '.codex', 'config.toml'));
12
+ const codexHome = path.resolve(opts.codexHome || process.env.CODEX_HOME || path.join(process.env.HOME || os.homedir(), '.codex'));
13
+ const codexHomeConfigPath = path.join(codexHome, 'config.toml');
11
14
  const before = await inspectCodexConfigReadability(root, { ...opts, configPath, writeReport: false });
12
- const policy = await splitCodexProjectConfigPolicy(root, { ...opts, configPath, apply: opts.fix === true, writeReport: false });
15
+ // Structural recovery FIRST: hoist machine-local keys that a prior buggy move
16
+ // absorbed into a table back to the root, on both the project config and the
17
+ // global CODEX_HOME config (the file Codex actually loads). Runs before the
18
+ // splitter so recovered keys can then be migrated cleanly.
19
+ const structureRepairs = [];
20
+ if (opts.fix === true) {
21
+ structureRepairs.push({ scope: 'project', ...(await repairCodexConfigStructure(configPath, { apply: true })) });
22
+ if (path.resolve(codexHomeConfigPath) !== path.resolve(configPath)) {
23
+ structureRepairs.push({ scope: 'codex_home', ...(await repairCodexConfigStructure(codexHomeConfigPath, { apply: true })) });
24
+ }
25
+ }
26
+ const policy = await splitCodexProjectConfigPolicy(root, { ...opts, configPath, codexHome, apply: opts.fix === true, writeReport: false });
13
27
  const repairActions = opts.fix === true ? await runScopedRepairs(configPath, before.blockers) : [];
14
28
  const after = await inspectCodexConfigReadability(root, { ...opts, configPath, writeReport: false });
15
29
  const blockers = [...new Set([...(policy.blockers || []), ...after.blockers])];
@@ -25,6 +39,7 @@ export async function repairCodexConfigEperm(rootInput = process.cwd(), opts = {
25
39
  fix: opts.fix === true,
26
40
  before,
27
41
  policy,
42
+ structure_repairs: structureRepairs,
28
43
  repair_actions: repairActions,
29
44
  after,
30
45
  tcc_risk: tccRisk(root),
@@ -32,6 +47,9 @@ export async function repairCodexConfigEperm(rootInput = process.cwd(), opts = {
32
47
  blockers,
33
48
  operator_actions: [
34
49
  ...(after.operator_actions || []),
50
+ ...structureRepairs
51
+ .filter((repair) => repair.applied && repair.hoisted_keys?.length)
52
+ .map((repair) => `Recovered misplaced machine-local keys (${repair.hoisted_keys.join(', ')}) back to the top of ${repair.scope === 'codex_home' ? 'CODEX_HOME' : 'project'} config; backup at ${repair.backup_path}.`),
35
53
  ...(tccProbable ? ['macOS probable TCC block: grant Full Disk Access and Files and Folders permissions to Warp/Terminal/iTerm, Codex app, and the Codex CLI launch context, then rerun `sks mad repair-config --apply`.'] : [])
36
54
  ]
37
55
  };
@@ -175,6 +175,8 @@ function operatorActions(blockers) {
175
175
  const actions = new Set();
176
176
  if (blockers.some((item) => /^missing_/.test(item)))
177
177
  actions.add('Run `sks doctor --fix` to regenerate the managed Codex project config, then rerun the preflight.');
178
+ if (blockers.includes('codex_cli_config_toml_parse_error'))
179
+ actions.add('Run `sks doctor --fix` (or `sks mad repair-config --apply`) to hoist misplaced machine-local keys back to the top of the Codex config and restore a loadable config.toml.');
178
180
  if (blockers.includes('codex_cli_config_eperm'))
179
181
  actions.add('Run `sks mad repair-config --apply`; if it still fails on macOS, grant Full Disk Access/Files and Folders access to the launching terminal, Warp, iTerm, Terminal, Codex app, or Codex CLI context.');
180
182
  if (blockers.includes('EPERM') || blockers.includes('tcc_possible'))
@@ -194,6 +196,24 @@ function operatorActions(blockers) {
194
196
  function errorDetail(err) {
195
197
  return { name: err?.name || 'Error', code: err?.code || '', message: err?.message || String(err) };
196
198
  }
199
+ function resolveCodexConfigLoadProbe() {
200
+ // Prefer the packaged copy under dist/scripts (shipped via the `dist` files
201
+ // allowlist); fall back to the repo-root scripts/ copy for local development.
202
+ const candidates = [
203
+ path.join(packageRoot(), 'dist', 'scripts', 'codex-config-load-probe.mjs'),
204
+ path.join(packageRoot(), 'scripts', 'codex-config-load-probe.mjs')
205
+ ];
206
+ for (const candidate of candidates) {
207
+ try {
208
+ if (fs.existsSync(candidate))
209
+ return candidate;
210
+ }
211
+ catch {
212
+ // ignore and try the next candidate
213
+ }
214
+ }
215
+ return null;
216
+ }
197
217
  async function codexCliConfigLoadCheck(root, configPath, opts = {}) {
198
218
  if (!opts.codexProbe && !opts.actualCodex && !opts.codexBin) {
199
219
  return {
@@ -203,7 +223,17 @@ async function codexCliConfigLoadCheck(root, configPath, opts = {}) {
203
223
  detail: { integration_optional: true, blockers: [] }
204
224
  };
205
225
  }
206
- const script = path.join(packageRoot(), 'scripts', 'codex-config-load-probe.mjs');
226
+ const script = resolveCodexConfigLoadProbe();
227
+ if (!script) {
228
+ // The probe ships in dist/scripts; if it is genuinely absent (packaging gap),
229
+ // do not block MAD preflight on an unverifiable check — degrade gracefully.
230
+ return {
231
+ name: 'actual_codex_cli_config_load',
232
+ ok: true,
233
+ status: 'integration_optional_probe_missing',
234
+ detail: { integration_optional: true, blockers: [] }
235
+ };
236
+ }
207
237
  const args = [script, '--root', root, '--config', configPath, '--json'];
208
238
  if (opts.actualCodex !== false)
209
239
  args.push('--actual-codex');
@@ -41,4 +41,27 @@ export declare function splitCodexProjectConfigPolicy(rootInput?: string, opts?:
41
41
  };
42
42
  blockers: string[];
43
43
  }>;
44
+ export declare function repairCodexConfigStructure(configPathInput: string, opts?: any): Promise<{
45
+ config_path: string;
46
+ ok: boolean;
47
+ status: string;
48
+ changed: boolean;
49
+ applied: boolean;
50
+ hoisted_keys: never[];
51
+ backup_path: null;
52
+ parse_smoke?: never;
53
+ } | {
54
+ config_path: string;
55
+ ok: boolean;
56
+ status: string;
57
+ changed: boolean;
58
+ applied: boolean;
59
+ hoisted_keys: string[];
60
+ backup_path: string | null;
61
+ parse_smoke: {
62
+ ok: boolean;
63
+ unterminated_multiline_string: boolean;
64
+ invalid_table_header: string | null;
65
+ };
66
+ }>;
44
67
  //# sourceMappingURL=codex-project-config-policy.d.ts.map
@@ -32,6 +32,30 @@ export async function splitCodexProjectConfigPolicy(rootInput = process.cwd(), o
32
32
  const configPath = path.resolve(opts.configPath || path.join(root, '.codex', 'config.toml'));
33
33
  const codexHome = path.resolve(opts.codexHome || process.env.CODEX_HOME || path.join(process.env.HOME || os.homedir(), '.codex'));
34
34
  const reportPath = opts.reportPath || path.join(root, '.sneakoscope', 'reports', 'codex-project-config-policy.json');
35
+ const codexHomeConfigPath = path.join(codexHome, 'config.toml');
36
+ // Guard: never split the global Codex home config against itself. When `sks`
37
+ // runs from (or near) the home directory the project config can resolve to the
38
+ // same file as the move target. Splitting it would strip machine-local keys and
39
+ // re-append them, corrupting the file. Treat this as a no-op.
40
+ if (await isSameFile(configPath, codexHomeConfigPath)) {
41
+ const report = {
42
+ schema: CODEX_PROJECT_CONFIG_POLICY_SCHEMA,
43
+ generated_at: nowIso(),
44
+ root,
45
+ config_path: configPath,
46
+ codex_home: codexHome,
47
+ ok: true,
48
+ status: 'project_config_is_codex_home_noop',
49
+ changed: false,
50
+ moved_keys: [],
51
+ moved_tables: [],
52
+ actions: [],
53
+ blockers: []
54
+ };
55
+ if (opts.writeReport !== false)
56
+ await writeJsonAtomic(reportPath, { ...report, report_path: reportPath });
57
+ return report;
58
+ }
35
59
  const original = await readText(configPath, null);
36
60
  if (original === null) {
37
61
  const report = {
@@ -70,16 +94,12 @@ export async function splitCodexProjectConfigPolicy(rootInput = process.cwd(), o
70
94
  }
71
95
  if (opts.apply && split.machine_text.trim()) {
72
96
  await ensureDir(codexHome);
73
- userConfigPath = path.join(codexHome, 'config.toml');
97
+ userConfigPath = codexHomeConfigPath;
74
98
  const currentUser = await readText(userConfigPath, '');
75
99
  const dedupedUser = removeConfigIds(String(currentUser || ''), configIds(split.machine_text));
76
- const movedBlock = [
77
- '',
78
- `# SKS moved machine-local Codex config from ${path.relative(root, configPath) || configPath} at ${nowIso()}`,
79
- split.machine_text.trim(),
80
- ''
81
- ].join('\n');
82
- await writeTextAtomic(userConfigPath, `${dedupedUser.replace(/\s+$/, '')}${movedBlock}`.replace(/^\n+/, ''));
100
+ const commentLine = `# SKS moved machine-local Codex config from ${path.relative(root, configPath) || configPath} at ${nowIso()}`;
101
+ const mergedUser = mergeMachineLocalIntoUserConfig(dedupedUser, split.machine_text.trim(), commentLine);
102
+ await writeTextAtomic(userConfigPath, mergedUser);
83
103
  actions.push('machine_local_keys_moved_to_codex_home_config');
84
104
  }
85
105
  if (profileName)
@@ -113,6 +133,116 @@ export async function splitCodexProjectConfigPolicy(rootInput = process.cwd(), o
113
133
  await writeJsonAtomic(reportPath, { ...report, report_path: reportPath });
114
134
  return report;
115
135
  }
136
+ // Recovery pass for already-corrupted configs. The pre-fix mover appended
137
+ // machine-local top-level keys after the last [table], so TOML absorbed them
138
+ // into that table (e.g. `notify`/`model_provider` landing inside
139
+ // `[mcp_servers.*.env]`, which Codex rejects with
140
+ // `invalid type: sequence, expected a string`). The splitter cannot recover this
141
+ // because it now sees those lines as table members, not top-level keys. This pass
142
+ // hoists them back above the first table so Codex can load the config again.
143
+ export async function repairCodexConfigStructure(configPathInput, opts = {}) {
144
+ const configPath = path.resolve(configPathInput);
145
+ const original = await readText(configPath, null);
146
+ if (original === null) {
147
+ return { config_path: configPath, ok: true, status: 'config_missing', changed: false, applied: false, hoisted_keys: [], backup_path: null };
148
+ }
149
+ const hoist = hoistMisplacedMachineLocalKeys(String(original));
150
+ let backupPath = null;
151
+ if (opts.apply && hoist.changed) {
152
+ backupPath = `${configPath}.struct-bak-${Date.now().toString(36)}`;
153
+ await ensureDir(path.dirname(configPath));
154
+ await fsp.copyFile(configPath, backupPath);
155
+ await writeTextAtomic(configPath, hoist.text);
156
+ }
157
+ const parseSmoke = tomlRewriteSmoke(hoist.text);
158
+ return {
159
+ config_path: configPath,
160
+ ok: parseSmoke.ok,
161
+ status: hoist.changed ? (opts.apply ? 'structure_repaired' : 'structure_repair_available') : 'structure_ok',
162
+ changed: hoist.changed,
163
+ applied: opts.apply === true && hoist.changed,
164
+ hoisted_keys: hoist.hoisted_keys,
165
+ backup_path: backupPath,
166
+ parse_smoke: parseSmoke
167
+ };
168
+ }
169
+ function hoistMisplacedMachineLocalKeys(text) {
170
+ const blocks = tomlBlocks(text);
171
+ const preamble = [];
172
+ const tables = [];
173
+ const hoisted = [];
174
+ const hoistedKeys = [];
175
+ for (const block of blocks) {
176
+ if (!block.table) {
177
+ preamble.push(block.text);
178
+ continue;
179
+ }
180
+ const lines = block.text.split('\n');
181
+ const header = lines[0];
182
+ // `mcp_servers.*` / `*.env` tables never legitimately contain machine-local
183
+ // Codex root keys; treat keys found there (or anything after an absorbed
184
+ // SKS-moved comment) as misplaced.
185
+ const corruptionProne = /^mcp_servers(\.|$)/.test(block.table) || /\.env$/.test(block.table) || block.table === 'env';
186
+ const kept = [header];
187
+ let sawMovedComment = false;
188
+ let i = 1;
189
+ while (i < lines.length) {
190
+ const line = lines[i];
191
+ if (/^\s*#\s*SKS moved machine-local Codex config/i.test(line)) {
192
+ sawMovedComment = true;
193
+ i += 1;
194
+ continue;
195
+ }
196
+ const key = topLevelKey(line);
197
+ const isMachineKey = Boolean(key) && MACHINE_LOCAL_TOP_LEVEL_KEYS.has(key);
198
+ if (isMachineKey && (corruptionProne || sawMovedComment)) {
199
+ const span = captureAssignmentSpan(lines, i);
200
+ for (const spanned of span.lines)
201
+ hoisted.push(spanned);
202
+ hoistedKeys.push(key);
203
+ i = span.next;
204
+ continue;
205
+ }
206
+ kept.push(line);
207
+ i += 1;
208
+ }
209
+ tables.push(kept.join('\n'));
210
+ }
211
+ if (!hoisted.length)
212
+ return { changed: false, text, hoisted_keys: [] };
213
+ const head = [preamble.join('\n').trim(), hoisted.join('\n').trim()].filter(Boolean).join('\n');
214
+ const sections = [head, ...tables.map((table) => table.trim())].filter(Boolean);
215
+ return { changed: true, text: normalizeProjectText(sections.join('\n\n')), hoisted_keys: [...new Set(hoistedKeys)] };
216
+ }
217
+ // Capture a TOML assignment that may span multiple lines (multiline arrays
218
+ // `[ ... ]` or triple-quoted strings) so hoisting never splits a value.
219
+ function captureAssignmentSpan(lines, start) {
220
+ const first = lines[start] ?? '';
221
+ const collected = [first];
222
+ let next = start + 1;
223
+ let triple = updateMultilineState(first, null);
224
+ let bracketDepth = bracketDelta(first);
225
+ while ((triple || bracketDepth > 0) && next < lines.length) {
226
+ const line = lines[next] ?? '';
227
+ collected.push(line);
228
+ triple = updateMultilineState(line, triple);
229
+ bracketDepth += bracketDelta(line);
230
+ next += 1;
231
+ }
232
+ return { lines: collected, next };
233
+ }
234
+ function bracketDelta(line) {
235
+ const noComment = stripCommentOutsideQuotes(String(line));
236
+ const noStrings = noComment.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, '');
237
+ let delta = 0;
238
+ for (const ch of noStrings) {
239
+ if (ch === '[')
240
+ delta += 1;
241
+ else if (ch === ']')
242
+ delta -= 1;
243
+ }
244
+ return delta;
245
+ }
116
246
  function splitProjectToml(text) {
117
247
  const blocks = tomlBlocks(text);
118
248
  const kept = [];
@@ -269,6 +399,59 @@ function configIds(text) {
269
399
  }
270
400
  return ids;
271
401
  }
402
+ async function isSameFile(a, b) {
403
+ const ra = path.resolve(a);
404
+ const rb = path.resolve(b);
405
+ if (ra === rb)
406
+ return true;
407
+ try {
408
+ const [realA, realB] = await Promise.all([fsp.realpath(ra), fsp.realpath(rb)]);
409
+ return realA === realB;
410
+ }
411
+ catch {
412
+ return false;
413
+ }
414
+ }
415
+ // Merge machine-local Codex config into the user (CODEX_HOME) config while
416
+ // preserving TOML structure: bare top-level keys must appear before any
417
+ // `[table]` header, otherwise they are parsed as members of the preceding
418
+ // table. Appending moved keys blindly at end-of-file corrupted configs
419
+ // (e.g. `notify`/`model_provider` landing inside `[mcp_servers.*.env]`).
420
+ function mergeMachineLocalIntoUserConfig(userText, machineText, commentLine) {
421
+ const preamble = [];
422
+ const tables = [];
423
+ collectTomlSections(userText, preamble, tables);
424
+ const movedPreamble = [];
425
+ const movedTables = [];
426
+ collectTomlSections(machineText, movedPreamble, movedTables);
427
+ const head = [];
428
+ const existingHead = preamble.join('\n').trim();
429
+ if (existingHead)
430
+ head.push(existingHead);
431
+ const movedHead = movedPreamble.join('\n').trim();
432
+ if (commentLine && (movedHead || movedTables.length))
433
+ head.push(commentLine);
434
+ if (movedHead)
435
+ head.push(movedHead);
436
+ const sections = [];
437
+ const headText = head.join('\n').trim();
438
+ if (headText)
439
+ sections.push(headText);
440
+ for (const table of [...tables, ...movedTables]) {
441
+ const trimmed = table.trim();
442
+ if (trimmed)
443
+ sections.push(trimmed);
444
+ }
445
+ return normalizeProjectText(sections.join('\n\n'));
446
+ }
447
+ function collectTomlSections(text, preamble, tables) {
448
+ for (const block of tomlBlocks(text)) {
449
+ if (block.table)
450
+ tables.push(block.text);
451
+ else
452
+ preamble.push(block.text);
453
+ }
454
+ }
272
455
  function removeConfigIds(text, ids) {
273
456
  const kept = [];
274
457
  for (const block of tomlBlocks(text)) {
@@ -113,6 +113,7 @@ export declare function madHighCommand(args?: any, deps?: any): Promise<void | {
113
113
  };
114
114
  blockers: string[];
115
115
  };
116
+ structure_repairs: any[];
116
117
  repair_actions: any[];
117
118
  after: import("../codex/codex-config-readability.js").CodexConfigReadabilityReport;
118
119
  tcc_risk: boolean;
@@ -1,4 +1,4 @@
1
- export declare const PACKAGE_VERSION = "1.18.13";
1
+ export declare const PACKAGE_VERSION = "1.18.14";
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.18.13';
8
+ export const PACKAGE_VERSION = '1.18.14';
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() {
@@ -87,6 +87,7 @@ export declare function runCodexLaunchPreflight(rootInput?: string, opts?: any):
87
87
  };
88
88
  blockers: string[];
89
89
  };
90
+ structure_repairs: any[];
90
91
  repair_actions: any[];
91
92
  after: import("../codex/codex-config-readability.js").CodexConfigReadabilityReport;
92
93
  tcc_risk: boolean;
@@ -1,2 +1,2 @@
1
- export declare const PACKAGE_VERSION = "1.18.13";
1
+ export declare const PACKAGE_VERSION = "1.18.14";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -1,2 +1,2 @@
1
- export const PACKAGE_VERSION = '1.18.13';
1
+ export const PACKAGE_VERSION = '1.18.14';
2
2
  //# sourceMappingURL=version.js.map
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+
6
+ const args = process.argv.slice(2);
7
+ const root = path.resolve(readOption('--root', process.cwd()));
8
+ const configPath = path.resolve(readOption('--config', path.join(root, '.codex', 'config.toml')));
9
+ const json = args.includes('--json');
10
+ const actualRequested = args.includes('--actual-codex')
11
+ || process.env.SKS_TEST_REAL_CODEX_CONFIG_LOAD === '1'
12
+ || Boolean(readOption('--codex-bin', ''));
13
+ const actualRequired = args.includes('--require-actual-codex')
14
+ || process.env.SKS_REQUIRE_REAL_CODEX_CONFIG_LOAD === '1';
15
+ const codexBin = readOption('--codex-bin', process.env.SKS_CODEX_CONFIG_PROBE_BIN || 'codex');
16
+ const outputLastMessage = path.resolve(readOption(
17
+ '--output-last-message',
18
+ path.join(root, '.sneakoscope', 'reports', 'codex-config-load-probe-output.json')
19
+ ));
20
+
21
+ const report = {
22
+ schema: 'sks.codex-config-load-probe.v2',
23
+ generated_at: new Date().toISOString(),
24
+ root,
25
+ config_path: configPath,
26
+ ok: false,
27
+ checks: [],
28
+ blockers: [],
29
+ warnings: [],
30
+ integration_optional: !actualRequired,
31
+ actual_codex_requested: actualRequested,
32
+ actual_codex_required: actualRequired
33
+ };
34
+
35
+ await check('node_read', async () => {
36
+ const text = await fs.readFile(configPath, 'utf8');
37
+ return { bytes: Buffer.byteLength(text) };
38
+ });
39
+
40
+ const child = spawnSync(process.execPath, ['-e', 'require("fs").readFileSync(process.argv[1], "utf8")', configPath], {
41
+ cwd: root,
42
+ encoding: 'utf8'
43
+ });
44
+ pushCheck({
45
+ name: 'spawned_node_child_read',
46
+ ok: child.status === 0,
47
+ exit_code: child.status,
48
+ stdout_tail: tail(child.stdout),
49
+ stderr_tail: tail(child.stderr),
50
+ signals: classifyText(`${child.stderr}\n${child.stdout}`)
51
+ });
52
+
53
+ if (actualRequested) {
54
+ await fs.mkdir(path.dirname(outputLastMessage), { recursive: true }).catch(() => {});
55
+ await fs.rm(outputLastMessage, { force: true }).catch(() => {});
56
+ const codexArgs = [
57
+ 'exec',
58
+ '--skip-git-repo-check',
59
+ '--sandbox',
60
+ 'read-only',
61
+ '--ignore-rules',
62
+ '--output-last-message',
63
+ outputLastMessage,
64
+ 'Reply exactly SKS_CONFIG_LOAD_PROBE_OK.'
65
+ ];
66
+ const command = commandForCodex(codexBin, codexArgs);
67
+ const result = spawnSync(command.command, command.args, {
68
+ cwd: root,
69
+ env: probeEnv(process.env),
70
+ encoding: 'utf8',
71
+ timeout: Number(process.env.SKS_CODEX_CONFIG_LOAD_TIMEOUT_MS || 20000),
72
+ maxBuffer: 1024 * 1024
73
+ });
74
+ const outputLastText = await readTextIfExists(outputLastMessage);
75
+ const observedText = `${result.stderr || ''}\n${result.stdout || ''}\n${outputLastText || ''}`;
76
+ const probeOkObserved = /SKS_CONFIG_LOAD_PROBE_OK/.test(observedText);
77
+ const signals = classifyText(observedText, { configLoaded: probeOkObserved });
78
+ const configFailure = signals.blockers.length > 0;
79
+ const unavailable = result.error?.code === 'ENOENT';
80
+ const timeoutAfterProbeOk = result.error?.code === 'ETIMEDOUT' && probeOkObserved;
81
+ const executionError = Boolean(result.error) && !timeoutAfterProbeOk;
82
+ const nonConfigFailure = (result.status !== 0 || executionError) && !configFailure && !unavailable;
83
+ const passed = (result.status === 0 && !executionError) || (probeOkObserved && !configFailure);
84
+ pushCheck({
85
+ name: 'actual_codex_cli_config_load',
86
+ ok: passed,
87
+ status: passed
88
+ ? timeoutAfterProbeOk
89
+ ? 'passed_after_probe_ok_timeout'
90
+ : 'passed'
91
+ : unavailable
92
+ ? 'integration_optional_unavailable'
93
+ : configFailure
94
+ ? 'failed_config_load'
95
+ : 'non_config_failure_after_config_load',
96
+ integration_optional: !actualRequired && (unavailable || nonConfigFailure),
97
+ exit_code: result.status,
98
+ signal: result.signal,
99
+ error: result.error ? { code: result.error.code, message: result.error.message } : null,
100
+ command: {
101
+ executable: redact(codexBin),
102
+ args: codexArgs.map(redact)
103
+ },
104
+ env_keys: Object.keys(probeEnv(process.env)).sort(),
105
+ stdout_tail: tail(result.stdout),
106
+ stderr_tail: tail(result.stderr),
107
+ output_last_message: outputLastMessage,
108
+ output_last_message_tail: tail(outputLastText),
109
+ probe_ok_observed: probeOkObserved,
110
+ signals
111
+ });
112
+ } else {
113
+ pushCheck({
114
+ name: 'actual_codex_cli_config_load',
115
+ ok: true,
116
+ status: 'integration_optional_not_requested',
117
+ integration_optional: true,
118
+ command: { executable: redact(codexBin), args: [] },
119
+ signals: { blockers: [], flags: {} }
120
+ });
121
+ }
122
+
123
+ report.ok = report.checks.every((check) => {
124
+ if (check.ok) return true;
125
+ return check.integration_optional === true && !actualRequired;
126
+ }) && report.blockers.length === 0;
127
+
128
+ if (json) console.log(JSON.stringify(report, null, 2));
129
+ else {
130
+ console.log(report.ok
131
+ ? `Codex config load probe ok: ${configPath}`
132
+ : `Codex config load probe failed: ${report.blockers.join(', ')}`);
133
+ }
134
+ if (!report.ok) process.exitCode = 1;
135
+
136
+ async function check(name, fn) {
137
+ try {
138
+ pushCheck({ name, ok: true, detail: await fn(), signals: { blockers: [], flags: {} } });
139
+ } catch (err) {
140
+ const signals = classifyText(`${err?.code || ''}\n${err?.message || String(err)}`);
141
+ pushCheck({
142
+ name,
143
+ ok: false,
144
+ error: { code: err?.code || '', message: err?.message || String(err) },
145
+ signals
146
+ });
147
+ }
148
+ }
149
+
150
+ function pushCheck(check) {
151
+ report.checks.push(check);
152
+ for (const warning of check.signals?.warnings || []) {
153
+ if (!report.warnings.includes(warning)) report.warnings.push(warning);
154
+ }
155
+ if (check.ok) return;
156
+ for (const blocker of check.signals?.blockers || classifyText(`${check.stderr_tail || ''}\n${check.error?.message || ''}`).blockers) {
157
+ if (!report.blockers.includes(blocker)) report.blockers.push(blocker);
158
+ }
159
+ if (!check.integration_optional && check.name === 'actual_codex_cli_config_load' && !check.signals?.blockers?.length) {
160
+ if (!report.blockers.includes('codex_cli_config_load_unverified')) report.blockers.push('codex_cli_config_load_unverified');
161
+ }
162
+ }
163
+
164
+ function classifyText(textInput, opts = {}) {
165
+ const text = String(textInput || '');
166
+ const flags = {
167
+ error_loading_config_toml: /Error loading config\.toml/i.test(text),
168
+ operation_not_permitted: /Operation not permitted|os error 1|EPERM/i.test(text),
169
+ permission_denied: /Permission denied|EACCES/i.test(text),
170
+ toml_parse: /TOML parse|toml.*parse|parse.*toml|invalid.*toml|duplicate key/i.test(text),
171
+ // serde/toml deserialize failures from a structurally-broken config, e.g.
172
+ // `invalid type: sequence, expected a string` when `notify` was absorbed into
173
+ // an env table. These carry no literal "toml" token so the patterns above miss them.
174
+ toml_type_error: /invalid type:|invalid value:|expected (a |an )?(string|sequence|array|table|map|integer|boolean|float)|missing field|unknown field|unknown variant/i.test(text),
175
+ untrusted_project: /untrusted project/i.test(text),
176
+ ignored_project_local_config_key: /ignored project-local config key|ignored.*project.*config/i.test(text)
177
+ };
178
+ const blockers = [];
179
+ const warnings = [];
180
+ if (flags.operation_not_permitted || (flags.error_loading_config_toml && /os error 1/i.test(text))) blockers.push('codex_cli_config_eperm');
181
+ else if (flags.permission_denied) blockers.push('codex_cli_config_permission_denied');
182
+ // A config-load failure that is not a permission problem is a parse/structure error.
183
+ const tomlLoadFailure = flags.toml_parse
184
+ || (!opts.configLoaded && flags.toml_type_error)
185
+ || (!opts.configLoaded && flags.error_loading_config_toml && !flags.operation_not_permitted && !flags.permission_denied);
186
+ if (tomlLoadFailure) blockers.push('codex_cli_config_toml_parse_error');
187
+ if (flags.untrusted_project) blockers.push('codex_cli_untrusted_project');
188
+ if (flags.ignored_project_local_config_key) {
189
+ if (opts.configLoaded) warnings.push('codex_cli_ignored_project_local_config_key');
190
+ else blockers.push('codex_cli_ignored_project_local_config_key');
191
+ }
192
+ return { blockers: [...new Set(blockers)], warnings: [...new Set(warnings)], flags };
193
+ }
194
+
195
+ function commandForCodex(bin, codexArgs) {
196
+ if (/\.mjs$/i.test(String(bin))) return { command: process.execPath, args: [bin, ...codexArgs] };
197
+ return { command: bin, args: codexArgs };
198
+ }
199
+
200
+ function probeEnv(env) {
201
+ const keys = [
202
+ 'CODEX_HOME',
203
+ 'SKS_FAST_MODE',
204
+ 'SKS_SERVICE_TIER',
205
+ 'PATH',
206
+ 'HOME',
207
+ 'WARP_SESSION_ID',
208
+ 'TMUX'
209
+ ];
210
+ const out = {};
211
+ for (const key of keys) {
212
+ if (env[key] !== undefined) out[key] = env[key];
213
+ }
214
+ for (const [key, value] of Object.entries(env)) {
215
+ if (/^(CODEX_LB|SKS_CODEX_LB)_/.test(key)) out[key] = value;
216
+ if (/^SKS_FAKE_CODEX_CONFIG_/.test(key)) out[key] = value;
217
+ }
218
+ return out;
219
+ }
220
+
221
+ function redact(value) {
222
+ return String(value || '')
223
+ .replace(/sk-[A-Za-z0-9_-]{8,}/g, '[REDACTED_OPENAI_KEY]')
224
+ .replace(/github_pat_[A-Za-z0-9_]+/g, '[REDACTED_GITHUB_PAT]')
225
+ .replace(/(CODEX_LB_API_KEY=)[^\s]+/g, '$1[REDACTED]')
226
+ .replace(/(OPENAI_API_KEY=)[^\s]+/g, '$1[REDACTED]');
227
+ }
228
+
229
+ function tail(value, limit = 4000) {
230
+ const text = redact(String(value || ''));
231
+ return text.length <= limit ? text : text.slice(-limit);
232
+ }
233
+
234
+ async function readTextIfExists(file) {
235
+ try {
236
+ return await fs.readFile(file, 'utf8');
237
+ } catch {
238
+ return '';
239
+ }
240
+ }
241
+
242
+ function readOption(name, fallback) {
243
+ const index = args.indexOf(name);
244
+ return index >= 0 && args[index + 1] ? args[index + 1] : fallback;
245
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "1.18.13",
4
+ "version": "1.18.14",
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",