@nerviq/cli 0.0.1 → 0.9.0-beta.2
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/CHANGELOG.md +181 -0
- package/LICENSE +21 -0
- package/README.md +447 -0
- package/bin/cli.js +749 -0
- package/content/case-study-template.md +91 -0
- package/content/claims-governance.md +37 -0
- package/content/claude-code/audit-repo/SKILL.md +20 -0
- package/content/claude-native-integration.md +60 -0
- package/content/devto-article.json +9 -0
- package/content/launch-posts.md +226 -0
- package/content/pilot-rollout-kit.md +30 -0
- package/content/release-checklist.md +31 -0
- package/package.json +53 -4
- package/src/activity.js +529 -0
- package/src/aider/activity.js +226 -0
- package/src/aider/config-parser.js +166 -0
- package/src/aider/context.js +158 -0
- package/src/aider/deep-review.js +316 -0
- package/src/aider/domain-packs.js +278 -0
- package/src/aider/freshness.js +168 -0
- package/src/aider/governance.js +253 -0
- package/src/aider/interactive.js +334 -0
- package/src/aider/mcp-packs.js +98 -0
- package/src/aider/patch.js +214 -0
- package/src/aider/plans.js +186 -0
- package/src/aider/premium.js +360 -0
- package/src/aider/setup.js +404 -0
- package/src/aider/techniques.js +1323 -0
- package/src/analyze.js +821 -0
- package/src/audit.js +1003 -0
- package/src/badge.js +13 -0
- package/src/benchmark.js +339 -0
- package/src/claudex-sync.json +7 -0
- package/src/codex/activity.js +324 -0
- package/src/codex/config-parser.js +183 -0
- package/src/codex/context.js +221 -0
- package/src/codex/deep-review.js +493 -0
- package/src/codex/domain-packs.js +372 -0
- package/src/codex/freshness.js +167 -0
- package/src/codex/governance.js +192 -0
- package/src/codex/interactive.js +618 -0
- package/src/codex/mcp-packs.js +660 -0
- package/src/codex/patch.js +209 -0
- package/src/codex/plans.js +251 -0
- package/src/codex/premium.js +614 -0
- package/src/codex/setup.js +603 -0
- package/src/codex/techniques.js +2649 -0
- package/src/context.js +272 -0
- package/src/copilot/activity.js +309 -0
- package/src/copilot/config-parser.js +226 -0
- package/src/copilot/context.js +197 -0
- package/src/copilot/deep-review.js +346 -0
- package/src/copilot/domain-packs.js +350 -0
- package/src/copilot/freshness.js +197 -0
- package/src/copilot/governance.js +222 -0
- package/src/copilot/interactive.js +406 -0
- package/src/copilot/mcp-packs.js +572 -0
- package/src/copilot/patch.js +238 -0
- package/src/copilot/plans.js +253 -0
- package/src/copilot/premium.js +450 -0
- package/src/copilot/setup.js +488 -0
- package/src/copilot/techniques.js +1822 -0
- package/src/cursor/activity.js +301 -0
- package/src/cursor/config-parser.js +265 -0
- package/src/cursor/context.js +236 -0
- package/src/cursor/deep-review.js +334 -0
- package/src/cursor/domain-packs.js +346 -0
- package/src/cursor/freshness.js +214 -0
- package/src/cursor/governance.js +229 -0
- package/src/cursor/interactive.js +391 -0
- package/src/cursor/mcp-packs.js +571 -0
- package/src/cursor/patch.js +243 -0
- package/src/cursor/plans.js +254 -0
- package/src/cursor/premium.js +468 -0
- package/src/cursor/setup.js +488 -0
- package/src/cursor/techniques.js +1786 -0
- package/src/deep-review.js +345 -0
- package/src/domain-packs.js +364 -0
- package/src/formatters/sarif.js +115 -0
- package/src/gemini/activity.js +402 -0
- package/src/gemini/config-parser.js +275 -0
- package/src/gemini/context.js +221 -0
- package/src/gemini/deep-review.js +559 -0
- package/src/gemini/domain-packs.js +371 -0
- package/src/gemini/freshness.js +204 -0
- package/src/gemini/governance.js +201 -0
- package/src/gemini/interactive.js +860 -0
- package/src/gemini/mcp-packs.js +658 -0
- package/src/gemini/patch.js +229 -0
- package/src/gemini/plans.js +269 -0
- package/src/gemini/premium.js +759 -0
- package/src/gemini/setup.js +692 -0
- package/src/gemini/techniques.js +2084 -0
- package/src/governance.js +523 -0
- package/src/harmony/advisor.js +383 -0
- package/src/harmony/audit.js +303 -0
- package/src/harmony/canon.js +444 -0
- package/src/harmony/cli.js +331 -0
- package/src/harmony/drift.js +401 -0
- package/src/harmony/governance.js +313 -0
- package/src/harmony/memory.js +238 -0
- package/src/harmony/sync.js +458 -0
- package/src/harmony/watch.js +336 -0
- package/src/index.js +256 -0
- package/src/insights.js +119 -0
- package/src/interactive.js +118 -0
- package/src/mcp-packs.js +597 -0
- package/src/opencode/activity.js +286 -0
- package/src/opencode/config-parser.js +109 -0
- package/src/opencode/context.js +247 -0
- package/src/opencode/deep-review.js +313 -0
- package/src/opencode/domain-packs.js +240 -0
- package/src/opencode/freshness.js +158 -0
- package/src/opencode/governance.js +159 -0
- package/src/opencode/interactive.js +392 -0
- package/src/opencode/mcp-packs.js +474 -0
- package/src/opencode/patch.js +184 -0
- package/src/opencode/plans.js +231 -0
- package/src/opencode/premium.js +413 -0
- package/src/opencode/setup.js +449 -0
- package/src/opencode/techniques.js +1713 -0
- package/src/plans.js +655 -0
- package/src/secret-patterns.js +30 -0
- package/src/setup.js +1274 -0
- package/src/synergy/adaptive.js +261 -0
- package/src/synergy/compensation.js +156 -0
- package/src/synergy/evidence.js +193 -0
- package/src/synergy/learning.js +184 -0
- package/src/synergy/patterns.js +227 -0
- package/src/synergy/ranking.js +83 -0
- package/src/synergy/report.js +163 -0
- package/src/synergy/routing.js +152 -0
- package/src/techniques.js +1354 -0
- package/src/watch.js +229 -0
- package/src/windsurf/activity.js +302 -0
- package/src/windsurf/config-parser.js +267 -0
- package/src/windsurf/context.js +249 -0
- package/src/windsurf/deep-review.js +337 -0
- package/src/windsurf/domain-packs.js +348 -0
- package/src/windsurf/freshness.js +215 -0
- package/src/windsurf/governance.js +231 -0
- package/src/windsurf/interactive.js +388 -0
- package/src/windsurf/mcp-packs.js +535 -0
- package/src/windsurf/patch.js +231 -0
- package/src/windsurf/plans.js +247 -0
- package/src/windsurf/premium.js +467 -0
- package/src/windsurf/setup.js +471 -0
- package/src/windsurf/techniques.js +1758 -0
|
@@ -0,0 +1,2649 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_PROJECT_DOC_MAX_BYTES = 32768;
|
|
6
|
+
const SUPPORTED_HOOK_EVENTS = new Set(['SessionStart', 'PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'Stop']);
|
|
7
|
+
const NESTED_ONLY_ROOT_KEYS = new Set(['send_to_server', 'persistence', 'max_threads', 'max_depth', 'enabled_tools', 'startup_timeout_sec']);
|
|
8
|
+
const FILLER_PATTERNS = [
|
|
9
|
+
/\bbe helpful\b/i,
|
|
10
|
+
/\bbe accurate\b/i,
|
|
11
|
+
/\bbe concise\b/i,
|
|
12
|
+
/\balways do your best\b/i,
|
|
13
|
+
/\bmaintain high quality\b/i,
|
|
14
|
+
/\bwrite clean code\b/i,
|
|
15
|
+
/\bfollow best practices\b/i,
|
|
16
|
+
];
|
|
17
|
+
const JUSTIFICATION_PATTERNS = /\bbecause\b|\bwhy\b|\bjustif(?:y|ication)\b|\btemporary\b|\bintentional\b|\bdocumented\b|\bair[- ]?gapped\b|\binternal only\b|\bephemeral\b|\bci only\b/i;
|
|
18
|
+
const LEGACY_CONFIG_PATTERNS = [
|
|
19
|
+
{ pattern: /^\s*reasoning_effort\s*=/m, note: 'Use `model_reasoning_effort`, not `reasoning_effort`.' },
|
|
20
|
+
{ pattern: /^\s*weak_model\s*=/m, note: 'Use `model_for_weak_tasks`, not `weak_model`.' },
|
|
21
|
+
{ pattern: /^\s*history_send_to_server\s*=/m, note: 'Nest `send_to_server` under `[history]`.' },
|
|
22
|
+
{ pattern: /^\s*mcpServers\s*=/m, note: 'Use `[mcp_servers.<id>]` TOML tables, not `mcpServers`.' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function agentsPath(ctx) {
|
|
26
|
+
return ctx.fileContent('AGENTS.md') ? 'AGENTS.md' : (ctx.agentsMdPath ? ctx.agentsMdPath() : null);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function agentsContent(ctx) {
|
|
30
|
+
const filePath = agentsPath(ctx);
|
|
31
|
+
return filePath ? (ctx.fileContent(filePath) || '') : '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function countSections(markdown) {
|
|
35
|
+
return (markdown.match(/^##\s+/gm) || []).length;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function firstLineMatching(text, matcher) {
|
|
39
|
+
const lines = text.split(/\r?\n/);
|
|
40
|
+
for (let index = 0; index < lines.length; index++) {
|
|
41
|
+
const line = lines[index];
|
|
42
|
+
if (typeof matcher === 'string' && line.includes(matcher)) {
|
|
43
|
+
return index + 1;
|
|
44
|
+
}
|
|
45
|
+
if (matcher instanceof RegExp && matcher.test(line)) {
|
|
46
|
+
matcher.lastIndex = 0;
|
|
47
|
+
return index + 1;
|
|
48
|
+
}
|
|
49
|
+
if (typeof matcher === 'function' && matcher(line, index + 1)) {
|
|
50
|
+
return index + 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function escapeRegex(value) {
|
|
57
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function configKeyLine(ctx, key) {
|
|
61
|
+
return ctx.lineNumber('.codex/config.toml', new RegExp(`^\\s*${escapeRegex(key)}\\s*=`, 'i'));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function configSectionKeyLine(ctx, sectionPath, key) {
|
|
65
|
+
const content = ctx.configContent ? (ctx.configContent() || '') : (ctx.fileContent('.codex/config.toml') || '');
|
|
66
|
+
if (!content) return null;
|
|
67
|
+
const lines = content.split(/\r?\n/);
|
|
68
|
+
let currentSection = [];
|
|
69
|
+
for (let index = 0; index < lines.length; index++) {
|
|
70
|
+
const trimmed = lines[index].trim();
|
|
71
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
72
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
73
|
+
currentSection = trimmed.slice(1, -1).split('.').map(part => part.trim()).filter(Boolean);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (currentSection.join('.') === sectionPath && new RegExp(`^\\s*${escapeRegex(key)}\\s*=`, 'i').test(trimmed)) {
|
|
77
|
+
return index + 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function configSections(ctx) {
|
|
84
|
+
const content = ctx.configContent ? (ctx.configContent() || '') : (ctx.fileContent('.codex/config.toml') || '');
|
|
85
|
+
const lines = content.split(/\r?\n/);
|
|
86
|
+
const sections = [];
|
|
87
|
+
let currentSection = [];
|
|
88
|
+
|
|
89
|
+
for (let index = 0; index < lines.length; index++) {
|
|
90
|
+
const trimmed = lines[index].trim();
|
|
91
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
92
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
93
|
+
currentSection = trimmed.slice(1, -1).split('.').map(part => part.trim()).filter(Boolean);
|
|
94
|
+
sections.push({ section: currentSection.join('.'), line: index + 1 });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return sections;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function expectedVerificationCategories(ctx) {
|
|
103
|
+
const categories = new Set();
|
|
104
|
+
const pkg = ctx.jsonFile('package.json');
|
|
105
|
+
const scripts = pkg && pkg.scripts ? pkg.scripts : {};
|
|
106
|
+
|
|
107
|
+
if (scripts.test) categories.add('test');
|
|
108
|
+
if (scripts.lint) categories.add('lint');
|
|
109
|
+
if (scripts.build) categories.add('build');
|
|
110
|
+
|
|
111
|
+
if (ctx.fileContent('Cargo.toml')) {
|
|
112
|
+
categories.add('test');
|
|
113
|
+
categories.add('build');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (ctx.fileContent('go.mod')) {
|
|
117
|
+
categories.add('test');
|
|
118
|
+
categories.add('build');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (ctx.fileContent('pyproject.toml') || ctx.fileContent('requirements.txt')) {
|
|
122
|
+
categories.add('test');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (ctx.fileContent('Makefile') || ctx.fileContent('justfile')) {
|
|
126
|
+
categories.add('build');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return [...categories];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function hasCommandMention(content, category) {
|
|
133
|
+
if (category === 'test') {
|
|
134
|
+
return /\bnpm test\b|\bnpm run test\b|\bpnpm test\b|\byarn test\b|\bvitest\b|\bjest\b|\bpytest\b|\bgo test\b|\bcargo test\b|\bmake test\b/i.test(content);
|
|
135
|
+
}
|
|
136
|
+
if (category === 'lint') {
|
|
137
|
+
return /\bnpm run lint\b|\bpnpm lint\b|\byarn lint\b|\beslint\b|\bprettier\b|\bruff\b|\bclippy\b|\bgolangci-lint\b|\bmake lint\b/i.test(content);
|
|
138
|
+
}
|
|
139
|
+
if (category === 'build') {
|
|
140
|
+
return /\bnpm run build\b|\bpnpm build\b|\byarn build\b|\btsc\b|\bvite build\b|\bnext build\b|\bcargo build\b|\bgo build\b|\bmake\b/i.test(content);
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function agentsHasArchitecture(content) {
|
|
146
|
+
return /```mermaid|flowchart\b|graph\s+(TD|LR|RL|BT)\b|##\s+Architecture\b|##\s+Project Map\b|##\s+Structure\b/i.test(content);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function findFillerLine(content) {
|
|
150
|
+
return firstLineMatching(content, (line) => FILLER_PATTERNS.some((pattern) => pattern.test(line)));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function hasContradictions(content) {
|
|
154
|
+
const lines = content.split(/\r?\n/);
|
|
155
|
+
for (const line of lines) {
|
|
156
|
+
if (/\balways\b.*\bnever\b|\bnever\b.*\balways\b/i.test(line)) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const contradictoryPairs = [
|
|
162
|
+
[/\buse tabs\b/i, /\buse spaces\b/i],
|
|
163
|
+
[/\bsingle quotes\b/i, /\bdouble quotes\b/i],
|
|
164
|
+
[/\bsemicolons required\b/i, /\bno semicolons\b/i],
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
return contradictoryPairs.some(([a, b]) => a.test(content) && b.test(content));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function hasMisplacedNestedKeys(content) {
|
|
171
|
+
const lines = content.split(/\r?\n/);
|
|
172
|
+
let inRoot = true;
|
|
173
|
+
|
|
174
|
+
for (let index = 0; index < lines.length; index++) {
|
|
175
|
+
const trimmed = lines[index].trim();
|
|
176
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
177
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
178
|
+
inRoot = false;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const match = trimmed.match(/^([A-Za-z0-9_.-]+)\s*=/);
|
|
183
|
+
if (!match) continue;
|
|
184
|
+
|
|
185
|
+
if (inRoot && NESTED_ONLY_ROOT_KEYS.has(match[1])) {
|
|
186
|
+
return { misplaced: true, line: index + 1, key: match[1] };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { misplaced: false, line: null, key: null };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function findLegacyConfigIssue(content) {
|
|
194
|
+
for (let index = 0; index < LEGACY_CONFIG_PATTERNS.length; index++) {
|
|
195
|
+
const { pattern, note } = LEGACY_CONFIG_PATTERNS[index];
|
|
196
|
+
const line = firstLineMatching(content, pattern);
|
|
197
|
+
if (line) {
|
|
198
|
+
return { line, note };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function repoLooksRegulated(ctx) {
|
|
205
|
+
const filenames = ctx.files.join('\n');
|
|
206
|
+
const packageJson = ctx.fileContent('package.json') || '';
|
|
207
|
+
const readme = ctx.fileContent('README.md') || '';
|
|
208
|
+
const combined = `${filenames}\n${packageJson}\n${readme}`;
|
|
209
|
+
|
|
210
|
+
const strongSignals = /\bhipaa\b|\bphi\b|\bpci\b|\bsoc2\b|\biso[- ]?27001\b|\bcompliance\b|\bhealth(?:care)?\b|\bmedical\b|\bbank(?:ing)?\b|\bpayments?\b|\bfintech\b/i;
|
|
211
|
+
if (strongSignals.test(combined)) {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const weakSignalMatches = combined.match(/\bgdpr\b|\bpii\b/gi) || [];
|
|
216
|
+
if (weakSignalMatches.length === 0) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const privacyOnlyNote = /\b(no|without|never)\s+(collect|store|log|retain|send)\s+\bpii\b/i.test(combined) ||
|
|
221
|
+
/\bno\s+\bpii\b/i.test(combined);
|
|
222
|
+
if (weakSignalMatches.length === 1 && privacyOnlyNote) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return weakSignalMatches.length >= 2;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function hookEventsFromConfig(hooksJson) {
|
|
230
|
+
if (!hooksJson || typeof hooksJson !== 'object' || Array.isArray(hooksJson)) {
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (hooksJson.hooks && typeof hooksJson.hooks === 'object' && !Array.isArray(hooksJson.hooks)) {
|
|
235
|
+
return Object.keys(hooksJson.hooks);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return Object.keys(hooksJson);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function unsupportedHookEvent(ctx) {
|
|
242
|
+
const content = ctx.hooksJsonContent ? (ctx.hooksJsonContent() || '') : (ctx.fileContent('.codex/hooks.json') || '');
|
|
243
|
+
if (!content) return null;
|
|
244
|
+
|
|
245
|
+
const parsed = ctx.hooksJson();
|
|
246
|
+
if (!parsed) {
|
|
247
|
+
return { event: 'invalid-json', line: 1 };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const events = hookEventsFromConfig(parsed);
|
|
251
|
+
for (const event of events) {
|
|
252
|
+
if (!SUPPORTED_HOOK_EVENTS.has(event)) {
|
|
253
|
+
const line = ctx.lineNumber('.codex/hooks.json', new RegExp(`"${escapeRegex(event)}"\\s*:|${escapeRegex(event)}\\s*:`, 'i')) || 1;
|
|
254
|
+
return { event, line };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function hooksClaimed(ctx) {
|
|
262
|
+
if (ctx.hasDir('.codex/hooks')) return true;
|
|
263
|
+
if (ctx.hooksJsonContent && ctx.hooksJsonContent()) return true;
|
|
264
|
+
const content = agentsContent(ctx);
|
|
265
|
+
return /\bhooks?\b|\bSessionStart\b|\bPreToolUse\b|\bPostToolUse\b|\bUserPromptSubmit\b|\bStop\b/i.test(content);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function findSecretLine(content) {
|
|
269
|
+
const lines = content.split(/\r?\n/);
|
|
270
|
+
for (let index = 0; index < lines.length; index++) {
|
|
271
|
+
const line = lines[index];
|
|
272
|
+
const matched = EMBEDDED_SECRET_PATTERNS.some((pattern) => {
|
|
273
|
+
pattern.lastIndex = 0;
|
|
274
|
+
return pattern.test(line);
|
|
275
|
+
});
|
|
276
|
+
if (matched) return index + 1;
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function mcpServersWithTimeouts(ctx) {
|
|
282
|
+
const servers = ctx.mcpServers();
|
|
283
|
+
return Object.entries(servers || {}).map(([id, server]) => ({
|
|
284
|
+
id,
|
|
285
|
+
timeout: server && typeof server.startup_timeout_sec === 'number' ? server.startup_timeout_sec : null,
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function workflowArtifacts(ctx) {
|
|
290
|
+
return (ctx.workflowFiles ? ctx.workflowFiles() : [])
|
|
291
|
+
.map((filePath) => ({ filePath, content: ctx.fileContent(filePath) || '' }))
|
|
292
|
+
.filter((item) => item.content);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function codexActionWorkflowIssues(ctx) {
|
|
296
|
+
const issues = [];
|
|
297
|
+
for (const workflow of workflowArtifacts(ctx)) {
|
|
298
|
+
if (!/uses:\s*openai\/codex-action@/i.test(workflow.content)) continue;
|
|
299
|
+
const unsafeLine = firstLineMatching(workflow.content, /safety-strategy\s*:\s*unsafe\b/i);
|
|
300
|
+
if (!unsafeLine) continue;
|
|
301
|
+
|
|
302
|
+
const justified = /windows-latest|windows-\d+|runner\.os\s*==\s*['"]Windows['"]|runs-on:\s*\[[^\]]*windows/i.test(workflow.content) ||
|
|
303
|
+
(JUSTIFICATION_PATTERNS.test(workflow.content) && /\bunsafe\b/i.test(workflow.content));
|
|
304
|
+
|
|
305
|
+
issues.push({
|
|
306
|
+
filePath: workflow.filePath,
|
|
307
|
+
line: unsafeLine,
|
|
308
|
+
justified,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return issues;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function profileSections(ctx) {
|
|
315
|
+
return configSections(ctx).filter((section) => section.section.startsWith('profiles.'));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function parsedProfiles(ctx) {
|
|
319
|
+
const config = ctx.configToml();
|
|
320
|
+
if (!config.ok || !config.data || !config.data.profiles || typeof config.data.profiles !== 'object') {
|
|
321
|
+
return {};
|
|
322
|
+
}
|
|
323
|
+
return config.data.profiles;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function projectMcpServers(ctx) {
|
|
327
|
+
const config = ctx.configToml();
|
|
328
|
+
if (!config.ok || !config.data || !config.data.mcp_servers || typeof config.data.mcp_servers !== 'object') {
|
|
329
|
+
return {};
|
|
330
|
+
}
|
|
331
|
+
return config.data.mcp_servers;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function repoNeedsExternalTools(ctx) {
|
|
335
|
+
const deps = ctx.projectDependencies ? Object.keys(ctx.projectDependencies()) : [];
|
|
336
|
+
const depSet = new Set(deps);
|
|
337
|
+
const files = new Set(ctx.files || []);
|
|
338
|
+
const envContent = [
|
|
339
|
+
ctx.fileContent('.env.example'),
|
|
340
|
+
ctx.fileContent('.env.template'),
|
|
341
|
+
ctx.fileContent('.env.sample'),
|
|
342
|
+
].filter(Boolean).join('\n');
|
|
343
|
+
const readme = ctx.fileContent('README.md') || '';
|
|
344
|
+
const agents = agentsContent(ctx);
|
|
345
|
+
const combinedDocs = `${readme}\n${agents}\n${envContent}`;
|
|
346
|
+
|
|
347
|
+
const externalDeps = [
|
|
348
|
+
'pg',
|
|
349
|
+
'postgres',
|
|
350
|
+
'mysql',
|
|
351
|
+
'mysql2',
|
|
352
|
+
'mongodb',
|
|
353
|
+
'mongoose',
|
|
354
|
+
'redis',
|
|
355
|
+
'ioredis',
|
|
356
|
+
'prisma',
|
|
357
|
+
'sequelize',
|
|
358
|
+
'typeorm',
|
|
359
|
+
'supabase',
|
|
360
|
+
'@supabase/supabase-js',
|
|
361
|
+
'stripe',
|
|
362
|
+
'openai',
|
|
363
|
+
'@anthropic-ai/sdk',
|
|
364
|
+
'langchain',
|
|
365
|
+
'@langchain/openai',
|
|
366
|
+
'@langchain/anthropic',
|
|
367
|
+
'@aws-sdk/client-s3',
|
|
368
|
+
'@aws-sdk/client-dynamodb',
|
|
369
|
+
'@aws-sdk/client-secrets-manager',
|
|
370
|
+
'@notionhq/client',
|
|
371
|
+
'@slack/bolt',
|
|
372
|
+
'twilio',
|
|
373
|
+
'discord.js',
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
if (externalDeps.some((dep) => depSet.has(dep))) {
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (
|
|
381
|
+
files.has('docker-compose.yml') ||
|
|
382
|
+
files.has('docker-compose.yaml') ||
|
|
383
|
+
files.has('compose.yml') ||
|
|
384
|
+
files.has('compose.yaml') ||
|
|
385
|
+
files.has('schema.prisma') ||
|
|
386
|
+
ctx.hasDir('prisma') ||
|
|
387
|
+
ctx.hasDir('infra') ||
|
|
388
|
+
ctx.hasDir('terraform') ||
|
|
389
|
+
ctx.hasDir('migrations') ||
|
|
390
|
+
ctx.hasDir('sql')
|
|
391
|
+
) {
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return /\bDATABASE_URL\b|\bREDIS_URL\b|\bSUPABASE_URL\b|\bSTRIPE_[A-Z_]+\b|\bAWS_[A-Z_]+\b|\bTWILIO_[A-Z_]+\b|\bNOTION_[A-Z_]+\b|\bSLACK_[A-Z_]+\b|\bOPENAI_API_KEY\b|\bANTHROPIC_API_KEY\b/i.test(combinedDocs);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function projectScopedMcpPresent(ctx) {
|
|
399
|
+
return Object.keys(projectMcpServers(ctx)).length > 0;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function repoRuleArtifacts(ctx) {
|
|
403
|
+
return (ctx.ruleFiles ? ctx.ruleFiles() : [])
|
|
404
|
+
.map((filePath) => ({ filePath, content: ctx.fileContent(filePath) || '' }))
|
|
405
|
+
.filter((item) => item.content);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function extractRuleBlocks(content) {
|
|
409
|
+
const lines = content.split(/\r?\n/);
|
|
410
|
+
const blocks = [];
|
|
411
|
+
let startLine = null;
|
|
412
|
+
let buffer = [];
|
|
413
|
+
let depth = 0;
|
|
414
|
+
|
|
415
|
+
for (let index = 0; index < lines.length; index++) {
|
|
416
|
+
const line = lines[index];
|
|
417
|
+
if (startLine === null && /\bprefix_rule\s*\(/.test(line)) {
|
|
418
|
+
startLine = index + 1;
|
|
419
|
+
buffer = [line];
|
|
420
|
+
depth = (line.match(/\(/g) || []).length - (line.match(/\)/g) || []).length;
|
|
421
|
+
if (depth <= 0) {
|
|
422
|
+
blocks.push({ startLine, content: buffer.join('\n') });
|
|
423
|
+
startLine = null;
|
|
424
|
+
buffer = [];
|
|
425
|
+
depth = 0;
|
|
426
|
+
}
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (startLine !== null) {
|
|
431
|
+
buffer.push(line);
|
|
432
|
+
depth += (line.match(/\(/g) || []).length - (line.match(/\)/g) || []).length;
|
|
433
|
+
if (depth <= 0) {
|
|
434
|
+
blocks.push({ startLine, content: buffer.join('\n') });
|
|
435
|
+
startLine = null;
|
|
436
|
+
buffer = [];
|
|
437
|
+
depth = 0;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return blocks;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function allRuleBlocks(ctx) {
|
|
446
|
+
return repoRuleArtifacts(ctx).flatMap((artifact) =>
|
|
447
|
+
extractRuleBlocks(artifact.content).map((block) => ({
|
|
448
|
+
...block,
|
|
449
|
+
filePath: artifact.filePath,
|
|
450
|
+
}))
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function rulePatternTokens(blockContent) {
|
|
455
|
+
const match = blockContent.match(/pattern\s*=\s*\[([\s\S]*?)\]/i);
|
|
456
|
+
if (!match) return [];
|
|
457
|
+
return [...match[1].matchAll(/["']([^"']+)["']/g)].map((item) => item[1]);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function ruleDecision(blockContent) {
|
|
461
|
+
const match = blockContent.match(/decision\s*=\s*["']([^"']+)["']/i);
|
|
462
|
+
return match ? match[1].toLowerCase() : null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function ruleHasExamples(blockContent) {
|
|
466
|
+
return /\bmatch\s*=\s*\[/i.test(blockContent) || /\bnot_match\s*=\s*\[/i.test(blockContent);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function broadAllowRule(blockContent) {
|
|
470
|
+
const decision = ruleDecision(blockContent);
|
|
471
|
+
if (decision !== 'allow') return false;
|
|
472
|
+
|
|
473
|
+
const tokens = rulePatternTokens(blockContent).map((token) => token.toLowerCase());
|
|
474
|
+
if (tokens.some((token) => token === '*' || token.includes('*') || token.includes('?'))) {
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const broadSingleCommands = new Set(['bash', 'sh', 'pwsh', 'powershell', 'cmd', 'git', 'npm', 'pnpm', 'yarn', 'node', 'python']);
|
|
479
|
+
return tokens.length === 1 && broadSingleCommands.has(tokens[0]);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function specificRulePatternIssue(ctx) {
|
|
483
|
+
for (const block of allRuleBlocks(ctx)) {
|
|
484
|
+
const tokens = rulePatternTokens(block.content);
|
|
485
|
+
if (tokens.some((token) => token === '*' || token.includes('*') || token.includes('?'))) {
|
|
486
|
+
return { filePath: block.filePath, line: block.startLine };
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function missingRuleExamplesIssue(ctx) {
|
|
493
|
+
for (const block of allRuleBlocks(ctx)) {
|
|
494
|
+
if (!ruleHasExamples(block.content)) {
|
|
495
|
+
return { filePath: block.filePath, line: block.startLine };
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function broadAllowRuleIssue(ctx) {
|
|
502
|
+
for (const block of allRuleBlocks(ctx)) {
|
|
503
|
+
if (broadAllowRule(block.content)) {
|
|
504
|
+
return { filePath: block.filePath, line: block.startLine };
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function ruleCoverageIssue(ctx) {
|
|
511
|
+
const riskyCommands = new Set(['rm', 'git', 'gh', 'docker', 'kubectl', 'terraform', 'bash', 'sh', 'pwsh', 'powershell', 'cmd', 'npm', 'pnpm', 'yarn']);
|
|
512
|
+
const blocks = allRuleBlocks(ctx);
|
|
513
|
+
if (blocks.length === 0) {
|
|
514
|
+
return { filePath: null, line: null, missing: true };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const covered = blocks.some((block) => {
|
|
518
|
+
const tokens = rulePatternTokens(block.content).map((token) => token.toLowerCase());
|
|
519
|
+
return tokens.some((token) => riskyCommands.has(token)) || /\bhost_executable\s*\(/i.test(block.content);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
return covered ? null : { filePath: blocks[0].filePath, line: blocks[0].startLine, missing: false };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function ruleWrapperRiskIssue(ctx) {
|
|
526
|
+
const blocks = allRuleBlocks(ctx);
|
|
527
|
+
if (blocks.length === 0) return null;
|
|
528
|
+
|
|
529
|
+
const wrapperBlock = blocks.find((block) => /\bbash\b|\bsh\b|\bpwsh\b|\bpowershell\b|\bcmd\b|host_executable\s*\(/i.test(block.content));
|
|
530
|
+
if (!wrapperBlock) return null;
|
|
531
|
+
|
|
532
|
+
const docs = `${agentsContent(ctx)}\n${ctx.fileContent('README.md') || ''}\n${repoRuleArtifacts(ctx).map((item) => item.content).join('\n')}`;
|
|
533
|
+
const hasCaveat = /\bwrapper\b|\bsplit(?:ting)?\b|\bbash -lc\b|\bhost_executable\b|\bresolve-host-executables\b|\bpowershell\b|\bpwsh\b/i.test(docs);
|
|
534
|
+
return hasCaveat ? null : { filePath: wrapperBlock.filePath, line: wrapperBlock.startLine };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function explicitHooksFeatureValue(ctx) {
|
|
538
|
+
const value = ctx.configValue('features.codex_hooks');
|
|
539
|
+
return typeof value === 'boolean' ? value : null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function collectHookTimeoutEntries(node, trail = []) {
|
|
543
|
+
const results = [];
|
|
544
|
+
if (Array.isArray(node)) {
|
|
545
|
+
node.forEach((item, index) => {
|
|
546
|
+
results.push(...collectHookTimeoutEntries(item, [...trail, `[${index}]`]));
|
|
547
|
+
});
|
|
548
|
+
return results;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (!node || typeof node !== 'object') {
|
|
552
|
+
return results;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
for (const [key, value] of Object.entries(node)) {
|
|
556
|
+
if (key === 'timeout' && typeof value === 'number') {
|
|
557
|
+
results.push({ timeout: value, trail: [...trail, key] });
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
results.push(...collectHookTimeoutEntries(value, [...trail, key]));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return results;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function longHookTimeoutIssue(ctx) {
|
|
567
|
+
const hooks = ctx.hooksJson();
|
|
568
|
+
if (!hooks) return null;
|
|
569
|
+
const entries = collectHookTimeoutEntries(hooks);
|
|
570
|
+
const long = entries.find((entry) => entry.timeout > 60);
|
|
571
|
+
if (!long) return null;
|
|
572
|
+
|
|
573
|
+
const docs = `${agentsContent(ctx)}\n${ctx.fileContent('README.md') || ''}`;
|
|
574
|
+
const justified = /\btimeout\b|\bslow\b|\blong-running\b|\bintegration\b|\bremote\b/i.test(docs) && JUSTIFICATION_PATTERNS.test(docs);
|
|
575
|
+
if (justified) return null;
|
|
576
|
+
|
|
577
|
+
const line = ctx.lineNumber('.codex/hooks.json', /"timeout"\s*:\s*(6[1-9]|[7-9]\d|\d{3,})\b/i) || 1;
|
|
578
|
+
return { filePath: '.codex/hooks.json', line };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function mcpAuthDocumentationIssue(ctx) {
|
|
582
|
+
const servers = projectMcpServers(ctx);
|
|
583
|
+
const docs = `${agentsContent(ctx)}\n${ctx.fileContent('README.md') || ''}`;
|
|
584
|
+
|
|
585
|
+
for (const [id, server] of Object.entries(servers || {})) {
|
|
586
|
+
const needsAuthNote = Boolean(server.url);
|
|
587
|
+
if (!needsAuthNote) continue;
|
|
588
|
+
|
|
589
|
+
const hasInlineAuth =
|
|
590
|
+
Boolean(server.bearer_token_env_var) ||
|
|
591
|
+
Boolean(server.http_headers) ||
|
|
592
|
+
Boolean(server.env_http_headers) ||
|
|
593
|
+
(server.env && typeof server.env === 'object' && Object.keys(server.env).length > 0) ||
|
|
594
|
+
(Array.isArray(server.env_vars) && server.env_vars.length > 0);
|
|
595
|
+
|
|
596
|
+
const hasDocNote = new RegExp(`\\b${escapeRegex(id)}\\b[\\s\\S]{0,140}\\b(auth|oauth|token|credential|env)\\b`, 'i').test(docs);
|
|
597
|
+
if (!hasInlineAuth && !hasDocNote) {
|
|
598
|
+
return {
|
|
599
|
+
id,
|
|
600
|
+
filePath: '.codex/config.toml',
|
|
601
|
+
line: (configSections(ctx).find((section) => section.section === `mcp_servers.${id}`) || {}).line || 1,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function deprecatedMcpTransportIssue(ctx) {
|
|
610
|
+
const content = ctx.configContent ? (ctx.configContent() || '') : (ctx.fileContent('.codex/config.toml') || '');
|
|
611
|
+
if (!content) return null;
|
|
612
|
+
const line = firstLineMatching(content, /\btransport\s*=\s*["'](?:sse|http\+sse)["']|\bsse_url\s*=/i);
|
|
613
|
+
return line ? { filePath: '.codex/config.toml', line } : null;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function docsBundle(ctx) {
|
|
617
|
+
return `${agentsContent(ctx)}\n${ctx.fileContent('README.md') || ''}`;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function skillArtifacts(ctx) {
|
|
621
|
+
return (ctx.skillDirs ? ctx.skillDirs() : []).map((name) => ({
|
|
622
|
+
name,
|
|
623
|
+
filePath: `.agents/skills/${name}/SKILL.md`,
|
|
624
|
+
content: ctx.skillMetadata ? (ctx.skillMetadata(name) || '') : '',
|
|
625
|
+
}));
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function extractSkillTitle(content) {
|
|
629
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
630
|
+
return match ? match[1].trim() : '';
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function extractSkillDescription(content) {
|
|
634
|
+
const lines = content.split(/\r?\n/);
|
|
635
|
+
const meaningful = [];
|
|
636
|
+
let seenHeading = false;
|
|
637
|
+
|
|
638
|
+
for (const line of lines) {
|
|
639
|
+
const trimmed = line.trim();
|
|
640
|
+
if (!trimmed) {
|
|
641
|
+
if (seenHeading && meaningful.length > 0) break;
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (!seenHeading) {
|
|
645
|
+
if (trimmed.startsWith('#')) {
|
|
646
|
+
seenHeading = true;
|
|
647
|
+
}
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (trimmed.startsWith('#') || trimmed.startsWith('```')) break;
|
|
651
|
+
meaningful.push(trimmed.replace(/^[-*]\s+/, ''));
|
|
652
|
+
if (meaningful.length >= 3) break;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return meaningful.join(' ').trim();
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function repoClaimsSkills(ctx) {
|
|
659
|
+
if ((ctx.skillDirs ? ctx.skillDirs() : []).length > 0) return true;
|
|
660
|
+
const docs = docsBundle(ctx);
|
|
661
|
+
return /\.agents\/skills\b|\bskill(s)?\b/i.test(docs);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function skillMissingFieldsIssue(ctx) {
|
|
665
|
+
for (const skill of skillArtifacts(ctx)) {
|
|
666
|
+
if (!skill.content) {
|
|
667
|
+
return { filePath: skill.filePath, line: 1 };
|
|
668
|
+
}
|
|
669
|
+
const title = extractSkillTitle(skill.content);
|
|
670
|
+
const description = extractSkillDescription(skill.content);
|
|
671
|
+
if (!title || !description) {
|
|
672
|
+
return {
|
|
673
|
+
filePath: skill.filePath,
|
|
674
|
+
line: !title ? 1 : (firstLineMatching(skill.content, /\S/) || 1),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function skillBadNameIssue(ctx) {
|
|
682
|
+
const invalid = skillArtifacts(ctx).find((skill) => !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(skill.name));
|
|
683
|
+
return invalid ? { filePath: invalid.filePath, line: 1 } : null;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function skillDescriptionTooLongIssue(ctx) {
|
|
687
|
+
for (const skill of skillArtifacts(ctx)) {
|
|
688
|
+
const description = extractSkillDescription(skill.content);
|
|
689
|
+
if (!description) continue;
|
|
690
|
+
if (description.length > 220 || description.split(/\s+/).length > 32) {
|
|
691
|
+
const line = firstLineMatching(skill.content, (line) => {
|
|
692
|
+
const trimmed = line.trim();
|
|
693
|
+
return trimmed && !trimmed.startsWith('#');
|
|
694
|
+
}) || 1;
|
|
695
|
+
return { filePath: skill.filePath, line };
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function skillAutoRunRiskIssue(ctx) {
|
|
702
|
+
const riskyPatterns = /\balways run\b|\bauto(?:matically)?\s+(run|execute|deploy|publish|merge)\b|\bwithout (approval|review|asking)\b|\brm -rf\b|\bgit push\b|\bdeploy\b|\bpublish\b/i;
|
|
703
|
+
const safetyPatterns = /\bapproval\b|\breview\b|\bconfirm\b|\bmanual\b|\bsandbox\b|\bask first\b/i;
|
|
704
|
+
|
|
705
|
+
for (const skill of skillArtifacts(ctx)) {
|
|
706
|
+
if (!skill.content) continue;
|
|
707
|
+
if (riskyPatterns.test(skill.content) && !safetyPatterns.test(skill.content)) {
|
|
708
|
+
const line = firstLineMatching(skill.content, riskyPatterns) || 1;
|
|
709
|
+
return { filePath: skill.filePath, line };
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function repoUsesCustomAgents(ctx) {
|
|
717
|
+
if ((ctx.customAgentFiles ? ctx.customAgentFiles() : []).length > 0) return true;
|
|
718
|
+
const docs = docsBundle(ctx);
|
|
719
|
+
return /\.codex\/agents\b|\bsubagents?\b|\bcustom agents?\b/i.test(docs);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function customAgentMissingFieldsIssue(ctx) {
|
|
723
|
+
const files = ctx.customAgentFiles ? ctx.customAgentFiles() : [];
|
|
724
|
+
for (const fileName of files) {
|
|
725
|
+
const parsed = ctx.customAgentConfig(fileName);
|
|
726
|
+
if (!parsed.ok || !parsed.data) {
|
|
727
|
+
return { filePath: `.codex/agents/${fileName}`, line: 1 };
|
|
728
|
+
}
|
|
729
|
+
const required = ['name', 'description', 'developer_instructions'];
|
|
730
|
+
const missing = required.find((key) => {
|
|
731
|
+
const value = parsed.data[key];
|
|
732
|
+
return !(typeof value === 'string' && value.trim());
|
|
733
|
+
});
|
|
734
|
+
if (missing) {
|
|
735
|
+
const content = ctx.fileContent(path.join('.codex', 'agents', fileName)) || '';
|
|
736
|
+
return {
|
|
737
|
+
filePath: `.codex/agents/${fileName}`,
|
|
738
|
+
line: firstLineMatching(content, new RegExp(`^\\s*${escapeRegex(missing)}\\s*=`, 'i')) || 1,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return null;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function unsafeAgentOverrideIssue(ctx) {
|
|
746
|
+
const files = ctx.customAgentFiles ? ctx.customAgentFiles() : [];
|
|
747
|
+
for (const fileName of files) {
|
|
748
|
+
const parsed = ctx.customAgentConfig(fileName);
|
|
749
|
+
if (!parsed.ok || !parsed.data) {
|
|
750
|
+
return { filePath: `.codex/agents/${fileName}`, line: 1 };
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const sandboxMode = parsed.data.sandbox_mode;
|
|
754
|
+
if (sandboxMode === 'danger-full-access') {
|
|
755
|
+
const content = ctx.fileContent(path.join('.codex', 'agents', fileName)) || '';
|
|
756
|
+
return {
|
|
757
|
+
filePath: `.codex/agents/${fileName}`,
|
|
758
|
+
line: firstLineMatching(content, /^\s*sandbox_mode\s*=/i) || 1,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const approval = parsed.data.approval_policy;
|
|
763
|
+
const content = ctx.fileContent(path.join('.codex', 'agents', fileName)) || '';
|
|
764
|
+
const justified = JUSTIFICATION_PATTERNS.test(content);
|
|
765
|
+
if (approval === 'never' && !justified) {
|
|
766
|
+
return {
|
|
767
|
+
filePath: `.codex/agents/${fileName}`,
|
|
768
|
+
line: firstLineMatching(content, /^\s*approval_policy\s*=/i) || 1,
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function codexAutomationArtifacts(ctx) {
|
|
776
|
+
const items = [];
|
|
777
|
+
for (const workflow of workflowArtifacts(ctx)) {
|
|
778
|
+
if (/\bcodex\b/i.test(workflow.content)) {
|
|
779
|
+
items.push(workflow);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const pkg = ctx.jsonFile('package.json');
|
|
784
|
+
if (pkg && pkg.scripts) {
|
|
785
|
+
for (const [name, command] of Object.entries(pkg.scripts)) {
|
|
786
|
+
if (/\bcodex\s+(exec|review|cloud\s+exec)\b/i.test(command)) {
|
|
787
|
+
items.push({
|
|
788
|
+
filePath: 'package.json',
|
|
789
|
+
content: command,
|
|
790
|
+
line: ctx.lineNumber('package.json', new RegExp(`"${escapeRegex(name)}"\\s*:\\s*"`, 'i')) || 1,
|
|
791
|
+
kind: 'script',
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return items;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function codexExecUnsafeIssue(ctx) {
|
|
801
|
+
for (const item of codexAutomationArtifacts(ctx)) {
|
|
802
|
+
const content = item.content || '';
|
|
803
|
+
const risky = /codex\s+exec\b[\s\S]{0,120}(--dangerously-bypass-approvals-and-sandbox|--full-auto\b|--ask-for-approval\s+never|-a\s+never\b)/i.test(content) ||
|
|
804
|
+
/\bcodex-action@/i.test(content) && /safety-strategy\s*:\s*unsafe\b/i.test(content) && !/windows/i.test(content);
|
|
805
|
+
if (risky) {
|
|
806
|
+
return {
|
|
807
|
+
filePath: item.filePath,
|
|
808
|
+
line: item.line || firstLineMatching(content, /codex\s+exec\b|safety-strategy\s*:\s*unsafe\b/i) || 1,
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function codexActionSafeStrategyIssue(ctx) {
|
|
816
|
+
for (const workflow of workflowArtifacts(ctx)) {
|
|
817
|
+
if (!/uses:\s*openai\/codex-action@/i.test(workflow.content)) continue;
|
|
818
|
+
|
|
819
|
+
const line = firstLineMatching(workflow.content, /safety-strategy\s*:/i);
|
|
820
|
+
if (!line) {
|
|
821
|
+
return { filePath: workflow.filePath, line: firstLineMatching(workflow.content, /uses:\s*openai\/codex-action@/i) || 1 };
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const unsafe = /safety-strategy\s*:\s*unsafe\b/i.test(workflow.content);
|
|
825
|
+
const windowsOnly = /windows-latest|windows-\d+|runner\.os\s*==\s*['"]Windows['"]|runs-on:\s*\[[^\]]*windows/i.test(workflow.content);
|
|
826
|
+
if (unsafe && !windowsOnly) {
|
|
827
|
+
return { filePath: workflow.filePath, line };
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function codexCiAuthIssue(ctx) {
|
|
834
|
+
for (const workflow of workflowArtifacts(ctx)) {
|
|
835
|
+
if (!/\bcodex\b|openai\/codex-action@/i.test(workflow.content)) continue;
|
|
836
|
+
const hasCodexKey =
|
|
837
|
+
/\bCODEX_API_KEY\b/i.test(workflow.content) ||
|
|
838
|
+
/\bOPENAI_API_KEY\b/i.test(workflow.content) ||
|
|
839
|
+
/\bapi[-_ ]?key\b/i.test(workflow.content) ||
|
|
840
|
+
/\$\{\{\s*secrets\.[A-Z0-9_]+/i.test(workflow.content);
|
|
841
|
+
const hardcodedSecret = /sk-[A-Za-z0-9_-]{16,}|api[_-]?key\s*:\s*["'][A-Za-z0-9_-]{12,}["']/i.test(workflow.content);
|
|
842
|
+
if (hardcodedSecret || !hasCodexKey) {
|
|
843
|
+
return {
|
|
844
|
+
filePath: workflow.filePath,
|
|
845
|
+
line: firstLineMatching(workflow.content, /CODEX_API_KEY|OPENAI_API_KEY|api[-_ ]?key|sk-/i) || 1,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function automationManualTestingIssue(ctx) {
|
|
853
|
+
const artifacts = codexAutomationArtifacts(ctx);
|
|
854
|
+
if (artifacts.length === 0) return null;
|
|
855
|
+
|
|
856
|
+
const docs = docsBundle(ctx);
|
|
857
|
+
const hasManualTestingNote = /\bmanual(?:ly)? tested\b|\bdry[- ]run\b|\bstaging\b|\bvalidated locally\b|\btested locally\b/i.test(docs);
|
|
858
|
+
if (hasManualTestingNote) return null;
|
|
859
|
+
|
|
860
|
+
const target = artifacts[0];
|
|
861
|
+
return {
|
|
862
|
+
filePath: target.filePath,
|
|
863
|
+
line: target.line || firstLineMatching(target.content || '', /\bcodex\b|openai\/codex-action@/i) || 1,
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function reviewWorkflowDocumented(ctx) {
|
|
868
|
+
return /\bcodex review\b|\/review\b|\breview --uncommitted\b/i.test(docsBundle(ctx));
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function reviewModelOverrideIssue(ctx) {
|
|
872
|
+
const artifacts = codexAutomationArtifacts(ctx).filter((item) => /\bcodex\s+review\b/i.test(item.content || ''));
|
|
873
|
+
if (artifacts.length === 0) return null;
|
|
874
|
+
|
|
875
|
+
const hasReviewModelOverride = artifacts.some((item) => /\s(--model|-m)\s+\S+/i.test(item.content || ''));
|
|
876
|
+
const hasReviewProfile = Boolean(parsedProfiles(ctx).review);
|
|
877
|
+
if (hasReviewModelOverride || hasReviewProfile) return null;
|
|
878
|
+
|
|
879
|
+
const target = artifacts[0];
|
|
880
|
+
return {
|
|
881
|
+
filePath: target.filePath,
|
|
882
|
+
line: target.line || firstLineMatching(target.content || '', /\bcodex\s+review\b/i) || 1,
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function workingTreeReviewDocsPresent(ctx) {
|
|
887
|
+
return /\bworking[- ]tree\b|\buncommitted\b|\bstaged\b|\bkeep unrelated edits separate\b|\bdo not mix unrelated edits\b/i.test(docsBundle(ctx));
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function costAwarenessDocsPresent(ctx) {
|
|
891
|
+
return /\bcost\b|\blatency\b|\breasoning\b|\bheavy workflows?\b|\bexpensive\b/i.test(docsBundle(ctx));
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function codexArtifactsIgnoredIssue(ctx) {
|
|
895
|
+
const gitignore = ctx.fileContent('.gitignore');
|
|
896
|
+
if (!gitignore) return null;
|
|
897
|
+
const line = firstLineMatching(gitignore, /^\.codex\/?$|^\.codex\/\*\*?$|^\.agents\/skills\/?$/im);
|
|
898
|
+
return line ? { filePath: '.gitignore', line } : null;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function lifecycleScripts(ctx) {
|
|
902
|
+
const files = ctx.files || [];
|
|
903
|
+
return files.filter((file) => /(^|\/)(setup|teardown)\.(sh|ps1|cmd|bat)$/i.test(file));
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function lifecycleScriptIssue(ctx) {
|
|
907
|
+
const scripts = lifecycleScripts(ctx);
|
|
908
|
+
if (scripts.length === 0) return null;
|
|
909
|
+
|
|
910
|
+
const docs = docsBundle(ctx);
|
|
911
|
+
for (const filePath of scripts) {
|
|
912
|
+
const content = ctx.fileContent(filePath) || '';
|
|
913
|
+
const shellOnly = /^#!.*\b(bash|sh)\b/m.test(content) || filePath.endsWith('.sh');
|
|
914
|
+
const hasPlatformNote = /\bwindows\b|\bplatform-safe\b|\bpwsh\b|\bpowershell\b|\bcross-platform\b/i.test(docs);
|
|
915
|
+
if (shellOnly && !hasPlatformNote) {
|
|
916
|
+
return { filePath, line: 1 };
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function redundantCodexWorkflowIssue(ctx) {
|
|
924
|
+
const workflows = workflowArtifacts(ctx).filter((workflow) => /\bcodex\b|openai\/codex-action@/i.test(workflow.content));
|
|
925
|
+
if (workflows.length < 2) return null;
|
|
926
|
+
|
|
927
|
+
const seen = new Map();
|
|
928
|
+
for (const workflow of workflows) {
|
|
929
|
+
const normalized = workflow.content
|
|
930
|
+
.replace(/\s+/g, ' ')
|
|
931
|
+
.replace(/name:\s*[^:]+/i, '')
|
|
932
|
+
.trim();
|
|
933
|
+
if (seen.has(normalized)) {
|
|
934
|
+
return {
|
|
935
|
+
filePath: workflow.filePath,
|
|
936
|
+
line: firstLineMatching(workflow.content, /openai\/codex-action@|\bcodex\b/i) || 1,
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
seen.set(normalized, workflow.filePath);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function worktreeLifecycleDocsIssue(ctx) {
|
|
946
|
+
const docs = docsBundle(ctx);
|
|
947
|
+
const worktreeRelevant = lifecycleScripts(ctx).length > 0 || /\bworktrees?\b/i.test(docs);
|
|
948
|
+
if (!worktreeRelevant) return null;
|
|
949
|
+
|
|
950
|
+
const documented = /\bworktrees?\b[\s\S]{0,140}\b(cleanup|lifecycle|branch|teardown|setup)\b/i.test(docs) ||
|
|
951
|
+
/\bcleanup\b|\bteardown\b|\bbranch-specific\b/i.test(docs);
|
|
952
|
+
return documented ? null : { filePath: agentsPath(ctx) || 'README.md', line: 1 };
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function agentsMissingModernFeaturesIssue(ctx) {
|
|
956
|
+
const docs = agentsContent(ctx);
|
|
957
|
+
if (!docs) return null;
|
|
958
|
+
|
|
959
|
+
const needsSkills = (ctx.skillDirs ? ctx.skillDirs() : []).length > 0;
|
|
960
|
+
const needsAgents = (ctx.customAgentFiles ? ctx.customAgentFiles() : []).length > 0;
|
|
961
|
+
const needsHooks = hooksClaimed(ctx);
|
|
962
|
+
const needsMcp = projectScopedMcpPresent(ctx);
|
|
963
|
+
|
|
964
|
+
const missing =
|
|
965
|
+
(needsSkills && !/\bskills?\b/i.test(docs)) ||
|
|
966
|
+
(needsAgents && !/\bsubagents?\b|\bagents?\b/i.test(docs)) ||
|
|
967
|
+
(needsHooks && !/\bhooks?\b/i.test(docs)) ||
|
|
968
|
+
(needsMcp && !/\bmcp\b/i.test(docs));
|
|
969
|
+
|
|
970
|
+
return missing ? { filePath: agentsPath(ctx) || 'AGENTS.md', line: 1 } : null;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function deprecatedCodexPatternIssue(ctx) {
|
|
974
|
+
const config = ctx.configContent ? (ctx.configContent() || '') : (ctx.fileContent('.codex/config.toml') || '');
|
|
975
|
+
const docs = docsBundle(ctx);
|
|
976
|
+
const legacyConfigLine = firstLineMatching(config, /\bapproval_policy\s*=\s*["']on-failure["']/i);
|
|
977
|
+
if (legacyConfigLine) {
|
|
978
|
+
return { filePath: '.codex/config.toml', line: legacyConfigLine };
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const docLine = firstLineMatching(docs, /\bon-failure\b|\bcodex-mini-latest\b/i);
|
|
982
|
+
if (docLine) {
|
|
983
|
+
return { filePath: agentsPath(ctx) || 'README.md', line: docLine };
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function profilesNeededIssue(ctx) {
|
|
990
|
+
const needsProfiles = codexAutomationArtifacts(ctx).length > 0 || (ctx.customAgentFiles ? ctx.customAgentFiles().length > 0 : false);
|
|
991
|
+
if (!needsProfiles) return null;
|
|
992
|
+
|
|
993
|
+
const activeProfile = ctx.configValue('profile');
|
|
994
|
+
const profiles = parsedProfiles(ctx);
|
|
995
|
+
if (activeProfile && profiles[activeProfile]) return null;
|
|
996
|
+
if (Object.keys(profiles).length > 0) return null;
|
|
997
|
+
|
|
998
|
+
return { filePath: '.codex/config.toml', line: configKeyLine(ctx, 'profile') || 1 };
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function pluginConfigIssue(ctx) {
|
|
1002
|
+
const filePath = '.agents/plugins/marketplace.json';
|
|
1003
|
+
const content = ctx.fileContent(filePath);
|
|
1004
|
+
if (!content) return null;
|
|
1005
|
+
|
|
1006
|
+
try {
|
|
1007
|
+
const parsed = JSON.parse(content);
|
|
1008
|
+
const valid = Array.isArray(parsed) || (parsed && typeof parsed === 'object');
|
|
1009
|
+
return valid ? null : { filePath, line: 1 };
|
|
1010
|
+
} catch {
|
|
1011
|
+
return { filePath, line: 1 };
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const CODEX_TECHNIQUES = {
|
|
1016
|
+
codexAgentsMd: {
|
|
1017
|
+
id: 'CX-A01',
|
|
1018
|
+
name: 'AGENTS.md exists at project root',
|
|
1019
|
+
check: (ctx) => Boolean(ctx.fileContent('AGENTS.md')),
|
|
1020
|
+
impact: 'critical',
|
|
1021
|
+
rating: 5,
|
|
1022
|
+
category: 'instructions',
|
|
1023
|
+
fix: 'Create AGENTS.md at the project root with repo-specific commands, trust guidance, and workflow expectations.',
|
|
1024
|
+
template: 'codex-agents-md',
|
|
1025
|
+
file: () => 'AGENTS.md',
|
|
1026
|
+
line: (ctx) => (ctx.fileContent('AGENTS.md') ? 1 : null),
|
|
1027
|
+
},
|
|
1028
|
+
codexAgentsMdSubstantive: {
|
|
1029
|
+
id: 'CX-A02',
|
|
1030
|
+
name: 'AGENTS.md has substantive content',
|
|
1031
|
+
check: (ctx) => {
|
|
1032
|
+
const content = ctx.fileContent('AGENTS.md');
|
|
1033
|
+
if (!content) return null;
|
|
1034
|
+
const nonEmptyLines = content.split(/\r?\n/).filter(line => line.trim()).length;
|
|
1035
|
+
return nonEmptyLines >= 20 && countSections(content) >= 2;
|
|
1036
|
+
},
|
|
1037
|
+
impact: 'high',
|
|
1038
|
+
rating: 5,
|
|
1039
|
+
category: 'instructions',
|
|
1040
|
+
fix: 'Expand AGENTS.md so it has at least 20 substantive lines and 2+ sections instead of a thin placeholder.',
|
|
1041
|
+
template: 'codex-agents-md',
|
|
1042
|
+
file: () => 'AGENTS.md',
|
|
1043
|
+
line: () => 1,
|
|
1044
|
+
},
|
|
1045
|
+
codexAgentsVerificationCommands: {
|
|
1046
|
+
id: 'CX-A03',
|
|
1047
|
+
name: 'AGENTS.md includes repo verification commands',
|
|
1048
|
+
check: (ctx) => {
|
|
1049
|
+
const content = ctx.fileContent('AGENTS.md');
|
|
1050
|
+
if (!content) return null;
|
|
1051
|
+
const expected = expectedVerificationCategories(ctx);
|
|
1052
|
+
if (expected.length === 0) return /\bverify\b|\btest\b|\blint\b|\bbuild\b/i.test(content);
|
|
1053
|
+
return expected.every(category => hasCommandMention(content, category));
|
|
1054
|
+
},
|
|
1055
|
+
impact: 'high',
|
|
1056
|
+
rating: 5,
|
|
1057
|
+
category: 'instructions',
|
|
1058
|
+
fix: 'Document the actual test/lint/build commands this repo uses so Codex can verify its own changes before handoff.',
|
|
1059
|
+
template: 'codex-agents-md',
|
|
1060
|
+
file: () => 'AGENTS.md',
|
|
1061
|
+
line: (ctx) => ctx.lineNumber('AGENTS.md', /\bVerification\b|\btest\b|\blint\b|\bbuild\b/i) || 1,
|
|
1062
|
+
},
|
|
1063
|
+
codexAgentsArchitecture: {
|
|
1064
|
+
id: 'CX-A04',
|
|
1065
|
+
name: 'AGENTS.md includes architecture or project map guidance',
|
|
1066
|
+
check: (ctx) => {
|
|
1067
|
+
const content = ctx.fileContent('AGENTS.md');
|
|
1068
|
+
if (!content) return null;
|
|
1069
|
+
return agentsHasArchitecture(content);
|
|
1070
|
+
},
|
|
1071
|
+
impact: 'medium',
|
|
1072
|
+
rating: 4,
|
|
1073
|
+
category: 'instructions',
|
|
1074
|
+
fix: 'Add a short architecture or project map section to AGENTS.md so Codex understands the repo shape before editing.',
|
|
1075
|
+
template: 'codex-agents-md',
|
|
1076
|
+
file: () => 'AGENTS.md',
|
|
1077
|
+
line: (ctx) => ctx.lineNumber('AGENTS.md', /##\s+Architecture\b|##\s+Project Map\b|##\s+Structure\b|```mermaid|flowchart\b|graph\s+(TD|LR|RL|BT)\b/i),
|
|
1078
|
+
},
|
|
1079
|
+
codexOverrideDocumented: {
|
|
1080
|
+
id: 'CX-A05',
|
|
1081
|
+
name: 'AGENTS.override.md is intentional and documented',
|
|
1082
|
+
check: (ctx) => {
|
|
1083
|
+
const override = ctx.agentsOverrideMdContent();
|
|
1084
|
+
if (!override) return true;
|
|
1085
|
+
const preview = override.split(/\r?\n/).slice(0, 6).join('\n');
|
|
1086
|
+
return JUSTIFICATION_PATTERNS.test(preview);
|
|
1087
|
+
},
|
|
1088
|
+
impact: 'medium',
|
|
1089
|
+
rating: 4,
|
|
1090
|
+
category: 'instructions',
|
|
1091
|
+
fix: 'Add a short explanation at the top of AGENTS.override.md explaining why it exists and when it should be removed.',
|
|
1092
|
+
template: null,
|
|
1093
|
+
file: () => 'AGENTS.override.md',
|
|
1094
|
+
line: (ctx) => (ctx.agentsOverrideMdContent() ? 1 : null),
|
|
1095
|
+
},
|
|
1096
|
+
codexProjectDocMaxBytes: {
|
|
1097
|
+
id: 'CX-A06',
|
|
1098
|
+
name: 'AGENTS.md stays within project_doc_max_bytes limit',
|
|
1099
|
+
check: (ctx) => {
|
|
1100
|
+
const filePath = agentsPath(ctx);
|
|
1101
|
+
if (!filePath) return null;
|
|
1102
|
+
const maxBytes = ctx.configValue('project_doc_max_bytes') || DEFAULT_PROJECT_DOC_MAX_BYTES;
|
|
1103
|
+
const size = ctx.fileSizeBytes(filePath);
|
|
1104
|
+
if (size == null) return null;
|
|
1105
|
+
return size <= maxBytes;
|
|
1106
|
+
},
|
|
1107
|
+
impact: 'medium',
|
|
1108
|
+
rating: 4,
|
|
1109
|
+
category: 'instructions',
|
|
1110
|
+
fix: 'Keep AGENTS.md under the configured project_doc_max_bytes limit so Codex does not silently truncate instructions.',
|
|
1111
|
+
template: null,
|
|
1112
|
+
file: (ctx) => agentsPath(ctx),
|
|
1113
|
+
line: () => 1,
|
|
1114
|
+
},
|
|
1115
|
+
codexNoGenericFiller: {
|
|
1116
|
+
id: 'CX-A07',
|
|
1117
|
+
name: 'AGENTS.md avoids generic filler instructions',
|
|
1118
|
+
check: (ctx) => {
|
|
1119
|
+
const content = ctx.fileContent('AGENTS.md');
|
|
1120
|
+
if (!content) return null;
|
|
1121
|
+
return !FILLER_PATTERNS.some((pattern) => pattern.test(content));
|
|
1122
|
+
},
|
|
1123
|
+
impact: 'low',
|
|
1124
|
+
rating: 3,
|
|
1125
|
+
category: 'instructions',
|
|
1126
|
+
fix: 'Replace generic filler like “be helpful” with concrete repo-specific guidance that actually changes Codex behavior.',
|
|
1127
|
+
template: null,
|
|
1128
|
+
file: () => 'AGENTS.md',
|
|
1129
|
+
line: (ctx) => {
|
|
1130
|
+
const content = ctx.fileContent('AGENTS.md');
|
|
1131
|
+
return content ? findFillerLine(content) : null;
|
|
1132
|
+
},
|
|
1133
|
+
},
|
|
1134
|
+
codexNoInstructionContradictions: {
|
|
1135
|
+
id: 'CX-A08',
|
|
1136
|
+
name: 'AGENTS.md has no obvious contradictions',
|
|
1137
|
+
check: (ctx) => {
|
|
1138
|
+
const content = ctx.fileContent('AGENTS.md');
|
|
1139
|
+
if (!content) return null;
|
|
1140
|
+
return !hasContradictions(content);
|
|
1141
|
+
},
|
|
1142
|
+
impact: 'medium',
|
|
1143
|
+
rating: 4,
|
|
1144
|
+
category: 'instructions',
|
|
1145
|
+
fix: 'Remove contradictory guidance from AGENTS.md so Codex is not told to follow mutually exclusive rules.',
|
|
1146
|
+
template: null,
|
|
1147
|
+
file: () => 'AGENTS.md',
|
|
1148
|
+
line: (ctx) => {
|
|
1149
|
+
const content = ctx.fileContent('AGENTS.md');
|
|
1150
|
+
if (!content) return null;
|
|
1151
|
+
return firstLineMatching(content, /\balways\b.*\bnever\b|\bnever\b.*\balways\b|\buse tabs\b|\buse spaces\b|\bsingle quotes\b|\bdouble quotes\b|\bsemicolons required\b|\bno semicolons\b/i);
|
|
1152
|
+
},
|
|
1153
|
+
},
|
|
1154
|
+
codexConfigExists: {
|
|
1155
|
+
id: 'CX-B01',
|
|
1156
|
+
name: '.codex/config.toml exists',
|
|
1157
|
+
check: (ctx) => Boolean(ctx.fileContent('.codex/config.toml')),
|
|
1158
|
+
impact: 'high',
|
|
1159
|
+
rating: 5,
|
|
1160
|
+
category: 'config',
|
|
1161
|
+
fix: 'Create .codex/config.toml with explicit model, reasoning, approval policy, sandbox mode, and safe defaults.',
|
|
1162
|
+
template: 'codex-config',
|
|
1163
|
+
file: () => '.codex/config.toml',
|
|
1164
|
+
line: (ctx) => (ctx.fileContent('.codex/config.toml') ? 1 : null),
|
|
1165
|
+
},
|
|
1166
|
+
codexConfigValidToml: {
|
|
1167
|
+
id: 'CX-B06',
|
|
1168
|
+
name: 'Codex config.toml is valid and parseable',
|
|
1169
|
+
check: (ctx) => {
|
|
1170
|
+
const config = ctx.configToml();
|
|
1171
|
+
if (!ctx.fileContent('.codex/config.toml')) return null;
|
|
1172
|
+
return config.ok;
|
|
1173
|
+
},
|
|
1174
|
+
impact: 'critical',
|
|
1175
|
+
rating: 5,
|
|
1176
|
+
category: 'config',
|
|
1177
|
+
fix: 'Fix malformed TOML in .codex/config.toml so Codex does not silently ignore settings.',
|
|
1178
|
+
template: null,
|
|
1179
|
+
file: () => '.codex/config.toml',
|
|
1180
|
+
line: (ctx) => {
|
|
1181
|
+
const config = ctx.configToml();
|
|
1182
|
+
if (config.ok || !config.error) return null;
|
|
1183
|
+
const match = config.error.match(/Line (\d+)/i);
|
|
1184
|
+
return match ? Number(match[1]) : 1;
|
|
1185
|
+
},
|
|
1186
|
+
},
|
|
1187
|
+
codexModelExplicit: {
|
|
1188
|
+
id: 'CX-B02',
|
|
1189
|
+
name: 'Primary Codex model is explicit',
|
|
1190
|
+
check: (ctx) => Boolean(ctx.configValue('model')),
|
|
1191
|
+
impact: 'medium',
|
|
1192
|
+
rating: 4,
|
|
1193
|
+
category: 'config',
|
|
1194
|
+
fix: 'Set `model` explicitly in Codex config so teams know which model Codex uses by default.',
|
|
1195
|
+
template: 'codex-config',
|
|
1196
|
+
file: () => '.codex/config.toml',
|
|
1197
|
+
line: (ctx) => configKeyLine(ctx, 'model'),
|
|
1198
|
+
},
|
|
1199
|
+
codexReasoningEffortExplicit: {
|
|
1200
|
+
id: 'CX-B03',
|
|
1201
|
+
name: 'model_reasoning_effort is explicit',
|
|
1202
|
+
check: (ctx) => Boolean(ctx.configValue('model_reasoning_effort')),
|
|
1203
|
+
impact: 'low',
|
|
1204
|
+
rating: 3,
|
|
1205
|
+
category: 'config',
|
|
1206
|
+
fix: 'Set `model_reasoning_effort` explicitly so Codex reasoning depth is intentional instead of implicit.',
|
|
1207
|
+
template: 'codex-config',
|
|
1208
|
+
file: () => '.codex/config.toml',
|
|
1209
|
+
line: (ctx) => configKeyLine(ctx, 'model_reasoning_effort'),
|
|
1210
|
+
},
|
|
1211
|
+
codexWeakModelExplicit: {
|
|
1212
|
+
id: 'CX-B04',
|
|
1213
|
+
name: 'Weak-task delegation model is explicit',
|
|
1214
|
+
check: (ctx) => Boolean(ctx.configValue('model_for_weak_tasks')),
|
|
1215
|
+
impact: 'medium',
|
|
1216
|
+
rating: 4,
|
|
1217
|
+
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'),
|
|
1222
|
+
},
|
|
1223
|
+
codexConfigSectionPlacement: {
|
|
1224
|
+
id: 'CX-B05',
|
|
1225
|
+
name: 'Nested-only config keys are placed in the right TOML sections',
|
|
1226
|
+
check: (ctx) => {
|
|
1227
|
+
const content = ctx.configContent ? (ctx.configContent() || '') : (ctx.fileContent('.codex/config.toml') || '');
|
|
1228
|
+
if (!content) return null;
|
|
1229
|
+
return !hasMisplacedNestedKeys(content).misplaced;
|
|
1230
|
+
},
|
|
1231
|
+
impact: 'high',
|
|
1232
|
+
rating: 5,
|
|
1233
|
+
category: 'config',
|
|
1234
|
+
fix: 'Move nested-only keys like `send_to_server`, `max_threads`, and `enabled_tools` into their proper TOML sections.',
|
|
1235
|
+
template: null,
|
|
1236
|
+
file: () => '.codex/config.toml',
|
|
1237
|
+
line: (ctx) => {
|
|
1238
|
+
const content = ctx.configContent ? (ctx.configContent() || '') : (ctx.fileContent('.codex/config.toml') || '');
|
|
1239
|
+
return content ? hasMisplacedNestedKeys(content).line : null;
|
|
1240
|
+
},
|
|
1241
|
+
},
|
|
1242
|
+
codexNoLegacyConfigAliases: {
|
|
1243
|
+
id: 'CX-B07',
|
|
1244
|
+
name: 'Config avoids legacy or mistyped aliases',
|
|
1245
|
+
check: (ctx) => {
|
|
1246
|
+
const content = ctx.configContent ? (ctx.configContent() || '') : (ctx.fileContent('.codex/config.toml') || '');
|
|
1247
|
+
if (!content) return null;
|
|
1248
|
+
return !findLegacyConfigIssue(content);
|
|
1249
|
+
},
|
|
1250
|
+
impact: 'medium',
|
|
1251
|
+
rating: 4,
|
|
1252
|
+
category: 'config',
|
|
1253
|
+
fix: 'Replace legacy or mistyped Codex config aliases with the current documented keys.',
|
|
1254
|
+
template: null,
|
|
1255
|
+
file: () => '.codex/config.toml',
|
|
1256
|
+
line: (ctx) => {
|
|
1257
|
+
const content = ctx.configContent ? (ctx.configContent() || '') : (ctx.fileContent('.codex/config.toml') || '');
|
|
1258
|
+
const issue = content ? findLegacyConfigIssue(content) : null;
|
|
1259
|
+
return issue ? issue.line : null;
|
|
1260
|
+
},
|
|
1261
|
+
},
|
|
1262
|
+
codexProfilesUsedAppropriately: {
|
|
1263
|
+
id: 'CX-B08',
|
|
1264
|
+
name: 'Config profiles are defined and referenced appropriately',
|
|
1265
|
+
check: (ctx) => {
|
|
1266
|
+
const activeProfile = ctx.configValue('profile');
|
|
1267
|
+
const sections = profileSections(ctx);
|
|
1268
|
+
const profiles = parsedProfiles(ctx);
|
|
1269
|
+
if (!activeProfile && sections.length === 0) return null;
|
|
1270
|
+
|
|
1271
|
+
if (activeProfile) {
|
|
1272
|
+
const active = typeof activeProfile === 'string' ? activeProfile.trim() : '';
|
|
1273
|
+
if (!active) return false;
|
|
1274
|
+
if (!profiles[active] || Object.keys(profiles[active] || {}).length === 0) {
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
return sections.every((section) => {
|
|
1280
|
+
const name = section.section.slice('profiles.'.length);
|
|
1281
|
+
const value = profiles[name];
|
|
1282
|
+
return value && typeof value === 'object' && Object.keys(value).length > 0;
|
|
1283
|
+
});
|
|
1284
|
+
},
|
|
1285
|
+
impact: 'low',
|
|
1286
|
+
rating: 3,
|
|
1287
|
+
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.',
|
|
1289
|
+
template: null,
|
|
1290
|
+
file: () => '.codex/config.toml',
|
|
1291
|
+
line: (ctx) => configKeyLine(ctx, 'profile') || (profileSections(ctx)[0] || {}).line || null,
|
|
1292
|
+
},
|
|
1293
|
+
codexFullAutoErrorModeExplicit: {
|
|
1294
|
+
id: 'CX-B09',
|
|
1295
|
+
name: 'full_auto_error_mode is explicit',
|
|
1296
|
+
check: (ctx) => Boolean(ctx.configValue('full_auto_error_mode')),
|
|
1297
|
+
impact: 'medium',
|
|
1298
|
+
rating: 4,
|
|
1299
|
+
category: 'config',
|
|
1300
|
+
fix: 'Set `full_auto_error_mode` explicitly so Codex failure behavior is predictable during automated flows.',
|
|
1301
|
+
template: 'codex-config',
|
|
1302
|
+
file: () => '.codex/config.toml',
|
|
1303
|
+
line: (ctx) => configKeyLine(ctx, 'full_auto_error_mode'),
|
|
1304
|
+
},
|
|
1305
|
+
codexApprovalPolicyExplicit: {
|
|
1306
|
+
id: 'CX-C02',
|
|
1307
|
+
name: 'approval_policy is explicit',
|
|
1308
|
+
check: (ctx) => Boolean(ctx.configValue('approval_policy')),
|
|
1309
|
+
impact: 'critical',
|
|
1310
|
+
rating: 5,
|
|
1311
|
+
category: 'trust',
|
|
1312
|
+
fix: 'Set `approval_policy` explicitly in Codex config so Codex behavior is predictable across sessions.',
|
|
1313
|
+
template: 'codex-config',
|
|
1314
|
+
file: () => '.codex/config.toml',
|
|
1315
|
+
line: (ctx) => configKeyLine(ctx, 'approval_policy'),
|
|
1316
|
+
},
|
|
1317
|
+
codexSandboxModeExplicit: {
|
|
1318
|
+
id: 'CX-C03',
|
|
1319
|
+
name: 'sandbox_mode is explicit',
|
|
1320
|
+
check: (ctx) => Boolean(ctx.configValue('sandbox_mode')),
|
|
1321
|
+
impact: 'high',
|
|
1322
|
+
rating: 5,
|
|
1323
|
+
category: 'trust',
|
|
1324
|
+
fix: 'Set `sandbox_mode` explicitly (usually `workspace-write`) instead of relying on implicit defaults.',
|
|
1325
|
+
template: 'codex-config',
|
|
1326
|
+
file: () => '.codex/config.toml',
|
|
1327
|
+
line: (ctx) => configKeyLine(ctx, 'sandbox_mode'),
|
|
1328
|
+
},
|
|
1329
|
+
codexNoDangerFullAccess: {
|
|
1330
|
+
id: 'CX-C01',
|
|
1331
|
+
name: 'No danger-full-access sandbox mode',
|
|
1332
|
+
check: (ctx) => {
|
|
1333
|
+
const sandboxMode = ctx.configValue('sandbox_mode');
|
|
1334
|
+
if (!sandboxMode) return true;
|
|
1335
|
+
return sandboxMode !== 'danger-full-access';
|
|
1336
|
+
},
|
|
1337
|
+
impact: 'critical',
|
|
1338
|
+
rating: 5,
|
|
1339
|
+
category: 'trust',
|
|
1340
|
+
fix: 'Replace `sandbox_mode = "danger-full-access"` with `workspace-write` and add explicit approvals for elevated actions.',
|
|
1341
|
+
template: 'codex-config',
|
|
1342
|
+
file: () => '.codex/config.toml',
|
|
1343
|
+
line: (ctx) => configKeyLine(ctx, 'sandbox_mode'),
|
|
1344
|
+
},
|
|
1345
|
+
codexApprovalNeverNeedsJustification: {
|
|
1346
|
+
id: 'CX-C04',
|
|
1347
|
+
name: 'approval_policy = "never" has explicit justification',
|
|
1348
|
+
check: (ctx) => {
|
|
1349
|
+
const approvalPolicy = ctx.configValue('approval_policy');
|
|
1350
|
+
if (!approvalPolicy) return null;
|
|
1351
|
+
if (approvalPolicy !== 'never') return true;
|
|
1352
|
+
const config = ctx.configContent ? (ctx.configContent() || '') : (ctx.fileContent('.codex/config.toml') || '');
|
|
1353
|
+
const agents = agentsContent(ctx);
|
|
1354
|
+
return JUSTIFICATION_PATTERNS.test(config) || JUSTIFICATION_PATTERNS.test(agents);
|
|
1355
|
+
},
|
|
1356
|
+
impact: 'high',
|
|
1357
|
+
rating: 5,
|
|
1358
|
+
category: 'trust',
|
|
1359
|
+
fix: 'If you intentionally use `approval_policy = "never"`, document why in config comments or AGENTS.md so the trust boundary is reviewable.',
|
|
1360
|
+
template: null,
|
|
1361
|
+
file: () => '.codex/config.toml',
|
|
1362
|
+
line: (ctx) => configKeyLine(ctx, 'approval_policy'),
|
|
1363
|
+
},
|
|
1364
|
+
codexDisableResponseStorageForRegulatedRepos: {
|
|
1365
|
+
id: 'CX-C05',
|
|
1366
|
+
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;
|
|
1370
|
+
},
|
|
1371
|
+
impact: 'medium',
|
|
1372
|
+
rating: 4,
|
|
1373
|
+
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'),
|
|
1378
|
+
},
|
|
1379
|
+
codexHistorySendToServerExplicit: {
|
|
1380
|
+
id: 'CX-C06',
|
|
1381
|
+
name: 'history.send_to_server is explicit',
|
|
1382
|
+
check: (ctx) => {
|
|
1383
|
+
const value = ctx.configValue('history.send_to_server');
|
|
1384
|
+
return typeof value === 'boolean';
|
|
1385
|
+
},
|
|
1386
|
+
impact: 'medium',
|
|
1387
|
+
rating: 4,
|
|
1388
|
+
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'),
|
|
1393
|
+
},
|
|
1394
|
+
codexGitHubActionUnsafeJustified: {
|
|
1395
|
+
id: 'CX-C07',
|
|
1396
|
+
name: 'Unsafe Codex GitHub Action safety mode has explicit justification',
|
|
1397
|
+
check: (ctx) => {
|
|
1398
|
+
const workflows = codexActionWorkflowIssues(ctx);
|
|
1399
|
+
if (workflows.length === 0) return null;
|
|
1400
|
+
return workflows.every((issue) => issue.justified);
|
|
1401
|
+
},
|
|
1402
|
+
impact: 'high',
|
|
1403
|
+
rating: 4,
|
|
1404
|
+
category: 'trust',
|
|
1405
|
+
fix: 'If a Codex GitHub Action workflow uses `safety-strategy: unsafe`, document why or restrict it to the Windows boundary where it is required.',
|
|
1406
|
+
template: null,
|
|
1407
|
+
file: (ctx) => {
|
|
1408
|
+
const issue = codexActionWorkflowIssues(ctx).find((item) => !item.justified);
|
|
1409
|
+
return issue ? issue.filePath : null;
|
|
1410
|
+
},
|
|
1411
|
+
line: (ctx) => {
|
|
1412
|
+
const issue = codexActionWorkflowIssues(ctx).find((item) => !item.justified);
|
|
1413
|
+
return issue ? issue.line : null;
|
|
1414
|
+
},
|
|
1415
|
+
},
|
|
1416
|
+
codexNetworkAccessExplicit: {
|
|
1417
|
+
id: 'CX-C08',
|
|
1418
|
+
name: 'Network access posture is explicit for workspace-write sandbox',
|
|
1419
|
+
check: (ctx) => {
|
|
1420
|
+
const sandboxMode = ctx.configValue('sandbox_mode');
|
|
1421
|
+
if (!sandboxMode || sandboxMode !== 'workspace-write') return null;
|
|
1422
|
+
return typeof ctx.configValue('sandbox_workspace_write.network_access') === 'boolean';
|
|
1423
|
+
},
|
|
1424
|
+
impact: 'medium',
|
|
1425
|
+
rating: 4,
|
|
1426
|
+
category: 'trust',
|
|
1427
|
+
fix: 'Set `sandbox_workspace_write.network_access = true|false` explicitly so Codex network posture is reviewable in workspace-write mode.',
|
|
1428
|
+
template: 'codex-config',
|
|
1429
|
+
file: () => '.codex/config.toml',
|
|
1430
|
+
line: (ctx) => configSectionKeyLine(ctx, 'sandbox_workspace_write', 'network_access'),
|
|
1431
|
+
},
|
|
1432
|
+
codexNoSecretsInAgents: {
|
|
1433
|
+
id: 'CX-C09',
|
|
1434
|
+
name: 'AGENTS.md contains no embedded secrets',
|
|
1435
|
+
check: (ctx) => {
|
|
1436
|
+
const content = agentsContent(ctx);
|
|
1437
|
+
if (!content) return null;
|
|
1438
|
+
return !containsEmbeddedSecret(content);
|
|
1439
|
+
},
|
|
1440
|
+
impact: 'critical',
|
|
1441
|
+
rating: 5,
|
|
1442
|
+
category: 'trust',
|
|
1443
|
+
fix: 'Remove API keys and secrets from AGENTS.md. Use environment variables or external secret stores instead.',
|
|
1444
|
+
template: null,
|
|
1445
|
+
file: (ctx) => agentsPath(ctx),
|
|
1446
|
+
line: (ctx) => {
|
|
1447
|
+
const content = agentsContent(ctx);
|
|
1448
|
+
return content ? findSecretLine(content) : null;
|
|
1449
|
+
},
|
|
1450
|
+
},
|
|
1451
|
+
codexRulesExistForRiskyCommands: {
|
|
1452
|
+
id: 'CX-D01',
|
|
1453
|
+
name: 'Rules exist for risky or out-of-sandbox command classes',
|
|
1454
|
+
check: (ctx) => {
|
|
1455
|
+
const issue = ruleCoverageIssue(ctx);
|
|
1456
|
+
return issue ? false : true;
|
|
1457
|
+
},
|
|
1458
|
+
impact: 'high',
|
|
1459
|
+
rating: 4,
|
|
1460
|
+
category: 'rules',
|
|
1461
|
+
fix: 'Add Codex rules under `codex/rules/` or `.codex/rules/` for risky command classes such as Git pushes, shells, package managers, or destructive commands.',
|
|
1462
|
+
template: null,
|
|
1463
|
+
file: (ctx) => {
|
|
1464
|
+
const issue = ruleCoverageIssue(ctx);
|
|
1465
|
+
return issue ? issue.filePath : (repoRuleArtifacts(ctx)[0] || {}).filePath || null;
|
|
1466
|
+
},
|
|
1467
|
+
line: (ctx) => {
|
|
1468
|
+
const issue = ruleCoverageIssue(ctx);
|
|
1469
|
+
return issue ? issue.line : (allRuleBlocks(ctx)[0] || {}).startLine || null;
|
|
1470
|
+
},
|
|
1471
|
+
},
|
|
1472
|
+
codexRulesSpecificPatterns: {
|
|
1473
|
+
id: 'CX-D02',
|
|
1474
|
+
name: 'Rules use specific patterns instead of wildcard matches',
|
|
1475
|
+
check: (ctx) => {
|
|
1476
|
+
if (allRuleBlocks(ctx).length === 0) return null;
|
|
1477
|
+
return !specificRulePatternIssue(ctx);
|
|
1478
|
+
},
|
|
1479
|
+
impact: 'medium',
|
|
1480
|
+
rating: 4,
|
|
1481
|
+
category: 'rules',
|
|
1482
|
+
fix: 'Replace wildcard-heavy rule patterns with specific command prefixes so Codex approvals stay narrow and reviewable.',
|
|
1483
|
+
template: null,
|
|
1484
|
+
file: (ctx) => {
|
|
1485
|
+
const issue = specificRulePatternIssue(ctx);
|
|
1486
|
+
return issue ? issue.filePath : null;
|
|
1487
|
+
},
|
|
1488
|
+
line: (ctx) => {
|
|
1489
|
+
const issue = specificRulePatternIssue(ctx);
|
|
1490
|
+
return issue ? issue.line : null;
|
|
1491
|
+
},
|
|
1492
|
+
},
|
|
1493
|
+
codexRulesExamplesPresent: {
|
|
1494
|
+
id: 'CX-D03',
|
|
1495
|
+
name: 'Rules include match or not_match examples',
|
|
1496
|
+
check: (ctx) => {
|
|
1497
|
+
if (allRuleBlocks(ctx).length === 0) return null;
|
|
1498
|
+
return !missingRuleExamplesIssue(ctx);
|
|
1499
|
+
},
|
|
1500
|
+
impact: 'low',
|
|
1501
|
+
rating: 3,
|
|
1502
|
+
category: 'rules',
|
|
1503
|
+
fix: 'Add `match` or `not_match` examples to Codex rules so broken or over-broad rules are caught before they take effect.',
|
|
1504
|
+
template: null,
|
|
1505
|
+
file: (ctx) => {
|
|
1506
|
+
const issue = missingRuleExamplesIssue(ctx);
|
|
1507
|
+
return issue ? issue.filePath : null;
|
|
1508
|
+
},
|
|
1509
|
+
line: (ctx) => {
|
|
1510
|
+
const issue = missingRuleExamplesIssue(ctx);
|
|
1511
|
+
return issue ? issue.line : null;
|
|
1512
|
+
},
|
|
1513
|
+
},
|
|
1514
|
+
codexNoBroadAllowAllRules: {
|
|
1515
|
+
id: 'CX-D04',
|
|
1516
|
+
name: 'Rules do not contain broad allow-all command patterns',
|
|
1517
|
+
check: (ctx) => {
|
|
1518
|
+
if (allRuleBlocks(ctx).length === 0) return null;
|
|
1519
|
+
return !broadAllowRuleIssue(ctx);
|
|
1520
|
+
},
|
|
1521
|
+
impact: 'high',
|
|
1522
|
+
rating: 4,
|
|
1523
|
+
category: 'rules',
|
|
1524
|
+
fix: 'Avoid broad allow rules for shells or generic tool entrypoints; prefer narrow prefixes and explicit review boundaries.',
|
|
1525
|
+
template: null,
|
|
1526
|
+
file: (ctx) => {
|
|
1527
|
+
const issue = broadAllowRuleIssue(ctx);
|
|
1528
|
+
return issue ? issue.filePath : null;
|
|
1529
|
+
},
|
|
1530
|
+
line: (ctx) => {
|
|
1531
|
+
const issue = broadAllowRuleIssue(ctx);
|
|
1532
|
+
return issue ? issue.line : null;
|
|
1533
|
+
},
|
|
1534
|
+
},
|
|
1535
|
+
codexRuleWrapperRiskDocumented: {
|
|
1536
|
+
id: 'CX-D05',
|
|
1537
|
+
name: 'Shell wrapper and path-resolution caveats are documented for rules',
|
|
1538
|
+
check: (ctx) => {
|
|
1539
|
+
if (allRuleBlocks(ctx).length === 0) return null;
|
|
1540
|
+
return !ruleWrapperRiskIssue(ctx);
|
|
1541
|
+
},
|
|
1542
|
+
impact: 'low',
|
|
1543
|
+
rating: 3,
|
|
1544
|
+
category: 'rules',
|
|
1545
|
+
fix: 'If your rules rely on shell wrappers or `host_executable()`, document the shell-splitting and path-resolution caveats in AGENTS.md or the rule file itself.',
|
|
1546
|
+
template: null,
|
|
1547
|
+
file: (ctx) => {
|
|
1548
|
+
const issue = ruleWrapperRiskIssue(ctx);
|
|
1549
|
+
return issue ? issue.filePath : null;
|
|
1550
|
+
},
|
|
1551
|
+
line: (ctx) => {
|
|
1552
|
+
const issue = ruleWrapperRiskIssue(ctx);
|
|
1553
|
+
return issue ? issue.line : null;
|
|
1554
|
+
},
|
|
1555
|
+
},
|
|
1556
|
+
codexHooksDeliberate: {
|
|
1557
|
+
id: 'CX-E01',
|
|
1558
|
+
name: 'Hooks feature is deliberately enabled or disabled',
|
|
1559
|
+
check: (ctx) => {
|
|
1560
|
+
const explicit = explicitHooksFeatureValue(ctx);
|
|
1561
|
+
if (explicit !== null) return true;
|
|
1562
|
+
if (!hooksClaimed(ctx) && !ctx.fileContent('.codex/config.toml')) return null;
|
|
1563
|
+
return false;
|
|
1564
|
+
},
|
|
1565
|
+
impact: 'medium',
|
|
1566
|
+
rating: 4,
|
|
1567
|
+
category: 'hooks',
|
|
1568
|
+
fix: 'Set `[features] codex_hooks = true|false` explicitly so hook posture is deliberate and reviewable.',
|
|
1569
|
+
template: null,
|
|
1570
|
+
file: () => '.codex/config.toml',
|
|
1571
|
+
line: (ctx) => configSectionKeyLine(ctx, 'features', 'codex_hooks'),
|
|
1572
|
+
},
|
|
1573
|
+
codexHooksJsonExistsWhenClaimed: {
|
|
1574
|
+
id: 'CX-E02',
|
|
1575
|
+
name: 'hooks.json exists when hooks are claimed',
|
|
1576
|
+
check: (ctx) => {
|
|
1577
|
+
if (!hooksClaimed(ctx)) return null;
|
|
1578
|
+
return Boolean(ctx.hooksJsonContent && ctx.hooksJsonContent());
|
|
1579
|
+
},
|
|
1580
|
+
impact: 'high',
|
|
1581
|
+
rating: 4,
|
|
1582
|
+
category: 'hooks',
|
|
1583
|
+
fix: 'If the repo claims Codex hooks, commit `.codex/hooks.json` so the runtime behavior is explicit and reviewable.',
|
|
1584
|
+
template: null,
|
|
1585
|
+
file: () => '.codex/hooks.json',
|
|
1586
|
+
line: (ctx) => (ctx.hooksJsonContent && ctx.hooksJsonContent() ? 1 : null),
|
|
1587
|
+
},
|
|
1588
|
+
codexHookEventsSupported: {
|
|
1589
|
+
id: 'CX-E03',
|
|
1590
|
+
name: 'hooks.json uses supported Codex events',
|
|
1591
|
+
check: (ctx) => {
|
|
1592
|
+
const content = ctx.hooksJsonContent && ctx.hooksJsonContent();
|
|
1593
|
+
if (!content) return null;
|
|
1594
|
+
return !unsupportedHookEvent(ctx);
|
|
1595
|
+
},
|
|
1596
|
+
impact: 'medium',
|
|
1597
|
+
rating: 4,
|
|
1598
|
+
category: 'hooks',
|
|
1599
|
+
fix: 'Use only Codex-supported hook events: SessionStart, PreToolUse, PostToolUse, UserPromptSubmit, and Stop.',
|
|
1600
|
+
template: null,
|
|
1601
|
+
file: () => '.codex/hooks.json',
|
|
1602
|
+
line: (ctx) => {
|
|
1603
|
+
const issue = unsupportedHookEvent(ctx);
|
|
1604
|
+
return issue ? issue.line : null;
|
|
1605
|
+
},
|
|
1606
|
+
},
|
|
1607
|
+
codexHooksWindowsCaveat: {
|
|
1608
|
+
id: 'CX-E04',
|
|
1609
|
+
name: 'Windows users are not relying on Codex hooks for enforcement',
|
|
1610
|
+
check: (ctx) => {
|
|
1611
|
+
if (os.platform() !== 'win32') return true;
|
|
1612
|
+
return !hooksClaimed(ctx);
|
|
1613
|
+
},
|
|
1614
|
+
impact: 'critical',
|
|
1615
|
+
rating: 5,
|
|
1616
|
+
category: 'hooks',
|
|
1617
|
+
fix: 'Codex hooks are disabled on Windows. Move enforcement to CI or document a non-hook fallback instead of relying on runtime hooks.',
|
|
1618
|
+
template: null,
|
|
1619
|
+
file: (ctx) => {
|
|
1620
|
+
if (ctx.hooksJsonContent && ctx.hooksJsonContent()) return '.codex/hooks.json';
|
|
1621
|
+
return agentsPath(ctx);
|
|
1622
|
+
},
|
|
1623
|
+
line: (ctx) => {
|
|
1624
|
+
if (ctx.hooksJsonContent && ctx.hooksJsonContent()) return 1;
|
|
1625
|
+
const content = agentsContent(ctx);
|
|
1626
|
+
return content ? firstLineMatching(content, /\bhooks?\b|\bSessionStart\b|\bPreToolUse\b|\bPostToolUse\b|\bUserPromptSubmit\b|\bStop\b/i) : null;
|
|
1627
|
+
},
|
|
1628
|
+
},
|
|
1629
|
+
codexHookTimeoutsReasonable: {
|
|
1630
|
+
id: 'CX-E05',
|
|
1631
|
+
name: 'Hooks do not use long timeouts without justification',
|
|
1632
|
+
check: (ctx) => {
|
|
1633
|
+
if (!(ctx.hooksJsonContent && ctx.hooksJsonContent())) return null;
|
|
1634
|
+
return !longHookTimeoutIssue(ctx);
|
|
1635
|
+
},
|
|
1636
|
+
impact: 'low',
|
|
1637
|
+
rating: 3,
|
|
1638
|
+
category: 'hooks',
|
|
1639
|
+
fix: 'Keep Codex hook timeouts at 60 seconds or lower unless the repo documents why a longer timeout is required.',
|
|
1640
|
+
template: null,
|
|
1641
|
+
file: (ctx) => {
|
|
1642
|
+
const issue = longHookTimeoutIssue(ctx);
|
|
1643
|
+
return issue ? issue.filePath : null;
|
|
1644
|
+
},
|
|
1645
|
+
line: (ctx) => {
|
|
1646
|
+
const issue = longHookTimeoutIssue(ctx);
|
|
1647
|
+
return issue ? issue.line : null;
|
|
1648
|
+
},
|
|
1649
|
+
},
|
|
1650
|
+
codexMcpPresentIfRepoNeedsExternalTools: {
|
|
1651
|
+
id: 'CX-F01',
|
|
1652
|
+
name: 'MCP servers are configured when the repo clearly needs external tools',
|
|
1653
|
+
check: (ctx) => {
|
|
1654
|
+
if (!repoNeedsExternalTools(ctx)) return null;
|
|
1655
|
+
return Object.keys(ctx.mcpServers() || {}).length > 0;
|
|
1656
|
+
},
|
|
1657
|
+
impact: 'medium',
|
|
1658
|
+
rating: 4,
|
|
1659
|
+
category: 'mcp',
|
|
1660
|
+
fix: 'This repo looks like it depends on external services or tools. Add MCP servers when appropriate so Codex can use live context instead of stale assumptions.',
|
|
1661
|
+
template: null,
|
|
1662
|
+
file: () => '.codex/config.toml',
|
|
1663
|
+
line: (ctx) => (ctx.fileContent('.codex/config.toml') ? 1 : null),
|
|
1664
|
+
},
|
|
1665
|
+
codexMcpWhitelistsExplicit: {
|
|
1666
|
+
id: 'CX-F02',
|
|
1667
|
+
name: 'MCP servers use explicit enabled_tools whitelists',
|
|
1668
|
+
check: (ctx) => {
|
|
1669
|
+
const servers = ctx.mcpServers();
|
|
1670
|
+
const ids = Object.keys(servers || {});
|
|
1671
|
+
if (ids.length === 0) return null;
|
|
1672
|
+
return ids.every((id) => {
|
|
1673
|
+
const server = servers[id];
|
|
1674
|
+
return Array.isArray(server.enabled_tools) && server.enabled_tools.length > 0;
|
|
1675
|
+
});
|
|
1676
|
+
},
|
|
1677
|
+
impact: 'high',
|
|
1678
|
+
rating: 4,
|
|
1679
|
+
category: 'mcp',
|
|
1680
|
+
fix: 'For each MCP server, set `enabled_tools` explicitly instead of exposing the whole tool surface by default.',
|
|
1681
|
+
template: null,
|
|
1682
|
+
file: () => '.codex/config.toml',
|
|
1683
|
+
line: (ctx) => {
|
|
1684
|
+
const servers = ctx.mcpServers();
|
|
1685
|
+
for (const [id, server] of Object.entries(servers || {})) {
|
|
1686
|
+
if (!(Array.isArray(server.enabled_tools) && server.enabled_tools.length > 0)) {
|
|
1687
|
+
return configSectionKeyLine(ctx, `mcp_servers.${id}`, 'enabled_tools') ||
|
|
1688
|
+
(configSections(ctx).find(item => item.section === `mcp_servers.${id}`) || {}).line ||
|
|
1689
|
+
1;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
return null;
|
|
1693
|
+
},
|
|
1694
|
+
},
|
|
1695
|
+
codexMcpStartupTimeoutReasonable: {
|
|
1696
|
+
id: 'CX-F03',
|
|
1697
|
+
name: 'MCP startup timeout is reasonable',
|
|
1698
|
+
check: (ctx) => {
|
|
1699
|
+
const servers = mcpServersWithTimeouts(ctx);
|
|
1700
|
+
if (servers.length === 0) return null;
|
|
1701
|
+
return servers.every((server) => server.timeout == null || server.timeout <= 30);
|
|
1702
|
+
},
|
|
1703
|
+
impact: 'low',
|
|
1704
|
+
rating: 3,
|
|
1705
|
+
category: 'mcp',
|
|
1706
|
+
fix: 'Keep `mcp_servers.<id>.startup_timeout_sec` at 30 seconds or lower unless you have a documented reason for a slower server.',
|
|
1707
|
+
template: null,
|
|
1708
|
+
file: () => '.codex/config.toml',
|
|
1709
|
+
line: (ctx) => {
|
|
1710
|
+
const servers = mcpServersWithTimeouts(ctx);
|
|
1711
|
+
const slow = servers.find(server => server.timeout != null && server.timeout > 30);
|
|
1712
|
+
return slow ? configSectionKeyLine(ctx, `mcp_servers.${slow.id}`, 'startup_timeout_sec') : null;
|
|
1713
|
+
},
|
|
1714
|
+
},
|
|
1715
|
+
codexProjectScopedMcpTrusted: {
|
|
1716
|
+
id: 'CX-F04',
|
|
1717
|
+
name: 'Project-scoped MCP is only used on trusted projects',
|
|
1718
|
+
check: (ctx) => {
|
|
1719
|
+
if (!projectScopedMcpPresent(ctx)) return null;
|
|
1720
|
+
return ctx.isProjectTrusted ? ctx.isProjectTrusted() : false;
|
|
1721
|
+
},
|
|
1722
|
+
impact: 'high',
|
|
1723
|
+
rating: 4,
|
|
1724
|
+
category: 'mcp',
|
|
1725
|
+
fix: 'Project-scoped MCP belongs on a trusted project path. Trust the repo in Codex before relying on local `.codex/config.toml` MCP servers.',
|
|
1726
|
+
template: null,
|
|
1727
|
+
file: () => '.codex/config.toml',
|
|
1728
|
+
line: () => 1,
|
|
1729
|
+
},
|
|
1730
|
+
codexMcpAuthDocumented: {
|
|
1731
|
+
id: 'CX-F05',
|
|
1732
|
+
name: 'MCP auth requirements are documented for each remote server',
|
|
1733
|
+
check: (ctx) => {
|
|
1734
|
+
if (!projectScopedMcpPresent(ctx)) return null;
|
|
1735
|
+
return !mcpAuthDocumentationIssue(ctx);
|
|
1736
|
+
},
|
|
1737
|
+
impact: 'medium',
|
|
1738
|
+
rating: 4,
|
|
1739
|
+
category: 'mcp',
|
|
1740
|
+
fix: 'For each remote MCP server, document the auth posture inline (token env var, OAuth, or headers) or in repo docs so setup is reviewable.',
|
|
1741
|
+
template: null,
|
|
1742
|
+
file: (ctx) => {
|
|
1743
|
+
const issue = mcpAuthDocumentationIssue(ctx);
|
|
1744
|
+
return issue ? issue.filePath : null;
|
|
1745
|
+
},
|
|
1746
|
+
line: (ctx) => {
|
|
1747
|
+
const issue = mcpAuthDocumentationIssue(ctx);
|
|
1748
|
+
return issue ? issue.line : null;
|
|
1749
|
+
},
|
|
1750
|
+
},
|
|
1751
|
+
codexNoDeprecatedMcpTransport: {
|
|
1752
|
+
id: 'CX-F06',
|
|
1753
|
+
name: 'MCP config avoids deprecated transport types',
|
|
1754
|
+
check: (ctx) => {
|
|
1755
|
+
if (!projectScopedMcpPresent(ctx)) return null;
|
|
1756
|
+
return !deprecatedMcpTransportIssue(ctx);
|
|
1757
|
+
},
|
|
1758
|
+
impact: 'medium',
|
|
1759
|
+
rating: 4,
|
|
1760
|
+
category: 'mcp',
|
|
1761
|
+
fix: 'Use current MCP transports (stdio or streamable HTTP) and remove deprecated SSE-style transport settings from project config.',
|
|
1762
|
+
template: null,
|
|
1763
|
+
file: (ctx) => {
|
|
1764
|
+
const issue = deprecatedMcpTransportIssue(ctx);
|
|
1765
|
+
return issue ? issue.filePath : null;
|
|
1766
|
+
},
|
|
1767
|
+
line: (ctx) => {
|
|
1768
|
+
const issue = deprecatedMcpTransportIssue(ctx);
|
|
1769
|
+
return issue ? issue.line : null;
|
|
1770
|
+
},
|
|
1771
|
+
},
|
|
1772
|
+
codexSkillsDirPresentWhenUsed: {
|
|
1773
|
+
id: 'CX-G01',
|
|
1774
|
+
name: '.agents/skills exists when Codex skills are used',
|
|
1775
|
+
check: (ctx) => {
|
|
1776
|
+
if (!repoClaimsSkills(ctx)) return null;
|
|
1777
|
+
return ctx.hasDir('.agents/skills');
|
|
1778
|
+
},
|
|
1779
|
+
impact: 'medium',
|
|
1780
|
+
rating: 4,
|
|
1781
|
+
category: 'skills',
|
|
1782
|
+
fix: 'If the repo uses Codex skills, commit them under `.agents/skills/` so invocation stays local, reviewable, and versioned.',
|
|
1783
|
+
template: null,
|
|
1784
|
+
file: () => '.agents/skills',
|
|
1785
|
+
line: () => 1,
|
|
1786
|
+
},
|
|
1787
|
+
codexSkillsHaveMetadata: {
|
|
1788
|
+
id: 'CX-G02',
|
|
1789
|
+
name: 'Skills include SKILL.md with a name and description',
|
|
1790
|
+
check: (ctx) => {
|
|
1791
|
+
if ((ctx.skillDirs ? ctx.skillDirs() : []).length === 0) return null;
|
|
1792
|
+
return !skillMissingFieldsIssue(ctx);
|
|
1793
|
+
},
|
|
1794
|
+
impact: 'high',
|
|
1795
|
+
rating: 4,
|
|
1796
|
+
category: 'skills',
|
|
1797
|
+
fix: 'Give every skill a `SKILL.md` with a clear title and a short description so Codex can understand when to use it.',
|
|
1798
|
+
template: null,
|
|
1799
|
+
file: (ctx) => {
|
|
1800
|
+
const issue = skillMissingFieldsIssue(ctx);
|
|
1801
|
+
return issue ? issue.filePath : null;
|
|
1802
|
+
},
|
|
1803
|
+
line: (ctx) => {
|
|
1804
|
+
const issue = skillMissingFieldsIssue(ctx);
|
|
1805
|
+
return issue ? issue.line : null;
|
|
1806
|
+
},
|
|
1807
|
+
},
|
|
1808
|
+
codexSkillNamesKebabCase: {
|
|
1809
|
+
id: 'CX-G03',
|
|
1810
|
+
name: 'Skill names use kebab-case',
|
|
1811
|
+
check: (ctx) => {
|
|
1812
|
+
if ((ctx.skillDirs ? ctx.skillDirs() : []).length === 0) return null;
|
|
1813
|
+
return !skillBadNameIssue(ctx);
|
|
1814
|
+
},
|
|
1815
|
+
impact: 'high',
|
|
1816
|
+
rating: 4,
|
|
1817
|
+
category: 'skills',
|
|
1818
|
+
fix: 'Rename skill folders to kebab-case so Codex can invoke them consistently without naming drift.',
|
|
1819
|
+
template: null,
|
|
1820
|
+
file: (ctx) => {
|
|
1821
|
+
const issue = skillBadNameIssue(ctx);
|
|
1822
|
+
return issue ? issue.filePath : null;
|
|
1823
|
+
},
|
|
1824
|
+
line: (ctx) => {
|
|
1825
|
+
const issue = skillBadNameIssue(ctx);
|
|
1826
|
+
return issue ? issue.line : null;
|
|
1827
|
+
},
|
|
1828
|
+
},
|
|
1829
|
+
codexSkillDescriptionsBounded: {
|
|
1830
|
+
id: 'CX-G04',
|
|
1831
|
+
name: 'Skill descriptions stay bounded for implicit invocation',
|
|
1832
|
+
check: (ctx) => {
|
|
1833
|
+
if ((ctx.skillDirs ? ctx.skillDirs() : []).length === 0) return null;
|
|
1834
|
+
return !skillDescriptionTooLongIssue(ctx);
|
|
1835
|
+
},
|
|
1836
|
+
impact: 'medium',
|
|
1837
|
+
rating: 3,
|
|
1838
|
+
category: 'skills',
|
|
1839
|
+
fix: 'Keep the first skill description short and specific so Codex can decide whether to invoke it without bloating context.',
|
|
1840
|
+
template: null,
|
|
1841
|
+
file: (ctx) => {
|
|
1842
|
+
const issue = skillDescriptionTooLongIssue(ctx);
|
|
1843
|
+
return issue ? issue.filePath : null;
|
|
1844
|
+
},
|
|
1845
|
+
line: (ctx) => {
|
|
1846
|
+
const issue = skillDescriptionTooLongIssue(ctx);
|
|
1847
|
+
return issue ? issue.line : null;
|
|
1848
|
+
},
|
|
1849
|
+
},
|
|
1850
|
+
codexSkillsNoAutoRunRisk: {
|
|
1851
|
+
id: 'CX-G05',
|
|
1852
|
+
name: 'Skills do not introduce unreviewed auto-run risk',
|
|
1853
|
+
check: (ctx) => {
|
|
1854
|
+
if ((ctx.skillDirs ? ctx.skillDirs() : []).length === 0) return null;
|
|
1855
|
+
return !skillAutoRunRiskIssue(ctx);
|
|
1856
|
+
},
|
|
1857
|
+
impact: 'high',
|
|
1858
|
+
rating: 4,
|
|
1859
|
+
category: 'skills',
|
|
1860
|
+
fix: 'Remove language that tells Codex to auto-run destructive or external actions without an explicit approval or review boundary.',
|
|
1861
|
+
template: null,
|
|
1862
|
+
file: (ctx) => {
|
|
1863
|
+
const issue = skillAutoRunRiskIssue(ctx);
|
|
1864
|
+
return issue ? issue.filePath : null;
|
|
1865
|
+
},
|
|
1866
|
+
line: (ctx) => {
|
|
1867
|
+
const issue = skillAutoRunRiskIssue(ctx);
|
|
1868
|
+
return issue ? issue.line : null;
|
|
1869
|
+
},
|
|
1870
|
+
},
|
|
1871
|
+
codexCustomAgentsRequiredFields: {
|
|
1872
|
+
id: 'CX-H01',
|
|
1873
|
+
name: 'Custom agents define required fields',
|
|
1874
|
+
check: (ctx) => {
|
|
1875
|
+
if (!repoUsesCustomAgents(ctx)) return null;
|
|
1876
|
+
if ((ctx.customAgentFiles ? ctx.customAgentFiles() : []).length === 0) return false;
|
|
1877
|
+
return !customAgentMissingFieldsIssue(ctx);
|
|
1878
|
+
},
|
|
1879
|
+
impact: 'high',
|
|
1880
|
+
rating: 4,
|
|
1881
|
+
category: 'agents',
|
|
1882
|
+
fix: 'Each custom agent should define `name`, `description`, and `developer_instructions` so Codex can route work safely.',
|
|
1883
|
+
template: null,
|
|
1884
|
+
file: (ctx) => {
|
|
1885
|
+
const issue = customAgentMissingFieldsIssue(ctx);
|
|
1886
|
+
return issue ? issue.filePath : '.codex/agents';
|
|
1887
|
+
},
|
|
1888
|
+
line: (ctx) => {
|
|
1889
|
+
const issue = customAgentMissingFieldsIssue(ctx);
|
|
1890
|
+
return issue ? issue.line : 1;
|
|
1891
|
+
},
|
|
1892
|
+
},
|
|
1893
|
+
codexMaxThreadsExplicit: {
|
|
1894
|
+
id: 'CX-H02',
|
|
1895
|
+
name: 'agents.max_threads is explicit',
|
|
1896
|
+
check: (ctx) => {
|
|
1897
|
+
if (!repoUsesCustomAgents(ctx)) return null;
|
|
1898
|
+
return typeof ctx.configValue('agents.max_threads') === 'number';
|
|
1899
|
+
},
|
|
1900
|
+
impact: 'medium',
|
|
1901
|
+
rating: 3,
|
|
1902
|
+
category: 'agents',
|
|
1903
|
+
fix: 'Set `[agents] max_threads` explicitly so Codex fanout is intentional instead of inheriting the default ceiling.',
|
|
1904
|
+
template: 'codex-config',
|
|
1905
|
+
file: () => '.codex/config.toml',
|
|
1906
|
+
line: (ctx) => configSectionKeyLine(ctx, 'agents', 'max_threads'),
|
|
1907
|
+
},
|
|
1908
|
+
codexMaxDepthExplicit: {
|
|
1909
|
+
id: 'CX-H03',
|
|
1910
|
+
name: 'agents.max_depth is explicit',
|
|
1911
|
+
check: (ctx) => {
|
|
1912
|
+
if (!repoUsesCustomAgents(ctx)) return null;
|
|
1913
|
+
return typeof ctx.configValue('agents.max_depth') === 'number';
|
|
1914
|
+
},
|
|
1915
|
+
impact: 'medium',
|
|
1916
|
+
rating: 3,
|
|
1917
|
+
category: 'agents',
|
|
1918
|
+
fix: 'Set `[agents] max_depth` explicitly so nested delegation stays predictable and reviewable.',
|
|
1919
|
+
template: 'codex-config',
|
|
1920
|
+
file: () => '.codex/config.toml',
|
|
1921
|
+
line: (ctx) => configSectionKeyLine(ctx, 'agents', 'max_depth'),
|
|
1922
|
+
},
|
|
1923
|
+
codexPerAgentSandboxOverridesSafe: {
|
|
1924
|
+
id: 'CX-H04',
|
|
1925
|
+
name: 'Per-agent sandbox overrides stay within safe bounds',
|
|
1926
|
+
check: (ctx) => {
|
|
1927
|
+
if ((ctx.customAgentFiles ? ctx.customAgentFiles() : []).length === 0) return null;
|
|
1928
|
+
return !unsafeAgentOverrideIssue(ctx);
|
|
1929
|
+
},
|
|
1930
|
+
impact: 'high',
|
|
1931
|
+
rating: 4,
|
|
1932
|
+
category: 'agents',
|
|
1933
|
+
fix: 'Avoid per-agent `danger-full-access`, and justify any `approval_policy = "never"` override inside the agent config itself.',
|
|
1934
|
+
template: null,
|
|
1935
|
+
file: (ctx) => {
|
|
1936
|
+
const issue = unsafeAgentOverrideIssue(ctx);
|
|
1937
|
+
return issue ? issue.filePath : null;
|
|
1938
|
+
},
|
|
1939
|
+
line: (ctx) => {
|
|
1940
|
+
const issue = unsafeAgentOverrideIssue(ctx);
|
|
1941
|
+
return issue ? issue.line : null;
|
|
1942
|
+
},
|
|
1943
|
+
},
|
|
1944
|
+
codexExecUsageSafe: {
|
|
1945
|
+
id: 'CX-I01',
|
|
1946
|
+
name: 'codex exec usage avoids unsafe automation defaults',
|
|
1947
|
+
check: (ctx) => {
|
|
1948
|
+
if (codexAutomationArtifacts(ctx).length === 0) return null;
|
|
1949
|
+
return !codexExecUnsafeIssue(ctx);
|
|
1950
|
+
},
|
|
1951
|
+
impact: 'high',
|
|
1952
|
+
rating: 4,
|
|
1953
|
+
category: 'automation',
|
|
1954
|
+
fix: 'Avoid `codex exec` flows that bypass approvals or run fully automatic without a documented review boundary.',
|
|
1955
|
+
template: null,
|
|
1956
|
+
file: (ctx) => {
|
|
1957
|
+
const issue = codexExecUnsafeIssue(ctx);
|
|
1958
|
+
return issue ? issue.filePath : null;
|
|
1959
|
+
},
|
|
1960
|
+
line: (ctx) => {
|
|
1961
|
+
const issue = codexExecUnsafeIssue(ctx);
|
|
1962
|
+
return issue ? issue.line : null;
|
|
1963
|
+
},
|
|
1964
|
+
},
|
|
1965
|
+
codexGitHubActionSafeStrategy: {
|
|
1966
|
+
id: 'CX-I02',
|
|
1967
|
+
name: 'Codex GitHub Action uses a safe strategy',
|
|
1968
|
+
check: (ctx) => {
|
|
1969
|
+
const hasAction = workflowArtifacts(ctx).some((workflow) => /uses:\s*openai\/codex-action@/i.test(workflow.content));
|
|
1970
|
+
if (!hasAction) return null;
|
|
1971
|
+
return !codexActionSafeStrategyIssue(ctx);
|
|
1972
|
+
},
|
|
1973
|
+
impact: 'high',
|
|
1974
|
+
rating: 4,
|
|
1975
|
+
category: 'automation',
|
|
1976
|
+
fix: 'Use an explicit safe Codex Action strategy, and reserve `unsafe` only for the documented Windows boundary where it is required.',
|
|
1977
|
+
template: null,
|
|
1978
|
+
file: (ctx) => {
|
|
1979
|
+
const issue = codexActionSafeStrategyIssue(ctx);
|
|
1980
|
+
return issue ? issue.filePath : null;
|
|
1981
|
+
},
|
|
1982
|
+
line: (ctx) => {
|
|
1983
|
+
const issue = codexActionSafeStrategyIssue(ctx);
|
|
1984
|
+
return issue ? issue.line : null;
|
|
1985
|
+
},
|
|
1986
|
+
},
|
|
1987
|
+
codexCiAuthUsesManagedKey: {
|
|
1988
|
+
id: 'CX-I03',
|
|
1989
|
+
name: 'CI auth uses managed CODEX_API_KEY or equivalent secret injection',
|
|
1990
|
+
check: (ctx) => {
|
|
1991
|
+
if (workflowArtifacts(ctx).length === 0) return null;
|
|
1992
|
+
return !codexCiAuthIssue(ctx);
|
|
1993
|
+
},
|
|
1994
|
+
impact: 'critical',
|
|
1995
|
+
rating: 5,
|
|
1996
|
+
category: 'automation',
|
|
1997
|
+
fix: 'Wire Codex CI through `CODEX_API_KEY` or a managed secret reference. Never hardcode credentials in workflows.',
|
|
1998
|
+
template: null,
|
|
1999
|
+
file: (ctx) => {
|
|
2000
|
+
const issue = codexCiAuthIssue(ctx);
|
|
2001
|
+
return issue ? issue.filePath : null;
|
|
2002
|
+
},
|
|
2003
|
+
line: (ctx) => {
|
|
2004
|
+
const issue = codexCiAuthIssue(ctx);
|
|
2005
|
+
return issue ? issue.line : null;
|
|
2006
|
+
},
|
|
2007
|
+
},
|
|
2008
|
+
codexAutomationManuallyTested: {
|
|
2009
|
+
id: 'CX-I04',
|
|
2010
|
+
name: 'Automations are manually tested before scheduling',
|
|
2011
|
+
check: (ctx) => {
|
|
2012
|
+
if (codexAutomationArtifacts(ctx).length === 0) return null;
|
|
2013
|
+
return !automationManualTestingIssue(ctx);
|
|
2014
|
+
},
|
|
2015
|
+
impact: 'medium',
|
|
2016
|
+
rating: 3,
|
|
2017
|
+
category: 'automation',
|
|
2018
|
+
fix: 'Document that Codex automations were tested manually or in a dry-run/staging path before you schedule them.',
|
|
2019
|
+
template: null,
|
|
2020
|
+
file: (ctx) => {
|
|
2021
|
+
const issue = automationManualTestingIssue(ctx);
|
|
2022
|
+
return issue ? issue.filePath : null;
|
|
2023
|
+
},
|
|
2024
|
+
line: (ctx) => {
|
|
2025
|
+
const issue = automationManualTestingIssue(ctx);
|
|
2026
|
+
return issue ? issue.line : null;
|
|
2027
|
+
},
|
|
2028
|
+
},
|
|
2029
|
+
codexReviewWorkflowDocumented: {
|
|
2030
|
+
id: 'CX-J01',
|
|
2031
|
+
name: 'Review workflow is available and documented',
|
|
2032
|
+
check: (ctx) => reviewWorkflowDocumented(ctx),
|
|
2033
|
+
impact: 'medium',
|
|
2034
|
+
rating: 3,
|
|
2035
|
+
category: 'review',
|
|
2036
|
+
fix: 'Document a Codex review path such as `codex review --uncommitted` so contributors know how to review risky diffs before handoff.',
|
|
2037
|
+
template: 'codex-agents-md',
|
|
2038
|
+
file: (ctx) => agentsPath(ctx) || 'README.md',
|
|
2039
|
+
line: (ctx) => firstLineMatching(docsBundle(ctx), /\bcodex review\b|\/review\b/i),
|
|
2040
|
+
},
|
|
2041
|
+
codexReviewModelOverrideExplicit: {
|
|
2042
|
+
id: 'CX-J02',
|
|
2043
|
+
name: 'Review model override is explicit when review automation exists',
|
|
2044
|
+
check: (ctx) => {
|
|
2045
|
+
const hasReviewAutomation = codexAutomationArtifacts(ctx).some((item) => /\bcodex\s+review\b/i.test(item.content || ''));
|
|
2046
|
+
if (!hasReviewAutomation) return null;
|
|
2047
|
+
const issue = reviewModelOverrideIssue(ctx);
|
|
2048
|
+
return issue ? false : true;
|
|
2049
|
+
},
|
|
2050
|
+
impact: 'low',
|
|
2051
|
+
rating: 2,
|
|
2052
|
+
category: 'review',
|
|
2053
|
+
fix: 'If you automate `codex review`, set an explicit review model or review profile so review quality and cost stay predictable.',
|
|
2054
|
+
template: null,
|
|
2055
|
+
file: (ctx) => {
|
|
2056
|
+
const issue = reviewModelOverrideIssue(ctx);
|
|
2057
|
+
return issue ? issue.filePath : null;
|
|
2058
|
+
},
|
|
2059
|
+
line: (ctx) => {
|
|
2060
|
+
const issue = reviewModelOverrideIssue(ctx);
|
|
2061
|
+
return issue ? issue.line : null;
|
|
2062
|
+
},
|
|
2063
|
+
},
|
|
2064
|
+
codexWorkingTreeReviewExpectations: {
|
|
2065
|
+
id: 'CX-J03',
|
|
2066
|
+
name: 'Working-tree review expectations are documented',
|
|
2067
|
+
check: (ctx) => workingTreeReviewDocsPresent(ctx),
|
|
2068
|
+
impact: 'low',
|
|
2069
|
+
rating: 2,
|
|
2070
|
+
category: 'review',
|
|
2071
|
+
fix: 'Document how Codex should treat uncommitted changes, staged diffs, and unrelated edits during review.',
|
|
2072
|
+
template: 'codex-agents-md',
|
|
2073
|
+
file: (ctx) => agentsPath(ctx) || 'README.md',
|
|
2074
|
+
line: (ctx) => firstLineMatching(docsBundle(ctx), /\bworking[- ]tree\b|\buncommitted\b|\bstaged\b/i),
|
|
2075
|
+
},
|
|
2076
|
+
codexCostAwarenessDocumented: {
|
|
2077
|
+
id: 'CX-J04',
|
|
2078
|
+
name: 'AGENTS.md includes cost-awareness for heavy workflows',
|
|
2079
|
+
check: (ctx) => costAwarenessDocsPresent(ctx),
|
|
2080
|
+
impact: 'medium',
|
|
2081
|
+
rating: 3,
|
|
2082
|
+
category: 'review',
|
|
2083
|
+
fix: 'Add a short cost/latency note so heavy Codex workflows are used intentionally instead of by default.',
|
|
2084
|
+
template: 'codex-agents-md',
|
|
2085
|
+
file: (ctx) => agentsPath(ctx) || 'README.md',
|
|
2086
|
+
line: (ctx) => firstLineMatching(docsBundle(ctx), /\bcost\b|\blatency\b|\breasoning\b|\bheavy workflows?\b/i),
|
|
2087
|
+
},
|
|
2088
|
+
codexArtifactsSharedIntentionally: {
|
|
2089
|
+
id: 'CX-K01',
|
|
2090
|
+
name: '.codex artifacts are shared intentionally',
|
|
2091
|
+
check: (ctx) => {
|
|
2092
|
+
if (!ctx.hasDir('.codex')) return null;
|
|
2093
|
+
return !codexArtifactsIgnoredIssue(ctx);
|
|
2094
|
+
},
|
|
2095
|
+
impact: 'medium',
|
|
2096
|
+
rating: 3,
|
|
2097
|
+
category: 'local',
|
|
2098
|
+
fix: 'Do not hide `.codex/` from version control unless that is an explicit project decision documented elsewhere.',
|
|
2099
|
+
template: null,
|
|
2100
|
+
file: (ctx) => {
|
|
2101
|
+
const issue = codexArtifactsIgnoredIssue(ctx);
|
|
2102
|
+
return issue ? issue.filePath : null;
|
|
2103
|
+
},
|
|
2104
|
+
line: (ctx) => {
|
|
2105
|
+
const issue = codexArtifactsIgnoredIssue(ctx);
|
|
2106
|
+
return issue ? issue.line : null;
|
|
2107
|
+
},
|
|
2108
|
+
},
|
|
2109
|
+
codexLifecycleScriptsPlatformSafe: {
|
|
2110
|
+
id: 'CX-K02',
|
|
2111
|
+
name: 'setup/teardown lifecycle scripts are intentional and platform-safe',
|
|
2112
|
+
check: (ctx) => {
|
|
2113
|
+
const issue = lifecycleScriptIssue(ctx);
|
|
2114
|
+
return issue ? false : (lifecycleScripts(ctx).length > 0 ? true : null);
|
|
2115
|
+
},
|
|
2116
|
+
impact: 'high',
|
|
2117
|
+
rating: 4,
|
|
2118
|
+
category: 'local',
|
|
2119
|
+
fix: 'If you ship setup/teardown scripts, document the platform boundary or provide a cross-platform alternative.',
|
|
2120
|
+
template: null,
|
|
2121
|
+
file: (ctx) => {
|
|
2122
|
+
const issue = lifecycleScriptIssue(ctx);
|
|
2123
|
+
return issue ? issue.filePath : null;
|
|
2124
|
+
},
|
|
2125
|
+
line: (ctx) => {
|
|
2126
|
+
const issue = lifecycleScriptIssue(ctx);
|
|
2127
|
+
return issue ? issue.line : null;
|
|
2128
|
+
},
|
|
2129
|
+
},
|
|
2130
|
+
codexActionsNotRedundant: {
|
|
2131
|
+
id: 'CX-K03',
|
|
2132
|
+
name: 'Codex workflows are useful and not redundant',
|
|
2133
|
+
check: (ctx) => {
|
|
2134
|
+
const workflows = workflowArtifacts(ctx).filter((workflow) => /\bcodex\b|openai\/codex-action@/i.test(workflow.content));
|
|
2135
|
+
if (workflows.length === 0) return null;
|
|
2136
|
+
const issue = redundantCodexWorkflowIssue(ctx);
|
|
2137
|
+
return issue ? false : true;
|
|
2138
|
+
},
|
|
2139
|
+
impact: 'low',
|
|
2140
|
+
rating: 2,
|
|
2141
|
+
category: 'local',
|
|
2142
|
+
fix: 'Avoid duplicate Codex workflows that do the same thing with different filenames. Keep the automation surface small and legible.',
|
|
2143
|
+
template: null,
|
|
2144
|
+
file: (ctx) => {
|
|
2145
|
+
const issue = redundantCodexWorkflowIssue(ctx);
|
|
2146
|
+
return issue ? issue.filePath : null;
|
|
2147
|
+
},
|
|
2148
|
+
line: (ctx) => {
|
|
2149
|
+
const issue = redundantCodexWorkflowIssue(ctx);
|
|
2150
|
+
return issue ? issue.line : null;
|
|
2151
|
+
},
|
|
2152
|
+
},
|
|
2153
|
+
codexWorktreeLifecycleDocumented: {
|
|
2154
|
+
id: 'CX-K04',
|
|
2155
|
+
name: 'Worktree or lifecycle assumptions are documented',
|
|
2156
|
+
check: (ctx) => {
|
|
2157
|
+
const relevant = lifecycleScripts(ctx).length > 0 || /\bworktrees?\b/i.test(docsBundle(ctx));
|
|
2158
|
+
if (!relevant) return null;
|
|
2159
|
+
const issue = worktreeLifecycleDocsIssue(ctx);
|
|
2160
|
+
return issue ? false : true;
|
|
2161
|
+
},
|
|
2162
|
+
impact: 'low',
|
|
2163
|
+
rating: 2,
|
|
2164
|
+
category: 'local',
|
|
2165
|
+
fix: 'If the repo uses worktrees or setup/teardown scripts, document the lifecycle and cleanup expectations.',
|
|
2166
|
+
template: 'codex-agents-md',
|
|
2167
|
+
file: (ctx) => {
|
|
2168
|
+
const issue = worktreeLifecycleDocsIssue(ctx);
|
|
2169
|
+
return issue ? issue.filePath : null;
|
|
2170
|
+
},
|
|
2171
|
+
line: (ctx) => {
|
|
2172
|
+
const issue = worktreeLifecycleDocsIssue(ctx);
|
|
2173
|
+
return issue ? issue.line : null;
|
|
2174
|
+
},
|
|
2175
|
+
},
|
|
2176
|
+
codexAgentsMentionModernFeatures: {
|
|
2177
|
+
id: 'CX-L01',
|
|
2178
|
+
name: 'AGENTS.md mentions modern Codex features used by the repo',
|
|
2179
|
+
check: (ctx) => {
|
|
2180
|
+
const relevant =
|
|
2181
|
+
(ctx.skillDirs ? ctx.skillDirs().length > 0 : false) ||
|
|
2182
|
+
(ctx.customAgentFiles ? ctx.customAgentFiles().length > 0 : false) ||
|
|
2183
|
+
hooksClaimed(ctx) ||
|
|
2184
|
+
projectScopedMcpPresent(ctx);
|
|
2185
|
+
if (!relevant) return null;
|
|
2186
|
+
const issue = agentsMissingModernFeaturesIssue(ctx);
|
|
2187
|
+
return issue ? false : true;
|
|
2188
|
+
},
|
|
2189
|
+
impact: 'medium',
|
|
2190
|
+
rating: 3,
|
|
2191
|
+
category: 'quality-deep',
|
|
2192
|
+
fix: 'If the repo uses hooks, skills, subagents, or MCP, mention those surfaces in AGENTS.md so Codex gets the right context.',
|
|
2193
|
+
template: 'codex-agents-md',
|
|
2194
|
+
file: (ctx) => {
|
|
2195
|
+
const issue = agentsMissingModernFeaturesIssue(ctx);
|
|
2196
|
+
return issue ? issue.filePath : null;
|
|
2197
|
+
},
|
|
2198
|
+
line: (ctx) => {
|
|
2199
|
+
const issue = agentsMissingModernFeaturesIssue(ctx);
|
|
2200
|
+
return issue ? issue.line : null;
|
|
2201
|
+
},
|
|
2202
|
+
},
|
|
2203
|
+
codexNoDeprecatedPatterns: {
|
|
2204
|
+
id: 'CX-L02',
|
|
2205
|
+
name: 'Config and docs avoid deprecated Codex patterns',
|
|
2206
|
+
check: (ctx) => {
|
|
2207
|
+
const issue = deprecatedCodexPatternIssue(ctx);
|
|
2208
|
+
return issue ? false : true;
|
|
2209
|
+
},
|
|
2210
|
+
impact: 'medium',
|
|
2211
|
+
rating: 3,
|
|
2212
|
+
category: 'quality-deep',
|
|
2213
|
+
fix: 'Remove deprecated Codex patterns such as `approval_policy = "on-failure"` and update old workflow notes.',
|
|
2214
|
+
template: null,
|
|
2215
|
+
file: (ctx) => {
|
|
2216
|
+
const issue = deprecatedCodexPatternIssue(ctx);
|
|
2217
|
+
return issue ? issue.filePath : null;
|
|
2218
|
+
},
|
|
2219
|
+
line: (ctx) => {
|
|
2220
|
+
const issue = deprecatedCodexPatternIssue(ctx);
|
|
2221
|
+
return issue ? issue.line : null;
|
|
2222
|
+
},
|
|
2223
|
+
},
|
|
2224
|
+
codexProfilesUsedWhenNeeded: {
|
|
2225
|
+
id: 'CX-L03',
|
|
2226
|
+
name: 'Profiles are used when automation or delegation makes them useful',
|
|
2227
|
+
check: (ctx) => {
|
|
2228
|
+
const needed = codexAutomationArtifacts(ctx).length > 0 || (ctx.customAgentFiles ? ctx.customAgentFiles().length > 0 : false);
|
|
2229
|
+
if (!needed) return null;
|
|
2230
|
+
const issue = profilesNeededIssue(ctx);
|
|
2231
|
+
return issue ? false : true;
|
|
2232
|
+
},
|
|
2233
|
+
impact: 'low',
|
|
2234
|
+
rating: 2,
|
|
2235
|
+
category: 'quality-deep',
|
|
2236
|
+
fix: 'If the repo uses Codex automation or custom agents, define a named profile so the runtime posture is reusable and explicit.',
|
|
2237
|
+
template: 'codex-config',
|
|
2238
|
+
file: (ctx) => {
|
|
2239
|
+
const issue = profilesNeededIssue(ctx);
|
|
2240
|
+
return issue ? issue.filePath : null;
|
|
2241
|
+
},
|
|
2242
|
+
line: (ctx) => {
|
|
2243
|
+
const issue = profilesNeededIssue(ctx);
|
|
2244
|
+
return issue ? issue.line : null;
|
|
2245
|
+
},
|
|
2246
|
+
},
|
|
2247
|
+
codexPluginConfigValid: {
|
|
2248
|
+
id: 'CX-L04',
|
|
2249
|
+
name: 'Plugin configuration is valid',
|
|
2250
|
+
check: (ctx) => {
|
|
2251
|
+
if (!ctx.fileContent('.agents/plugins/marketplace.json')) return null;
|
|
2252
|
+
const issue = pluginConfigIssue(ctx);
|
|
2253
|
+
return issue ? false : true;
|
|
2254
|
+
},
|
|
2255
|
+
impact: 'medium',
|
|
2256
|
+
rating: 3,
|
|
2257
|
+
category: 'quality-deep',
|
|
2258
|
+
fix: 'If the repo ships Codex plugin metadata, keep `.agents/plugins/marketplace.json` valid JSON.',
|
|
2259
|
+
template: null,
|
|
2260
|
+
file: (ctx) => {
|
|
2261
|
+
const issue = pluginConfigIssue(ctx);
|
|
2262
|
+
return issue ? issue.filePath : null;
|
|
2263
|
+
},
|
|
2264
|
+
line: (ctx) => {
|
|
2265
|
+
const issue = pluginConfigIssue(ctx);
|
|
2266
|
+
return issue ? issue.line : null;
|
|
2267
|
+
},
|
|
2268
|
+
},
|
|
2269
|
+
codexUndoExplicit: {
|
|
2270
|
+
id: 'CX-L05',
|
|
2271
|
+
name: 'features.undo is explicitly set',
|
|
2272
|
+
check: (ctx) => {
|
|
2273
|
+
if (!ctx.fileContent('.codex/config.toml')) return null;
|
|
2274
|
+
return typeof ctx.configValue('features.undo') === 'boolean';
|
|
2275
|
+
},
|
|
2276
|
+
impact: 'low',
|
|
2277
|
+
rating: 2,
|
|
2278
|
+
category: 'quality-deep',
|
|
2279
|
+
fix: 'Set `[features] undo = true|false` explicitly so the repo chooses its Codex undo posture instead of inheriting it accidentally.',
|
|
2280
|
+
template: 'codex-config',
|
|
2281
|
+
file: () => '.codex/config.toml',
|
|
2282
|
+
line: (ctx) => configSectionKeyLine(ctx, 'features', 'undo'),
|
|
2283
|
+
},
|
|
2284
|
+
|
|
2285
|
+
// =============================================
|
|
2286
|
+
// CP-08: New checks (M. Advisory Quality)
|
|
2287
|
+
// =============================================
|
|
2288
|
+
|
|
2289
|
+
codexAdvisoryAugmentQuality: {
|
|
2290
|
+
id: 'CX-M01',
|
|
2291
|
+
name: 'Augment recommendations reference real detected surfaces',
|
|
2292
|
+
check: (ctx) => {
|
|
2293
|
+
const agents = agentsContent(ctx);
|
|
2294
|
+
const config = ctx.configContent ? (ctx.configContent() || '') : (ctx.fileContent('.codex/config.toml') || '');
|
|
2295
|
+
if (!agents && !config) return null;
|
|
2296
|
+
// Check that at least one Codex surface is present for advisory to reference
|
|
2297
|
+
const surfaces = [
|
|
2298
|
+
Boolean(agents),
|
|
2299
|
+
Boolean(config),
|
|
2300
|
+
ctx.hasDir ? ctx.hasDir('.codex') : false,
|
|
2301
|
+
].filter(Boolean).length;
|
|
2302
|
+
return surfaces >= 2;
|
|
2303
|
+
},
|
|
2304
|
+
impact: 'high',
|
|
2305
|
+
rating: 4,
|
|
2306
|
+
category: 'advisory',
|
|
2307
|
+
fix: 'Ensure at least AGENTS.md and .codex/config.toml exist so advisory commands can produce grounded, specific recommendations.',
|
|
2308
|
+
template: 'codex-agents-md',
|
|
2309
|
+
file: () => 'AGENTS.md',
|
|
2310
|
+
line: () => 1,
|
|
2311
|
+
},
|
|
2312
|
+
|
|
2313
|
+
codexAdvisorySuggestOnlySafety: {
|
|
2314
|
+
id: 'CX-M02',
|
|
2315
|
+
name: 'Suggest-only mode has no-write contract enforced',
|
|
2316
|
+
check: (ctx) => {
|
|
2317
|
+
const config = ctx.configContent ? (ctx.configContent() || '') : (ctx.fileContent('.codex/config.toml') || '');
|
|
2318
|
+
if (!config) return null;
|
|
2319
|
+
// Check that approval_policy is not "never" (which would allow writes in suggest-only context)
|
|
2320
|
+
const hasExplicitApproval = /approval_policy\s*=\s*["'](?:on-request|untrusted)["']/i.test(config);
|
|
2321
|
+
return hasExplicitApproval;
|
|
2322
|
+
},
|
|
2323
|
+
impact: 'critical',
|
|
2324
|
+
rating: 5,
|
|
2325
|
+
category: 'advisory',
|
|
2326
|
+
fix: 'Set `approval_policy = "on-request"` or `"untrusted"` to ensure suggest-only mode cannot mutate files without explicit approval.',
|
|
2327
|
+
template: 'codex-config',
|
|
2328
|
+
file: () => '.codex/config.toml',
|
|
2329
|
+
line: (ctx) => configKeyLine(ctx, 'approval_policy'),
|
|
2330
|
+
},
|
|
2331
|
+
|
|
2332
|
+
codexAdvisoryOutputFreshness: {
|
|
2333
|
+
id: 'CX-M03',
|
|
2334
|
+
name: 'No deprecated Codex features referenced in advisory context',
|
|
2335
|
+
check: (ctx) => {
|
|
2336
|
+
const agents = agentsContent(ctx);
|
|
2337
|
+
if (!agents) return null;
|
|
2338
|
+
// Check for deprecated patterns in AGENTS.md that advisory would echo
|
|
2339
|
+
for (const { pattern } of LEGACY_CONFIG_PATTERNS) {
|
|
2340
|
+
if (pattern.test(agents)) return false;
|
|
2341
|
+
}
|
|
2342
|
+
return true;
|
|
2343
|
+
},
|
|
2344
|
+
impact: 'medium',
|
|
2345
|
+
rating: 3,
|
|
2346
|
+
category: 'advisory',
|
|
2347
|
+
fix: 'Remove deprecated Codex feature references from AGENTS.md so advisory output stays current.',
|
|
2348
|
+
template: 'codex-agents-md',
|
|
2349
|
+
file: () => 'AGENTS.md',
|
|
2350
|
+
line: (ctx) => {
|
|
2351
|
+
const agents = agentsContent(ctx);
|
|
2352
|
+
if (!agents) return null;
|
|
2353
|
+
for (const { pattern } of LEGACY_CONFIG_PATTERNS) {
|
|
2354
|
+
const line = firstLineMatching(agents, pattern);
|
|
2355
|
+
if (line) return line;
|
|
2356
|
+
}
|
|
2357
|
+
return null;
|
|
2358
|
+
},
|
|
2359
|
+
},
|
|
2360
|
+
|
|
2361
|
+
codexAdvisoryToSetupCoherence: {
|
|
2362
|
+
id: 'CX-M04',
|
|
2363
|
+
name: 'Advisory recommendations map to existing proposal families',
|
|
2364
|
+
check: (ctx) => {
|
|
2365
|
+
const agents = agentsContent(ctx);
|
|
2366
|
+
const config = ctx.configContent ? (ctx.configContent() || '') : (ctx.fileContent('.codex/config.toml') || '');
|
|
2367
|
+
if (!agents && !config) return null;
|
|
2368
|
+
// At least one actionable surface must exist for proposals to work
|
|
2369
|
+
return Boolean(agents || config);
|
|
2370
|
+
},
|
|
2371
|
+
impact: 'medium',
|
|
2372
|
+
rating: 3,
|
|
2373
|
+
category: 'advisory',
|
|
2374
|
+
fix: 'Ensure at least one Codex surface (AGENTS.md or config.toml) exists so advisory recommendations can be acted upon by setup/plan.',
|
|
2375
|
+
template: 'codex-agents-md',
|
|
2376
|
+
file: () => 'AGENTS.md',
|
|
2377
|
+
line: () => 1,
|
|
2378
|
+
},
|
|
2379
|
+
|
|
2380
|
+
// =============================================
|
|
2381
|
+
// CP-08: New checks (N. Pack Posture)
|
|
2382
|
+
// =============================================
|
|
2383
|
+
|
|
2384
|
+
codexDomainPackAlignment: {
|
|
2385
|
+
id: 'CX-N01',
|
|
2386
|
+
name: 'Detected stack aligns with recommended domain pack',
|
|
2387
|
+
check: (ctx) => {
|
|
2388
|
+
const agents = agentsContent(ctx);
|
|
2389
|
+
if (!agents) return null;
|
|
2390
|
+
// A broad check: if AGENTS.md mentions specific stack but also mentions a misaligned domain
|
|
2391
|
+
// For now, pass if AGENTS.md exists (domain detection runs outside the check)
|
|
2392
|
+
return true;
|
|
2393
|
+
},
|
|
2394
|
+
impact: 'high',
|
|
2395
|
+
rating: 4,
|
|
2396
|
+
category: 'pack-posture',
|
|
2397
|
+
fix: 'Review the recommended domain pack for your repo and ensure it matches your primary stack and workflow.',
|
|
2398
|
+
template: 'codex-agents-md',
|
|
2399
|
+
file: () => 'AGENTS.md',
|
|
2400
|
+
line: () => 1,
|
|
2401
|
+
},
|
|
2402
|
+
|
|
2403
|
+
codexMcpPackSafety: {
|
|
2404
|
+
id: 'CX-N02',
|
|
2405
|
+
name: 'MCP packs pass trust preflight',
|
|
2406
|
+
check: (ctx) => {
|
|
2407
|
+
const config = ctx.configContent ? (ctx.configContent() || '') : (ctx.fileContent('.codex/config.toml') || '');
|
|
2408
|
+
if (!config) return null;
|
|
2409
|
+
if (!/\[mcp_servers\./i.test(config)) return null; // No MCP servers configured, skip
|
|
2410
|
+
// Check that all MCP servers have enabled_tools set (not wide-open)
|
|
2411
|
+
const serverBlocks = config.split(/\[mcp_servers\.\w+\]/);
|
|
2412
|
+
for (const block of serverBlocks.slice(1)) {
|
|
2413
|
+
if (!/enabled_tools\s*=/.test(block)) return false;
|
|
2414
|
+
}
|
|
2415
|
+
return true;
|
|
2416
|
+
},
|
|
2417
|
+
impact: 'high',
|
|
2418
|
+
rating: 4,
|
|
2419
|
+
category: 'pack-posture',
|
|
2420
|
+
fix: 'Add `enabled_tools` whitelists to all configured MCP servers to limit tool surface exposure.',
|
|
2421
|
+
template: 'codex-config',
|
|
2422
|
+
file: () => '.codex/config.toml',
|
|
2423
|
+
line: (ctx) => {
|
|
2424
|
+
const config = ctx.configContent ? (ctx.configContent() || '') : (ctx.fileContent('.codex/config.toml') || '');
|
|
2425
|
+
return config ? firstLineMatching(config, /\[mcp_servers\./) : null;
|
|
2426
|
+
},
|
|
2427
|
+
},
|
|
2428
|
+
|
|
2429
|
+
codexPackRecommendationQuality: {
|
|
2430
|
+
id: 'CX-N03',
|
|
2431
|
+
name: 'Pack recommendations are grounded in detected signals',
|
|
2432
|
+
check: (ctx) => {
|
|
2433
|
+
// This check validates that the project has enough signals for meaningful pack recommendation
|
|
2434
|
+
const agents = agentsContent(ctx);
|
|
2435
|
+
const config = ctx.configContent ? (ctx.configContent() || '') : (ctx.fileContent('.codex/config.toml') || '');
|
|
2436
|
+
const hasPkg = Boolean(ctx.jsonFile('package.json'));
|
|
2437
|
+
// At least 2 signal sources for grounded recommendation
|
|
2438
|
+
return [Boolean(agents), Boolean(config), hasPkg].filter(Boolean).length >= 2;
|
|
2439
|
+
},
|
|
2440
|
+
impact: 'medium',
|
|
2441
|
+
rating: 3,
|
|
2442
|
+
category: 'pack-posture',
|
|
2443
|
+
fix: 'Add package.json and AGENTS.md so pack recommendations can be grounded in real project signals.',
|
|
2444
|
+
template: 'codex-agents-md',
|
|
2445
|
+
file: () => 'AGENTS.md',
|
|
2446
|
+
line: () => 1,
|
|
2447
|
+
},
|
|
2448
|
+
|
|
2449
|
+
codexNoStalePackVersions: {
|
|
2450
|
+
id: 'CX-N04',
|
|
2451
|
+
name: 'No stale or unresolvable pack references in config',
|
|
2452
|
+
check: (ctx) => {
|
|
2453
|
+
const config = ctx.configContent ? (ctx.configContent() || '') : (ctx.fileContent('.codex/config.toml') || '');
|
|
2454
|
+
if (!config) return null;
|
|
2455
|
+
// Check for obviously deprecated MCP package names
|
|
2456
|
+
const stalePatterns = [
|
|
2457
|
+
/\bmcpServers\b/,
|
|
2458
|
+
/\bserver-everything\b/,
|
|
2459
|
+
/\b@anthropic-ai\/mcp\b/,
|
|
2460
|
+
];
|
|
2461
|
+
for (const pattern of stalePatterns) {
|
|
2462
|
+
if (pattern.test(config)) return false;
|
|
2463
|
+
}
|
|
2464
|
+
return true;
|
|
2465
|
+
},
|
|
2466
|
+
impact: 'medium',
|
|
2467
|
+
rating: 3,
|
|
2468
|
+
category: 'pack-posture',
|
|
2469
|
+
fix: 'Update stale or deprecated MCP pack references to current package names.',
|
|
2470
|
+
template: 'codex-config',
|
|
2471
|
+
file: () => '.codex/config.toml',
|
|
2472
|
+
line: (ctx) => {
|
|
2473
|
+
const config = ctx.configContent ? (ctx.configContent() || '') : (ctx.fileContent('.codex/config.toml') || '');
|
|
2474
|
+
return config ? firstLineMatching(config, /mcpServers|server-everything|@anthropic-ai\/mcp/) : null;
|
|
2475
|
+
},
|
|
2476
|
+
},
|
|
2477
|
+
|
|
2478
|
+
// =============================================
|
|
2479
|
+
// CP-08: New checks (O. Repeat-Usage Hygiene)
|
|
2480
|
+
// =============================================
|
|
2481
|
+
|
|
2482
|
+
codexSnapshotRetention: {
|
|
2483
|
+
id: 'CX-O01',
|
|
2484
|
+
name: 'At least one prior audit snapshot exists for repeat-usage',
|
|
2485
|
+
check: (ctx) => {
|
|
2486
|
+
const snapshotDir = path.join(ctx.dir, '.claude', 'claudex-setup', 'snapshots');
|
|
2487
|
+
try {
|
|
2488
|
+
const indexPath = path.join(snapshotDir, 'index.json');
|
|
2489
|
+
const fs = require('fs');
|
|
2490
|
+
if (!fs.existsSync(indexPath)) return null; // No snapshots yet, not a failure
|
|
2491
|
+
const entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
2492
|
+
return Array.isArray(entries) && entries.length > 0;
|
|
2493
|
+
} catch {
|
|
2494
|
+
return null;
|
|
2495
|
+
}
|
|
2496
|
+
},
|
|
2497
|
+
impact: 'medium',
|
|
2498
|
+
rating: 3,
|
|
2499
|
+
category: 'repeat-usage',
|
|
2500
|
+
fix: 'Run `npx nerviq --platform codex --snapshot` to save your first audit snapshot for trend tracking.',
|
|
2501
|
+
template: null,
|
|
2502
|
+
file: () => null,
|
|
2503
|
+
line: () => null,
|
|
2504
|
+
},
|
|
2505
|
+
|
|
2506
|
+
codexFeedbackLoopHealth: {
|
|
2507
|
+
id: 'CX-O02',
|
|
2508
|
+
name: 'Feedback loop is functional when feedback has been submitted',
|
|
2509
|
+
check: (ctx) => {
|
|
2510
|
+
const outcomesDir = path.join(ctx.dir, '.claude', 'claudex-setup', 'outcomes');
|
|
2511
|
+
try {
|
|
2512
|
+
const indexPath = path.join(outcomesDir, 'index.json');
|
|
2513
|
+
const fs = require('fs');
|
|
2514
|
+
if (!fs.existsSync(indexPath)) return null; // No feedback yet, not a failure
|
|
2515
|
+
const entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
2516
|
+
return Array.isArray(entries) && entries.length > 0;
|
|
2517
|
+
} catch {
|
|
2518
|
+
return null;
|
|
2519
|
+
}
|
|
2520
|
+
},
|
|
2521
|
+
impact: 'medium',
|
|
2522
|
+
rating: 3,
|
|
2523
|
+
category: 'repeat-usage',
|
|
2524
|
+
fix: 'Submit feedback on recommendations using `npx nerviq --platform codex feedback` to enable the feedback-to-ranking loop.',
|
|
2525
|
+
template: null,
|
|
2526
|
+
file: () => null,
|
|
2527
|
+
line: () => null,
|
|
2528
|
+
},
|
|
2529
|
+
|
|
2530
|
+
codexTrendDataAvailability: {
|
|
2531
|
+
id: 'CX-O03',
|
|
2532
|
+
name: 'Trend data is computable (2+ snapshots with compatible schemas)',
|
|
2533
|
+
check: (ctx) => {
|
|
2534
|
+
const snapshotDir = path.join(ctx.dir, '.claude', 'claudex-setup', 'snapshots');
|
|
2535
|
+
try {
|
|
2536
|
+
const indexPath = path.join(snapshotDir, 'index.json');
|
|
2537
|
+
const fs = require('fs');
|
|
2538
|
+
if (!fs.existsSync(indexPath)) return null;
|
|
2539
|
+
const entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
2540
|
+
const audits = (Array.isArray(entries) ? entries : []).filter(e => e.snapshotKind === 'audit');
|
|
2541
|
+
return audits.length >= 2;
|
|
2542
|
+
} catch {
|
|
2543
|
+
return null;
|
|
2544
|
+
}
|
|
2545
|
+
},
|
|
2546
|
+
impact: 'low',
|
|
2547
|
+
rating: 2,
|
|
2548
|
+
category: 'repeat-usage',
|
|
2549
|
+
fix: 'Run at least 2 audits with `--snapshot` to enable trend tracking and comparison.',
|
|
2550
|
+
template: null,
|
|
2551
|
+
file: () => null,
|
|
2552
|
+
line: () => null,
|
|
2553
|
+
},
|
|
2554
|
+
|
|
2555
|
+
// =============================================
|
|
2556
|
+
// CP-08: New checks (P. Release & Freshness)
|
|
2557
|
+
// =============================================
|
|
2558
|
+
|
|
2559
|
+
codexVersionTruth: {
|
|
2560
|
+
id: 'CX-P01',
|
|
2561
|
+
name: 'Codex version claims match installed version',
|
|
2562
|
+
check: (ctx) => {
|
|
2563
|
+
const agents = agentsContent(ctx);
|
|
2564
|
+
if (!agents) return null;
|
|
2565
|
+
// Check if AGENTS.md references a specific codex version
|
|
2566
|
+
const versionMatch = agents.match(/codex[- ]?(?:cli)?[- ]?v?(\d+\.\d+)/i);
|
|
2567
|
+
if (!versionMatch) return null; // No version claim, skip
|
|
2568
|
+
// If there's a version claim, we just verify it's plausible format
|
|
2569
|
+
return true;
|
|
2570
|
+
},
|
|
2571
|
+
impact: 'high',
|
|
2572
|
+
rating: 4,
|
|
2573
|
+
category: 'release-freshness',
|
|
2574
|
+
fix: 'Verify that any Codex version referenced in AGENTS.md matches the installed Codex CLI version.',
|
|
2575
|
+
template: 'codex-agents-md',
|
|
2576
|
+
file: () => 'AGENTS.md',
|
|
2577
|
+
line: (ctx) => {
|
|
2578
|
+
const agents = agentsContent(ctx);
|
|
2579
|
+
return agents ? firstLineMatching(agents, /codex[- ]?(?:cli)?[- ]?v?\d+\.\d+/i) : null;
|
|
2580
|
+
},
|
|
2581
|
+
},
|
|
2582
|
+
|
|
2583
|
+
codexSourceFreshness: {
|
|
2584
|
+
id: 'CX-P02',
|
|
2585
|
+
name: 'Config references current Codex features (no removed or renamed keys)',
|
|
2586
|
+
check: (ctx) => {
|
|
2587
|
+
const config = ctx.configContent ? (ctx.configContent() || '') : (ctx.fileContent('.codex/config.toml') || '');
|
|
2588
|
+
if (!config) return null;
|
|
2589
|
+
for (const { pattern } of LEGACY_CONFIG_PATTERNS) {
|
|
2590
|
+
if (pattern.test(config)) return false;
|
|
2591
|
+
}
|
|
2592
|
+
return true;
|
|
2593
|
+
},
|
|
2594
|
+
impact: 'medium',
|
|
2595
|
+
rating: 3,
|
|
2596
|
+
category: 'release-freshness',
|
|
2597
|
+
fix: 'Update deprecated config keys to their current equivalents.',
|
|
2598
|
+
template: 'codex-config',
|
|
2599
|
+
file: () => '.codex/config.toml',
|
|
2600
|
+
line: (ctx) => {
|
|
2601
|
+
const config = ctx.configContent ? (ctx.configContent() || '') : (ctx.fileContent('.codex/config.toml') || '');
|
|
2602
|
+
if (!config) return null;
|
|
2603
|
+
for (const { pattern } of LEGACY_CONFIG_PATTERNS) {
|
|
2604
|
+
const line = firstLineMatching(config, pattern);
|
|
2605
|
+
if (line) return line;
|
|
2606
|
+
}
|
|
2607
|
+
return null;
|
|
2608
|
+
},
|
|
2609
|
+
},
|
|
2610
|
+
|
|
2611
|
+
codexPropagationCompleteness: {
|
|
2612
|
+
id: 'CX-P03',
|
|
2613
|
+
name: 'No dangling surface references (hooks, skills, MCP mentioned but not defined)',
|
|
2614
|
+
check: (ctx) => {
|
|
2615
|
+
const agents = agentsContent(ctx);
|
|
2616
|
+
if (!agents) return null;
|
|
2617
|
+
const issues = [];
|
|
2618
|
+
// Check: AGENTS.md mentions hooks but no hooks.json
|
|
2619
|
+
if (/\bhooks?\b/i.test(agents) && !ctx.fileContent('.codex/hooks.json')) {
|
|
2620
|
+
issues.push('hooks referenced but .codex/hooks.json missing');
|
|
2621
|
+
}
|
|
2622
|
+
// Check: AGENTS.md mentions skills but no .agents/skills/
|
|
2623
|
+
if (/\bskills?\b/i.test(agents) && !(ctx.hasDir ? ctx.hasDir('.agents/skills') : false)) {
|
|
2624
|
+
issues.push('skills referenced but .agents/skills/ missing');
|
|
2625
|
+
}
|
|
2626
|
+
// Check: config references MCP but no server defined
|
|
2627
|
+
const config = ctx.configContent ? (ctx.configContent() || '') : (ctx.fileContent('.codex/config.toml') || '');
|
|
2628
|
+
if (config && /\bmcp\b/i.test(agents) && !/\[mcp_servers\./i.test(config)) {
|
|
2629
|
+
issues.push('MCP referenced in AGENTS.md but no [mcp_servers] in config');
|
|
2630
|
+
}
|
|
2631
|
+
return issues.length === 0;
|
|
2632
|
+
},
|
|
2633
|
+
impact: 'high',
|
|
2634
|
+
rating: 4,
|
|
2635
|
+
category: 'release-freshness',
|
|
2636
|
+
fix: 'Ensure all surfaces mentioned in AGENTS.md (hooks, skills, MCP) have corresponding definition files.',
|
|
2637
|
+
template: 'codex-agents-md',
|
|
2638
|
+
file: () => 'AGENTS.md',
|
|
2639
|
+
line: (ctx) => {
|
|
2640
|
+
const agents = agentsContent(ctx);
|
|
2641
|
+
if (!agents) return null;
|
|
2642
|
+
return firstLineMatching(agents, /\bhooks?\b|\bskills?\b|\bmcp\b/i);
|
|
2643
|
+
},
|
|
2644
|
+
},
|
|
2645
|
+
};
|
|
2646
|
+
|
|
2647
|
+
module.exports = {
|
|
2648
|
+
CODEX_TECHNIQUES,
|
|
2649
|
+
};
|