@nerviq/cli 0.9.0-beta.2 → 0.9.1

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.
@@ -1012,6 +1012,345 @@ function pluginConfigIssue(ctx) {
1012
1012
  }
1013
1013
  }
1014
1014
 
1015
+ function primaryDocsPath(ctx) {
1016
+ return agentsPath(ctx) || (ctx.fileContent('README.md') ? 'README.md' : null);
1017
+ }
1018
+
1019
+ function webSearchModeRelevant(ctx) {
1020
+ const config = ctx.configToml();
1021
+ if (!config.ok || !config.data) return false;
1022
+
1023
+ const profiles = config.data.profiles && typeof config.data.profiles === 'object'
1024
+ ? Object.values(config.data.profiles)
1025
+ : [];
1026
+ if (config.data.web_search !== undefined || profiles.some((profile) => profile && typeof profile === 'object' && profile.web_search !== undefined)) {
1027
+ return true;
1028
+ }
1029
+
1030
+ const docs = docsBundle(ctx);
1031
+ const workflowUsesSearch = workflowArtifacts(ctx).some((workflow) => /\s--search\b|\bweb_search\b/i.test(workflow.content));
1032
+ return workflowUsesSearch || /\s--search\b|\bweb_search\b|\blive search\b|\bcached search\b/i.test(docs);
1033
+ }
1034
+
1035
+ function webSearchModeIssue(ctx) {
1036
+ const config = ctx.configToml();
1037
+ if (!config.ok || !config.data) return null;
1038
+
1039
+ const validModes = new Set(['cached', 'live', 'disabled']);
1040
+ const docs = docsBundle(ctx);
1041
+ const searchHintPresent = workflowArtifacts(ctx).some((workflow) => /\s--search\b|\bweb_search\b/i.test(workflow.content)) ||
1042
+ /\s--search\b|\bweb_search\b|\blive search\b|\bcached search\b/i.test(docs);
1043
+ const rootSearch = config.data.web_search;
1044
+ const rootEffort = config.data.model_reasoning_effort;
1045
+ const profiles = config.data.profiles && typeof config.data.profiles === 'object' ? config.data.profiles : {};
1046
+
1047
+ if (rootSearch !== undefined && !validModes.has(`${rootSearch}`)) {
1048
+ return { filePath: '.codex/config.toml', line: configKeyLine(ctx, 'web_search') || 1 };
1049
+ }
1050
+
1051
+ if (rootEffort === 'minimal' && ((typeof rootSearch === 'string' && rootSearch !== 'disabled') || (rootSearch === undefined && searchHintPresent))) {
1052
+ return {
1053
+ filePath: '.codex/config.toml',
1054
+ line: configKeyLine(ctx, 'model_reasoning_effort') || configKeyLine(ctx, 'web_search') || 1,
1055
+ };
1056
+ }
1057
+
1058
+ for (const [name, profile] of Object.entries(profiles)) {
1059
+ if (!profile || typeof profile !== 'object') continue;
1060
+ if (profile.web_search !== undefined && !validModes.has(`${profile.web_search}`)) {
1061
+ return {
1062
+ filePath: '.codex/config.toml',
1063
+ line: configSectionKeyLine(ctx, `profiles.${name}`, 'web_search') ||
1064
+ (configSections(ctx).find((section) => section.section === `profiles.${name}`) || {}).line ||
1065
+ 1,
1066
+ };
1067
+ }
1068
+
1069
+ const effectiveSearch = profile.web_search !== undefined ? profile.web_search : rootSearch;
1070
+ if (profile.model_reasoning_effort === 'minimal' &&
1071
+ ((typeof effectiveSearch === 'string' && effectiveSearch !== 'disabled') || (effectiveSearch === undefined && searchHintPresent))) {
1072
+ return {
1073
+ filePath: '.codex/config.toml',
1074
+ line: configSectionKeyLine(ctx, `profiles.${name}`, 'model_reasoning_effort') ||
1075
+ configSectionKeyLine(ctx, `profiles.${name}`, 'web_search') ||
1076
+ configKeyLine(ctx, 'web_search') ||
1077
+ (configSections(ctx).find((section) => section.section === `profiles.${name}`) || {}).line ||
1078
+ 1,
1079
+ };
1080
+ }
1081
+ }
1082
+
1083
+ return null;
1084
+ }
1085
+
1086
+ function requirementsTomlIssue(ctx) {
1087
+ const content = ctx.fileContent('requirements.toml');
1088
+ if (!content) return null;
1089
+
1090
+ const hasMeaningfulContent = content.split(/\r?\n/).some((line) => {
1091
+ const trimmed = line.trim();
1092
+ return trimmed && !trimmed.startsWith('#');
1093
+ });
1094
+ if (!hasMeaningfulContent) {
1095
+ return { filePath: 'requirements.toml', line: 1 };
1096
+ }
1097
+
1098
+ const docs = docsBundle(ctx);
1099
+ const acknowledged = /\brequirements\.toml\b|\badmin[- ]enforced\b|\bmanaged configuration\b|\bmanaged\b/i.test(docs);
1100
+ return acknowledged ? null : { filePath: primaryDocsPath(ctx) || 'requirements.toml', line: 1 };
1101
+ }
1102
+
1103
+ function sharedOrManagedMachineSignals(ctx) {
1104
+ const docs = docsBundle(ctx);
1105
+ return Boolean(ctx.fileContent('requirements.toml')) ||
1106
+ /\bshared\b|\bmanaged\b|\badmin[- ]enforced\b|\bmulti-user\b|\benterprise\b|\bkiosk\b|\bvdi\b/i.test(docs);
1107
+ }
1108
+
1109
+ function authCredentialsStoreIssue(ctx) {
1110
+ if (!ctx.fileContent('.codex/config.toml')) return null;
1111
+ if (!sharedOrManagedMachineSignals(ctx)) return null;
1112
+
1113
+ const value = ctx.configValue('cli_auth_credentials_store');
1114
+ if (value === undefined) {
1115
+ return { filePath: '.codex/config.toml', line: configKeyLine(ctx, 'cli_auth_credentials_store') || 1 };
1116
+ }
1117
+
1118
+ return ['auto', 'file', 'keyring'].includes(`${value}`)
1119
+ ? null
1120
+ : { filePath: '.codex/config.toml', line: configKeyLine(ctx, 'cli_auth_credentials_store') || 1 };
1121
+ }
1122
+
1123
+ function protectedPathAssumptionRelevant(ctx) {
1124
+ return ctx.configValue('sandbox_mode') === 'workspace-write' && /\.(git|codex|agents)\b/i.test(docsBundle(ctx));
1125
+ }
1126
+
1127
+ function protectedPathAssumptionIssue(ctx) {
1128
+ if (!protectedPathAssumptionRelevant(ctx)) return null;
1129
+
1130
+ const docs = docsBundle(ctx);
1131
+ const riskyPattern = /\.(git|codex|agents)\b[\s\S]{0,120}\b(edit|modify|write|delete|remove|patch|update|commit)\b/i;
1132
+ const safePattern = /\.(git|codex|agents)\b[\s\S]{0,120}\b(read-only|read only|protected|not writable|cannot (?:be )?(?:edited|modified|written)|blocked)\b/i;
1133
+ if (safePattern.test(docs)) return null;
1134
+ if (!riskyPattern.test(docs)) return null;
1135
+
1136
+ return {
1137
+ filePath: primaryDocsPath(ctx) || '.codex/config.toml',
1138
+ line: firstLineMatching(docs, /\.(git|codex|agents)\b/i) || 1,
1139
+ };
1140
+ }
1141
+
1142
+ function mcpHttpAuthAndCallbackRelevant(ctx) {
1143
+ if (!projectScopedMcpPresent(ctx)) return false;
1144
+ const servers = projectMcpServers(ctx);
1145
+ const hasRemoteHeaderAuth = Object.values(servers || {}).some((server) => server && server.url && (server.env_http_headers || server.http_headers));
1146
+ return hasRemoteHeaderAuth ||
1147
+ typeof ctx.configValue('mcp_oauth_callback_port') === 'number' ||
1148
+ Boolean(ctx.configValue('mcp_oauth_callback_url'));
1149
+ }
1150
+
1151
+ function mcpHttpAuthAndCallbackIssue(ctx) {
1152
+ const docs = docsBundle(ctx);
1153
+ const callbackPort = ctx.configValue('mcp_oauth_callback_port');
1154
+ const callbackUrl = ctx.configValue('mcp_oauth_callback_url');
1155
+
1156
+ if ((typeof callbackPort === 'number' || callbackUrl) && !/\boauth\b|\bcallback\b|\bredirect\b|\bloopback\b/i.test(docs)) {
1157
+ return {
1158
+ filePath: '.codex/config.toml',
1159
+ line: configKeyLine(ctx, 'mcp_oauth_callback_url') || configKeyLine(ctx, 'mcp_oauth_callback_port') || 1,
1160
+ };
1161
+ }
1162
+
1163
+ for (const [id, server] of Object.entries(projectMcpServers(ctx))) {
1164
+ if (!server || !server.url) continue;
1165
+ if (!(server.env_http_headers || server.http_headers)) continue;
1166
+
1167
+ const hasDocNote = new RegExp(`\\b${escapeRegex(id)}\\b[\\s\\S]{0,180}\\b(header|oauth|callback|auth|token)\\b`, 'i').test(docs);
1168
+ if (!hasDocNote) {
1169
+ return {
1170
+ filePath: '.codex/config.toml',
1171
+ line: configSectionKeyLine(ctx, `mcp_servers.${id}`, 'env_http_headers') ||
1172
+ configSectionKeyLine(ctx, `mcp_servers.${id}`, 'http_headers') ||
1173
+ (configSections(ctx).find((section) => section.section === `mcp_servers.${id}`) || {}).line ||
1174
+ 1,
1175
+ };
1176
+ }
1177
+ }
1178
+
1179
+ return null;
1180
+ }
1181
+
1182
+ function batchStyleSubagentFlowPresent(ctx) {
1183
+ const files = ctx.customAgentFiles ? ctx.customAgentFiles() : [];
1184
+ const csvPattern = /\bspawn_agents_on_csv\b|\breport_agent_job_result\b|\boutput_csv_path\b|\boutput_schema\b|\bmax_concurrency\b|\bmax_runtime_seconds\b/i;
1185
+ return files.some((fileName) => {
1186
+ const content = ctx.fileContent(path.join('.codex', 'agents', fileName)) || '';
1187
+ return csvPattern.test(content);
1188
+ });
1189
+ }
1190
+
1191
+ function csvBatchAgentIssue(ctx) {
1192
+ const files = ctx.customAgentFiles ? ctx.customAgentFiles() : [];
1193
+ const csvPattern = /\bspawn_agents_on_csv\b|\breport_agent_job_result\b|\boutput_csv_path\b|\boutput_schema\b|\bmax_concurrency\b|\bmax_runtime_seconds\b/i;
1194
+
1195
+ for (const fileName of files) {
1196
+ const content = ctx.fileContent(path.join('.codex', 'agents', fileName)) || '';
1197
+ if (!csvPattern.test(content)) continue;
1198
+ if (typeof ctx.configValue('agents.job_max_runtime_seconds') === 'number') return null;
1199
+ return {
1200
+ filePath: '.codex/config.toml',
1201
+ line: configSectionKeyLine(ctx, 'agents', 'job_max_runtime_seconds') || 1,
1202
+ };
1203
+ }
1204
+
1205
+ return null;
1206
+ }
1207
+
1208
+ function nicknameCandidatesIssue(ctx) {
1209
+ const files = ctx.customAgentFiles ? ctx.customAgentFiles() : [];
1210
+ const seen = new Map();
1211
+
1212
+ for (const fileName of files) {
1213
+ const parsed = ctx.customAgentConfig(fileName);
1214
+ if (!parsed.ok || !parsed.data) {
1215
+ return { filePath: `.codex/agents/${fileName}`, line: 1 };
1216
+ }
1217
+
1218
+ const candidates = parsed.data.nickname_candidates;
1219
+ if (candidates === undefined) continue;
1220
+ if (!Array.isArray(candidates) || candidates.length === 0) {
1221
+ const content = ctx.fileContent(path.join('.codex', 'agents', fileName)) || '';
1222
+ return {
1223
+ filePath: `.codex/agents/${fileName}`,
1224
+ line: firstLineMatching(content, /^\s*nickname_candidates\s*=/i) || 1,
1225
+ };
1226
+ }
1227
+
1228
+ const localSeen = new Set();
1229
+ for (const candidate of candidates) {
1230
+ const normalized = typeof candidate === 'string' ? candidate.trim() : '';
1231
+ const canonical = normalized.toLowerCase();
1232
+ if (!normalized || !/^[A-Za-z0-9 _-]+$/.test(normalized) || localSeen.has(canonical) || seen.has(canonical)) {
1233
+ const content = ctx.fileContent(path.join('.codex', 'agents', fileName)) || '';
1234
+ return {
1235
+ filePath: `.codex/agents/${fileName}`,
1236
+ line: firstLineMatching(content, /^\s*nickname_candidates\s*=/i) || 1,
1237
+ };
1238
+ }
1239
+ localSeen.add(canonical);
1240
+ seen.set(canonical, fileName);
1241
+ }
1242
+ }
1243
+
1244
+ return null;
1245
+ }
1246
+
1247
+ function nativeWindowsConfigRelevant(ctx) {
1248
+ return configSections(ctx).some((section) => section.section === 'windows') ||
1249
+ /\bnative windows\b|\bwindows sandbox\b|\bprivate desktop\b/i.test(docsBundle(ctx));
1250
+ }
1251
+
1252
+ function windowsSandboxModeIssue(ctx) {
1253
+ if (!nativeWindowsConfigRelevant(ctx)) return null;
1254
+
1255
+ const value = ctx.configValue('windows.sandbox');
1256
+ if (value === undefined) {
1257
+ return {
1258
+ filePath: '.codex/config.toml',
1259
+ line: configSectionKeyLine(ctx, 'windows', 'sandbox') ||
1260
+ (configSections(ctx).find((section) => section.section === 'windows') || {}).line ||
1261
+ 1,
1262
+ };
1263
+ }
1264
+
1265
+ return ['elevated', 'unelevated'].includes(`${value}`)
1266
+ ? null
1267
+ : {
1268
+ filePath: '.codex/config.toml',
1269
+ line: configSectionKeyLine(ctx, 'windows', 'sandbox') ||
1270
+ (configSections(ctx).find((section) => section.section === 'windows') || {}).line ||
1271
+ 1,
1272
+ };
1273
+ }
1274
+
1275
+ function appAutomationRelevant(ctx) {
1276
+ return /\bautomations?\b|\btriage inbox\b|\bbackground tasks?\b/i.test(docsBundle(ctx));
1277
+ }
1278
+
1279
+ function automationAppRunningIssue(ctx) {
1280
+ if (!appAutomationRelevant(ctx)) return null;
1281
+
1282
+ const docs = docsBundle(ctx);
1283
+ const acknowledged = /\bapp needs to be running\b|\bkeep the app running\b|\bCodex app\b[\s\S]{0,80}\brunning\b|\bselected project\b[\s\S]{0,80}\bon disk\b/i.test(docs);
1284
+ return acknowledged
1285
+ ? null
1286
+ : {
1287
+ filePath: primaryDocsPath(ctx) || 'README.md',
1288
+ line: firstLineMatching(docs, /\bautomations?\b|\btriage inbox\b|\bbackground tasks?\b/i) || 1,
1289
+ };
1290
+ }
1291
+
1292
+ function codexActionPromptSourceIssue(ctx) {
1293
+ for (const workflow of workflowArtifacts(ctx)) {
1294
+ if (!/uses:\s*openai\/codex-action@/i.test(workflow.content)) continue;
1295
+
1296
+ const hasPrompt = /^\s*prompt\s*:/im.test(workflow.content);
1297
+ const hasPromptFile = /^\s*prompt-file\s*:/im.test(workflow.content);
1298
+ if (hasPrompt && hasPromptFile) {
1299
+ return {
1300
+ filePath: workflow.filePath,
1301
+ line: firstLineMatching(workflow.content, /^\s*prompt(?:-file)?\s*:/im) || 1,
1302
+ };
1303
+ }
1304
+
1305
+ if (!hasPrompt && !hasPromptFile) {
1306
+ return {
1307
+ filePath: workflow.filePath,
1308
+ line: firstLineMatching(workflow.content, /uses:\s*openai\/codex-action@/i) || 1,
1309
+ };
1310
+ }
1311
+ }
1312
+
1313
+ return null;
1314
+ }
1315
+
1316
+ function codexActionTriggerAllowlistIssue(ctx) {
1317
+ for (const workflow of workflowArtifacts(ctx)) {
1318
+ if (!/uses:\s*openai\/codex-action@/i.test(workflow.content)) continue;
1319
+
1320
+ const triggerLine = firstLineMatching(workflow.content, /\bissue_comment\b|\bpull_request_target\b|\bpull_request_review_comment\b|\bdiscussion_comment\b/i);
1321
+ if (!triggerLine) continue;
1322
+
1323
+ const hasAllowUsers = /^\s*allow-users\s*:/im.test(workflow.content);
1324
+ const hasAllowBots = /^\s*allow-bots\s*:/im.test(workflow.content);
1325
+ if (!hasAllowUsers && !hasAllowBots) {
1326
+ return { filePath: workflow.filePath, line: triggerLine };
1327
+ }
1328
+ }
1329
+
1330
+ return null;
1331
+ }
1332
+
1333
+ function codexActionExternalTriggersPresent(ctx) {
1334
+ return workflowArtifacts(ctx).some((workflow) =>
1335
+ /uses:\s*openai\/codex-action@/i.test(workflow.content) &&
1336
+ /\bissue_comment\b|\bpull_request_target\b|\bpull_request_review_comment\b|\bdiscussion_comment\b/i.test(workflow.content));
1337
+ }
1338
+
1339
+ function desktopProjectMcpCaveatIssue(ctx) {
1340
+ if (!projectScopedMcpPresent(ctx)) return null;
1341
+
1342
+ const docs = docsBundle(ctx);
1343
+ if (!/\bdesktop\b|\bide\b|\bextension\b/i.test(docs)) return null;
1344
+
1345
+ const caveated = /\btrusted project\b|\btrust\b|\brepo-local\b|\bproject-scoped\b|\bglobal config\b|\buser-global\b|\bmay be ignored\b/i.test(docs);
1346
+ return caveated
1347
+ ? null
1348
+ : {
1349
+ filePath: primaryDocsPath(ctx) || '.codex/config.toml',
1350
+ line: firstLineMatching(docs, /\bdesktop\b|\bide\b|\bextension\b/i) || 1,
1351
+ };
1352
+ }
1353
+
1015
1354
  const CODEX_TECHNIQUES = {
1016
1355
  codexAgentsMd: {
1017
1356
  id: 'CX-A01',
@@ -1203,7 +1542,7 @@ const CODEX_TECHNIQUES = {
1203
1542
  impact: 'low',
1204
1543
  rating: 3,
1205
1544
  category: 'config',
1206
- fix: 'Set `model_reasoning_effort` explicitly so Codex reasoning depth is intentional instead of implicit.',
1545
+ fix: 'Set `model_reasoning_effort` explicitly only when the repo needs a non-default reasoning posture; this setting is optional, and minimal effort should stay compatible with any `web_search` usage.',
1207
1546
  template: 'codex-config',
1208
1547
  file: () => '.codex/config.toml',
1209
1548
  line: (ctx) => configKeyLine(ctx, 'model_reasoning_effort'),
@@ -1211,14 +1550,17 @@ const CODEX_TECHNIQUES = {
1211
1550
  codexWeakModelExplicit: {
1212
1551
  id: 'CX-B04',
1213
1552
  name: 'Weak-task delegation model is explicit',
1214
- check: (ctx) => Boolean(ctx.configValue('model_for_weak_tasks')),
1553
+ check: () => {
1554
+ // Retired: config key removed from official schema as of 2026-04-05
1555
+ return null;
1556
+ },
1215
1557
  impact: 'medium',
1216
1558
  rating: 4,
1217
1559
  category: 'config',
1218
- fix: 'Set `model_for_weak_tasks` explicitly so Codex delegation is predictable and cost-aware.',
1219
- template: 'codex-config',
1220
- file: () => '.codex/config.toml',
1221
- line: (ctx) => configKeyLine(ctx, 'model_for_weak_tasks'),
1560
+ fix: '`model_for_weak_tasks` was removed from the official Codex config schema as of 2026-04-05. This check is retired and no repo change is required.',
1561
+ template: null,
1562
+ file: () => null,
1563
+ line: () => null,
1222
1564
  },
1223
1565
  codexConfigSectionPlacement: {
1224
1566
  id: 'CX-B05',
@@ -1285,7 +1627,7 @@ const CODEX_TECHNIQUES = {
1285
1627
  impact: 'low',
1286
1628
  rating: 3,
1287
1629
  category: 'config',
1288
- fix: 'If you define Codex profiles, make sure each profile contains real settings and any selected `profile` points to an existing profile section.',
1630
+ fix: 'Profiles are an advanced feature, not a baseline requirement. If you use them, make sure each profile contains real settings and any selected `profile` points to an existing profile section.',
1289
1631
  template: null,
1290
1632
  file: () => '.codex/config.toml',
1291
1633
  line: (ctx) => configKeyLine(ctx, 'profile') || (profileSections(ctx)[0] || {}).line || null,
@@ -1293,14 +1635,80 @@ const CODEX_TECHNIQUES = {
1293
1635
  codexFullAutoErrorModeExplicit: {
1294
1636
  id: 'CX-B09',
1295
1637
  name: 'full_auto_error_mode is explicit',
1296
- check: (ctx) => Boolean(ctx.configValue('full_auto_error_mode')),
1638
+ check: () => {
1639
+ // Retired: config key removed from official schema as of 2026-04-05
1640
+ return null;
1641
+ },
1297
1642
  impact: 'medium',
1298
1643
  rating: 4,
1299
1644
  category: 'config',
1300
- fix: 'Set `full_auto_error_mode` explicitly so Codex failure behavior is predictable during automated flows.',
1645
+ fix: '`full_auto_error_mode` was removed from the official Codex config schema as of 2026-04-05. This check is retired and no repo change is required.',
1646
+ template: null,
1647
+ file: () => null,
1648
+ line: () => null,
1649
+ },
1650
+ codexWebSearchModeCompatible: {
1651
+ id: 'CX-B10',
1652
+ name: 'web_search mode is explicit and compatible when search is part of the workflow',
1653
+ check: (ctx) => {
1654
+ if (!webSearchModeRelevant(ctx)) return null;
1655
+ return !webSearchModeIssue(ctx);
1656
+ },
1657
+ impact: 'medium',
1658
+ rating: 4,
1659
+ category: 'config',
1660
+ fix: 'When the repo uses search-aware Codex flows, set `web_search = "cached" | "live" | "disabled"` intentionally and avoid pairing search with `model_reasoning_effort = "minimal"` in the same effective profile.',
1301
1661
  template: 'codex-config',
1302
- file: () => '.codex/config.toml',
1303
- line: (ctx) => configKeyLine(ctx, 'full_auto_error_mode'),
1662
+ file: (ctx) => {
1663
+ const issue = webSearchModeIssue(ctx);
1664
+ return issue ? issue.filePath : '.codex/config.toml';
1665
+ },
1666
+ line: (ctx) => {
1667
+ const issue = webSearchModeIssue(ctx);
1668
+ return issue ? issue.line : configKeyLine(ctx, 'web_search');
1669
+ },
1670
+ },
1671
+ codexRequirementsTomlRecognized: {
1672
+ id: 'CX-B11',
1673
+ name: 'requirements.toml posture is recognized when a managed layer exists',
1674
+ check: (ctx) => {
1675
+ if (!ctx.fileContent('requirements.toml')) return null;
1676
+ return !requirementsTomlIssue(ctx);
1677
+ },
1678
+ impact: 'medium',
1679
+ rating: 3,
1680
+ category: 'config',
1681
+ fix: 'If the repo uses `requirements.toml`, keep it non-empty and acknowledge that it is a managed/admin layer rather than an ordinary project preference file.',
1682
+ template: null,
1683
+ file: (ctx) => {
1684
+ const issue = requirementsTomlIssue(ctx);
1685
+ return issue ? issue.filePath : 'requirements.toml';
1686
+ },
1687
+ line: (ctx) => {
1688
+ const issue = requirementsTomlIssue(ctx);
1689
+ return issue ? issue.line : 1;
1690
+ },
1691
+ },
1692
+ codexCliAuthCredentialsStoreExplicit: {
1693
+ id: 'CX-B12',
1694
+ name: 'cli_auth_credentials_store is explicit on shared or managed setups',
1695
+ check: (ctx) => {
1696
+ const issue = authCredentialsStoreIssue(ctx);
1697
+ return issue ? false : (sharedOrManagedMachineSignals(ctx) ? true : null);
1698
+ },
1699
+ impact: 'high',
1700
+ rating: 4,
1701
+ category: 'config',
1702
+ fix: 'On shared or managed machines, set `cli_auth_credentials_store = "auto" | "keyring" | "file"` explicitly so Codex auth-cache handling is reviewable.',
1703
+ template: 'codex-config',
1704
+ file: (ctx) => {
1705
+ const issue = authCredentialsStoreIssue(ctx);
1706
+ return issue ? issue.filePath : '.codex/config.toml';
1707
+ },
1708
+ line: (ctx) => {
1709
+ const issue = authCredentialsStoreIssue(ctx);
1710
+ return issue ? issue.line : configKeyLine(ctx, 'cli_auth_credentials_store');
1711
+ },
1304
1712
  },
1305
1713
  codexApprovalPolicyExplicit: {
1306
1714
  id: 'CX-C02',
@@ -1364,32 +1772,32 @@ const CODEX_TECHNIQUES = {
1364
1772
  codexDisableResponseStorageForRegulatedRepos: {
1365
1773
  id: 'CX-C05',
1366
1774
  name: 'disable_response_storage is explicit for regulated repos',
1367
- check: (ctx) => {
1368
- if (!repoLooksRegulated(ctx)) return null;
1369
- return ctx.configValue('disable_response_storage') === true;
1775
+ check: () => {
1776
+ // Retired: config key removed from official schema as of 2026-04-05
1777
+ return null;
1370
1778
  },
1371
1779
  impact: 'medium',
1372
1780
  rating: 4,
1373
1781
  category: 'trust',
1374
- fix: 'For regulated or compliance-heavy repos, set `disable_response_storage = true` so response retention is not left implicit.',
1375
- template: 'codex-config',
1376
- file: () => '.codex/config.toml',
1377
- line: (ctx) => configKeyLine(ctx, 'disable_response_storage'),
1782
+ fix: '`disable_response_storage` was removed from the official Codex config schema as of 2026-04-05. This check is retired and no repo change is required.',
1783
+ template: null,
1784
+ file: () => null,
1785
+ line: () => null,
1378
1786
  },
1379
1787
  codexHistorySendToServerExplicit: {
1380
1788
  id: 'CX-C06',
1381
1789
  name: 'history.send_to_server is explicit',
1382
- check: (ctx) => {
1383
- const value = ctx.configValue('history.send_to_server');
1384
- return typeof value === 'boolean';
1790
+ check: () => {
1791
+ // Retired: config key removed from official schema as of 2026-04-05
1792
+ return null;
1385
1793
  },
1386
1794
  impact: 'medium',
1387
1795
  rating: 4,
1388
1796
  category: 'trust',
1389
- fix: 'Set `[history] send_to_server = true|false` explicitly so Codex history sync posture is reviewable.',
1390
- template: 'codex-config',
1391
- file: () => '.codex/config.toml',
1392
- line: (ctx) => configSectionKeyLine(ctx, 'history', 'send_to_server'),
1797
+ fix: '`history.send_to_server` was removed from the official Codex config schema as of 2026-04-05. This check is retired and no repo change is required.',
1798
+ template: null,
1799
+ file: () => null,
1800
+ line: () => null,
1393
1801
  },
1394
1802
  codexGitHubActionUnsafeJustified: {
1395
1803
  id: 'CX-C07',
@@ -1448,6 +1856,48 @@ const CODEX_TECHNIQUES = {
1448
1856
  return content ? findSecretLine(content) : null;
1449
1857
  },
1450
1858
  },
1859
+ codexProtectedPathsRespectedInWorkspaceWriteDocs: {
1860
+ id: 'CX-C10',
1861
+ name: 'Workspace-write docs do not imply protected paths are writable',
1862
+ check: (ctx) => {
1863
+ if (!protectedPathAssumptionRelevant(ctx)) return null;
1864
+ return !protectedPathAssumptionIssue(ctx);
1865
+ },
1866
+ impact: 'high',
1867
+ rating: 4,
1868
+ category: 'trust',
1869
+ fix: 'If repo docs mention `.git`, `.codex`, or `.agents` under workspace-write, describe them as protected/read-only rather than writable runtime surfaces.',
1870
+ template: 'codex-agents-md',
1871
+ file: (ctx) => {
1872
+ const issue = protectedPathAssumptionIssue(ctx);
1873
+ return issue ? issue.filePath : primaryDocsPath(ctx);
1874
+ },
1875
+ line: (ctx) => {
1876
+ const issue = protectedPathAssumptionIssue(ctx);
1877
+ return issue ? issue.line : null;
1878
+ },
1879
+ },
1880
+ codexWindowsSandboxModeExplicit: {
1881
+ id: 'CX-C11',
1882
+ name: 'Native Windows sandbox mode is explicit when Windows config is used',
1883
+ check: (ctx) => {
1884
+ const issue = windowsSandboxModeIssue(ctx);
1885
+ return issue ? false : (nativeWindowsConfigRelevant(ctx) ? true : null);
1886
+ },
1887
+ impact: 'high',
1888
+ rating: 4,
1889
+ category: 'trust',
1890
+ fix: 'If the repo relies on native Windows Codex settings, set `[windows] sandbox = "elevated" | "unelevated"` explicitly so the trust boundary is reviewable.',
1891
+ template: 'codex-config',
1892
+ file: (ctx) => {
1893
+ const issue = windowsSandboxModeIssue(ctx);
1894
+ return issue ? issue.filePath : '.codex/config.toml';
1895
+ },
1896
+ line: (ctx) => {
1897
+ const issue = windowsSandboxModeIssue(ctx);
1898
+ return issue ? issue.line : configSectionKeyLine(ctx, 'windows', 'sandbox');
1899
+ },
1900
+ },
1451
1901
  codexRulesExistForRiskyCommands: {
1452
1902
  id: 'CX-D01',
1453
1903
  name: 'Rules exist for risky or out-of-sandbox command classes',
@@ -1769,6 +2219,48 @@ const CODEX_TECHNIQUES = {
1769
2219
  return issue ? issue.line : null;
1770
2220
  },
1771
2221
  },
2222
+ codexMcpHttpAuthAndCallbacksDocumented: {
2223
+ id: 'CX-F07',
2224
+ name: 'MCP HTTP auth and callback fields are documented when used',
2225
+ check: (ctx) => {
2226
+ if (!mcpHttpAuthAndCallbackRelevant(ctx)) return null;
2227
+ return !mcpHttpAuthAndCallbackIssue(ctx);
2228
+ },
2229
+ impact: 'medium',
2230
+ rating: 4,
2231
+ category: 'mcp',
2232
+ fix: 'If remote MCP uses header-based auth or custom OAuth callback settings, document the header/callback posture in repo docs so setup stays reviewable.',
2233
+ template: null,
2234
+ file: (ctx) => {
2235
+ const issue = mcpHttpAuthAndCallbackIssue(ctx);
2236
+ return issue ? issue.filePath : '.codex/config.toml';
2237
+ },
2238
+ line: (ctx) => {
2239
+ const issue = mcpHttpAuthAndCallbackIssue(ctx);
2240
+ return issue ? issue.line : null;
2241
+ },
2242
+ },
2243
+ codexDesktopProjectMcpCaveatDocumented: {
2244
+ id: 'CX-F08',
2245
+ name: 'Project-scoped MCP docs caveat desktop or IDE behavior when relevant',
2246
+ check: (ctx) => {
2247
+ const issue = desktopProjectMcpCaveatIssue(ctx);
2248
+ return issue ? false : (projectScopedMcpPresent(ctx) && /\bdesktop\b|\bide\b|\bextension\b/i.test(docsBundle(ctx)) ? true : null);
2249
+ },
2250
+ impact: 'medium',
2251
+ rating: 3,
2252
+ category: 'mcp',
2253
+ fix: 'If repo-local MCP config is discussed for desktop or IDE use, note the trusted-project boundary and the possibility that user-global config may still matter on some surfaces.',
2254
+ template: 'codex-agents-md',
2255
+ file: (ctx) => {
2256
+ const issue = desktopProjectMcpCaveatIssue(ctx);
2257
+ return issue ? issue.filePath : primaryDocsPath(ctx);
2258
+ },
2259
+ line: (ctx) => {
2260
+ const issue = desktopProjectMcpCaveatIssue(ctx);
2261
+ return issue ? issue.line : null;
2262
+ },
2263
+ },
1772
2264
  codexSkillsDirPresentWhenUsed: {
1773
2265
  id: 'CX-G01',
1774
2266
  name: '.agents/skills exists when Codex skills are used',
@@ -1941,6 +2433,54 @@ const CODEX_TECHNIQUES = {
1941
2433
  return issue ? issue.line : null;
1942
2434
  },
1943
2435
  },
2436
+ codexJobMaxRuntimeExplicitForBatchAgents: {
2437
+ id: 'CX-H05',
2438
+ name: 'agents.job_max_runtime_seconds is explicit for batch-style subagent flows',
2439
+ check: (ctx) => {
2440
+ if (!batchStyleSubagentFlowPresent(ctx)) return null;
2441
+ const issue = csvBatchAgentIssue(ctx);
2442
+ return !issue;
2443
+ },
2444
+ impact: 'medium',
2445
+ rating: 3,
2446
+ category: 'agents',
2447
+ fix: 'If custom agents use CSV or batch-style fanout fields, set `[agents] job_max_runtime_seconds` explicitly so worker runtime is bounded and reviewable.',
2448
+ template: 'codex-config',
2449
+ file: (ctx) => {
2450
+ const issue = csvBatchAgentIssue(ctx);
2451
+ return issue ? issue.filePath : '.codex/config.toml';
2452
+ },
2453
+ line: (ctx) => {
2454
+ const issue = csvBatchAgentIssue(ctx);
2455
+ return issue ? issue.line : configSectionKeyLine(ctx, 'agents', 'job_max_runtime_seconds');
2456
+ },
2457
+ },
2458
+ codexNicknameCandidatesValid: {
2459
+ id: 'CX-H06',
2460
+ name: 'nickname_candidates are valid and unique when used',
2461
+ check: (ctx) => {
2462
+ const issue = nicknameCandidatesIssue(ctx);
2463
+ const hasNicknameCandidates = (ctx.customAgentFiles ? ctx.customAgentFiles() : []).some((fileName) => {
2464
+ const parsed = ctx.customAgentConfig(fileName);
2465
+ return parsed.ok && parsed.data && parsed.data.nickname_candidates !== undefined;
2466
+ });
2467
+ if (!hasNicknameCandidates) return null;
2468
+ return !issue;
2469
+ },
2470
+ impact: 'medium',
2471
+ rating: 3,
2472
+ category: 'agents',
2473
+ fix: 'When custom agents use `nickname_candidates`, keep them non-empty, ASCII-safe, and unique across the repo so display names stay deterministic.',
2474
+ template: null,
2475
+ file: (ctx) => {
2476
+ const issue = nicknameCandidatesIssue(ctx);
2477
+ return issue ? issue.filePath : '.codex/agents';
2478
+ },
2479
+ line: (ctx) => {
2480
+ const issue = nicknameCandidatesIssue(ctx);
2481
+ return issue ? issue.line : null;
2482
+ },
2483
+ },
1944
2484
  codexExecUsageSafe: {
1945
2485
  id: 'CX-I01',
1946
2486
  name: 'codex exec usage avoids unsafe automation defaults',
@@ -2026,6 +2566,71 @@ const CODEX_TECHNIQUES = {
2026
2566
  return issue ? issue.line : null;
2027
2567
  },
2028
2568
  },
2569
+ codexAutomationAppRunningAcknowledged: {
2570
+ id: 'CX-I05',
2571
+ name: 'App-running prerequisite is acknowledged for Codex app automations',
2572
+ check: (ctx) => {
2573
+ const issue = automationAppRunningIssue(ctx);
2574
+ return issue ? false : (appAutomationRelevant(ctx) ? true : null);
2575
+ },
2576
+ impact: 'medium',
2577
+ rating: 3,
2578
+ category: 'automation',
2579
+ fix: 'If the repo documents Codex app automations, note that the app must be running and the selected project must be available on disk.',
2580
+ template: 'codex-agents-md',
2581
+ file: (ctx) => {
2582
+ const issue = automationAppRunningIssue(ctx);
2583
+ return issue ? issue.filePath : primaryDocsPath(ctx);
2584
+ },
2585
+ line: (ctx) => {
2586
+ const issue = automationAppRunningIssue(ctx);
2587
+ return issue ? issue.line : null;
2588
+ },
2589
+ },
2590
+ codexGitHubActionSinglePromptSource: {
2591
+ id: 'CX-I06',
2592
+ name: 'Codex GitHub Action uses exactly one prompt source',
2593
+ check: (ctx) => {
2594
+ const hasAction = workflowArtifacts(ctx).some((workflow) => /uses:\s*openai\/codex-action@/i.test(workflow.content));
2595
+ if (!hasAction) return null;
2596
+ return !codexActionPromptSourceIssue(ctx);
2597
+ },
2598
+ impact: 'high',
2599
+ rating: 4,
2600
+ category: 'automation',
2601
+ fix: 'For each `openai/codex-action` workflow, choose exactly one prompt input: `prompt` or `prompt-file`.',
2602
+ template: null,
2603
+ file: (ctx) => {
2604
+ const issue = codexActionPromptSourceIssue(ctx);
2605
+ return issue ? issue.filePath : null;
2606
+ },
2607
+ line: (ctx) => {
2608
+ const issue = codexActionPromptSourceIssue(ctx);
2609
+ return issue ? issue.line : null;
2610
+ },
2611
+ },
2612
+ codexGitHubActionTriggerAllowlistsExplicit: {
2613
+ id: 'CX-I07',
2614
+ name: 'Codex GitHub Action uses trigger allowlists on externally triggered workflows',
2615
+ check: (ctx) => {
2616
+ if (!codexActionExternalTriggersPresent(ctx)) return null;
2617
+ const issue = codexActionTriggerAllowlistIssue(ctx);
2618
+ return !issue;
2619
+ },
2620
+ impact: 'high',
2621
+ rating: 4,
2622
+ category: 'automation',
2623
+ fix: 'If a Codex Action workflow is triggered by comments or `pull_request_target`, set `allow-users` or `allow-bots` explicitly to constrain who can invoke it.',
2624
+ template: null,
2625
+ file: (ctx) => {
2626
+ const issue = codexActionTriggerAllowlistIssue(ctx);
2627
+ return issue ? issue.filePath : null;
2628
+ },
2629
+ line: (ctx) => {
2630
+ const issue = codexActionTriggerAllowlistIssue(ctx);
2631
+ return issue ? issue.line : null;
2632
+ },
2633
+ },
2029
2634
  codexReviewWorkflowDocumented: {
2030
2635
  id: 'CX-J01',
2031
2636
  name: 'Review workflow is available and documented',