sneakoscope 3.1.12 → 3.1.13

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.
Files changed (38) hide show
  1. package/README.md +10 -12
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +1 -1
  5. package/dist/bin/sks.js +1 -1
  6. package/dist/commands/doctor.js +62 -32
  7. package/dist/core/agents/agent-role-config.js +12 -1
  8. package/dist/core/codex/agent-config-file-repair.js +115 -19
  9. package/dist/core/codex/codex-startup-config-postcheck.js +57 -4
  10. package/dist/core/codex-control/codex-0140-capability.js +72 -7
  11. package/dist/core/codex-control/codex-0140-feature-probes.js +174 -16
  12. package/dist/core/codex-control/codex-0140-real-probes.js +43 -3
  13. package/dist/core/codex-control/codex-0140-usage-parser.js +81 -0
  14. package/dist/core/codex-native/native-capability-postcheck.js +5 -2
  15. package/dist/core/codex-native/native-capability-repair-matrix.js +4 -4
  16. package/dist/core/config/secret-preservation.js +107 -10
  17. package/dist/core/doctor/context7-mcp-repair.js +15 -0
  18. package/dist/core/doctor/doctor-repair-postcheck.js +9 -3
  19. package/dist/core/doctor/doctor-transaction.js +98 -2
  20. package/dist/core/doctor/supabase-mcp-repair.js +36 -6
  21. package/dist/core/fsx.js +1 -1
  22. package/dist/core/loops/loop-concurrency-budget.js +22 -0
  23. package/dist/core/mcp/mcp-config-preservation.js +30 -7
  24. package/dist/core/naruto/naruto-loop-mesh.js +5 -1
  25. package/dist/core/version.js +1 -1
  26. package/dist/core/zellij/zellij-fake-adapter.js +8 -2
  27. package/dist/core/zellij/zellij-launcher.js +16 -0
  28. package/dist/scripts/codex-0140-feature-gate-lib.js +4 -2
  29. package/dist/scripts/release-3113-required-gates.js +25 -0
  30. package/package.json +11 -3
  31. package/dist/scripts/loop-directive-check-lib.js +0 -388
  32. package/dist/scripts/loop-hardening-check-lib.js +0 -289
  33. package/dist/scripts/sks-1-12-real-execution-check-lib.js +0 -27
  34. package/dist/scripts/sks-3-1-4-directive-check-lib.js +0 -212
  35. package/dist/scripts/sks-3-1-5-directive-check-lib.js +0 -318
  36. package/dist/scripts/sks-3-1-6-directive-check-lib.js +0 -522
  37. package/dist/scripts/sks-3-1-7-directive-check-lib.js +0 -58
  38. package/dist/scripts/sks-3-1-8-check-lib.js +0 -30
package/README.md CHANGED
@@ -35,18 +35,16 @@ Set up this agent project with Sneakoscope Codex. Use [[mandarange/Sneakoscope-C
35
35
 
36
36
  ## 🚀 Current Release
37
37
 
38
- SKS **3.1.12** is a release-ready repair pass for `sks doctor --fix` production recovery, Codex 0.140 capability coverage, and MAD Zellij right-column stack reliability.
39
-
40
- What changed in 3.1.12:
41
-
42
- - **MAD Zellij panes are stack-reconciled.** Second and later visible workers still launch with native `new-pane --stacked`, then SKS calls Zellij `stack-panes` with the observed worker pane ids so the right column stays one stack instead of drifting into automatic split geometry.
43
- - **Codex 0.140 readiness is gated.** The release records hermetic coverage for usage metadata, goal attachment preservation, session delete/import, unified mentions, Bedrock managed auth, MCP reliability, SQLite recovery, non-TTY interrupt behavior, large-repo performance, and optional real-probe enforcement.
44
- - **Doctor production repair is transactional.** `sks doctor --fix` now writes a doctor fix transaction and postcheck report so startup config, Context7 MCP, Supabase MCP, command alias, and native capability repair results are visible in JSON output instead of disappearing into console-only repair steps.
45
- - **Context7 stdio lockups are doctor-repairable.** `sks doctor --fix` detects local `@upstash/context7-mcp` stdio config and migrates it to the remote Context7 MCP endpoint so Codex launches do not stall at the stdio server banner.
46
- - **Codex startup warnings are repaired more completely.** `sks doctor --fix` rewrites stale SKS agent `config_file` paths, removes unsupported managed `message_role_prefix` role fields, preserves optional `supabase_sauron`, and now repairs `node_repl` to a valid Codex App command when available or removes both the stale parent table and child env table when it is not.
47
- - **Doctor JSON exposes the Context7, startup, Supabase, and production postcheck reports.** `context7_repair`, `codex_startup_repair`, `startup_config_repair`, `context7_mcp_repair`, `supabase_mcp_repair`, `doctor_fix_transaction`, `doctor_fix_postcheck`, and their `repair.*` entries carry migration status, backups, actions, warnings, and any manual auth actions.
48
- - **Secret rollback is stricter.** The secret-preservation guard treats protected-value changes the same way as missing values, rolls back affected files from redacted backups, and records rollback status without writing raw secret values.
49
- - **Release metadata is aligned for 3.1.12.** Package, lockfile, CLI version constants, Rust helper metadata, README, and changelog all point at the same release.
38
+ SKS **3.1.13** is a production-hardening release for Codex 0.140 evidence, transactional `sks doctor --fix` repair, MCP readiness, native capability proof, and protected-secret rollback.
39
+
40
+ What changed in 3.1.13:
41
+
42
+ - **Codex 0.140 readiness carries evidence.** Capability reports now expose per-feature state and certainty, real usage parsing, goal attachment roundtrip proof, and usage-budget provenance for loop/Naruto runtime decisions.
43
+ - **Doctor repair is phase-based.** `sks doctor --fix` records phase durations, postchecks, optional manual readiness, and rollback evidence instead of collapsing repair work into a summary writer.
44
+ - **Startup and MCP repair are safer.** Managed agent TOML blocks are repaired without touching unrelated config, missing role files are regenerated from real managed templates, Context7 disabled servers stay disabled, and Supabase write scope is separated from read-only readiness.
45
+ - **Secret rollback is line-level when possible.** Protected key changes are restored without discarding unrelated operator edits, nested guard operations are recorded, and backup artifacts remain ignored.
46
+ - **Native capability proof is stricter.** Computer Use and Chrome/web review no longer become verified from environment variables outside explicit fixture/test modes.
47
+ - **Release metadata is aligned for 3.1.13.** Package, lockfile, CLI version constants, Rust helper metadata, README, and changelog all point at the same release.
50
48
 
51
49
  SKS 3.0.0 was the parallel-runtime stabilization release. The whole live-swarm experience — what you actually *see* while 5, 20, or 100 workers run — was rebuilt and proven end-to-end.
52
50
 
@@ -76,7 +76,7 @@ dependencies = [
76
76
 
77
77
  [[package]]
78
78
  name = "sks-core"
79
- version = "3.1.12"
79
+ version = "3.1.13"
80
80
  dependencies = [
81
81
  "serde_json",
82
82
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "3.1.12"
3
+ version = "3.1.13"
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 3.1.12"),
7
+ Some("--version") => println!("sks-rs 3.1.13"),
8
8
  Some("compact-info") => {
9
9
  let mut input = String::new();
10
10
  let _ = io::stdin().read_to_string(&mut input);
package/dist/bin/sks.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const FAST_PACKAGE_VERSION = '3.1.12';
2
+ const FAST_PACKAGE_VERSION = '3.1.13';
3
3
  const args = process.argv.slice(2);
4
4
  try {
5
5
  if (args[0] === '--agent' && args[1] === 'worker') {
@@ -34,7 +34,7 @@ import { runDoctorCommandAliasCleanup } from '../core/doctor/command-alias-clean
34
34
  import { repairCodexStartupConfig } from '../core/doctor/codex-startup-config-repair.js';
35
35
  import { repairContext7Mcp } from '../core/doctor/context7-mcp-repair.js';
36
36
  import { repairSupabaseMcp } from '../core/doctor/supabase-mcp-repair.js';
37
- import { writeDoctorFixTransaction } from '../core/doctor/doctor-transaction.js';
37
+ import { runDoctorFixTransaction } from '../core/doctor/doctor-transaction.js';
38
38
  import { doctorRepairPostcheck } from '../core/doctor/doctor-repair-postcheck.js';
39
39
  import { withSecretPreservationGuard } from '../core/config/config-migration-journal.js';
40
40
  export async function run(_command, args = []) {
@@ -271,8 +271,12 @@ async function runDoctor(args = [], root, doctorFix) {
271
271
  apply: true,
272
272
  configured: false,
273
273
  disabled: false,
274
+ disabled_preserved: false,
274
275
  token_env_present: false,
275
276
  unsafe_write_access: false,
277
+ read_only_migrated: false,
278
+ write_scope_requires_confirmation: false,
279
+ ready_blocking: true,
276
280
  manual_required: true,
277
281
  next_action: 'Review Supabase MCP configuration manually.',
278
282
  blockers: [err?.message || String(err)],
@@ -281,62 +285,88 @@ async function runDoctor(args = [], root, doctorFix) {
281
285
  }))
282
286
  : null;
283
287
  const doctorFixTransaction = doctorFix
284
- ? await writeDoctorFixTransaction({
288
+ ? await runDoctorFixTransaction({
285
289
  root,
286
290
  phases: [
287
291
  {
288
292
  id: 'setup',
289
- ok: setupRepair !== null,
290
- repaired: setupRepair !== null,
291
- blockers: setupRepair === null ? ['setup_repair_not_recorded'] : []
293
+ run: async () => ({
294
+ id: 'setup',
295
+ ok: setupRepair !== null,
296
+ repaired: setupRepair !== null,
297
+ blockers: setupRepair === null ? ['setup_repair_not_recorded'] : []
298
+ })
292
299
  },
293
300
  {
294
301
  id: 'codex_startup_repair',
295
- ok: codexStartupRepair?.ok !== false,
296
- repaired: doctorFix,
297
- blockers: codexStartupRepair?.blockers || [],
298
- warnings: codexStartupRepair?.warnings || []
302
+ run: async () => ({
303
+ id: 'codex_startup_repair',
304
+ ok: codexStartupRepair?.ok !== false,
305
+ repaired: doctorFix,
306
+ blockers: codexStartupRepair?.blockers || [],
307
+ warnings: codexStartupRepair?.warnings || []
308
+ })
299
309
  },
300
310
  {
301
311
  id: 'startup_config_repair',
302
- ok: startupConfigRepair?.ok === true,
303
- repaired: startupConfigRepair?.apply === true,
304
- blockers: startupConfigRepair?.blockers || []
312
+ run: async () => ({
313
+ id: 'startup_config_repair',
314
+ ok: startupConfigRepair?.ok === true,
315
+ repaired: startupConfigRepair?.apply === true,
316
+ blockers: startupConfigRepair?.blockers || []
317
+ })
305
318
  },
306
319
  {
307
320
  id: 'context7_repair',
308
- ok: context7Repair?.ok !== false,
309
- repaired: doctorFix,
310
- blockers: context7Repair?.blockers || [],
311
- warnings: context7Repair?.warnings || []
321
+ run: async () => ({
322
+ id: 'context7_repair',
323
+ ok: context7Repair?.ok !== false,
324
+ repaired: doctorFix,
325
+ blockers: context7Repair?.blockers || [],
326
+ warnings: context7Repair?.warnings || []
327
+ })
312
328
  },
313
329
  {
314
330
  id: 'context7_mcp_repair',
315
- ok: context7McpRepair?.ok === true,
316
- repaired: context7McpRepair?.repaired === true,
317
- manual_required: context7McpRepair?.manual_required === true,
318
- blockers: context7McpRepair?.blockers || [],
319
- warnings: context7McpRepair?.warnings || []
331
+ run: async () => ({
332
+ id: 'context7_mcp_repair',
333
+ ok: context7McpRepair?.ok === true,
334
+ repaired: context7McpRepair?.repaired === true,
335
+ manual_required: context7McpRepair?.manual_required === true,
336
+ blockers: context7McpRepair?.blockers || [],
337
+ warnings: context7McpRepair?.warnings || []
338
+ })
320
339
  },
321
340
  {
322
341
  id: 'supabase_mcp_repair',
323
- ok: supabaseMcpRepair?.ok === true,
324
- repaired: false,
325
- manual_required: supabaseMcpRepair?.manual_required === true,
326
- blockers: supabaseMcpRepair?.blockers || [],
327
- warnings: supabaseMcpRepair?.warnings || []
342
+ required_for_ready: false,
343
+ run: async () => ({
344
+ id: 'supabase_mcp_repair',
345
+ ok: supabaseMcpRepair?.ok === true,
346
+ repaired: false,
347
+ manual_required: supabaseMcpRepair?.manual_required === true,
348
+ required_for_ready: false,
349
+ blockers: supabaseMcpRepair?.blockers || [],
350
+ warnings: supabaseMcpRepair?.warnings || []
351
+ })
328
352
  },
329
353
  {
330
354
  id: 'command_alias_cleanup',
331
- ok: commandAliasCleanup?.ok !== false,
332
- repaired: Array.isArray(commandAliasCleanup?.actions) && commandAliasCleanup.actions.length > 0,
333
- blockers: commandAliasCleanup?.blockers || []
355
+ run: async () => ({
356
+ id: 'command_alias_cleanup',
357
+ ok: commandAliasCleanup?.ok !== false,
358
+ repaired: Array.isArray(commandAliasCleanup?.actions) && commandAliasCleanup.actions.length > 0,
359
+ blockers: commandAliasCleanup?.blockers || []
360
+ })
334
361
  },
335
362
  {
336
363
  id: 'native_capability_repair',
337
- ok: doctorNativeCapabilityRepair?.ok !== false,
338
- repaired: doctorFix,
339
- blockers: doctorNativeCapabilityRepair?.blockers || []
364
+ run: async () => ({
365
+ id: 'native_capability_repair',
366
+ ok: doctorNativeCapabilityRepair?.ok !== false,
367
+ repaired: doctorFix,
368
+ blockers: doctorNativeCapabilityRepair?.blockers || []
369
+ })
340
370
  }
341
371
  ]
342
372
  }).catch((err) => ({
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import { ensureDir, nowIso, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
4
4
  import { REQUIRED_CODEX_MODEL } from '../codex-model-guard.js';
5
5
  export const AGENT_ROLE_CONFIG_REPAIR_SCHEMA = 'sks.agent-role-config-repair.v1';
6
- const SKS_OWNED_AGENT_CONFIGS = new Map([
6
+ export const SKS_OWNED_AGENT_CONFIGS = new Map([
7
7
  ['analysis-scout.toml', roleConfig('analysis_scout', 'Read-only SKS analysis scout retained for stale Codex agent-role config repair.', 'read-only')],
8
8
  ['native-agent-intake.toml', roleConfig('native_agent', 'Read-only Team native agent for repository/docs/tests/API/risk slices.', 'read-only')],
9
9
  ['team-consensus.toml', roleConfig('team_consensus', 'Planning and debate specialist for SKS Team mode.', 'read-only')],
@@ -11,6 +11,17 @@ const SKS_OWNED_AGENT_CONFIGS = new Map([
11
11
  ['db-safety-reviewer.toml', roleConfig('db_safety_reviewer', 'Read-only database safety reviewer for SQL, migrations, Supabase, and rollback safety.', 'read-only')],
12
12
  ['qa-reviewer.toml', roleConfig('qa_reviewer', 'Strict read-only verification reviewer for correctness, regressions, and final evidence.', 'read-only')]
13
13
  ]);
14
+ export function managedAgentRoleConfigForFile(file) {
15
+ return SKS_OWNED_AGENT_CONFIGS.get(path.basename(file))?.content || null;
16
+ }
17
+ export function managedAgentRoleConfigForRole(role) {
18
+ const normalized = String(role || '').trim().replace(/-/g, '_');
19
+ for (const [file, config] of SKS_OWNED_AGENT_CONFIGS) {
20
+ if (config.name === normalized || path.basename(file, '.toml').replace(/-/g, '_') === normalized)
21
+ return { file, content: config.content };
22
+ }
23
+ return null;
24
+ }
14
25
  export async function repairAgentRoleConfigs(input) {
15
26
  const root = path.resolve(input.root);
16
27
  const codexHome = input.codexHome || process.env.CODEX_HOME || path.join(process.env.HOME || '', '.codex');
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { ensureDir, nowIso, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
4
+ import { managedAgentRoleConfigForFile, managedAgentRoleConfigForRole } from '../agents/agent-role-config.js';
4
5
  export async function repairAgentConfigFileReferences(input) {
5
6
  const root = path.resolve(input.root);
6
7
  const configPath = path.join(root, '.codex', 'config.toml');
@@ -8,27 +9,42 @@ export async function repairAgentConfigFileReferences(input) {
8
9
  const createdFiles = [];
9
10
  const repairedPaths = [];
10
11
  const removedUnsupportedFields = [];
11
- let text = original.replace(/^\s*message_role_prefix\s*=.*$/gm, (line) => {
12
- removedUnsupportedFields.push(line.trim());
13
- return '';
14
- });
15
- text = text.replace(/config_file\s*=\s*"([^"]+)"/g, (_match, value) => {
16
- const absolute = path.isAbsolute(value) ? value : path.join(root, value);
17
- repairedPaths.push(absolute);
18
- return `config_file = "${absolute}"`;
19
- });
20
- if (input.apply && text !== original) {
21
- for (const file of repairedPaths) {
22
- const exists = await fs.stat(file).then((stat) => stat.isFile()).catch(() => false);
12
+ const skippedUnmanagedPaths = [];
13
+ const edits = [];
14
+ let text = original;
15
+ for (const block of tomlBlocks(original)) {
16
+ const managed = managedBlockTarget(root, block);
17
+ const currentConfigFile = stringValue(block.text, 'config_file');
18
+ if (!managed) {
19
+ if (currentConfigFile && !path.isAbsolute(currentConfigFile))
20
+ skippedUnmanagedPaths.push(currentConfigFile);
21
+ continue;
22
+ }
23
+ const target = path.join(root, '.codex', 'agents', managed.file);
24
+ let replacement = removeKey(block.text, 'message_role_prefix', removedUnsupportedFields);
25
+ replacement = replaceOrInsertKey(replacement, 'config_file', `"${escapeToml(target)}"`);
26
+ if (replacement !== block.text) {
27
+ edits.push({ start: block.start, end: block.end, replacement });
28
+ repairedPaths.push(target);
29
+ }
30
+ if (input.apply) {
31
+ const exists = await fs.stat(target).then((stat) => stat.isFile()).catch(() => false);
23
32
  if (!exists) {
24
- await ensureDir(path.dirname(file));
25
- await writeTextAtomic(file, '# SKS managed agent config placeholder\n');
26
- createdFiles.push(file);
33
+ await ensureDir(path.dirname(target));
34
+ await writeTextAtomic(target, managed.content);
35
+ createdFiles.push(target);
27
36
  }
28
37
  }
29
- await writeTextAtomic(configPath, text);
30
38
  }
31
- const missing = await missingAgentConfigFiles(text);
39
+ if (edits.length)
40
+ text = applyEdits(original, edits);
41
+ if (input.apply && text !== original) {
42
+ await writeTextAtomic(configPath, text.replace(/\n{3,}/g, '\n\n').replace(/\s*$/, '\n'));
43
+ }
44
+ const effectiveText = input.apply ? await fs.readFile(configPath, 'utf8').catch(() => text) : text;
45
+ const missing = await missingAgentConfigFiles(effectiveText);
46
+ const unsupportedManagedFields = managedAgentBlocks(effectiveText)
47
+ .flatMap((block) => block.text.split(/\r?\n/).filter((line) => /^\s*message_role_prefix\s*=/.test(line)));
32
48
  const report = {
33
49
  schema: 'sks.agent-config-file-repair.v1',
34
50
  generated_at: nowIso(),
@@ -38,14 +54,22 @@ export async function repairAgentConfigFileReferences(input) {
38
54
  repaired_paths: repairedPaths,
39
55
  created_files: createdFiles,
40
56
  removed_unsupported_fields: removedUnsupportedFields,
41
- blockers: missing.map((file) => `missing_agent_config_file:${file}`)
57
+ skipped_unmanaged_paths: skippedUnmanagedPaths,
58
+ manual_required: skippedUnmanagedPaths.length > 0,
59
+ blockers: [
60
+ ...missing.map((file) => `missing_agent_config_file:${file}`),
61
+ ...unsupportedManagedFields.map(() => 'unsupported_message_role_prefix_field')
62
+ ]
42
63
  };
64
+ report.ok = report.blockers.length === 0;
43
65
  if (input.reportPath !== null)
44
66
  await writeJsonAtomic(input.reportPath || path.join(root, '.sneakoscope', 'reports', 'agent-config-file-repair.json'), report).catch(() => undefined);
45
67
  return report;
46
68
  }
47
69
  export async function missingAgentConfigFiles(text) {
48
- const rows = [...String(text || '').matchAll(/config_file\s*=\s*"([^"]+)"/g)].map((match) => match[1]).filter((file) => Boolean(file));
70
+ const rows = managedAgentBlocks(text)
71
+ .map((block) => stringValue(block.text, 'config_file'))
72
+ .filter((file) => Boolean(file));
49
73
  const missing = [];
50
74
  for (const file of rows) {
51
75
  if (!path.isAbsolute(file)) {
@@ -58,4 +82,76 @@ export async function missingAgentConfigFiles(text) {
58
82
  }
59
83
  return missing;
60
84
  }
85
+ function tomlBlocks(text) {
86
+ const source = String(text || '');
87
+ const matches = [...source.matchAll(/(^|\n)\s*\[([^\]]+)\]\s*(?:#.*)?(?:\n|$)/g)];
88
+ return matches.map((match, index) => {
89
+ const start = Number(match.index || 0) + (match[1] ? 1 : 0);
90
+ const next = matches[index + 1];
91
+ const end = next ? Number(next.index || 0) + (next[1] ? 1 : 0) : source.length;
92
+ return {
93
+ header: String(match[2] || '').trim(),
94
+ start,
95
+ end,
96
+ text: source.slice(start, end)
97
+ };
98
+ });
99
+ }
100
+ function managedAgentBlocks(text) {
101
+ return tomlBlocks(text).filter((block) => Boolean(managedBlockTarget(process.cwd(), block)));
102
+ }
103
+ function managedBlockTarget(root, block) {
104
+ if (!block.header.startsWith('agents.'))
105
+ return null;
106
+ const role = block.header.slice('agents.'.length);
107
+ const byRole = managedAgentRoleConfigForRole(role);
108
+ if (byRole)
109
+ return byRole;
110
+ const configFile = stringValue(block.text, 'config_file');
111
+ if (configFile) {
112
+ const content = managedAgentRoleConfigForFile(configFile);
113
+ if (content)
114
+ return { file: path.basename(configFile), content };
115
+ }
116
+ if (/SKS managed|sks_/i.test(block.text)) {
117
+ const fallback = managedAgentRoleConfigForRole(role);
118
+ if (fallback)
119
+ return fallback;
120
+ }
121
+ void root;
122
+ return null;
123
+ }
124
+ function stringValue(text, key) {
125
+ const match = text.match(new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*"([^"]*)"`, 'm'));
126
+ return match && typeof match[1] === 'string' ? match[1] : null;
127
+ }
128
+ function removeKey(text, key, removed) {
129
+ return text.split(/\r?\n/).filter((line) => {
130
+ const match = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`).test(line);
131
+ if (match)
132
+ removed.push(line.trim());
133
+ return !match;
134
+ }).join('\n');
135
+ }
136
+ function replaceOrInsertKey(text, key, encodedValue) {
137
+ const lines = text.replace(/\s*$/, '').split('\n');
138
+ const re = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
139
+ const index = lines.findIndex((line) => re.test(line));
140
+ if (index >= 0)
141
+ lines[index] = `${key} = ${encodedValue}`;
142
+ else
143
+ lines.push(`${key} = ${encodedValue}`);
144
+ return `${lines.join('\n')}\n`;
145
+ }
146
+ function applyEdits(text, edits) {
147
+ return [...edits]
148
+ .sort((a, b) => b.start - a.start)
149
+ .reduce((current, edit) => `${current.slice(0, edit.start)}${edit.replacement}${current.slice(edit.end)}`, text);
150
+ }
151
+ function escapeToml(value) {
152
+ return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
153
+ }
154
+ function escapeRegExp(value) {
155
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
156
+ }
61
157
  //# sourceMappingURL=agent-config-file-repair.js.map
@@ -2,29 +2,82 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { nowIso, writeJsonAtomic } from '../fsx.js';
4
4
  import { missingAgentConfigFiles } from './agent-config-file-repair.js';
5
+ import { managedAgentRoleConfigForFile, managedAgentRoleConfigForRole } from '../agents/agent-role-config.js';
5
6
  export async function postcheckCodexStartupConfig(input) {
6
7
  const root = path.resolve(input.root);
7
8
  const configPath = path.join(root, '.codex', 'config.toml');
8
9
  const text = await fs.readFile(configPath, 'utf8').catch(() => '');
9
10
  const missing = await missingAgentConfigFiles(text);
10
- const unsupportedRoleFields = /^\s*message_role_prefix\s*=/m.test(text);
11
- const relativePaths = [...text.matchAll(/config_file\s*=\s*"([^"]+)"/g)].map((match) => match[1]).filter((file) => file && !path.isAbsolute(file));
11
+ const managedBlocks = managedAgentBlocks(text);
12
+ const unsupportedRoleFields = managedBlocks.some((block) => /^\s*message_role_prefix\s*=/m.test(block.text));
13
+ const relativePaths = managedBlocks
14
+ .map((block) => block.text.match(/^\s*config_file\s*=\s*"([^"]+)"/m)?.[1])
15
+ .filter((file) => Boolean(file && !path.isAbsolute(file)));
16
+ const tomlSmoke = tomlSyntaxSmoke(text);
17
+ const orphanChildTables = orphanMcpChildTables(text);
12
18
  const report = {
13
19
  schema: 'sks.codex-startup-config-postcheck.v1',
14
20
  generated_at: nowIso(),
15
- ok: missing.length === 0 && relativePaths.length === 0 && !unsupportedRoleFields,
21
+ ok: missing.length === 0 && relativePaths.length === 0 && !unsupportedRoleFields && tomlSmoke.ok && orphanChildTables.length === 0,
16
22
  config_path: configPath,
17
23
  missing_config_files: missing,
18
24
  relative_config_files: relativePaths,
19
25
  unsupported_managed_role_fields: unsupportedRoleFields,
26
+ toml_syntax_smoke_ok: tomlSmoke.ok,
27
+ orphan_mcp_child_tables: orphanChildTables,
20
28
  blockers: [
21
29
  ...missing.map((file) => `missing_agent_config_file:${file}`),
22
30
  ...relativePaths.map((file) => `relative_agent_config_file:${file}`),
23
- ...(unsupportedRoleFields ? ['unsupported_message_role_prefix_field'] : [])
31
+ ...(unsupportedRoleFields ? ['unsupported_message_role_prefix_field'] : []),
32
+ ...tomlSmoke.blockers,
33
+ ...orphanChildTables.map((table) => `orphan_mcp_child_table:${table}`)
24
34
  ]
25
35
  };
26
36
  if (input.reportPath !== null)
27
37
  await writeJsonAtomic(input.reportPath || path.join(root, '.sneakoscope', 'reports', 'codex-startup-config-postcheck.json'), report).catch(() => undefined);
28
38
  return report;
29
39
  }
40
+ function managedAgentBlocks(text) {
41
+ const blocks = tomlBlocks(text);
42
+ return blocks.filter((block) => {
43
+ if (!block.header.startsWith('agents.'))
44
+ return false;
45
+ const role = block.header.slice('agents.'.length);
46
+ if (managedAgentRoleConfigForRole(role))
47
+ return true;
48
+ const configFile = block.text.match(/^\s*config_file\s*=\s*"([^"]+)"/m)?.[1] || '';
49
+ return Boolean(configFile && managedAgentRoleConfigForFile(configFile));
50
+ });
51
+ }
52
+ function tomlBlocks(text) {
53
+ const source = String(text || '');
54
+ const matches = [...source.matchAll(/(^|\n)\s*\[([^\]]+)\]\s*(?:#.*)?(?:\n|$)/g)];
55
+ return matches.map((match, index) => {
56
+ const start = Number(match.index || 0) + (match[1] ? 1 : 0);
57
+ const next = matches[index + 1];
58
+ const end = next ? Number(next.index || 0) + (next[1] ? 1 : 0) : source.length;
59
+ return { header: String(match[2] || '').trim(), text: source.slice(start, end) };
60
+ });
61
+ }
62
+ function tomlSyntaxSmoke(text) {
63
+ const blockers = [];
64
+ for (const [index, line] of String(text || '').split(/\r?\n/).entries()) {
65
+ const trimmed = line.trim();
66
+ if (!trimmed.startsWith('['))
67
+ continue;
68
+ if (!/^\[[^\]]+\]\s*(?:#.*)?$/.test(trimmed))
69
+ blockers.push(`toml_table_header_invalid:${index + 1}`);
70
+ }
71
+ const tripleQuotes = (String(text || '').match(/"""/g) || []).length;
72
+ if (tripleQuotes % 2 !== 0)
73
+ blockers.push('toml_multiline_string_unbalanced');
74
+ return { ok: blockers.length === 0, blockers };
75
+ }
76
+ function orphanMcpChildTables(text) {
77
+ const headers = new Set(tomlBlocks(text).map((block) => block.header));
78
+ return [...headers].filter((header) => {
79
+ const match = header.match(/^mcp_servers\.([^.]+)\./);
80
+ return Boolean(match && !headers.has(`mcp_servers.${match[1]}`));
81
+ });
82
+ }
30
83
  //# sourceMappingURL=codex-startup-config-postcheck.js.map
@@ -2,7 +2,7 @@ import path from 'node:path';
2
2
  import { findCodexBinary } from '../codex-adapter.js';
3
3
  import { compareSemverLike, parseCodexVersionText } from '../codex-compat/codex-version-policy.js';
4
4
  import { nowIso, runProcess, writeJsonAtomic } from '../fsx.js';
5
- import { CODEX_0140_FEATURE_KEYS, probeCodex0140Features } from './codex-0140-feature-probes.js';
5
+ import { CODEX_0140_FEATURE_KEYS, probeCodex0140FeatureDetails, probeCodex0140Features } from './codex-0140-feature-probes.js';
6
6
  export async function detectCodex0140Capability(input = {}) {
7
7
  const fake = process.env.SKS_CODEX_0140_FAKE === '1';
8
8
  const codexBin = fake ? input.codexBin || process.env.CODEX_BIN || 'codex' : input.codexBin || process.env.CODEX_BIN || await findCodexBinary();
@@ -10,10 +10,16 @@ export async function detectCodex0140Capability(input = {}) {
10
10
  const parsed = parseCodexVersionText(versionText);
11
11
  const supports0140 = Boolean(parsed && compareSemverLike(parsed, '0.140.0') >= 0);
12
12
  const probeMode = process.env.SKS_CODEX_0140_PROBE === '1' ? 'feature-probe' : 'version-only';
13
- const probeResults = probeMode === 'feature-probe'
14
- ? await probeCodex0140Features(codexBin, { fake, timeoutMs: Number(process.env.SKS_CODEX_0140_PROBE_TIMEOUT_MS || 3000) })
13
+ const probeTimeoutMs = Number(process.env.SKS_CODEX_0140_PROBE_TIMEOUT_MS || 3000);
14
+ const probeDetails = probeMode === 'feature-probe'
15
+ ? await probeCodex0140FeatureDetails(codexBin, { fake, timeoutMs: probeTimeoutMs })
16
+ : null;
17
+ const probeResults = probeDetails
18
+ ? Object.fromEntries(CODEX_0140_FEATURE_KEYS.map((key) => [key, probeDetails[key].status]))
15
19
  : Object.fromEntries(CODEX_0140_FEATURE_KEYS.map((key) => [key, 'skipped']));
16
- const featureOk = (key) => supports0140 && (probeMode === 'version-only' || probeResults[key] !== 'failed');
20
+ const featureStates = Object.fromEntries(CODEX_0140_FEATURE_KEYS.map((key) => [key, featureStateFor(key, supports0140, probeMode, probeDetails)]));
21
+ const featureCertainty = Object.fromEntries(CODEX_0140_FEATURE_KEYS.map((key) => [key, featureStates[key].certainty]));
22
+ const featureOk = (key) => featureStates[key].supported;
17
23
  const features = {
18
24
  usage_views: featureOk('usage_views'),
19
25
  goal_attachment_preservation: featureOk('goal_attachment_preservation'),
@@ -26,25 +32,84 @@ export async function detectCodex0140Capability(input = {}) {
26
32
  non_tty_interrupt: featureOk('non_tty_interrupt'),
27
33
  large_repo_responsiveness: featureOk('large_repo_responsiveness')
28
34
  };
29
- const failed = Object.entries(probeResults).filter(([, status]) => status === 'failed').map(([key]) => `codex_0140_${key}_probe_failed`);
35
+ const failed = Object.entries(featureStates).flatMap(([key, state]) => state.certainty === 'failed' ? [`codex_0140_${key}_probe_failed`] : []);
36
+ const assumedWarnings = Object.entries(featureStates)
37
+ .filter(([, state]) => state.certainty === 'assumed_by_version')
38
+ .map(([key]) => `codex_0140_${key}_assumed_by_version`);
39
+ const unverifiedWarnings = Object.entries(featureStates)
40
+ .filter(([, state]) => state.certainty === 'unverified')
41
+ .map(([key]) => `codex_0140_${key}_unverified`);
30
42
  const blockers = [
31
43
  ...(!codexBin ? ['codex_cli_missing'] : []),
32
44
  ...(supports0140 ? [] : ['codex_0_140_required_for_0140_features']),
33
45
  ...(probeMode === 'feature-probe' ? failed : [])
34
46
  ];
35
- return {
47
+ const report = {
36
48
  schema: 'sks.codex-0140-capability.v1',
37
49
  generated_at: nowIso(),
38
50
  ok: blockers.length === 0,
39
51
  codex_version: parsed,
40
52
  supports_0140: supports0140,
41
53
  features,
54
+ feature_states: featureStates,
55
+ feature_certainty: featureCertainty,
42
56
  blockers,
43
- warnings: [],
57
+ warnings: [...assumedWarnings, ...unverifiedWarnings],
44
58
  codex_bin: codexBin || null,
45
59
  probe_mode: probeMode,
46
60
  feature_probe_results: probeResults
47
61
  };
62
+ if (probeDetails)
63
+ report.feature_probe_details = probeDetails;
64
+ return report;
65
+ }
66
+ function featureStateFor(key, supports0140, probeMode, probeDetails) {
67
+ if (!supports0140) {
68
+ return {
69
+ supported: false,
70
+ certainty: 'failed',
71
+ evidence: [],
72
+ blockers: ['codex_0_140_required_for_0140_features']
73
+ };
74
+ }
75
+ if (probeMode === 'version-only') {
76
+ return {
77
+ supported: true,
78
+ certainty: 'assumed_by_version',
79
+ evidence: ['codex_version>=0.140.0'],
80
+ blockers: []
81
+ };
82
+ }
83
+ const detail = probeDetails?.[key];
84
+ if (!detail) {
85
+ return {
86
+ supported: false,
87
+ certainty: 'unverified',
88
+ evidence: [],
89
+ blockers: [`codex_0140_${key}_probe_missing`]
90
+ };
91
+ }
92
+ if (detail.status === 'failed') {
93
+ return {
94
+ supported: false,
95
+ certainty: 'failed',
96
+ evidence: detail.evidence,
97
+ blockers: detail.blockers.length ? detail.blockers : [`codex_0140_${key}_probe_failed`]
98
+ };
99
+ }
100
+ const certainty = normalizeCertainty(detail.certainty);
101
+ const supported = detail.status === 'passed' || detail.status === 'discovered';
102
+ return {
103
+ supported,
104
+ certainty: supported ? certainty : 'unverified',
105
+ evidence: detail.evidence,
106
+ blockers: supported ? [] : detail.blockers
107
+ };
108
+ }
109
+ function normalizeCertainty(certainty) {
110
+ if (certainty === 'actual' || certainty === 'discovered' || certainty === 'fixture' || certainty === 'assumed_by_version')
111
+ return certainty;
112
+ return 'unverified';
48
113
  }
49
114
  export async function writeCodex0140CapabilityArtifacts(root, input = {}) {
50
115
  const report = await detectCodex0140Capability({ codexBin: input.codexBin || null });