sneakoscope 3.1.11 → 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 (49) hide show
  1. package/README.md +8 -7
  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 +161 -2
  7. package/dist/core/agents/agent-role-config.js +12 -1
  8. package/dist/core/codex/agent-config-file-repair.js +157 -0
  9. package/dist/core/codex/codex-startup-config-postcheck.js +83 -0
  10. package/dist/core/codex-control/codex-0140-capability.js +136 -0
  11. package/dist/core/codex-control/codex-0140-feature-probes.js +195 -0
  12. package/dist/core/codex-control/codex-0140-probe-runner.js +5 -0
  13. package/dist/core/codex-control/codex-0140-real-probe-summary.js +12 -0
  14. package/dist/core/codex-control/codex-0140-real-probes.js +69 -0
  15. package/dist/core/codex-control/codex-0140-usage-parser.js +81 -0
  16. package/dist/core/codex-native/codex-native-feature-broker.js +15 -1
  17. package/dist/core/codex-native/native-capability-postcheck.js +5 -2
  18. package/dist/core/codex-native/native-capability-repair-matrix.js +4 -4
  19. package/dist/core/config/config-migration-journal.js +2 -0
  20. package/dist/core/config/secret-preservation.js +108 -11
  21. package/dist/core/config/supabase-secret-preservation.js +1 -0
  22. package/dist/core/doctor/codex-startup-config-repair.js +40 -0
  23. package/dist/core/doctor/context7-mcp-repair.js +77 -0
  24. package/dist/core/doctor/doctor-codex-startup-repair.js +127 -15
  25. package/dist/core/doctor/doctor-context7-repair.js +40 -1
  26. package/dist/core/doctor/doctor-repair-postcheck.js +17 -0
  27. package/dist/core/doctor/doctor-transaction.js +126 -0
  28. package/dist/core/doctor/supabase-mcp-repair.js +66 -0
  29. package/dist/core/fsx.js +1 -1
  30. package/dist/core/loops/loop-concurrency-budget.js +22 -0
  31. package/dist/core/mcp/mcp-config-preservation.js +53 -0
  32. package/dist/core/naruto/naruto-loop-mesh.js +5 -1
  33. package/dist/core/version.js +1 -1
  34. package/dist/core/zellij/zellij-fake-adapter.js +8 -2
  35. package/dist/core/zellij/zellij-launcher.js +16 -0
  36. package/dist/core/zellij/zellij-worker-pane-manager.js +19 -2
  37. package/dist/scripts/codex-0140-feature-gate-lib.js +14 -0
  38. package/dist/scripts/release-3112-required-gates.js +30 -0
  39. package/dist/scripts/release-3113-required-gates.js +25 -0
  40. package/package.json +38 -2
  41. package/dist/.sks-build-stamp.json +0 -8
  42. package/dist/scripts/loop-directive-check-lib.js +0 -388
  43. package/dist/scripts/loop-hardening-check-lib.js +0 -289
  44. package/dist/scripts/sks-1-12-real-execution-check-lib.js +0 -27
  45. package/dist/scripts/sks-3-1-4-directive-check-lib.js +0 -212
  46. package/dist/scripts/sks-3-1-5-directive-check-lib.js +0 -318
  47. package/dist/scripts/sks-3-1-6-directive-check-lib.js +0 -522
  48. package/dist/scripts/sks-3-1-7-directive-check-lib.js +0 -58
  49. package/dist/scripts/sks-3-1-8-check-lib.js +0 -30
package/README.md CHANGED
@@ -35,15 +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.11** is a release-ready repair pass for MAD Zellij stacked panes, Context7 MCP doctor recovery, and stale Codex startup config.
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
39
 
40
- What changed in 3.1.11:
40
+ What changed in 3.1.13:
41
41
 
42
- - **MAD Zellij panes require native stacked-pane support.** `sks doctor --fix` now treats Zellij 0.43.0 as the minimum interactive runtime so `sks --mad` can use native stacked worker panes instead of fragmenting into plain splits.
43
- - **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.
44
- - **Codex startup warnings are doctor-repairable.** `sks doctor --fix` rewrites stale SKS agent `config_file` paths to existing absolute files, removes unsupported managed `message_role_prefix` role fields, preserves optional `supabase_sauron`, and drops missing-command `node_repl` MCP blocks that would otherwise spam startup.
45
- - **Doctor JSON exposes the Context7 and startup repair reports.** `context7_repair`, `codex_startup_repair`, and their `repair.*` entries carry migration status, backups, actions, warnings, and any manual auth actions.
46
- - **Release metadata is aligned for 3.1.11.** Package, lockfile, CLI version constants, Rust helper metadata, README, and changelog all point at the same release.
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.
47
48
 
48
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.
49
50
 
@@ -76,7 +76,7 @@ dependencies = [
76
76
 
77
77
  [[package]]
78
78
  name = "sks-core"
79
- version = "3.1.11"
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.11"
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.11"),
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.11';
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') {
@@ -31,6 +31,11 @@ import { buildCodexNativeFeatureMatrix } from '../core/codex-native/codex-native
31
31
  import { repairCodexNativeManagedAssets } from '../core/codex-native/codex-native-repair-transaction.js';
32
32
  import { runDoctorNativeCapabilityRepair } from '../core/doctor/doctor-native-capability-repair.js';
33
33
  import { runDoctorCommandAliasCleanup } from '../core/doctor/command-alias-cleanup.js';
34
+ import { repairCodexStartupConfig } from '../core/doctor/codex-startup-config-repair.js';
35
+ import { repairContext7Mcp } from '../core/doctor/context7-mcp-repair.js';
36
+ import { repairSupabaseMcp } from '../core/doctor/supabase-mcp-repair.js';
37
+ import { runDoctorFixTransaction } from '../core/doctor/doctor-transaction.js';
38
+ import { doctorRepairPostcheck } from '../core/doctor/doctor-repair-postcheck.js';
34
39
  import { withSecretPreservationGuard } from '../core/config/config-migration-journal.js';
35
40
  export async function run(_command, args = []) {
36
41
  const root = await projectRoot();
@@ -240,6 +245,150 @@ async function runDoctor(args = [], root, doctorFix) {
240
245
  warnings: [],
241
246
  report_path: `${root}/.sneakoscope/reports/doctor-context7-repair.json`
242
247
  }));
248
+ const startupConfigRepair = doctorFix
249
+ ? await repairCodexStartupConfig({ root, apply: true }).catch((err) => ({
250
+ schema: 'sks.codex-startup-config-repair.v1',
251
+ ok: false,
252
+ apply: true,
253
+ blockers: [err?.message || String(err)]
254
+ }))
255
+ : null;
256
+ const context7McpRepair = doctorFix
257
+ ? await repairContext7Mcp({ root, apply: true }).catch((err) => ({
258
+ schema: 'sks.doctor-context7-mcp-repair.v1',
259
+ ok: false,
260
+ apply: true,
261
+ repaired: false,
262
+ manual_required: false,
263
+ blockers: [err?.message || String(err)],
264
+ warnings: []
265
+ }))
266
+ : null;
267
+ const supabaseMcpRepair = doctorFix
268
+ ? await repairSupabaseMcp({ root, apply: true }).catch((err) => ({
269
+ schema: 'sks.doctor-supabase-mcp-repair.v1',
270
+ ok: false,
271
+ apply: true,
272
+ configured: false,
273
+ disabled: false,
274
+ disabled_preserved: false,
275
+ token_env_present: false,
276
+ unsafe_write_access: false,
277
+ read_only_migrated: false,
278
+ write_scope_requires_confirmation: false,
279
+ ready_blocking: true,
280
+ manual_required: true,
281
+ next_action: 'Review Supabase MCP configuration manually.',
282
+ blockers: [err?.message || String(err)],
283
+ warnings: [],
284
+ raw_secret_values_recorded: false
285
+ }))
286
+ : null;
287
+ const doctorFixTransaction = doctorFix
288
+ ? await runDoctorFixTransaction({
289
+ root,
290
+ phases: [
291
+ {
292
+ id: 'setup',
293
+ run: async () => ({
294
+ id: 'setup',
295
+ ok: setupRepair !== null,
296
+ repaired: setupRepair !== null,
297
+ blockers: setupRepair === null ? ['setup_repair_not_recorded'] : []
298
+ })
299
+ },
300
+ {
301
+ id: 'codex_startup_repair',
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
+ })
309
+ },
310
+ {
311
+ id: 'startup_config_repair',
312
+ run: async () => ({
313
+ id: 'startup_config_repair',
314
+ ok: startupConfigRepair?.ok === true,
315
+ repaired: startupConfigRepair?.apply === true,
316
+ blockers: startupConfigRepair?.blockers || []
317
+ })
318
+ },
319
+ {
320
+ id: 'context7_repair',
321
+ run: async () => ({
322
+ id: 'context7_repair',
323
+ ok: context7Repair?.ok !== false,
324
+ repaired: doctorFix,
325
+ blockers: context7Repair?.blockers || [],
326
+ warnings: context7Repair?.warnings || []
327
+ })
328
+ },
329
+ {
330
+ id: 'context7_mcp_repair',
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
+ })
339
+ },
340
+ {
341
+ id: 'supabase_mcp_repair',
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
+ })
352
+ },
353
+ {
354
+ id: 'command_alias_cleanup',
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
+ })
361
+ },
362
+ {
363
+ id: 'native_capability_repair',
364
+ run: async () => ({
365
+ id: 'native_capability_repair',
366
+ ok: doctorNativeCapabilityRepair?.ok !== false,
367
+ repaired: doctorFix,
368
+ blockers: doctorNativeCapabilityRepair?.blockers || []
369
+ })
370
+ }
371
+ ]
372
+ }).catch((err) => ({
373
+ schema: 'sks.doctor-fix-transaction.v1',
374
+ ok: false,
375
+ postcheck_ok: false,
376
+ phases: [
377
+ {
378
+ id: 'doctor_fix_transaction',
379
+ ok: false,
380
+ repaired: false,
381
+ manual_required: false,
382
+ blockers: [err?.message || String(err)],
383
+ warnings: [],
384
+ artifact_path: null
385
+ }
386
+ ],
387
+ rollback_performed: false,
388
+ raw_secret_values_recorded: false
389
+ }))
390
+ : null;
391
+ const doctorFixPostcheck = doctorFix ? doctorRepairPostcheck(doctorFixTransaction) : null;
243
392
  const zellij = await checkZellijCapability({ root, require: process.env.SKS_REQUIRE_ZELLIJ === '1' });
244
393
  const localModel = await readLocalModelConfig().catch(() => null);
245
394
  const permissionProfiles = await inventoryCodexPermissionProfiles(root, { writeReport: true });
@@ -321,6 +470,11 @@ async function runDoctor(args = [], root, doctorFix) {
321
470
  zellij,
322
471
  context7_repair: context7Repair,
323
472
  codex_startup_repair: codexStartupRepair,
473
+ startup_config_repair: startupConfigRepair,
474
+ context7_mcp_repair: context7McpRepair,
475
+ supabase_mcp_repair: supabaseMcpRepair,
476
+ doctor_fix_transaction: doctorFixTransaction,
477
+ doctor_fix_postcheck: doctorFixPostcheck,
324
478
  local_model: localModel,
325
479
  agent_role_config: agentRoleConfigRepair,
326
480
  repair: configRepair,
@@ -342,7 +496,7 @@ async function runDoctor(args = [], root, doctorFix) {
342
496
  const runtimeReadiness = buildRuntimeReadiness(zellijReadiness, codexNativeFeatureMatrix);
343
497
  const result = {
344
498
  schema: 'sks.doctor-status.v1',
345
- ok: ready.ready && (!sksUpdate || sksUpdate.ok !== false) && commandAliasCleanup.ok !== false && codexStartupRepair.ok !== false,
499
+ ok: ready.ready && (!sksUpdate || sksUpdate.ok !== false) && commandAliasCleanup.ok !== false && codexStartupRepair.ok !== false && (!doctorFixPostcheck || doctorFixPostcheck.ok !== false),
346
500
  root,
347
501
  node: { ok: Number(process.versions.node.split('.')[0]) >= 20, version: process.version },
348
502
  codex,
@@ -358,6 +512,11 @@ async function runDoctor(args = [], root, doctorFix) {
358
512
  zellij_repair: zellijRepair,
359
513
  context7_repair: context7Repair,
360
514
  codex_startup_repair: codexStartupRepair,
515
+ startup_config_repair: startupConfigRepair,
516
+ context7_mcp_repair: context7McpRepair,
517
+ supabase_mcp_repair: supabaseMcpRepair,
518
+ doctor_fix_transaction: doctorFixTransaction,
519
+ doctor_fix_postcheck: doctorFixPostcheck,
361
520
  local_model: localModel,
362
521
  agent_role_config: agentRoleConfigRepair,
363
522
  zellij_readiness: zellijReadiness,
@@ -381,7 +540,7 @@ async function runDoctor(args = [], root, doctorFix) {
381
540
  ready,
382
541
  sneakoscope: { ok: await exists(`${root}/.sneakoscope`) },
383
542
  package: { bytes: pkgBytes, human: formatBytes(pkgBytes) },
384
- repair: { sks_update: sksUpdate, setup: setupRepair, codex_config: configRepair, migration_journal: migrationJournal, global_sks_installs: globalSksInstallCleanup, agent_role_config: agentRoleConfigRepair, zellij: zellijRepair, context7: context7Repair, codex_startup: codexStartupRepair, codex_native: codexNativeRepair, doctor_native_capability: doctorNativeCapabilityRepair, command_aliases: commandAliasCleanup }
543
+ repair: { sks_update: sksUpdate, setup: setupRepair, codex_config: configRepair, migration_journal: migrationJournal, global_sks_installs: globalSksInstallCleanup, agent_role_config: agentRoleConfigRepair, zellij: zellijRepair, context7: context7Repair, codex_startup: codexStartupRepair, startup_config: startupConfigRepair, context7_mcp: context7McpRepair, supabase_mcp: supabaseMcpRepair, doctor_transaction: doctorFixTransaction, doctor_postcheck: doctorFixPostcheck, codex_native: codexNativeRepair, doctor_native_capability: doctorNativeCapabilityRepair, command_aliases: commandAliasCleanup }
385
544
  };
386
545
  if (flag(args, '--json')) {
387
546
  printJson(result);
@@ -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');
@@ -0,0 +1,157 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { ensureDir, nowIso, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
4
+ import { managedAgentRoleConfigForFile, managedAgentRoleConfigForRole } from '../agents/agent-role-config.js';
5
+ export async function repairAgentConfigFileReferences(input) {
6
+ const root = path.resolve(input.root);
7
+ const configPath = path.join(root, '.codex', 'config.toml');
8
+ const original = await fs.readFile(configPath, 'utf8').catch(() => '');
9
+ const createdFiles = [];
10
+ const repairedPaths = [];
11
+ const removedUnsupportedFields = [];
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);
32
+ if (!exists) {
33
+ await ensureDir(path.dirname(target));
34
+ await writeTextAtomic(target, managed.content);
35
+ createdFiles.push(target);
36
+ }
37
+ }
38
+ }
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)));
48
+ const report = {
49
+ schema: 'sks.agent-config-file-repair.v1',
50
+ generated_at: nowIso(),
51
+ ok: missing.length === 0 && !/^\s*message_role_prefix\s*=/m.test(text),
52
+ apply: input.apply === true,
53
+ config_path: configPath,
54
+ repaired_paths: repairedPaths,
55
+ created_files: createdFiles,
56
+ removed_unsupported_fields: removedUnsupportedFields,
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
+ ]
63
+ };
64
+ report.ok = report.blockers.length === 0;
65
+ if (input.reportPath !== null)
66
+ await writeJsonAtomic(input.reportPath || path.join(root, '.sneakoscope', 'reports', 'agent-config-file-repair.json'), report).catch(() => undefined);
67
+ return report;
68
+ }
69
+ export async function missingAgentConfigFiles(text) {
70
+ const rows = managedAgentBlocks(text)
71
+ .map((block) => stringValue(block.text, 'config_file'))
72
+ .filter((file) => Boolean(file));
73
+ const missing = [];
74
+ for (const file of rows) {
75
+ if (!path.isAbsolute(file)) {
76
+ missing.push(file);
77
+ continue;
78
+ }
79
+ const ok = await fs.stat(file).then((stat) => stat.isFile()).catch(() => false);
80
+ if (!ok)
81
+ missing.push(file);
82
+ }
83
+ return missing;
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
+ }
157
+ //# sourceMappingURL=agent-config-file-repair.js.map
@@ -0,0 +1,83 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { nowIso, writeJsonAtomic } from '../fsx.js';
4
+ import { missingAgentConfigFiles } from './agent-config-file-repair.js';
5
+ import { managedAgentRoleConfigForFile, managedAgentRoleConfigForRole } from '../agents/agent-role-config.js';
6
+ export async function postcheckCodexStartupConfig(input) {
7
+ const root = path.resolve(input.root);
8
+ const configPath = path.join(root, '.codex', 'config.toml');
9
+ const text = await fs.readFile(configPath, 'utf8').catch(() => '');
10
+ const missing = await missingAgentConfigFiles(text);
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);
18
+ const report = {
19
+ schema: 'sks.codex-startup-config-postcheck.v1',
20
+ generated_at: nowIso(),
21
+ ok: missing.length === 0 && relativePaths.length === 0 && !unsupportedRoleFields && tomlSmoke.ok && orphanChildTables.length === 0,
22
+ config_path: configPath,
23
+ missing_config_files: missing,
24
+ relative_config_files: relativePaths,
25
+ unsupported_managed_role_fields: unsupportedRoleFields,
26
+ toml_syntax_smoke_ok: tomlSmoke.ok,
27
+ orphan_mcp_child_tables: orphanChildTables,
28
+ blockers: [
29
+ ...missing.map((file) => `missing_agent_config_file:${file}`),
30
+ ...relativePaths.map((file) => `relative_agent_config_file:${file}`),
31
+ ...(unsupportedRoleFields ? ['unsupported_message_role_prefix_field'] : []),
32
+ ...tomlSmoke.blockers,
33
+ ...orphanChildTables.map((table) => `orphan_mcp_child_table:${table}`)
34
+ ]
35
+ };
36
+ if (input.reportPath !== null)
37
+ await writeJsonAtomic(input.reportPath || path.join(root, '.sneakoscope', 'reports', 'codex-startup-config-postcheck.json'), report).catch(() => undefined);
38
+ return report;
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
+ }
83
+ //# sourceMappingURL=codex-startup-config-postcheck.js.map