sneakoscope 8.4.0 → 8.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,7 +22,7 @@ Proof-first orchestration for Codex CLI, ChatGPT Desktop, AI coding agents, mult
22
22
  Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.
23
23
  <!-- END SKS SEARCH VISIBILITY MARKETING -->
24
24
 
25
- This README documents package **SKS 8.4.0** — its own identity, read from `package.json` and subject to release-gate verification, not advice about what to install.
25
+ This README documents package **SKS 8.6.0** — its own identity, read from `package.json` and subject to release-gate verification, not advice about what to install.
26
26
 
27
27
  Use the official latest stable SKS and Codex CLI releases. The Codex compatibility SSOT is always the **current latest stable** host; capability probes measure what that host can actually do. Product docs do not crown a fixed `0.x.y` string as SSOT (release pins and schema directories are measured artifacts for the current package, not a permanent product version claim). Menu Bar / Center induce updates to the latest stable build. Run `sks update-check` for what is installed and read the capability report for what is supported. Install SSOT is npm `sneakoscope@latest`; PATH `sks` and Menu Bar stamped generation must match that version or gates fail. It resolves managed SKS skills from the authoritative global install, preserves a runnable Naruto child slot when `max_threads=2`, and keeps Menu Bar repair transactional so stamped generations remain verifiable. Naruto uses stable opt-in multi-agent V2 when the host exposes it (Codex official multi-agent wrap-only; SKS does not reimplement a parallel runtime). Local code search is mode-separated (`sks search files|text|structure|symbol|context`); `context` is answered by the compiled TriWiki Context Graph (`context-graph.json` is exhaustive authority; `context-pack.json` and managed `AGENTS.md` are bounded projections) — see [docs/architecture/context-graph.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/docs/architecture/context-graph.md) and [docs/PRODUCT-CONTRACT.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/docs/PRODUCT-CONTRACT.md). See [CHANGELOG.md](https://github.com/mandarange/Sneakoscope-Codex/blob/main/CHANGELOG.md).
28
28
 
@@ -259,7 +259,7 @@ dependencies = [
259
259
 
260
260
  [[package]]
261
261
  name = "sks-core"
262
- version = "8.4.0"
262
+ version = "8.6.0"
263
263
  dependencies = [
264
264
  "globset",
265
265
  "grep-matcher",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "8.4.0"
3
+ version = "8.6.0"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -825,6 +825,7 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
825
825
  phases: [
826
826
  {
827
827
  id: 'setup',
828
+ report: () => setupRepair,
828
829
  run: async () => ({
829
830
  id: 'setup',
830
831
  ok: setupRepair !== null,
@@ -835,6 +836,7 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
835
836
  },
836
837
  {
837
838
  id: 'codex_startup_repair',
839
+ report: () => codexStartupRepair,
838
840
  run: async () => ({
839
841
  id: 'codex_startup_repair',
840
842
  ok: codexStartupRepair?.ok !== false,
@@ -846,6 +848,7 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
846
848
  },
847
849
  {
848
850
  id: 'startup_config_repair',
851
+ report: () => startupConfigRepair,
849
852
  run: async () => ({
850
853
  id: 'startup_config_repair',
851
854
  ok: startupConfigRepair?.ok === true,
@@ -856,6 +859,7 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
856
859
  },
857
860
  {
858
861
  id: 'codex_config_syntax_repair',
862
+ report: () => codexConfigSyntaxRepair,
859
863
  run: async () => ({
860
864
  id: 'codex_config_syntax_repair',
861
865
  ok: codexConfigSyntaxRepair?.ok !== false,
@@ -867,6 +871,7 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
867
871
  },
868
872
  {
869
873
  id: 'context7_repair',
874
+ report: () => context7Repair,
870
875
  run: async () => ({
871
876
  id: 'context7_repair',
872
877
  ok: context7Repair?.ok !== false,
@@ -878,6 +883,7 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
878
883
  },
879
884
  {
880
885
  id: 'context7_mcp_repair',
886
+ report: () => context7McpRepair,
881
887
  run: async () => ({
882
888
  id: 'context7_mcp_repair',
883
889
  ok: context7McpRepair?.ok === true,
@@ -890,6 +896,7 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
890
896
  },
891
897
  {
892
898
  id: 'supabase_mcp_repair',
899
+ report: () => supabaseMcpRepair,
893
900
  required_for_ready: false,
894
901
  run: async () => ({
895
902
  id: 'supabase_mcp_repair',
@@ -904,6 +911,7 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
904
911
  },
905
912
  {
906
913
  id: 'hook_trust_repair',
914
+ report: () => hookTrustRepair,
907
915
  run: async () => ({
908
916
  id: 'hook_trust_repair',
909
917
  ok: hookTrustRepair?.ok !== false,
@@ -917,6 +925,7 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
917
925
  },
918
926
  {
919
927
  id: 'sks_menubar',
928
+ report: () => sksMenuBar,
920
929
  required_for_ready: false,
921
930
  run: async () => ({
922
931
  id: 'sks_menubar',
@@ -961,6 +970,7 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
961
970
  },
962
971
  {
963
972
  id: 'command_alias_cleanup',
973
+ report: () => commandAliasCleanupBeforeReceipt,
964
974
  run: async () => ({
965
975
  id: 'command_alias_cleanup',
966
976
  ok: commandAliasCleanupBeforeReceipt?.ok !== false,
@@ -972,6 +982,7 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
972
982
  },
973
983
  {
974
984
  id: 'native_capability_repair',
985
+ report: () => doctorNativeCapabilityRepair,
975
986
  required_for_ready: false,
976
987
  run: async () => ({
977
988
  id: 'native_capability_repair',
@@ -1214,6 +1225,20 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
1214
1225
  warnings: []
1215
1226
  }))
1216
1227
  : null;
1228
+ const configDiskVerification = doctorFix
1229
+ ? await (await import('../core/doctor/doctor-repair-postcheck.js')).verifyCodexConfigsOnDisk({ root })
1230
+ .catch((err) => ({
1231
+ schema: 'sks.doctor-config-disk-verification.v1',
1232
+ ok: false,
1233
+ project_config_path: '',
1234
+ codex_home_config_path: '',
1235
+ project_config_present: false,
1236
+ codex_home_config_present: false,
1237
+ multi_agent_v2_enabled: null,
1238
+ agents_enabled: null,
1239
+ blockers: [`config_disk_verification_failed:${err?.message || String(err)}`]
1240
+ }))
1241
+ : null;
1217
1242
  const codexAppHarnessMatrix = deepDiagnostics
1218
1243
  ? await buildCodexAppHarnessMatrix({ root, mode: 'read-only' }).catch((err) => ({
1219
1244
  schema: 'sks.codex-app-harness-matrix.v1',
@@ -1385,16 +1410,36 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
1385
1410
  }
1386
1411
  }
1387
1412
  const runtimeReadiness = buildRuntimeReadiness(codexNativeFeatureMatrix);
1388
- const resultOk = ready.ready
1389
- && (!sksUpdate || sksUpdate.ok !== false)
1390
- && (!doctorFix || migrationReceiptOwnsReconcile || skillsReconcile?.convergence?.ok === true)
1391
- && commandAliasCleanup.ok !== false
1392
- && codexStartupRepair.ok !== false
1393
- && codexConfigSyntaxRepair?.ok !== false
1394
- && agentRoleConfigRepair.ok !== false
1395
- && openRouterProviderRepair.ok !== false
1396
- && (officialSubagentConfig.blockers || []).length === 0
1397
- && (!doctorProfileRequiresDesktopBridgeReadiness(doctorProfile) || desktopBridge.ok !== false);
1413
+ const resultBlockers = [
1414
+ ...(ready.ready
1415
+ ? []
1416
+ : (Array.isArray(ready.blockers) && ready.blockers.length
1417
+ ? ready.blockers.map((blocker) => `readiness:${String(blocker)}`)
1418
+ : ['readiness_not_ready'])),
1419
+ ...(sksUpdate && sksUpdate.ok === false ? ['sks_update_blocked'] : []),
1420
+ ...(doctorFix && !migrationReceiptOwnsReconcile && skillsReconcile?.convergence?.ok !== true
1421
+ ? ['managed_skill_reconcile_not_converged']
1422
+ : []),
1423
+ ...(commandAliasCleanup.ok === false ? ['command_alias_cleanup_blocked'] : []),
1424
+ ...(codexStartupRepair.ok === false ? ['codex_startup_repair_blocked'] : []),
1425
+ ...(codexConfigSyntaxRepair?.ok === false ? ['codex_config_syntax_repair_blocked'] : []),
1426
+ ...(agentRoleConfigRepair.ok === false ? ['agent_role_config_repair_blocked'] : []),
1427
+ ...(openRouterProviderRepair.ok === false ? ['openrouter_provider_repair_blocked'] : []),
1428
+ ...((officialSubagentConfig.blockers || [])
1429
+ .map((blocker) => `official_subagent_config:${String(blocker)}`)),
1430
+ ...(doctorProfileRequiresDesktopBridgeReadiness(doctorProfile) && desktopBridge.ok === false
1431
+ ? ['desktop_bridge_not_ready']
1432
+ : []),
1433
+ ...(mcpTransportCollisionRepair?.ok === false ? ['mcp_transport_collision_repair_blocked'] : []),
1434
+ ...(codexNativeRepair?.ok === false ? ['codex_native_repair_blocked'] : []),
1435
+ ...(configDiskVerification?.blockers || [])
1436
+ .map((blocker) => `config_disk_verification:${String(blocker)}`)
1437
+ ];
1438
+ const resultOk = resultBlockers.length === 0;
1439
+ const repairOperatorActions = [
1440
+ ...(codexStartupRepair?.config_file_repair?.operator_actions || []),
1441
+ ...(agentRoleConfigRepair?.operator_actions || [])
1442
+ ].map(String).filter(Boolean);
1398
1443
  const result = {
1399
1444
  schema: 'sks.doctor-status.v3',
1400
1445
  elapsed_ms: Date.now() - startedAtMs,
@@ -1409,11 +1454,14 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
1409
1454
  not_counted_as_full_doctor: !deepDiagnostics,
1410
1455
  root,
1411
1456
  arg_warnings: argWarnings,
1457
+ blockers: [...new Set(resultBlockers.map(String))],
1412
1458
  warnings: [...oauthCallbackPortDiagnostic.warnings, ...(desktopBridge.warnings || [])],
1413
- operator_actions: [
1414
- ...oauthCallbackOperatorActions,
1415
- ...(desktopBridge.ok === false ? desktopBridge.recovery_actions || [] : [])
1416
- ],
1459
+ operator_actions: [...new Set([
1460
+ ...oauthCallbackOperatorActions,
1461
+ ...(desktopBridge.ok === false ? desktopBridge.recovery_actions || [] : []),
1462
+ ...repairOperatorActions,
1463
+ ...(resultOk ? [] : ['Each blocker above names a report under .sneakoscope/reports/; re-run `sks doctor --full --json` after resolving them.'])
1464
+ ].map(String).filter(Boolean))],
1417
1465
  node: { ok: Number(process.versions.node.split('.')[0]) >= 20, version: process.version },
1418
1466
  codex,
1419
1467
  oauth_callback_port_diagnostic: oauthCallbackPortDiagnostic,
@@ -1438,8 +1486,9 @@ async function runDoctor(args = [], root, doctorFix, deps = {}) {
1438
1486
  supabase_mcp_repair: supabaseMcpRepair,
1439
1487
  doctor_fix_transaction: doctorFixTransaction,
1440
1488
  doctor_fix_postcheck: doctorFixPostcheck,
1489
+ config_disk_verification: configDiskVerification,
1441
1490
  postcheck: doctorFixPostcheck ? {
1442
- ok: doctorFixPostcheck.ok === true,
1491
+ ok: doctorFixPostcheck.ok === true && configDiskVerification?.ok !== false,
1443
1492
  pending_manual: doctorFixPostcheck.pending_manual || [],
1444
1493
  required_blockers: doctorFixPostcheck.required_blockers || [],
1445
1494
  optional_warnings: doctorFixPostcheck.optional_warnings || []
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": "sks.skills-manifest.v1",
3
- "package_version": "8.4.0",
3
+ "package_version": "8.6.0",
4
4
  "skills": [
5
5
  {
6
6
  "canonical_name": "sks",
@@ -46,9 +46,10 @@ export async function seedArchitectureMapPlanBinding(dir, plan) {
46
46
  return plan;
47
47
  }
48
48
  export async function maybeSeedArchitectureMapForPlan(input) {
49
- if (!input.plan?.architecture_map || !input.missionId)
50
- return;
51
- await seedArchitectureMapBaselineArtifacts({
49
+ if (!input.plan?.architecture_map || !input.missionId) {
50
+ return { ok: false, reason: 'architecture_map_not_planned' };
51
+ }
52
+ return seedArchitectureMapBaselineArtifacts({
52
53
  root: input.root,
53
54
  dir: input.dir,
54
55
  missionId: String(input.missionId),
@@ -1,8 +1,13 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { nowIso, readJson, writeJsonAtomic } from '../fsx.js';
4
- import { backupInvalidToml, inspectOfficialSubagentToml, mergeOfficialSubagentConfig, officialSubagentConfigOwnershipProof, officialSubagentConfigWarnings, readInheritedOfficialSubagentConfigText } from '../subagents/official-subagent-config.js';
4
+ import { SKS_MANAGED_CODEX_CONFIG_MARKER, backupInvalidToml, inspectOfficialSubagentToml, mergeOfficialSubagentConfigResult, officialSubagentConfigOwnershipProof, officialSubagentConfigWarnings, readInheritedOfficialSubagentConfigText } from '../subagents/official-subagent-config.js';
5
5
  import { writeCodexConfigGuarded } from './codex-config-guard.js';
6
+ function unmanagedConfigOperatorAction(configPath) {
7
+ return `Refused to modify ${configPath}: no SKS ownership proof. `
8
+ + `If SKS owns this file, add the line \`${SKS_MANAGED_CODEX_CONFIG_MARKER}\` as its first line `
9
+ + 'and re-run `sks doctor --fix`. Otherwise run `sks setup` to regenerate a managed config.';
10
+ }
6
11
  export async function repairAgentConfigFileReferences(input) {
7
12
  const root = path.resolve(input.root);
8
13
  const configPath = path.join(root, '.codex', 'config.toml');
@@ -37,6 +42,10 @@ export async function repairAgentConfigFileReferences(input) {
37
42
  ...(!ownershipProof.owned ? ['user_owned_file_without_sks_marker'] : [])
38
43
  ],
39
44
  warnings: [],
45
+ operator_actions: [
46
+ `${configPath} is not valid TOML; SKS preserved it and wrote a backup. Fix the syntax, then re-run \`sks doctor --fix\`.`,
47
+ ...(!ownershipProof.owned ? [unmanagedConfigOperatorAction(configPath)] : [])
48
+ ],
40
49
  ownership_proof: ownershipProof
41
50
  });
42
51
  }
@@ -55,6 +64,7 @@ export async function repairAgentConfigFileReferences(input) {
55
64
  manual_required: true,
56
65
  blockers: ['user_owned_file_without_sks_marker'],
57
66
  warnings: [],
67
+ operator_actions: [unmanagedConfigOperatorAction(configPath)],
58
68
  ownership_proof: ownershipProof
59
69
  });
60
70
  }
@@ -62,13 +72,14 @@ export async function repairAgentConfigFileReferences(input) {
62
72
  ...(input.home ? { home: input.home } : {}),
63
73
  ...(input.codexHome ? { codexHome: input.codexHome } : {})
64
74
  });
65
- const merged = mergeOfficialSubagentConfig(original, {
75
+ const mergeResult = mergeOfficialSubagentConfigResult(original, {
66
76
  sksOwned: ownershipProof.owned,
67
77
  inheritedText
68
78
  });
79
+ const merged = mergeResult.text;
69
80
  const validation = inspectOfficialSubagentToml(merged);
70
81
  const warnings = officialSubagentConfigWarnings(merged, inheritedText);
71
- const blockers = [];
82
+ const blockers = [...mergeResult.blockers];
72
83
  let changed = merged !== original;
73
84
  let writeSucceeded = input.apply !== true;
74
85
  let backupPath = null;
@@ -104,6 +115,9 @@ export async function repairAgentConfigFileReferences(input) {
104
115
  manual_required: blockers.length > 0,
105
116
  blockers,
106
117
  warnings,
118
+ operator_actions: blockers.map((blocker) => blocker === 'user_owned_file_without_sks_marker'
119
+ ? unmanagedConfigOperatorAction(configPath)
120
+ : `${configPath}: ${blocker}. Re-run \`sks doctor --fix\` after resolving it, or run \`sks setup\` to regenerate a managed config.`),
107
121
  ownership_proof: ownershipProof
108
122
  };
109
123
  return writeReport(input.reportPath, root, report);
@@ -4,6 +4,7 @@ import { randomBytes } from 'node:crypto';
4
4
  import { appendJsonl, ensureDir, nowIso, readText, sha256, writeTextAtomic } from '../fsx.js';
5
5
  import { diffCodexAppUiSnapshots, snapshotCodexAppUiState } from '../codex-app/codex-app-ui-state-snapshot.js';
6
6
  import { cleanupCodexConfigBackups, validateCodexConfigRoundTrip } from './codex-config-toml.js';
7
+ import { hasManagedAgentsConfigFingerprint } from '../subagents/official-subagent-config.js';
7
8
  import { escapeRegExp } from '../text/regex.js';
8
9
  import { withFileLock } from '../locks/file-lock.js';
9
10
  export const SKS_MANAGED_CODEX_CONFIG_MARKER = '# SKS-MANAGED-CODEX-CONFIG';
@@ -576,7 +577,8 @@ export function isProjectCodexConfig(root, configPath) {
576
577
  export function hasSksManagedCodexConfigMarker(text) {
577
578
  const source = String(text || '');
578
579
  return hasExplicitSksManagedCodexConfigMarker(source)
579
- || /(?:SKS managed|Sneakoscope|sneakoscope|sks_|agents\.native_agent|agents\.implementation_worker|multi_agent)/i.test(source)
580
+ || /(?:SKS managed|Sneakoscope|sneakoscope|sks_|agents\.native_agent|agents\.implementation_worker)/i.test(source)
581
+ || hasManagedAgentsConfigFingerprint(source)
580
582
  || /^\s*model_provider\s*=\s*["']codex-lb["']\s*(?:#.*)?$/mi.test(source)
581
583
  || /^\s*default_profile\s*=\s*["']sks-fast-high["']\s*(?:#.*)?$/mi.test(source)
582
584
  || /^\s*\[(?:user\.fast_mode|model_providers\.(?:"codex-lb"|codex-lb)|profiles\.(?:"sks-fast-high"|sks-fast-high))\]\s*(?:#.*)?$/mi.test(source);
@@ -0,0 +1,10 @@
1
+ export const MANAGED_CODEX_FEATURE_FLAGS = Object.freeze(['hooks', 'fast_mode', 'apps']);
2
+ export const REMOVED_CODEX_FEATURE_FLAGS = Object.freeze([
3
+ 'fast_mode_ui',
4
+ 'codex_hooks',
5
+ 'remote_control',
6
+ 'codex_git_commit',
7
+ 'plugin_hooks',
8
+ 'js_repl',
9
+ 'multi_agent_mode'
10
+ ]);
@@ -118,20 +118,8 @@ export async function splitCodexProjectConfigPolicy(rootInput = process.cwd(), o
118
118
  let backupPath = null;
119
119
  let userConfigPath = null;
120
120
  let profileConfigPath = null;
121
- if (opts.apply && changed) {
122
- backupPath = `${configPath}.bak-${Date.now().toString(36)}`;
123
- await ensureDir(path.dirname(configPath));
124
- await fsp.copyFile(configPath, backupPath);
125
- await cleanupCodexConfigBackups(configPath, { keepPerTag: 3, maxAgeMs: 30 * 24 * 60 * 60 * 1000 }).catch(() => undefined);
126
- await writeCodexConfigGuarded({
127
- root,
128
- configPath,
129
- before: String(original || ''),
130
- cause: 'codex-project-config-policy',
131
- mutate: () => projectText
132
- });
133
- actions.push('project_config_rewritten_with_backup');
134
- }
121
+ const writeBlockers = [];
122
+ let machineLocalMoved = true;
135
123
  if (opts.apply && split.machine_text.trim()) {
136
124
  await ensureDir(codexHome);
137
125
  userConfigPath = codexHomeConfigPath;
@@ -139,15 +127,39 @@ export async function splitCodexProjectConfigPolicy(rootInput = process.cwd(), o
139
127
  const machineText = preserveExistingGlobalFastModeConfig(String(currentUser || ''), split.machine_text);
140
128
  const dedupedUser = removeConfigIds(String(currentUser || ''), configIds(machineText));
141
129
  const commentLine = `# SKS moved machine-local Codex config from ${path.relative(root, configPath) || configPath} at ${nowIso()}`;
142
- const mergedUser = mergeMachineLocalIntoUserConfig(dedupedUser, machineText.trim(), commentLine);
143
- await writeCodexConfigGuarded({
130
+ const mergedUser = mergeMachineLocalIntoUserConfig(stripSksMovedComments(dedupedUser), machineText.trim(), commentLine);
131
+ const homeWrite = await writeCodexConfigGuarded({
144
132
  root,
145
133
  configPath: userConfigPath,
146
134
  before: String(currentUser || ''),
147
135
  cause: 'codex-project-config-policy-machine-local',
148
136
  mutate: () => mergedUser
149
137
  });
150
- actions.push('machine_local_keys_moved_to_codex_home_config');
138
+ machineLocalMoved = homeWrite.ok;
139
+ if (homeWrite.ok)
140
+ actions.push('machine_local_keys_moved_to_codex_home_config');
141
+ else
142
+ writeBlockers.push(`machine_local_move_refused:${homeWrite.status}`);
143
+ }
144
+ if (opts.apply && changed && machineLocalMoved) {
145
+ backupPath = `${configPath}.bak-${Date.now().toString(36)}`;
146
+ await ensureDir(path.dirname(configPath));
147
+ await fsp.copyFile(configPath, backupPath);
148
+ await cleanupCodexConfigBackups(configPath, { keepPerTag: 3, maxAgeMs: 30 * 24 * 60 * 60 * 1000 }).catch(() => undefined);
149
+ const projectWrite = await writeCodexConfigGuarded({
150
+ root,
151
+ configPath,
152
+ before: String(original || ''),
153
+ cause: 'codex-project-config-policy',
154
+ mutate: () => projectText
155
+ });
156
+ if (projectWrite.ok)
157
+ actions.push('project_config_rewritten_with_backup');
158
+ else
159
+ writeBlockers.push(`project_config_write_refused:${projectWrite.status}`);
160
+ }
161
+ else if (opts.apply && changed) {
162
+ writeBlockers.push('project_config_rewrite_skipped_machine_local_move_refused');
151
163
  }
152
164
  if (profileName)
153
165
  actions.push('legacy_project_profile_selector_removed');
@@ -157,7 +169,7 @@ export async function splitCodexProjectConfigPolicy(rootInput = process.cwd(), o
157
169
  root,
158
170
  config_path: configPath,
159
171
  codex_home: codexHome,
160
- ok: parseSmoke.ok && split.blockers.length === 0,
172
+ ok: parseSmoke.ok && split.blockers.length === 0 && writeBlockers.length === 0,
161
173
  changed,
162
174
  applied: opts.apply === true,
163
175
  backup_path: backupPath,
@@ -175,7 +187,11 @@ export async function splitCodexProjectConfigPolicy(rootInput = process.cwd(), o
175
187
  deprecated_approval_policy_fixed: split.deprecated_approval_policy_fixed,
176
188
  actions,
177
189
  parse_smoke: parseSmoke,
178
- blockers: [...split.blockers, ...(parseSmoke.ok ? [] : ['project_config_rewrite_parse_smoke_failed'])]
190
+ blockers: [
191
+ ...split.blockers,
192
+ ...writeBlockers,
193
+ ...(parseSmoke.ok ? [] : ['project_config_rewrite_parse_smoke_failed'])
194
+ ]
179
195
  };
180
196
  if (opts.writeReport !== false)
181
197
  await writeJsonAtomic(reportPath, { ...report, report_path: reportPath });
@@ -201,28 +217,36 @@ export async function repairCodexConfigStructure(configPathInput, opts = {}) {
201
217
  }
202
218
  const hoist = hoistMisplacedMachineLocalKeys(String(original));
203
219
  let backupPath = null;
220
+ let applied = false;
221
+ const writeBlockers = [];
204
222
  if (opts.apply && hoist.changed) {
205
223
  backupPath = `${configPath}.struct-bak-${Date.now().toString(36)}`;
206
224
  await ensureDir(path.dirname(configPath));
207
225
  await fsp.copyFile(configPath, backupPath);
208
226
  await cleanupCodexConfigBackups(configPath, { keepPerTag: 3, maxAgeMs: 30 * 24 * 60 * 60 * 1000 }).catch(() => undefined);
209
- await writeCodexConfigGuarded({
227
+ const guarded = await writeCodexConfigGuarded({
210
228
  configPath,
211
229
  before: String(original || ''),
212
230
  cause: 'codex-config-structure-repair',
213
231
  mutate: () => hoist.text
214
232
  });
233
+ applied = guarded.ok;
234
+ if (!guarded.ok)
235
+ writeBlockers.push(`config_write_guard:${guarded.status}`);
215
236
  }
216
237
  const parseSmoke = tomlRewriteSmoke(hoist.text);
217
238
  return {
218
239
  config_path: configPath,
219
- ok: parseSmoke.ok,
220
- status: hoist.changed ? (opts.apply ? 'structure_repaired' : 'structure_repair_available') : 'structure_ok',
240
+ ok: parseSmoke.ok && writeBlockers.length === 0,
241
+ status: hoist.changed
242
+ ? (applied ? 'structure_repaired' : opts.apply ? 'structure_repair_refused' : 'structure_repair_available')
243
+ : 'structure_ok',
221
244
  changed: hoist.changed,
222
- applied: opts.apply === true && hoist.changed,
245
+ applied,
223
246
  hoisted_keys: hoist.hoisted_keys,
224
247
  backup_path: backupPath,
225
- parse_smoke: parseSmoke
248
+ parse_smoke: parseSmoke,
249
+ blockers: writeBlockers
226
250
  };
227
251
  }
228
252
  function isCodexConfigToml(file) {
@@ -451,12 +475,6 @@ function tomlRewriteSmoke(text) {
451
475
  invalid_table_header: badHeader || null
452
476
  };
453
477
  }
454
- function profileTableBody(blocks, profile) {
455
- const block = blocks.find((item) => item.table === `profiles.${profile}`);
456
- if (!block)
457
- return '';
458
- return block.text.split('\n').filter((line) => !/^\s*\[/.test(line)).join('\n');
459
- }
460
478
  function configIds(text) {
461
479
  const ids = { keys: new Set(), tables: new Set() };
462
480
  for (const block of tomlBlocks(text)) {
@@ -512,6 +530,12 @@ function mergeMachineLocalIntoUserConfig(userText, machineText, commentLine) {
512
530
  }
513
531
  return normalizeProjectText(sections.join('\n\n'));
514
532
  }
533
+ function stripSksMovedComments(text) {
534
+ return normalizeProjectText(String(text || '')
535
+ .split('\n')
536
+ .filter((line) => !/^\s*#\s*SKS moved machine-local Codex config\b/i.test(line))
537
+ .join('\n'));
538
+ }
515
539
  function collectTomlSections(text, preamble, tables) {
516
540
  for (const block of tomlBlocks(text)) {
517
541
  if (block.table)
@@ -16,9 +16,14 @@ export async function postcheckCodexStartupConfig(input) {
16
16
  const rolePlan = await installOfficialSubagentAgentConfigs(root, { apply: false });
17
17
  const tomlSmoke = tomlSyntaxSmoke(text);
18
18
  const orphanChildTables = orphanMcpChildTables(text);
19
+ const multiAgentV2Disabled = officialConfig.multiAgentV2.enabled === false;
19
20
  const blockers = [
20
21
  ...(!configPresent ? ['project_official_subagent_config_missing'] : []),
21
22
  ...(!tomlValidation.ok ? ['project_official_subagent_config_toml_parse_failed'] : []),
23
+ ...(multiAgentV2Disabled ? ['official_subagent_multi_agent_v2_disabled'] : []),
24
+ ...(multiAgentV2Disabled && officialConfig.enabled === false
25
+ ? ['official_subagent_agents_disabled']
26
+ : []),
22
27
  ...officialConfig.blockers.map((item) => `official_subagent_config:${item}`),
23
28
  ...rolePlan.missing.map((file) => `missing_official_subagent_agent:${file}`),
24
29
  ...rolePlan.stale.map((file) => `stale_official_subagent_agent:${file}`),
@@ -160,6 +160,8 @@ function upsertTomlTable(text, table, block) {
160
160
  break;
161
161
  }
162
162
  }
163
+ while (end > start + 1 && !String(lines[end - 1] || '').trim())
164
+ end -= 1;
163
165
  lines.splice(start, end - start, ...blockLines);
164
166
  return lines.join('\n').replace(/\n{3,}/g, '\n\n');
165
167
  }
@@ -158,22 +158,25 @@ export function resolveBridgeRequestRoute(request, policy, pins) {
158
158
  const sessionId = request.session_id ? canonicalSessionId(request.session_id) : null;
159
159
  const pin = sessionId ? pins.find((entry) => entry.thread_id === sessionId) || null : null;
160
160
  if (pin && pin.public_model === publicModel) {
161
- if (pin.public_model !== publicModel
162
- || pin.catalog_generation !== policy.catalog_generation
163
- || pin.route_policy_generation !== policy.policy_generation) {
164
- throw new DesktopBridgeError('session_pin_route_unavailable');
165
- }
166
161
  const current = policy.model_routes[publicModel];
167
162
  if (!current || current.provider_id !== pin.provider_id || current.upstream_model !== pin.upstream_model) {
168
163
  throw new DesktopBridgeError('session_pin_route_unavailable');
169
164
  }
165
+ const aged = pin.catalog_generation !== policy.catalog_generation
166
+ || pin.route_policy_generation !== policy.policy_generation;
170
167
  return {
171
168
  provider_id: pin.provider_id,
172
169
  public_model: publicModel,
173
170
  upstream_model: pin.upstream_model,
174
- catalog_generation: pin.catalog_generation,
175
- route_policy_generation: pin.route_policy_generation,
176
- session_pin: pin,
171
+ catalog_generation: policy.catalog_generation,
172
+ route_policy_generation: policy.policy_generation,
173
+ session_pin: aged
174
+ ? {
175
+ ...pin,
176
+ catalog_generation: policy.catalog_generation,
177
+ route_policy_generation: policy.policy_generation,
178
+ }
179
+ : pin,
177
180
  };
178
181
  }
179
182
  const route = policy.model_routes[publicModel];
@@ -22,11 +22,13 @@ export function resolveBridgeRequestRoute(request, policy, options) {
22
22
  if (options.active_catalog_generation && options.active_catalog_generation !== policy.catalog_generation) {
23
23
  return blocked(base, 'catalog_route_index_stale');
24
24
  }
25
- const pin = request.thread_id ? findSessionPin(options.session_pins, request.thread_id) : null;
26
- if (pin && canonicalizeBridgeModelId(pin.public_model) === model) {
27
- const pinBlocker = validateSessionPin(pin, policy, options.route_index, options.registry);
28
- if (pinBlocker)
29
- return blocked(base, pinBlocker);
25
+ const threadPin = request.thread_id ? findSessionPin(options.session_pins, request.thread_id) : null;
26
+ const pin = threadPin && canonicalizeBridgeModelId(threadPin.public_model) === model ? threadPin : null;
27
+ const pinAffinity = pin ? sessionPinAffinity(pin, policy, options.route_index, options.registry) : null;
28
+ if (pinAffinity === 'unprovable')
29
+ return blocked(base, 'session_pin_route_unavailable');
30
+ const pinClaim = pinAffinity && pinAffinity !== 'honored' ? pinAffinity : null;
31
+ if (pin && pinAffinity === 'honored') {
30
32
  const route = { provider_id: pin.provider_id, upstream_model: pin.upstream_model };
31
33
  const providerBlocker = validateProviderRoute(route, request.requested_endpoint_origin, options.registry, options.route_index);
32
34
  if (providerBlocker)
@@ -51,6 +53,8 @@ export function resolveBridgeRequestRoute(request, policy, options) {
51
53
  return blocked(base, 'catalog_model_route_missing');
52
54
  if (!sameTarget(indexed, policyTarget))
53
55
  return blocked(base, 'bridge_route_policy_route_index_mismatch');
56
+ if (pinClaim && !sameTarget(indexed, pinClaim))
57
+ return blocked(base, 'session_pin_route_unavailable');
54
58
  const providerBlocker = validateProviderRoute(indexed, request.requested_endpoint_origin, options.registry, options.route_index);
55
59
  if (providerBlocker)
56
60
  return blocked(base, providerBlocker);
@@ -76,23 +80,22 @@ export function resolveBridgeRequestRoute(request, policy, options) {
76
80
  recovery_action: null
77
81
  };
78
82
  }
79
- function validateSessionPin(pin, policy, routeIndex, registry) {
80
- if (pin.catalog_generation !== policy.catalog_generation
81
- || pin.route_policy_generation !== policy.policy_generation) {
82
- return 'session_pin_route_unavailable';
83
- }
84
- if (!isProviderId(pin.provider_id) || !registry.profiles[pin.provider_id]) {
85
- return 'session_pin_route_unavailable';
86
- }
83
+ function sessionPinAffinity(pin, policy, routeIndex, registry) {
84
+ if (!isProviderId(pin.provider_id) || !registry.profiles[pin.provider_id])
85
+ return 'unprovable';
87
86
  const model = canonicalizeBridgeModelId(pin.public_model);
88
87
  const upstream = normalizeBridgeUpstreamModelId(pin.upstream_model);
89
88
  if (!model || !upstream)
90
- return 'session_pin_route_unavailable';
91
- const indexed = routeIndex.routes[model] || routeIndex.routes[`${pin.provider_id}:${model}`];
92
- if (!indexed || indexed.provider_id !== pin.provider_id || indexed.upstream_model !== upstream) {
93
- return 'session_pin_route_unavailable';
89
+ return 'unprovable';
90
+ const claim = { provider_id: pin.provider_id, upstream_model: upstream };
91
+ if (pin.catalog_generation !== policy.catalog_generation
92
+ || pin.route_policy_generation !== policy.policy_generation) {
93
+ return claim;
94
94
  }
95
- return null;
95
+ const indexed = routeIndex.routes[model] || routeIndex.routes[`${pin.provider_id}:${model}`];
96
+ if (!indexed || !sameTarget(indexed, claim))
97
+ return claim;
98
+ return 'honored';
96
99
  }
97
100
  function validateProviderRoute(route, requestedOrigin, registry, routeIndex) {
98
101
  if (!isProviderId(route.provider_id))
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import os from 'node:os';
2
3
  import path from 'node:path';
3
4
  import { spawnSync } from 'node:child_process';
4
5
  import { MANAGED_ASSET_VERSION } from '../managed-assets/managed-assets-manifest.js';
@@ -104,7 +105,21 @@ function phaseInputHash(root, id) {
104
105
  return { rel, hash: 'missing' };
105
106
  return hashSemanticPath(root, rel);
106
107
  });
107
- return hashJson({ id, package: packageIdentity(), files, env: phaseEnvPresence(id), semantic_state: phaseSemanticState(root, id), phase_schema_version: 2 });
108
+ return hashJson({
109
+ id,
110
+ package: packageIdentity(),
111
+ files,
112
+ env: phaseEnvPresence(id),
113
+ semantic_state: phaseSemanticState(root, id),
114
+ project_codex_config: hashSemanticPath(root, '.codex/config.toml').hash,
115
+ codex_home_config: hashFileIfExists(codexHomeConfigPath()),
116
+ phase_schema_version: 3
117
+ });
118
+ }
119
+ function codexHomeConfigPath() {
120
+ const codexHome = process.env.CODEX_HOME
121
+ || path.join(process.env.HOME || os.homedir(), '.codex');
122
+ return path.join(codexHome, 'config.toml');
108
123
  }
109
124
  function phaseInputFiles(id) {
110
125
  if (id.includes('context7'))
@@ -17,6 +17,8 @@ export async function runDoctorIdempotence(root) {
17
17
  blockers.push(`second_doctor_exit_${second.exit_code}`);
18
18
  if (changedFilesSecond.length > 0)
19
19
  blockers.push('second_doctor_not_noop');
20
+ if (!reportsChangedFiles(second))
21
+ blockers.push('second_doctor_changed_files_unavailable');
20
22
  const report = {
21
23
  schema: 'sks.doctor-idempotence.v1',
22
24
  ok: blockers.length === 0,
@@ -60,6 +62,15 @@ async function runDoctor(root, home, codexHome) {
60
62
  parsed
61
63
  };
62
64
  }
65
+ function reportsChangedFiles(run) {
66
+ if (!run.parsed)
67
+ return false;
68
+ return [
69
+ run.parsed?.repair?.doctor_transaction?.changed_files,
70
+ run.parsed?.doctor_fix_transaction?.changed_files,
71
+ run.parsed?.changed_files
72
+ ].some((value) => Array.isArray(value));
73
+ }
63
74
  function extractChangedFiles(run) {
64
75
  const repair = run.parsed?.repair || {};
65
76
  const candidates = [
@@ -1,4 +1,53 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { inspectOfficialSubagentToml, readOfficialSubagentConfig } from '../subagents/official-subagent-config.js';
1
5
  import {} from './doctor-transaction.js';
6
+ export async function verifyCodexConfigsOnDisk(input) {
7
+ const root = path.resolve(input.root);
8
+ const home = input.home || process.env.HOME || os.homedir();
9
+ const codexHome = input.codexHome || process.env.CODEX_HOME || path.join(home, '.codex');
10
+ const projectConfigPath = path.join(root, '.codex', 'config.toml');
11
+ const codexHomeConfigPath = path.join(codexHome, 'config.toml');
12
+ const blockers = [];
13
+ const [projectText, homeText] = await Promise.all([
14
+ fs.readFile(projectConfigPath, 'utf8').catch(() => null),
15
+ fs.readFile(codexHomeConfigPath, 'utf8').catch(() => null)
16
+ ]);
17
+ if (projectText !== null && !inspectOfficialSubagentToml(projectText).ok) {
18
+ blockers.push('project_codex_config_unparseable_after_repair');
19
+ }
20
+ if (homeText !== null && !inspectOfficialSubagentToml(homeText).ok) {
21
+ blockers.push('codex_home_config_unparseable_after_repair');
22
+ }
23
+ let multiAgentV2Enabled = null;
24
+ let agentsEnabled = null;
25
+ if (!blockers.length) {
26
+ const config = await readOfficialSubagentConfig(root, { home, codexHome }).catch(() => null);
27
+ if (config) {
28
+ multiAgentV2Enabled = config.multiAgentV2.enabled;
29
+ agentsEnabled = config.enabled;
30
+ for (const blocker of config.blockers)
31
+ blockers.push(`official_subagent_config:${blocker}`);
32
+ if (multiAgentV2Enabled === false) {
33
+ blockers.push('official_subagent_multi_agent_v2_disabled_after_repair');
34
+ if (agentsEnabled === false)
35
+ blockers.push('official_subagent_agents_disabled_after_repair');
36
+ }
37
+ }
38
+ }
39
+ return {
40
+ schema: 'sks.doctor-config-disk-verification.v1',
41
+ ok: blockers.length === 0,
42
+ project_config_path: projectConfigPath,
43
+ codex_home_config_path: codexHomeConfigPath,
44
+ project_config_present: projectText !== null,
45
+ codex_home_config_present: homeText !== null,
46
+ multi_agent_v2_enabled: multiAgentV2Enabled,
47
+ agents_enabled: agentsEnabled,
48
+ blockers: [...new Set(blockers)]
49
+ };
50
+ }
2
51
  export function doctorRepairPostcheck(transaction) {
3
52
  const phases = transaction?.phases || [];
4
53
  const requiredBlockers = phases
@@ -3,6 +3,47 @@ import { nowIso, writeJsonAtomic } from '../fsx.js';
3
3
  import { isDoctorPhaseClean, markDoctorPhaseClean } from './doctor-dirty-planner.js';
4
4
  import { ui as cliUi } from '../../cli/cli-theme.js';
5
5
  import { messageOf } from '../errors/message.js';
6
+ const CHANGED_PATH_KEYS = Object.freeze([
7
+ 'repaired_paths', 'created_files', 'created', 'updated', 'changed_files',
8
+ 'written_files', 'removed_files', 'removed', 'quarantined', 'backups'
9
+ ]);
10
+ export function changedFilesFromRepairReport(report) {
11
+ const found = new Set();
12
+ const walk = (value, depth) => {
13
+ if (depth > 6 || !value || typeof value !== 'object')
14
+ return;
15
+ if (Array.isArray(value)) {
16
+ for (const item of value)
17
+ walk(item, depth + 1);
18
+ return;
19
+ }
20
+ const row = value;
21
+ for (const key of CHANGED_PATH_KEYS) {
22
+ const entries = row[key];
23
+ if (Array.isArray(entries)) {
24
+ for (const entry of entries) {
25
+ const text = String(entry || '').trim();
26
+ if (text)
27
+ found.add(text);
28
+ }
29
+ }
30
+ else if (typeof entries === 'string' && entries.trim()) {
31
+ found.add(entries.trim());
32
+ }
33
+ }
34
+ if (row.changed === true || row.applied === true) {
35
+ for (const key of ['config_path', 'backup_path', 'path', 'file']) {
36
+ const text = String(row[key] || '').trim();
37
+ if (text)
38
+ found.add(text);
39
+ }
40
+ }
41
+ for (const nested of Object.values(row))
42
+ walk(nested, depth + 1);
43
+ };
44
+ walk(report, 0);
45
+ return [...found].sort();
46
+ }
6
47
  export async function runDoctorFixTransaction(input) {
7
48
  const startedAt = nowIso();
8
49
  const phases = [];
@@ -110,7 +151,8 @@ export async function writeDoctorFixTransaction(input) {
110
151
  started_at: phase.started_at || null,
111
152
  completed_at: phase.completed_at || null,
112
153
  duration_ms: Number.isFinite(phase.duration_ms) ? Number(phase.duration_ms) : null,
113
- rollback_performed: phase.rollback_performed === true
154
+ rollback_performed: phase.rollback_performed === true,
155
+ changed_files: [...new Set(phase.changed_files || [])].sort()
114
156
  }));
115
157
  const postcheckOk = phases.every((phase) => phase.ok || phase.required_for_ready === false);
116
158
  const mutationsWithoutRollback = phases.filter((phase) => phase.required_for_ready && phase.repaired && !phase.rollback_evidence).length;
@@ -124,6 +166,7 @@ export async function writeDoctorFixTransaction(input) {
124
166
  postcheck_ok: postcheckOk && mutationsWithoutRollback === 0,
125
167
  rollback_performed: input.rollbackPerformed === true,
126
168
  mutations_without_rollback: mutationsWithoutRollback,
169
+ changed_files: [...new Set(phases.flatMap((phase) => phase.changed_files))].sort(),
127
170
  raw_secret_values_recorded: false,
128
171
  skipped_clean_phases: phases.filter((phase) => phase.warnings.some((warning) => warning.startsWith('dirty_plan_skipped_clean_phase'))).map((phase) => phase.id),
129
172
  dirty_phases: input.dirtyPlan?.phases.filter((phase) => phase.status === 'dirty').map((phase) => phase.id) || phases.filter((phase) => !phase.warnings.some((warning) => warning.startsWith('dirty_plan_skipped_clean_phase'))).map((phase) => phase.id),
@@ -158,7 +201,9 @@ function normalizePhase(definition, result, fallback, startedMs) {
158
201
  started_at: phase.started_at || fallback.started_at || nowIso(),
159
202
  completed_at: phase.completed_at || nowIso(),
160
203
  duration_ms: phase.duration_ms ?? Math.max(0, Date.now() - startedMs),
161
- rollback_performed: phase.rollback_performed === true
204
+ rollback_performed: phase.rollback_performed === true,
205
+ changed_files: phase.changed_files
206
+ ?? (definition.report ? changedFilesFromRepairReport(definition.report()) : [])
162
207
  };
163
208
  }
164
209
  function mergePhase(phase, update) {
@@ -144,7 +144,7 @@ function projectSksHookCommand(value) {
144
144
  const command = String(value || '').trim();
145
145
  if (/[;&|`$<>\r\n]/.test(command))
146
146
  return null;
147
- return command.match(/^node\s+\.\/dist\/bin\/sks\.js\s+hook\s+([a-z0-9-]+)$/i)?.[1] || null;
147
+ return command.match(/^(?:\S*[\\/])?(?:node\s+\S*sks\.js|sks)\s+hook\s+([a-z0-9-]+)$/i)?.[1] || null;
148
148
  }
149
149
  function hookRef(event, matcher, command) {
150
150
  return `${event}:${String(matcher ?? '')}:${command}`;
package/dist/core/init.js CHANGED
@@ -11,13 +11,15 @@ import { OFFICIAL_SUBAGENT_REVIEW_POLICY_TEXT } from './official-subagent-review
11
11
  import { AWESOME_DESIGN_MD_REFERENCE, CODEX_APP_IMAGE_GENERATION_DOC_URL, CODEX_COMPUTER_USE_ONLY_POLICY, CODEX_IMAGEGEN_REQUIRED_POLICY, CODEX_WEB_VERIFICATION_POLICY, DEFAULT_CODEX_APP_PLUGINS, DESIGN_SYSTEM_SSOT, DOLLAR_COMMANDS, DOLLAR_COMMAND_ALIASES, DOLLAR_SKILL_NAMES, FROM_CHAT_IMG_CHECKLIST_ARTIFACT, FROM_CHAT_IMG_COVERAGE_ARTIFACT, FROM_CHAT_IMG_QA_LOOP_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_ARTIFACT, FROM_CHAT_IMG_TEMP_TRIWIKI_SESSIONS, GETDESIGN_REFERENCE, IMAGEGEN_SOCIAL_SOURCE_POLICY, LEGACY_DOLLAR_SKILL_NAMES, OPENAI_CHATGPT_IMAGES_2_DOC_URL, OPENAI_GPT_IMAGE_2_MODEL_DOC_URL, OPENAI_IMAGE_GENERATION_DOC_URL, PPT_CONDITIONAL_SKILL_ALLOWLIST, PPT_PIPELINE_MCP_ALLOWLIST, PPT_PIPELINE_SKILL_ALLOWLIST, RECOMMENDED_DESIGN_REFERENCES, RECOMMENDED_MCP_SERVERS, RECOMMENDED_SKILLS, RESERVED_CODEX_PLUGIN_SKILL_NAMES, SOLUTION_SCOUT_SKILL_NAME, chatCaptureIntakeText, context7ConfigToml, getdesignReferencePolicyText, imageUxReviewPipelinePolicyText, outcomeRubricPolicyText, pptPipelineAllowlistPolicyText, prefixKnownSksDollarReferences, productDesignPluginPolicyText, sksPrefixedDollarCommand, speedLanePolicyText, stackCurrentDocsPolicyText, triwikiContextTracking, triwikiContextTrackingText, triwikiStagePolicyText } from './routes.js';
12
12
  import { SKILL_DREAM_POLICY, skillDreamPolicyText } from './skill-forge.js';
13
13
  import { CODEX_HOOK_EVENT_STATE_KEYS } from './codex-compat/codex-hook-events.js';
14
+ import { MANAGED_CODEX_FEATURE_FLAGS, REMOVED_CODEX_FEATURE_FLAGS } from './codex/codex-feature-flags.js';
15
+ import { writeCodexConfigGuarded } from './codex/codex-config-guard.js';
14
16
  import { codexCommandHookCurrentHash } from './codex-hooks/codex-hook-hash.js';
15
17
  import { buildSksCoreSkillManifest, isCoreSkillName, legacyCoreSkillNames } from './codex-native/core-skill-manifest.js';
16
18
  import { syncCoreSkillsIntegrity } from './codex-native/core-skill-integrity.js';
17
19
  import { AUTHORITATIVE_SKS_SKILL_ROOT_REFERENCE } from './codex-native/sks-skill-paths.js';
18
20
  import { currentGeneratedFileInventory, installCodexAgents, pruneStaleGeneratedFiles, REMOVED_SKS_SKILL_NAMES } from './init/skills.js';
19
21
  import { reconcileManagedSkillInstallation } from './init/managed-skill-install.js';
20
- import { backupInvalidToml, inspectOfficialSubagentToml, mergeOfficialSubagentConfig, officialSubagentConfigOwnershipProof, officialSubagentConfigWarnings, readInheritedOfficialSubagentConfigText, resolveInheritedOfficialSubagentConfigPath } from './subagents/official-subagent-config.js';
22
+ import { backupInvalidToml, inspectOfficialSubagentToml, mergeOfficialSubagentConfig, officialSubagentConfigOwnershipProof, officialSubagentConfigWarnings, readInheritedOfficialSubagentConfigText, resolveInheritedOfficialSubagentConfigPath, stampSksManagedCodexConfigMarker } from './subagents/official-subagent-config.js';
21
23
  import { escapeRegExp } from './text/regex.js';
22
24
  export { installGlobalSkills, installProjectSkills, installSkills } from './init/skills.js';
23
25
  const REFLECTION_MEMORY_PATH = '.sneakoscope/memory/q2_facts/post-route-reflection.md';
@@ -206,6 +208,8 @@ function upsertCodexTrustTomlTable(text, table, block) {
206
208
  break;
207
209
  }
208
210
  }
211
+ while (end > start + 1 && !String(lines[end - 1] || '').trim())
212
+ end -= 1;
209
213
  lines.splice(start, end - start, ...blockLines);
210
214
  return lines.join('\n').replace(/\n{3,}/g, '\n\n');
211
215
  }
@@ -631,14 +635,11 @@ export async function initProject(root, opts = {}) {
631
635
  project_install: 'npm i -D sneakoscope && npx sks setup --install-scope project'
632
636
  };
633
637
  }
634
- const MANAGED_CODEX_FEATURE_FLAGS = ['hooks', 'fast_mode', 'apps'];
635
- const REMOVED_CODEX_FEATURE_FLAGS = [
636
- 'remote_control', 'fast_mode_ui', 'codex_git_commit', 'computer_use', 'browser_use',
637
- 'browser_use_external', 'image_generation', 'in_app_browser', 'guardian_approval',
638
- 'tool_suggest', 'plugins', 'codex_hooks', 'multi_agent'
639
- ];
640
638
  function mergeManagedCodexConfigToml(existingContent = '', opts = {}) {
641
639
  let next = String(existingContent || '').trimEnd();
640
+ if (opts.sksOwned === true || opts.configWasFresh === true) {
641
+ next = stampSksManagedCodexConfigMarker(next);
642
+ }
642
643
  next = removeTomlTableKey(next, 'notice', 'fast_default_opt_out');
643
644
  next = removeTomlTableKey(next, 'features', 'codex_hooks');
644
645
  next = upsertTopLevelTomlBooleanIfAbsent(next, 'suppress_unstable_features_warning', true);
@@ -918,6 +919,7 @@ export async function initProject(root, opts = {}) {
918
919
  else {
919
920
  const managedCodexConfig = await mergeGlobalCodexConfigIfAvailable(mergeManagedCodexConfigToml(existingCodexConfig, {
920
921
  sksOwned: configPreviouslySksOwned,
922
+ configWasFresh,
921
923
  inheritedText: inheritedCodexConfig
922
924
  }), generatedCodexConfigPath, { home: opts.home, codexHome: opts.codexHome });
923
925
  const managedConfigValidation = inspectOfficialSubagentToml(managedCodexConfig);
@@ -934,16 +936,37 @@ export async function initProject(root, opts = {}) {
934
936
  created.push('.codex/config.toml merge skipped (manual repair required)');
935
937
  }
936
938
  else {
937
- await writeTextAtomic(generatedCodexConfigPath, managedCodexConfig);
938
- codexConfigInstall = {
939
- ok: true,
940
- status: managedCodexConfig === existingCodexConfig ? 'present' : 'written',
941
- config_path: generatedCodexConfigPath,
942
- backup_path: null,
943
- manual_blockers: [],
944
- warnings: officialSubagentConfigWarnings(managedCodexConfig, inheritedCodexConfig)
945
- };
946
- created.push('.codex/config.toml');
939
+ const guarded = await writeCodexConfigGuarded({
940
+ root,
941
+ configPath: generatedCodexConfigPath,
942
+ before: existingCodexConfig,
943
+ cause: 'sks-init-managed-project-config',
944
+ ownershipVerified: configWasFresh || configPreviouslySksOwned,
945
+ preserveTextFormatting: true,
946
+ mutate: () => managedCodexConfig
947
+ });
948
+ codexConfigInstall = guarded.ok
949
+ ? {
950
+ ok: true,
951
+ status: managedCodexConfig === existingCodexConfig ? 'present' : 'written',
952
+ config_path: generatedCodexConfigPath,
953
+ backup_path: guarded.backup_path,
954
+ manual_blockers: [],
955
+ warnings: officialSubagentConfigWarnings(managedCodexConfig, inheritedCodexConfig)
956
+ }
957
+ : {
958
+ ok: false,
959
+ status: guarded.status,
960
+ config_path: generatedCodexConfigPath,
961
+ backup_path: guarded.backup_path,
962
+ manual_blockers: [`manual_unmanaged_project_codex_config:${guarded.status}`],
963
+ warnings: [],
964
+ operator_actions: [
965
+ `${generatedCodexConfigPath} is not proven SKS-owned, so setup left it unchanged. `
966
+ + 'Run `sks config adopt` to bring it under management, or delete it and re-run `sks setup`.'
967
+ ]
968
+ };
969
+ created.push(guarded.ok ? '.codex/config.toml' : '.codex/config.toml preserved (user-owned)');
947
970
  }
948
971
  }
949
972
  }
@@ -148,6 +148,7 @@ export function buildPipelinePlan(input = {}) {
148
148
  verification,
149
149
  engineering_sanity_review: null,
150
150
  architecture_map: null,
151
+ architecture_map_unavailable: null,
151
152
  invariants,
152
153
  proof_field: proof,
153
154
  ssot_guard: buildSsotGuard({ route: route?.id || 'SKS', mode: route?.mode || 'SKS', task }),
@@ -187,6 +188,13 @@ export async function writePipelinePlan(dir, input = {}) {
187
188
  }
188
189
  if (planStagesArchitectureMap(plan)) {
189
190
  plan.architecture_map = createArchitectureMapPlanBinding();
191
+ const seeded = await maybeSeedArchitectureMapForPlan({
192
+ root, dir, plan, missionId: input.missionId, taskProfile
193
+ });
194
+ if (seeded && seeded.ok === false) {
195
+ plan.architecture_map = null;
196
+ plan.architecture_map_unavailable = { reason: seeded.reason || 'architecture_map_baseline_unavailable' };
197
+ }
190
198
  }
191
199
  await writeJsonAtomic(path.join(dir, PIPELINE_PLAN_ARTIFACT), plan);
192
200
  if (plan.engineering_sanity_review) {
@@ -201,9 +209,6 @@ export async function writePipelinePlan(dir, input = {}) {
201
209
  await writeJsonAtomic(reviewFile, createEngineeringSanityReviewSeed(plan.route?.command || plan.route?.id || null, report, reportText));
202
210
  }
203
211
  }
204
- await maybeSeedArchitectureMapForPlan({
205
- root, dir, plan, missionId: input.missionId, taskProfile
206
- });
207
212
  return plan;
208
213
  }
209
214
  function planStagesEngineeringSanityReview(plan) {
@@ -100,7 +100,8 @@ export function officialSubagentFanoutPolicy(input = {}) {
100
100
  ...(input.suggestedRoles === undefined ? {} : { suggestedRoles: input.suggestedRoles }),
101
101
  ...(input.independentSliceCount === undefined ? {} : { independentSliceCount: input.independentSliceCount }),
102
102
  ...(input.maxThreads === undefined ? {} : { maxThreads: input.maxThreads }),
103
- ...(input.hardware === undefined ? {} : { hardware: input.hardware })
103
+ ...(input.hardware === undefined ? {} : { hardware: input.hardware }),
104
+ ...(input.requiresWrite === undefined ? {} : { requiresWrite: input.requiresWrite })
104
105
  });
105
106
  const requested = explicit ? explicitRequested : automatic.count;
106
107
  const automaticReviewerCeiling = automatic.criticalMultiDomain
@@ -141,7 +142,8 @@ function automaticSubagentFanout(input) {
141
142
  const critical = CRITICAL_RISK_RE.test(text);
142
143
  const largeScale = LARGE_SCALE_WORK_RE.test(text);
143
144
  const criticalMultiDomain = highRisk && critical && riskDomains.length >= 3;
144
- const reviewerOnly = isReviewerOnlyFanout(input.suggestedRoles || []);
145
+ const reviewerOnly = isReviewerOnlyFanout(input.suggestedRoles || [])
146
+ && input.requiresWrite !== true;
145
147
  const massLane = decideSubagentModel({ description: text }).policy;
146
148
  const massParallel = MASS_PARALLEL_WORK_RE.test(text)
147
149
  && !NON_CHEAP_MASS_ACTION_RE.test(text)
@@ -20,17 +20,23 @@ export const LEGACY_SKS_MAX_THREAD_VALUES = Object.freeze([4, 5, 6, 12]);
20
20
  export const AGENTS_MAX_CONCURRENT_THREADS_KEY = 'max_concurrent_threads_per_session';
21
21
  export const LEGACY_AGENTS_MAX_THREADS_KEY = 'max_threads';
22
22
  export const LEGACY_AGENTS_JOB_MAX_RUNTIME_KEY = 'job_max_runtime_seconds';
23
+ export const SKS_MANAGED_CODEX_CONFIG_MARKER = '# SKS-MANAGED-CODEX-CONFIG';
23
24
  export function mergeOfficialSubagentConfig(text = '', opts = {}) {
25
+ return mergeOfficialSubagentConfigResult(text, opts).text;
26
+ }
27
+ export function mergeOfficialSubagentConfigResult(text = '', opts = {}) {
24
28
  const source = String(text || '');
25
- if (!inspectToml(source).ok)
26
- return source;
29
+ if (!inspectToml(source).ok) {
30
+ return { text: source, blockers: ['project_official_subagent_config_toml_parse_failed'] };
31
+ }
27
32
  const inherited = parsedToml(opts.inheritedText || '');
28
33
  const inheritedAgents = objectValue(inherited?.agents);
29
34
  const inheritedFeatures = objectValue(inherited?.features);
30
35
  let next = source.trimEnd();
31
36
  if (!next.trim())
32
- next = '# SKS-MANAGED-CODEX-CONFIG';
37
+ next = SKS_MANAGED_CODEX_CONFIG_MARKER;
33
38
  if (opts.sksOwned === true) {
39
+ next = stampManagedConfigMarker(next);
34
40
  next = removeExactLegacyManagedAgentBlocks(next);
35
41
  next = stripLegacyUnsupportedAgentKeys(next);
36
42
  }
@@ -48,10 +54,12 @@ export function mergeOfficialSubagentConfig(text = '', opts = {}) {
48
54
  next = mergeOfficialMultiAgentV2FeatureConfig(next, {
49
55
  sksOwned: opts.sksOwned === true,
50
56
  inheritedFeatures,
51
- maxThreads: readAgentsMaxThreads(next) || targetMaxThreads
57
+ maxThreads: Math.min(readAgentsMaxThreads(next) || targetMaxThreads, HARD_NARUTO_MAX_THREADS)
52
58
  });
53
59
  const merged = ensureTrailingNewline(next);
54
- return inspectToml(merged).ok ? merged : source;
60
+ if (inspectToml(merged).ok)
61
+ return { text: merged, blockers: [] };
62
+ return { text: source, blockers: ['official_subagent_config_merge_produced_invalid_toml'] };
55
63
  }
56
64
  export function mergeOfficialMultiAgentV2FeatureConfig(text = '', opts = {}) {
57
65
  const source = String(text || '');
@@ -61,18 +69,21 @@ export function mergeOfficialMultiAgentV2FeatureConfig(text = '', opts = {}) {
61
69
  const inheritedMaV2 = featureTomlObject(inheritedFeatures.multi_agent_v2);
62
70
  let next = source.trimEnd();
63
71
  if (!next.trim())
64
- next = '# SKS-MANAGED-CODEX-CONFIG';
65
- if (hasTomlTableKey(next, 'features', 'multi_agent_v2')) {
72
+ next = SKS_MANAGED_CODEX_CONFIG_MARKER;
73
+ if (hasUnmanageableMultiAgentV2Declaration(next))
74
+ return ensureTrailingNewline(next);
75
+ const booleanForm = readTomlTableBoolean(next, 'features', 'multi_agent_v2');
76
+ if (booleanForm !== null) {
66
77
  next = removeTomlTableKey(next, 'features', 'multi_agent_v2');
67
78
  }
68
79
  const maxThreads = boundedManagedMaxThreads(opts.maxThreads, DEFAULT_OFFICIAL_SUBAGENT_MAX_THREADS);
69
80
  const targetTotal = Math.max(1, maxThreads + 1);
70
81
  if (!hasTomlTable(next, 'features.multi_agent_v2')) {
71
- if (inheritedMaV2)
82
+ if (inheritedMaV2 && booleanForm === null)
72
83
  return ensureTrailingNewline(next);
73
84
  next = upsertTomlTable(next, 'features.multi_agent_v2', [
74
85
  '[features.multi_agent_v2]',
75
- 'enabled = true',
86
+ `enabled = ${booleanForm === null ? true : booleanForm}`,
76
87
  `max_concurrent_threads_per_session = ${targetTotal}`,
77
88
  'expose_spawn_agent_model_overrides = true'
78
89
  ].join('\n'));
@@ -81,7 +92,7 @@ export function mergeOfficialMultiAgentV2FeatureConfig(text = '', opts = {}) {
81
92
  if (opts.sksOwned === true) {
82
93
  next = upsertTomlTableKey(next, 'features.multi_agent_v2', 'enabled = true');
83
94
  const currentTotal = readTomlTableInteger(next, 'features.multi_agent_v2', 'max_concurrent_threads_per_session');
84
- if (currentTotal === DEFAULT_NARUTO_LEGACY_TOTAL_THREADS || currentTotal === null) {
95
+ if (currentTotal === null || LEGACY_MANAGED_V2_TOTAL_THREADS.includes(currentTotal)) {
85
96
  next = upsertTomlTableKey(next, 'features.multi_agent_v2', `max_concurrent_threads_per_session = ${targetTotal}`);
86
97
  }
87
98
  if (!hasTomlTableKey(next, 'features.multi_agent_v2', 'expose_spawn_agent_model_overrides')) {
@@ -105,6 +116,10 @@ export function mergeOfficialMultiAgentV2FeatureConfig(text = '', opts = {}) {
105
116
  return ensureTrailingNewline(next);
106
117
  }
107
118
  const DEFAULT_NARUTO_LEGACY_TOTAL_THREADS = 13;
119
+ const LEGACY_MANAGED_V2_TOTAL_THREADS = Object.freeze([
120
+ ...LEGACY_SKS_MAX_THREAD_VALUES.map((value) => value + 1),
121
+ DEFAULT_NARUTO_LEGACY_TOTAL_THREADS
122
+ ]);
108
123
  export async function readOfficialSubagentConfig(root, opts = {}) {
109
124
  const projectConfigPath = path.resolve(opts.projectConfigPath || path.join(root, '.codex', 'config.toml'));
110
125
  const home = opts.home || process.env.HOME || os.homedir();
@@ -173,6 +188,9 @@ export function officialSubagentConfigWarnings(text = '', inheritedText = '') {
173
188
  ? [`official_subagent_max_depth_coerced_to_one:${maxDepth.value}:${maxDepth.source}`]
174
189
  : []),
175
190
  ...capacityNormalizationWarnings(maxThreads, multiAgentV2),
191
+ ...(hasUnmanageableMultiAgentV2Declaration(text)
192
+ ? ['project_multi_agent_v2_declaration_form_unmanaged']
193
+ : []),
176
194
  ...project.legacyWarnings,
177
195
  ...inherited.legacyWarnings
178
196
  ];
@@ -315,6 +333,8 @@ export function officialSubagentConfigOwnershipProof(input = {}) {
315
333
  }
316
334
  if (hasExactManagedConfigMarker(text))
317
335
  reasons.push('managed_marker_or_hash');
336
+ if (hasManagedAgentsConfigFingerprint(text))
337
+ reasons.push('managed_agents_config_fingerprint');
318
338
  if (migrationReceiptProvesManagedMaxThreads(input.migrationReceipt)) {
319
339
  reasons.push('migration_receipt:agents.max_concurrent_threads_per_session');
320
340
  }
@@ -482,7 +502,38 @@ function featureTomlObject(value) {
482
502
  return Object.keys(row).length ? row : null;
483
503
  }
484
504
  function hasExactManagedConfigMarker(text) {
485
- return /^(?:#\s*SKS-MANAGED-CODEX-CONFIG\b|#\s*SKS managed Codex config\b|#\s*sks_managed_(?:body_)?sha256\s*=\s*["'][a-f0-9]{64}["'])/mi.test(text);
505
+ return /^\s*(?:#\s*SKS-MANAGED-CODEX-CONFIG\b|#\s*SKS managed Codex config\b|#\s*sks_managed_(?:body_)?sha256\s*=\s*["'][a-f0-9]{64}["'])/mi.test(text);
506
+ }
507
+ export function stampSksManagedCodexConfigMarker(text) {
508
+ return stampManagedConfigMarker(text);
509
+ }
510
+ function stampManagedConfigMarker(text) {
511
+ const source = String(text || '');
512
+ if (hasExactManagedConfigMarker(source))
513
+ return source;
514
+ const body = source.replace(/^\n+/, '');
515
+ return body ? `${SKS_MANAGED_CODEX_CONFIG_MARKER}\n${body}` : SKS_MANAGED_CODEX_CONFIG_MARKER;
516
+ }
517
+ const MANAGED_AGENTS_FINGERPRINT_KEYS = Object.freeze([
518
+ 'enabled',
519
+ 'max_depth',
520
+ 'interrupt_message',
521
+ 'default_subagent_model',
522
+ 'default_subagent_reasoning_effort'
523
+ ]);
524
+ export function hasManagedAgentsConfigFingerprint(text) {
525
+ const parsed = parsedToml(text);
526
+ if (!parsed)
527
+ return false;
528
+ const agents = objectValue(parsed.agents);
529
+ if (!MANAGED_AGENTS_FINGERPRINT_KEYS.every((key) => hasOwn(agents, key)))
530
+ return false;
531
+ if (readAgentsMaxThreadsFromRecord(agents) === undefined)
532
+ return false;
533
+ if (agents.max_depth !== DEFAULT_OFFICIAL_SUBAGENT_MAX_DEPTH)
534
+ return false;
535
+ const multiAgentV2 = featureTomlObject(objectValue(parsed.features).multi_agent_v2);
536
+ return multiAgentV2 !== null && hasOwn(multiAgentV2, 'expose_spawn_agent_model_overrides');
486
537
  }
487
538
  function migrationReceiptProvesManagedMaxThreads(value) {
488
539
  const root = objectValue(value);
@@ -676,11 +727,37 @@ function upsertDefaultUnlessInherited(text, inheritedAgents, key, line) {
676
727
  }
677
728
  function readTomlTableInteger(text, table, key) {
678
729
  const line = tomlTableKeyLine(text, table, key);
679
- const match = line?.match(/^\s*[^=]+\s*=\s*([0-9]+)\s*(?:#.*)?$/);
680
- if (!match?.[1])
730
+ const raw = line?.match(/^\s*[^=]+\s*=\s*([^#]+?)\s*(?:#.*)?$/)?.[1];
731
+ return parseTomlInteger(raw);
732
+ }
733
+ function parseTomlInteger(raw) {
734
+ const value = String(raw ?? '').trim().replace(/_/g, '');
735
+ const match = value.match(/^([+-]?)(?:0x([0-9a-fA-F]+)|0o([0-7]+)|0b([01]+)|([0-9]+))$/);
736
+ if (!match)
681
737
  return null;
682
- const value = Number(match[1]);
683
- return Number.isSafeInteger(value) ? value : null;
738
+ const magnitude = match[2] !== undefined
739
+ ? Number.parseInt(match[2], 16)
740
+ : match[3] !== undefined
741
+ ? Number.parseInt(match[3], 8)
742
+ : match[4] !== undefined
743
+ ? Number.parseInt(match[4], 2)
744
+ : Number(match[5]);
745
+ const parsed = (match[1] === '-' ? -1 : 1) * magnitude;
746
+ return Number.isSafeInteger(parsed) ? parsed : null;
747
+ }
748
+ function hasUnmanageableMultiAgentV2Declaration(text) {
749
+ const parsed = parsedToml(text);
750
+ if (!parsed)
751
+ return false;
752
+ if (!hasOwn(objectValue(parsed.features), 'multi_agent_v2'))
753
+ return false;
754
+ return !hasTomlTable(text, 'features.multi_agent_v2')
755
+ && readTomlTableBoolean(text, 'features', 'multi_agent_v2') === null;
756
+ }
757
+ function readTomlTableBoolean(text, table, key) {
758
+ const line = tomlTableKeyLine(text, table, key);
759
+ const match = line?.match(/^\s*[^=]+\s*=\s*(true|false)\s*(?:#.*)?$/);
760
+ return match?.[1] ? match[1] === 'true' : null;
684
761
  }
685
762
  function hasTomlTableKey(text, table, key) {
686
763
  return tomlTableKeyLine(text, table, key) !== null;
@@ -107,6 +107,7 @@ async function prepareOfficialSubagentMissionLocked(input) {
107
107
  taskProfile,
108
108
  suggestedRoles: suggestedAgents,
109
109
  goal,
110
+ requiresWrite: input.readOnly !== true,
110
111
  maxThreads: input.maxThreads ?? officialConfig.maxThreads,
111
112
  ...(input.hardware ? { hardware: input.hardware } : {}),
112
113
  ...(slices.length > 0 ? { independentSliceCount: slices.length } : {})
@@ -1 +1 @@
1
- export const PACKAGE_VERSION = '8.4.0';
1
+ export const PACKAGE_VERSION = '8.6.0';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "8.4.0",
4
+ "version": "8.6.0",
5
5
  "description": "Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",