@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,1713 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Techniques — 68+ checks (OC-A01 through OC-P03)
|
|
3
|
+
*
|
|
4
|
+
* Categories:
|
|
5
|
+
* A. Instructions (7 checks)
|
|
6
|
+
* B. Config (6 checks)
|
|
7
|
+
* C. Permissions (8 checks)
|
|
8
|
+
* D. Plugins (5 checks)
|
|
9
|
+
* E. Security (6 checks)
|
|
10
|
+
* F. MCP (5 checks)
|
|
11
|
+
* G. CI & Automation (4 checks)
|
|
12
|
+
* H. Quality Deep (5 checks)
|
|
13
|
+
* I. Skills (5 checks)
|
|
14
|
+
* J. Agents & Subagents (4 checks)
|
|
15
|
+
* K. Commands & Workflow (3 checks)
|
|
16
|
+
* L. Themes & TUI (3 checks)
|
|
17
|
+
* M. Review & Governance (3 checks)
|
|
18
|
+
* N. Release Freshness (3 checks)
|
|
19
|
+
* O. Mixed-Agent (3 checks)
|
|
20
|
+
* P. Propagation (3 checks)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const os = require('os');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
|
|
26
|
+
|
|
27
|
+
const DEFAULT_PROJECT_DOC_MAX_BYTES = 32768;
|
|
28
|
+
|
|
29
|
+
const FILLER_PATTERNS = [
|
|
30
|
+
/\bbe helpful\b/i,
|
|
31
|
+
/\bbe accurate\b/i,
|
|
32
|
+
/\bbe concise\b/i,
|
|
33
|
+
/\balways do your best\b/i,
|
|
34
|
+
/\bmaintain high quality\b/i,
|
|
35
|
+
/\bwrite clean code\b/i,
|
|
36
|
+
/\bfollow best practices\b/i,
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
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;
|
|
40
|
+
|
|
41
|
+
const PERMISSIONED_TOOLS = [
|
|
42
|
+
'read', 'edit', 'glob', 'grep', 'list', 'bash', 'task', 'skill',
|
|
43
|
+
'lsp', 'question', 'webfetch', 'websearch', 'codesearch',
|
|
44
|
+
'external_directory', 'doom_loop',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const VALID_PERMISSION_STATES = new Set(['allow', 'ask', 'deny']);
|
|
48
|
+
|
|
49
|
+
const VALID_PLUGIN_EVENTS = new Set([
|
|
50
|
+
'tool.execute.before', 'tool.execute.after', 'tool.execute.error',
|
|
51
|
+
'message.before', 'message.after', 'message.error',
|
|
52
|
+
'session.start', 'session.end', 'session.error',
|
|
53
|
+
'agent.start', 'agent.end', 'agent.error',
|
|
54
|
+
'conversation.start', 'conversation.end',
|
|
55
|
+
'command.before', 'command.after',
|
|
56
|
+
'file.read', 'file.write', 'file.delete',
|
|
57
|
+
'bash.before', 'bash.after',
|
|
58
|
+
'compaction.before', 'compaction.after',
|
|
59
|
+
'permission.request', 'permission.response',
|
|
60
|
+
'mcp.connect', 'mcp.disconnect', 'mcp.tool.call',
|
|
61
|
+
'skill.invoke', 'task.spawn', 'task.complete',
|
|
62
|
+
'error', 'warning',
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
const DEPRECATED_CONFIG_KEYS = [
|
|
66
|
+
{ key: 'mode', replacement: 'agent', note: 'Use `agent` field instead of deprecated `mode`.' },
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
// --- Helpers ---
|
|
70
|
+
|
|
71
|
+
function agentsPath(ctx) {
|
|
72
|
+
return ctx.agentsMdPath ? ctx.agentsMdPath() : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function agentsContent(ctx) {
|
|
76
|
+
return ctx.agentsMdContent ? (ctx.agentsMdContent() || '') : '';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function configFileName(ctx) {
|
|
80
|
+
return ctx.configFileName ? ctx.configFileName() : 'opencode.json';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function countSections(markdown) {
|
|
84
|
+
return (markdown.match(/^##\s+/gm) || []).length;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function firstLineMatching(text, matcher) {
|
|
88
|
+
const lines = text.split(/\r?\n/);
|
|
89
|
+
for (let index = 0; index < lines.length; index++) {
|
|
90
|
+
const line = lines[index];
|
|
91
|
+
if (typeof matcher === 'string' && line.includes(matcher)) return index + 1;
|
|
92
|
+
if (matcher instanceof RegExp && matcher.test(line)) {
|
|
93
|
+
matcher.lastIndex = 0;
|
|
94
|
+
return index + 1;
|
|
95
|
+
}
|
|
96
|
+
if (typeof matcher === 'function' && matcher(line, index + 1)) return index + 1;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function escapeRegex(value) {
|
|
102
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function findFillerLine(content) {
|
|
106
|
+
return firstLineMatching(content, (line) => FILLER_PATTERNS.some((pattern) => pattern.test(line)));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function hasContradictions(content) {
|
|
110
|
+
const lines = content.split(/\r?\n/);
|
|
111
|
+
for (const line of lines) {
|
|
112
|
+
if (/\balways\b.*\bnever\b|\bnever\b.*\balways\b/i.test(line)) return true;
|
|
113
|
+
}
|
|
114
|
+
const contradictoryPairs = [
|
|
115
|
+
[/\buse tabs\b/i, /\buse spaces\b/i],
|
|
116
|
+
[/\bsingle quotes\b/i, /\bdouble quotes\b/i],
|
|
117
|
+
[/\bsemicolons required\b/i, /\bno semicolons\b/i],
|
|
118
|
+
];
|
|
119
|
+
return contradictoryPairs.some(([a, b]) => a.test(content) && b.test(content));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function findSecretLine(content) {
|
|
123
|
+
const lines = content.split(/\r?\n/);
|
|
124
|
+
for (let index = 0; index < lines.length; index++) {
|
|
125
|
+
const matched = EMBEDDED_SECRET_PATTERNS.some((pattern) => {
|
|
126
|
+
pattern.lastIndex = 0;
|
|
127
|
+
return pattern.test(lines[index]);
|
|
128
|
+
});
|
|
129
|
+
if (matched) return index + 1;
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function agentsHasArchitecture(content) {
|
|
135
|
+
return /```mermaid|flowchart\b|graph\s+(TD|LR|RL|BT)\b|##\s+Architecture\b|##\s+Project Map\b|##\s+Structure\b/i.test(content);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function expectedVerificationCategories(ctx) {
|
|
139
|
+
const categories = new Set();
|
|
140
|
+
const pkg = ctx.jsonFile('package.json');
|
|
141
|
+
const scripts = pkg && pkg.scripts ? pkg.scripts : {};
|
|
142
|
+
if (scripts.test) categories.add('test');
|
|
143
|
+
if (scripts.lint) categories.add('lint');
|
|
144
|
+
if (scripts.build) categories.add('build');
|
|
145
|
+
if (ctx.fileContent('Cargo.toml')) { categories.add('test'); categories.add('build'); }
|
|
146
|
+
if (ctx.fileContent('go.mod')) { categories.add('test'); categories.add('build'); }
|
|
147
|
+
if (ctx.fileContent('pyproject.toml') || ctx.fileContent('requirements.txt')) categories.add('test');
|
|
148
|
+
if (ctx.fileContent('Makefile') || ctx.fileContent('justfile')) categories.add('build');
|
|
149
|
+
return [...categories];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function hasCommandMention(content, category) {
|
|
153
|
+
if (category === 'test') {
|
|
154
|
+
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);
|
|
155
|
+
}
|
|
156
|
+
if (category === 'lint') {
|
|
157
|
+
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);
|
|
158
|
+
}
|
|
159
|
+
if (category === 'build') {
|
|
160
|
+
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);
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function docsBundle(ctx) {
|
|
166
|
+
return `${agentsContent(ctx)}\n${ctx.fileContent('README.md') || ''}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function repoLooksRegulated(ctx) {
|
|
170
|
+
const filenames = ctx.files.join('\n');
|
|
171
|
+
const packageJson = ctx.fileContent('package.json') || '';
|
|
172
|
+
const readme = ctx.fileContent('README.md') || '';
|
|
173
|
+
const combined = `${filenames}\n${packageJson}\n${readme}`;
|
|
174
|
+
|
|
175
|
+
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;
|
|
176
|
+
if (strongSignals.test(combined)) return true;
|
|
177
|
+
|
|
178
|
+
const weakSignalMatches = combined.match(/\bgdpr\b|\bpii\b/gi) || [];
|
|
179
|
+
return weakSignalMatches.length >= 2;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function workflowArtifacts(ctx) {
|
|
183
|
+
return (ctx.workflowFiles ? ctx.workflowFiles() : [])
|
|
184
|
+
.map((filePath) => ({ filePath, content: ctx.fileContent(filePath) || '' }))
|
|
185
|
+
.filter((item) => item.content);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// --- OPENCODE_TECHNIQUES ---
|
|
189
|
+
|
|
190
|
+
const OPENCODE_TECHNIQUES = {
|
|
191
|
+
// ==============================
|
|
192
|
+
// A. Instructions (7 checks)
|
|
193
|
+
// ==============================
|
|
194
|
+
|
|
195
|
+
opencodeAgentsMdExists: {
|
|
196
|
+
id: 'OC-A01',
|
|
197
|
+
name: 'AGENTS.md exists at project root',
|
|
198
|
+
check: (ctx) => Boolean(ctx.fileContent('AGENTS.md')),
|
|
199
|
+
impact: 'critical',
|
|
200
|
+
rating: 5,
|
|
201
|
+
category: 'instructions',
|
|
202
|
+
fix: 'Create an AGENTS.md at the project root with project-specific guidance for the OpenCode agent.',
|
|
203
|
+
template: 'opencode-agents-md',
|
|
204
|
+
file: () => 'AGENTS.md',
|
|
205
|
+
line: () => null,
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
opencodeAgentsMdQuality: {
|
|
209
|
+
id: 'OC-A02',
|
|
210
|
+
name: 'AGENTS.md has substantive content (>20 lines, 2+ sections, commands)',
|
|
211
|
+
check: (ctx) => {
|
|
212
|
+
const content = agentsContent(ctx);
|
|
213
|
+
if (!content) return null;
|
|
214
|
+
const lines = content.split(/\r?\n/).filter(l => l.trim()).length;
|
|
215
|
+
const sections = countSections(content);
|
|
216
|
+
return lines > 20 && sections >= 2;
|
|
217
|
+
},
|
|
218
|
+
impact: 'high',
|
|
219
|
+
rating: 4,
|
|
220
|
+
category: 'instructions',
|
|
221
|
+
fix: 'Add at least 20 meaningful lines and 2+ sections (## Verification, ## Architecture, etc.) to AGENTS.md.',
|
|
222
|
+
template: 'opencode-agents-md',
|
|
223
|
+
file: () => 'AGENTS.md',
|
|
224
|
+
line: () => 1,
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
opencodeAgentsMdVerification: {
|
|
228
|
+
id: 'OC-A03',
|
|
229
|
+
name: 'AGENTS.md has build/test/lint commands',
|
|
230
|
+
check: (ctx) => {
|
|
231
|
+
const content = agentsContent(ctx);
|
|
232
|
+
if (!content) return null;
|
|
233
|
+
const expected = expectedVerificationCategories(ctx);
|
|
234
|
+
if (expected.length === 0) return true;
|
|
235
|
+
return expected.some((cat) => hasCommandMention(content, cat));
|
|
236
|
+
},
|
|
237
|
+
impact: 'high',
|
|
238
|
+
rating: 4,
|
|
239
|
+
category: 'instructions',
|
|
240
|
+
fix: 'Add verification commands (test, lint, build) to AGENTS.md so the agent can validate its work.',
|
|
241
|
+
template: 'opencode-agents-md',
|
|
242
|
+
file: () => 'AGENTS.md',
|
|
243
|
+
line: () => null,
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
opencodeAgentsMdArchitecture: {
|
|
247
|
+
id: 'OC-A04',
|
|
248
|
+
name: 'AGENTS.md has Mermaid or architecture section',
|
|
249
|
+
check: (ctx) => {
|
|
250
|
+
const content = agentsContent(ctx);
|
|
251
|
+
if (!content) return null;
|
|
252
|
+
return agentsHasArchitecture(content);
|
|
253
|
+
},
|
|
254
|
+
impact: 'medium',
|
|
255
|
+
rating: 3,
|
|
256
|
+
category: 'instructions',
|
|
257
|
+
fix: 'Add a ```mermaid diagram or ## Architecture section describing the project structure.',
|
|
258
|
+
template: 'opencode-agents-md',
|
|
259
|
+
file: () => 'AGENTS.md',
|
|
260
|
+
line: () => null,
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
opencodeNoCoexistenceConflict: {
|
|
264
|
+
id: 'OC-A05',
|
|
265
|
+
name: 'No CLAUDE.md coexistence conflict (AGENTS.md wins if both exist in same dir)',
|
|
266
|
+
check: (ctx) => {
|
|
267
|
+
if (!ctx.hasAgentsMdAndClaudeMd || !ctx.hasAgentsMdAndClaudeMd()) return true;
|
|
268
|
+
// Both exist: check that AGENTS.md is primary, warn about potential confusion
|
|
269
|
+
const agentsMd = ctx.fileContent('AGENTS.md') || '';
|
|
270
|
+
return agentsMd.length > 0;
|
|
271
|
+
},
|
|
272
|
+
impact: 'high',
|
|
273
|
+
rating: 4,
|
|
274
|
+
category: 'instructions',
|
|
275
|
+
fix: 'When both AGENTS.md and CLAUDE.md exist, AGENTS.md takes precedence in OpenCode. Keep CLAUDE.md for Claude Code and use AGENTS.md for OpenCode instructions.',
|
|
276
|
+
template: 'opencode-agents-md',
|
|
277
|
+
file: () => 'AGENTS.md',
|
|
278
|
+
line: () => null,
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
opencodeNoFillerInstructions: {
|
|
282
|
+
id: 'OC-A06',
|
|
283
|
+
name: 'No generic filler instructions ("Be helpful", "Be accurate")',
|
|
284
|
+
check: (ctx) => {
|
|
285
|
+
const content = agentsContent(ctx);
|
|
286
|
+
if (!content) return null;
|
|
287
|
+
return !findFillerLine(content);
|
|
288
|
+
},
|
|
289
|
+
impact: 'low',
|
|
290
|
+
rating: 2,
|
|
291
|
+
category: 'instructions',
|
|
292
|
+
fix: 'Remove generic filler ("Be helpful", "Write clean code") and replace with specific, actionable project instructions.',
|
|
293
|
+
template: 'opencode-agents-md',
|
|
294
|
+
file: () => agentsPath,
|
|
295
|
+
line: (ctx) => findFillerLine(agentsContent(ctx)),
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
opencodeNoContradictions: {
|
|
299
|
+
id: 'OC-A07',
|
|
300
|
+
name: 'No contradictions within same AGENTS.md',
|
|
301
|
+
check: (ctx) => {
|
|
302
|
+
const content = agentsContent(ctx);
|
|
303
|
+
if (!content) return null;
|
|
304
|
+
return !hasContradictions(content);
|
|
305
|
+
},
|
|
306
|
+
impact: 'medium',
|
|
307
|
+
rating: 3,
|
|
308
|
+
category: 'instructions',
|
|
309
|
+
fix: 'Remove contradictory statements (e.g., "always" and "never" in the same line, conflicting style rules).',
|
|
310
|
+
template: 'opencode-agents-md',
|
|
311
|
+
file: () => 'AGENTS.md',
|
|
312
|
+
line: () => null,
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
// ==============================
|
|
316
|
+
// B. Config (6 checks)
|
|
317
|
+
// ==============================
|
|
318
|
+
|
|
319
|
+
opencodeConfigExists: {
|
|
320
|
+
id: 'OC-B01',
|
|
321
|
+
name: 'opencode.json exists at project root',
|
|
322
|
+
check: (ctx) => Boolean(ctx.configContent()),
|
|
323
|
+
impact: 'high',
|
|
324
|
+
rating: 4,
|
|
325
|
+
category: 'config',
|
|
326
|
+
fix: 'Create an opencode.json or opencode.jsonc at the project root with explicit model and permission settings.',
|
|
327
|
+
template: 'opencode-config',
|
|
328
|
+
file: () => 'opencode.json',
|
|
329
|
+
line: () => null,
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
opencodeConfigValidJsonc: {
|
|
333
|
+
id: 'OC-B02',
|
|
334
|
+
name: 'opencode.json is valid JSONC (parseable)',
|
|
335
|
+
check: (ctx) => {
|
|
336
|
+
const content = ctx.configContent();
|
|
337
|
+
if (!content) return null;
|
|
338
|
+
const result = ctx.configJson();
|
|
339
|
+
return result.ok;
|
|
340
|
+
},
|
|
341
|
+
impact: 'critical',
|
|
342
|
+
rating: 5,
|
|
343
|
+
category: 'config',
|
|
344
|
+
fix: 'Fix JSONC syntax errors in opencode.json. Ensure comments use // or /* */ and trailing commas are removed.',
|
|
345
|
+
template: 'opencode-config',
|
|
346
|
+
file: (ctx) => configFileName(ctx),
|
|
347
|
+
line: () => 1,
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
opencodeConfigSchema: {
|
|
351
|
+
id: 'OC-B03',
|
|
352
|
+
name: '$schema references opencode.ai/config.json',
|
|
353
|
+
check: (ctx) => {
|
|
354
|
+
const config = ctx.configJson();
|
|
355
|
+
if (!config.ok || !config.data) return null;
|
|
356
|
+
return Boolean(config.data.$schema);
|
|
357
|
+
},
|
|
358
|
+
impact: 'low',
|
|
359
|
+
rating: 2,
|
|
360
|
+
category: 'config',
|
|
361
|
+
fix: 'Add "$schema": "https://opencode.ai/config.json" to enable IDE validation and autocompletion.',
|
|
362
|
+
template: 'opencode-config',
|
|
363
|
+
file: (ctx) => configFileName(ctx),
|
|
364
|
+
line: () => 1,
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
opencodeModelExplicit: {
|
|
368
|
+
id: 'OC-B04',
|
|
369
|
+
name: 'model is set explicitly (not relying on silent default)',
|
|
370
|
+
check: (ctx) => {
|
|
371
|
+
const config = ctx.configJson();
|
|
372
|
+
if (!config.ok || !config.data) return null;
|
|
373
|
+
return Boolean(config.data.model);
|
|
374
|
+
},
|
|
375
|
+
impact: 'medium',
|
|
376
|
+
rating: 3,
|
|
377
|
+
category: 'config',
|
|
378
|
+
fix: 'Set "model" explicitly in opencode.json to avoid relying on silent provider defaults.',
|
|
379
|
+
template: 'opencode-config',
|
|
380
|
+
file: (ctx) => configFileName(ctx),
|
|
381
|
+
line: () => null,
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
opencodeSmallModelSet: {
|
|
385
|
+
id: 'OC-B05',
|
|
386
|
+
name: 'small_model is set for task delegation',
|
|
387
|
+
check: (ctx) => {
|
|
388
|
+
const config = ctx.configJson();
|
|
389
|
+
if (!config.ok || !config.data) return null;
|
|
390
|
+
return Boolean(config.data.small_model);
|
|
391
|
+
},
|
|
392
|
+
impact: 'medium',
|
|
393
|
+
rating: 3,
|
|
394
|
+
category: 'config',
|
|
395
|
+
fix: 'Set "small_model" in opencode.json for efficient task delegation and cost control.',
|
|
396
|
+
template: 'opencode-config',
|
|
397
|
+
file: (ctx) => configFileName(ctx),
|
|
398
|
+
line: () => null,
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
opencodeNoSecretsInConfig: {
|
|
402
|
+
id: 'OC-B06',
|
|
403
|
+
name: 'No secrets in opencode.json (API keys, tokens, passwords)',
|
|
404
|
+
check: (ctx) => {
|
|
405
|
+
const content = ctx.configContent();
|
|
406
|
+
if (!content) return null;
|
|
407
|
+
return !findSecretLine(content);
|
|
408
|
+
},
|
|
409
|
+
impact: 'critical',
|
|
410
|
+
rating: 5,
|
|
411
|
+
category: 'config',
|
|
412
|
+
fix: 'Remove API keys and tokens from opencode.json. Use environment variables or {env:VAR_NAME} substitution instead.',
|
|
413
|
+
template: 'opencode-config',
|
|
414
|
+
file: (ctx) => configFileName(ctx),
|
|
415
|
+
line: (ctx) => {
|
|
416
|
+
const content = ctx.configContent();
|
|
417
|
+
return content ? findSecretLine(content) : null;
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
|
|
421
|
+
// ==============================
|
|
422
|
+
// C. Permissions (8 checks)
|
|
423
|
+
// ==============================
|
|
424
|
+
|
|
425
|
+
opencodeNoBlanketAllow: {
|
|
426
|
+
id: 'OC-C01',
|
|
427
|
+
name: 'No blanket "allow" for all tools without justification',
|
|
428
|
+
check: (ctx) => {
|
|
429
|
+
const perms = ctx.toolPermissions();
|
|
430
|
+
if (!perms || Object.keys(perms).length === 0) return null;
|
|
431
|
+
// Check for wildcard "*": "allow" or all tools set to "allow"
|
|
432
|
+
if (perms['*'] === 'allow') {
|
|
433
|
+
const docs = docsBundle(ctx);
|
|
434
|
+
return JUSTIFICATION_PATTERNS.test(docs);
|
|
435
|
+
}
|
|
436
|
+
const allAllow = PERMISSIONED_TOOLS.every(tool => perms[tool] === 'allow');
|
|
437
|
+
if (allAllow) {
|
|
438
|
+
const docs = docsBundle(ctx);
|
|
439
|
+
return JUSTIFICATION_PATTERNS.test(docs);
|
|
440
|
+
}
|
|
441
|
+
return true;
|
|
442
|
+
},
|
|
443
|
+
impact: 'critical',
|
|
444
|
+
rating: 5,
|
|
445
|
+
category: 'permissions',
|
|
446
|
+
fix: 'Remove blanket "allow" permission for all tools. Use specific permissions per tool and justify any broad access.',
|
|
447
|
+
template: 'opencode-permissions',
|
|
448
|
+
file: (ctx) => configFileName(ctx),
|
|
449
|
+
line: () => null,
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
opencodeBashPermissionExplicit: {
|
|
453
|
+
id: 'OC-C02',
|
|
454
|
+
name: 'bash tool permission is explicit (not defaulting silently)',
|
|
455
|
+
check: (ctx) => {
|
|
456
|
+
const perms = ctx.toolPermissions();
|
|
457
|
+
if (!perms || Object.keys(perms).length === 0) return null;
|
|
458
|
+
return perms.bash !== undefined;
|
|
459
|
+
},
|
|
460
|
+
impact: 'critical',
|
|
461
|
+
rating: 5,
|
|
462
|
+
category: 'permissions',
|
|
463
|
+
fix: 'Set an explicit permission for the "bash" tool: "ask" (recommended) or "deny" for read-only repos.',
|
|
464
|
+
template: 'opencode-permissions',
|
|
465
|
+
file: (ctx) => configFileName(ctx),
|
|
466
|
+
line: () => null,
|
|
467
|
+
},
|
|
468
|
+
|
|
469
|
+
opencodeBashPatternSpecific: {
|
|
470
|
+
id: 'OC-C03',
|
|
471
|
+
name: 'Pattern-based bash permissions use specific patterns (not "*": "allow")',
|
|
472
|
+
check: (ctx) => {
|
|
473
|
+
const perms = ctx.toolPermissions();
|
|
474
|
+
if (!perms) return null;
|
|
475
|
+
const bashPerms = perms.bash;
|
|
476
|
+
if (!bashPerms || typeof bashPerms !== 'object') return null;
|
|
477
|
+
// Check for overly broad patterns
|
|
478
|
+
if (bashPerms['*'] === 'allow') return false;
|
|
479
|
+
if (bashPerms['**'] === 'allow') return false;
|
|
480
|
+
return true;
|
|
481
|
+
},
|
|
482
|
+
impact: 'high',
|
|
483
|
+
rating: 4,
|
|
484
|
+
category: 'permissions',
|
|
485
|
+
fix: 'Replace "*": "allow" in bash permissions with specific command patterns (e.g., "npm *": "allow").',
|
|
486
|
+
template: 'opencode-permissions',
|
|
487
|
+
file: (ctx) => configFileName(ctx),
|
|
488
|
+
line: () => null,
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
opencodeDestructiveBashDeny: {
|
|
492
|
+
id: 'OC-C04',
|
|
493
|
+
name: 'rm * and destructive bash patterns are "deny" or "ask"',
|
|
494
|
+
check: (ctx) => {
|
|
495
|
+
const perms = ctx.toolPermissions();
|
|
496
|
+
if (!perms) return null;
|
|
497
|
+
const bashPerms = perms.bash;
|
|
498
|
+
if (!bashPerms || typeof bashPerms !== 'object') return true;
|
|
499
|
+
const destructivePatterns = ['rm *', 'rm -rf *', 'git push --force*', 'git reset --hard*'];
|
|
500
|
+
for (const pattern of destructivePatterns) {
|
|
501
|
+
if (bashPerms[pattern] === 'allow') return false;
|
|
502
|
+
}
|
|
503
|
+
return true;
|
|
504
|
+
},
|
|
505
|
+
impact: 'high',
|
|
506
|
+
rating: 4,
|
|
507
|
+
category: 'permissions',
|
|
508
|
+
fix: 'Ensure destructive bash patterns (rm *, git push --force) are set to "deny" or "ask", never "allow".',
|
|
509
|
+
template: 'opencode-permissions',
|
|
510
|
+
file: (ctx) => configFileName(ctx),
|
|
511
|
+
line: () => null,
|
|
512
|
+
},
|
|
513
|
+
|
|
514
|
+
opencodeDoomLoopExplicit: {
|
|
515
|
+
id: 'OC-C05',
|
|
516
|
+
name: 'doom_loop permission is explicit (defaults to "ask")',
|
|
517
|
+
check: (ctx) => {
|
|
518
|
+
const perms = ctx.toolPermissions();
|
|
519
|
+
if (!perms || Object.keys(perms).length === 0) return null;
|
|
520
|
+
return perms.doom_loop !== undefined;
|
|
521
|
+
},
|
|
522
|
+
impact: 'medium',
|
|
523
|
+
rating: 3,
|
|
524
|
+
category: 'permissions',
|
|
525
|
+
fix: 'Set "doom_loop" permission explicitly. This controls behavior when the agent makes 3+ identical calls.',
|
|
526
|
+
template: 'opencode-permissions',
|
|
527
|
+
file: (ctx) => configFileName(ctx),
|
|
528
|
+
line: () => null,
|
|
529
|
+
},
|
|
530
|
+
|
|
531
|
+
opencodeExternalDirExplicit: {
|
|
532
|
+
id: 'OC-C06',
|
|
533
|
+
name: 'external_directory permission is explicit (defaults to "ask")',
|
|
534
|
+
check: (ctx) => {
|
|
535
|
+
const perms = ctx.toolPermissions();
|
|
536
|
+
if (!perms || Object.keys(perms).length === 0) return null;
|
|
537
|
+
return perms.external_directory !== undefined;
|
|
538
|
+
},
|
|
539
|
+
impact: 'medium',
|
|
540
|
+
rating: 3,
|
|
541
|
+
category: 'permissions',
|
|
542
|
+
fix: 'Set "external_directory" permission explicitly. This controls access to files outside the project root.',
|
|
543
|
+
template: 'opencode-permissions',
|
|
544
|
+
file: (ctx) => configFileName(ctx),
|
|
545
|
+
line: () => null,
|
|
546
|
+
},
|
|
547
|
+
|
|
548
|
+
opencodeEnvFileDeny: {
|
|
549
|
+
id: 'OC-C07',
|
|
550
|
+
name: '.env file reads default to "deny" (verify not overridden to "allow")',
|
|
551
|
+
check: (ctx) => {
|
|
552
|
+
const perms = ctx.toolPermissions();
|
|
553
|
+
if (!perms) return null;
|
|
554
|
+
const readPerms = perms.read;
|
|
555
|
+
if (!readPerms || typeof readPerms !== 'object') return true;
|
|
556
|
+
// Check if .env patterns are explicitly allowed
|
|
557
|
+
const envPatterns = ['.env', '.env.*', '*.env'];
|
|
558
|
+
for (const pattern of envPatterns) {
|
|
559
|
+
if (readPerms[pattern] === 'allow') return false;
|
|
560
|
+
}
|
|
561
|
+
return true;
|
|
562
|
+
},
|
|
563
|
+
impact: 'high',
|
|
564
|
+
rating: 4,
|
|
565
|
+
category: 'permissions',
|
|
566
|
+
fix: 'Ensure .env file read permissions are "deny" or "ask", not "allow". Secrets should not be accessible.',
|
|
567
|
+
template: 'opencode-permissions',
|
|
568
|
+
file: (ctx) => configFileName(ctx),
|
|
569
|
+
line: () => null,
|
|
570
|
+
},
|
|
571
|
+
|
|
572
|
+
opencodeAllToolsCovered: {
|
|
573
|
+
id: 'OC-C08',
|
|
574
|
+
name: 'All 15 permissioned tools have explicit state',
|
|
575
|
+
check: (ctx) => {
|
|
576
|
+
const perms = ctx.toolPermissions();
|
|
577
|
+
if (!perms || Object.keys(perms).length === 0) return null;
|
|
578
|
+
// At least the critical tools should be covered
|
|
579
|
+
const critical = ['bash', 'edit', 'read', 'task'];
|
|
580
|
+
return critical.every(tool => perms[tool] !== undefined);
|
|
581
|
+
},
|
|
582
|
+
impact: 'high',
|
|
583
|
+
rating: 4,
|
|
584
|
+
category: 'permissions',
|
|
585
|
+
fix: 'Set explicit permissions for at least the critical tools: bash, edit, read, task.',
|
|
586
|
+
template: 'opencode-permissions',
|
|
587
|
+
file: (ctx) => configFileName(ctx),
|
|
588
|
+
line: () => null,
|
|
589
|
+
},
|
|
590
|
+
|
|
591
|
+
// ==============================
|
|
592
|
+
// D. Plugins (5 checks)
|
|
593
|
+
// ==============================
|
|
594
|
+
|
|
595
|
+
opencodePluginsValid: {
|
|
596
|
+
id: 'OC-D01',
|
|
597
|
+
name: 'Plugin files are valid JS/TS and import from @opencode-ai/plugin',
|
|
598
|
+
check: (ctx) => {
|
|
599
|
+
const pluginFiles = ctx.pluginFiles();
|
|
600
|
+
if (pluginFiles.length === 0) return null;
|
|
601
|
+
// Check that plugin directory exists and files are present
|
|
602
|
+
return pluginFiles.length > 0;
|
|
603
|
+
},
|
|
604
|
+
impact: 'high',
|
|
605
|
+
rating: 4,
|
|
606
|
+
category: 'plugins',
|
|
607
|
+
fix: 'Ensure plugin files in .opencode/plugins/ are valid JS/TS and properly import from @opencode-ai/plugin.',
|
|
608
|
+
template: 'opencode-plugins',
|
|
609
|
+
file: () => '.opencode/plugins/',
|
|
610
|
+
line: () => null,
|
|
611
|
+
},
|
|
612
|
+
|
|
613
|
+
opencodePluginsDocumented: {
|
|
614
|
+
id: 'OC-D02',
|
|
615
|
+
name: 'Project plugins (.opencode/plugins/) are documented and reviewed',
|
|
616
|
+
check: (ctx) => {
|
|
617
|
+
const pluginFiles = ctx.pluginFiles();
|
|
618
|
+
if (pluginFiles.length === 0) return null;
|
|
619
|
+
const docs = docsBundle(ctx);
|
|
620
|
+
return /\bplugins?\b/i.test(docs);
|
|
621
|
+
},
|
|
622
|
+
impact: 'high',
|
|
623
|
+
rating: 4,
|
|
624
|
+
category: 'plugins',
|
|
625
|
+
fix: 'Document plugins in AGENTS.md or README.md. Plugins run in-process and are a critical security surface.',
|
|
626
|
+
template: 'opencode-agents-md',
|
|
627
|
+
file: () => 'AGENTS.md',
|
|
628
|
+
line: () => null,
|
|
629
|
+
},
|
|
630
|
+
|
|
631
|
+
opencodePluginsPinned: {
|
|
632
|
+
id: 'OC-D03',
|
|
633
|
+
name: 'npm plugin packages use pinned versions (not latest/ranges)',
|
|
634
|
+
check: (ctx) => {
|
|
635
|
+
const plugins = ctx.plugins();
|
|
636
|
+
if (!Array.isArray(plugins) || plugins.length === 0) return null;
|
|
637
|
+
for (const plugin of plugins) {
|
|
638
|
+
const name = typeof plugin === 'string' ? plugin : (plugin && plugin.name);
|
|
639
|
+
if (!name) continue;
|
|
640
|
+
if (name.includes('@latest') || name.includes('@*')) return false;
|
|
641
|
+
}
|
|
642
|
+
return true;
|
|
643
|
+
},
|
|
644
|
+
impact: 'high',
|
|
645
|
+
rating: 4,
|
|
646
|
+
category: 'plugins',
|
|
647
|
+
fix: 'Pin plugin versions (e.g., "my-plugin@1.2.3") instead of using @latest or ranges for supply chain security.',
|
|
648
|
+
template: 'opencode-config',
|
|
649
|
+
file: (ctx) => configFileName(ctx),
|
|
650
|
+
line: () => null,
|
|
651
|
+
},
|
|
652
|
+
|
|
653
|
+
opencodePluginEventsValid: {
|
|
654
|
+
id: 'OC-D04',
|
|
655
|
+
name: 'Plugin event handlers match available events (30+ valid events)',
|
|
656
|
+
check: (ctx) => {
|
|
657
|
+
// This is a heuristic check — full validation requires parsing plugin code
|
|
658
|
+
const pluginFiles = ctx.pluginFiles();
|
|
659
|
+
if (pluginFiles.length === 0) return null;
|
|
660
|
+
return true; // Pass by default; deep-review handles thorough checks
|
|
661
|
+
},
|
|
662
|
+
impact: 'medium',
|
|
663
|
+
rating: 3,
|
|
664
|
+
category: 'plugins',
|
|
665
|
+
fix: 'Ensure plugin event handlers use valid event names from the OpenCode plugin API.',
|
|
666
|
+
template: 'opencode-plugins',
|
|
667
|
+
file: () => '.opencode/plugins/',
|
|
668
|
+
line: () => null,
|
|
669
|
+
},
|
|
670
|
+
|
|
671
|
+
opencodePluginHookGapAware: {
|
|
672
|
+
id: 'OC-D05',
|
|
673
|
+
name: 'No plugins relying on tool.execute.before for subagent/MCP coverage (broken: #5894, #2319)',
|
|
674
|
+
check: (ctx) => {
|
|
675
|
+
const pluginFiles = ctx.pluginFiles();
|
|
676
|
+
if (pluginFiles.length === 0) return null;
|
|
677
|
+
const docs = docsBundle(ctx);
|
|
678
|
+
// If plugins reference tool interception, check for gap awareness
|
|
679
|
+
if (/\btool\.execute\.before\b/i.test(docs) && !/\b(bypass|gap|limitation|bug|5894|2319)\b/i.test(docs)) {
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
return true;
|
|
683
|
+
},
|
|
684
|
+
impact: 'high',
|
|
685
|
+
rating: 4,
|
|
686
|
+
category: 'plugins',
|
|
687
|
+
fix: 'Document that tool.execute.before hooks do not intercept subagent/MCP calls (known bugs #5894, #2319).',
|
|
688
|
+
template: 'opencode-agents-md',
|
|
689
|
+
file: () => 'AGENTS.md',
|
|
690
|
+
line: () => null,
|
|
691
|
+
},
|
|
692
|
+
|
|
693
|
+
// ==============================
|
|
694
|
+
// E. Security (6 checks)
|
|
695
|
+
// ==============================
|
|
696
|
+
|
|
697
|
+
opencodeNoSecretsInAgentsMd: {
|
|
698
|
+
id: 'OC-E01',
|
|
699
|
+
name: 'No secrets/API keys in AGENTS.md',
|
|
700
|
+
check: (ctx) => {
|
|
701
|
+
const content = agentsContent(ctx);
|
|
702
|
+
if (!content) return null;
|
|
703
|
+
return !findSecretLine(content);
|
|
704
|
+
},
|
|
705
|
+
impact: 'critical',
|
|
706
|
+
rating: 5,
|
|
707
|
+
category: 'security',
|
|
708
|
+
fix: 'Remove any API keys, tokens, or passwords from AGENTS.md. Use environment variables instead.',
|
|
709
|
+
template: 'opencode-agents-md',
|
|
710
|
+
file: () => 'AGENTS.md',
|
|
711
|
+
line: (ctx) => findSecretLine(agentsContent(ctx)),
|
|
712
|
+
},
|
|
713
|
+
|
|
714
|
+
opencodeToolInterceptionGap: {
|
|
715
|
+
id: 'OC-E02',
|
|
716
|
+
name: 'tool.execute.before hook gap documented (#5894, #2319)',
|
|
717
|
+
check: (ctx) => {
|
|
718
|
+
const pluginFiles = ctx.pluginFiles();
|
|
719
|
+
if (pluginFiles.length === 0) return null;
|
|
720
|
+
// Check if the project uses plugins and has documented the gap
|
|
721
|
+
const docs = docsBundle(ctx);
|
|
722
|
+
const usesToolHooks = /\btool\.execute/i.test(docs);
|
|
723
|
+
if (!usesToolHooks) return true;
|
|
724
|
+
return /\b(bypass|gap|limitation|5894|2319)\b/i.test(docs);
|
|
725
|
+
},
|
|
726
|
+
impact: 'high',
|
|
727
|
+
rating: 4,
|
|
728
|
+
category: 'security',
|
|
729
|
+
fix: 'Document that subagent and MCP tool calls bypass tool.execute.before hooks (bugs #5894, #2319).',
|
|
730
|
+
template: 'opencode-agents-md',
|
|
731
|
+
file: () => 'AGENTS.md',
|
|
732
|
+
line: () => null,
|
|
733
|
+
},
|
|
734
|
+
|
|
735
|
+
opencodeAgentDenyNotBypassable: {
|
|
736
|
+
id: 'OC-E03',
|
|
737
|
+
name: 'Agent deny permissions not bypassable via SDK (#6396)',
|
|
738
|
+
check: (ctx) => {
|
|
739
|
+
const agents = ctx.customAgents();
|
|
740
|
+
if (!agents || Object.keys(agents).length === 0) return null;
|
|
741
|
+
const docs = docsBundle(ctx);
|
|
742
|
+
const usesAgentPerms = Object.values(agents).some(a => a && a.permissions);
|
|
743
|
+
if (!usesAgentPerms) return true;
|
|
744
|
+
return /\b(bypass|gap|limitation|6396)\b/i.test(docs);
|
|
745
|
+
},
|
|
746
|
+
impact: 'high',
|
|
747
|
+
rating: 4,
|
|
748
|
+
category: 'security',
|
|
749
|
+
fix: 'Document that agent deny permissions can be bypassed via SDK (bug #6396). Add compensating controls.',
|
|
750
|
+
template: 'opencode-agents-md',
|
|
751
|
+
file: () => 'AGENTS.md',
|
|
752
|
+
line: () => null,
|
|
753
|
+
},
|
|
754
|
+
|
|
755
|
+
opencodeServerPasswordSet: {
|
|
756
|
+
id: 'OC-E04',
|
|
757
|
+
name: 'Server mode (opencode serve) is protected with OPENCODE_SERVER_PASSWORD',
|
|
758
|
+
check: (ctx) => {
|
|
759
|
+
const docs = docsBundle(ctx);
|
|
760
|
+
if (!/\bopencode\s+serve\b|\bserver\s+mode\b/i.test(docs)) return null;
|
|
761
|
+
return /\bOPENCODE_SERVER_PASSWORD\b/i.test(docs);
|
|
762
|
+
},
|
|
763
|
+
impact: 'high',
|
|
764
|
+
rating: 4,
|
|
765
|
+
category: 'security',
|
|
766
|
+
fix: 'Document that OPENCODE_SERVER_PASSWORD must be set when using `opencode serve` for HTTP API security.',
|
|
767
|
+
template: 'opencode-agents-md',
|
|
768
|
+
file: () => 'AGENTS.md',
|
|
769
|
+
line: () => null,
|
|
770
|
+
},
|
|
771
|
+
|
|
772
|
+
opencodeNoSecretExposure: {
|
|
773
|
+
id: 'OC-E05',
|
|
774
|
+
name: 'No secrets exposed through config variable substitution',
|
|
775
|
+
check: (ctx) => {
|
|
776
|
+
const content = ctx.configContent();
|
|
777
|
+
if (!content) return null;
|
|
778
|
+
// Check for hardcoded secrets in variable substitution values
|
|
779
|
+
return !findSecretLine(content);
|
|
780
|
+
},
|
|
781
|
+
impact: 'critical',
|
|
782
|
+
rating: 5,
|
|
783
|
+
category: 'security',
|
|
784
|
+
fix: 'Use {env:VAR_NAME} substitution for secrets in opencode.json instead of hardcoded values.',
|
|
785
|
+
template: 'opencode-config',
|
|
786
|
+
file: (ctx) => configFileName(ctx),
|
|
787
|
+
line: (ctx) => {
|
|
788
|
+
const content = ctx.configContent();
|
|
789
|
+
return content ? findSecretLine(content) : null;
|
|
790
|
+
},
|
|
791
|
+
},
|
|
792
|
+
|
|
793
|
+
opencodeRegulatedRepoExplicitPerms: {
|
|
794
|
+
id: 'OC-E06',
|
|
795
|
+
name: 'Regulated repos have explicit restrictive permission posture',
|
|
796
|
+
check: (ctx) => {
|
|
797
|
+
if (!repoLooksRegulated(ctx)) return null;
|
|
798
|
+
const perms = ctx.toolPermissions();
|
|
799
|
+
if (!perms || Object.keys(perms).length === 0) return false;
|
|
800
|
+
// Regulated repos should have explicit bash permissions
|
|
801
|
+
return perms.bash !== undefined && perms.bash !== 'allow';
|
|
802
|
+
},
|
|
803
|
+
impact: 'high',
|
|
804
|
+
rating: 4,
|
|
805
|
+
category: 'security',
|
|
806
|
+
fix: 'This repo has compliance signals. Set restrictive permissions: bash should be "ask" or "deny".',
|
|
807
|
+
template: 'opencode-permissions',
|
|
808
|
+
file: (ctx) => configFileName(ctx),
|
|
809
|
+
line: () => null,
|
|
810
|
+
},
|
|
811
|
+
|
|
812
|
+
// ==============================
|
|
813
|
+
// F. MCP (5 checks)
|
|
814
|
+
// ==============================
|
|
815
|
+
|
|
816
|
+
opencodeMcpSchemaCorrect: {
|
|
817
|
+
id: 'OC-F01',
|
|
818
|
+
name: 'MCP servers use correct schema (command: [] array, environment: {} not env)',
|
|
819
|
+
check: (ctx) => {
|
|
820
|
+
const mcp = ctx.mcpServers();
|
|
821
|
+
if (!mcp || Object.keys(mcp).length === 0) return null;
|
|
822
|
+
for (const [id, server] of Object.entries(mcp)) {
|
|
823
|
+
if (!server) continue;
|
|
824
|
+
// command should be an array in OpenCode MCP config
|
|
825
|
+
if (server.command && !Array.isArray(server.command) && typeof server.command !== 'string') return false;
|
|
826
|
+
// env is the wrong key — should be environment
|
|
827
|
+
if (server.env && !server.environment) return false;
|
|
828
|
+
}
|
|
829
|
+
return true;
|
|
830
|
+
},
|
|
831
|
+
impact: 'critical',
|
|
832
|
+
rating: 5,
|
|
833
|
+
category: 'mcp',
|
|
834
|
+
fix: 'Fix MCP config schema: use "command": ["npx", ...] (array) and "environment": {} (not "env").',
|
|
835
|
+
template: 'opencode-config',
|
|
836
|
+
file: (ctx) => configFileName(ctx),
|
|
837
|
+
line: () => null,
|
|
838
|
+
},
|
|
839
|
+
|
|
840
|
+
opencodeMcpToolWhitelisting: {
|
|
841
|
+
id: 'OC-F02',
|
|
842
|
+
name: 'Tool whitelisting uses glob patterns to limit MCP tool access',
|
|
843
|
+
check: (ctx) => {
|
|
844
|
+
const mcp = ctx.mcpServers();
|
|
845
|
+
if (!mcp || Object.keys(mcp).length === 0) return null;
|
|
846
|
+
// Check if any MCP servers have tool restrictions configured
|
|
847
|
+
const perms = ctx.toolPermissions();
|
|
848
|
+
if (!perms) return null;
|
|
849
|
+
// Look for MCP-related tool permission patterns
|
|
850
|
+
const hasMcpToolRestrictions = Object.keys(perms).some(key => key.includes('mcp') || key.includes('*'));
|
|
851
|
+
return hasMcpToolRestrictions || Object.keys(mcp).length <= 2;
|
|
852
|
+
},
|
|
853
|
+
impact: 'high',
|
|
854
|
+
rating: 4,
|
|
855
|
+
category: 'mcp',
|
|
856
|
+
fix: 'Add tool whitelisting for MCP servers: { "tools": { "my-mcp*": false } } to limit tool access.',
|
|
857
|
+
template: 'opencode-config',
|
|
858
|
+
file: (ctx) => configFileName(ctx),
|
|
859
|
+
line: () => null,
|
|
860
|
+
},
|
|
861
|
+
|
|
862
|
+
opencodeMcpTimeoutReasonable: {
|
|
863
|
+
id: 'OC-F03',
|
|
864
|
+
name: 'MCP timeout is reasonable (default 5000ms, max justified)',
|
|
865
|
+
check: (ctx) => {
|
|
866
|
+
const mcp = ctx.mcpServers();
|
|
867
|
+
if (!mcp || Object.keys(mcp).length === 0) return null;
|
|
868
|
+
for (const [id, server] of Object.entries(mcp)) {
|
|
869
|
+
if (!server) continue;
|
|
870
|
+
const timeout = server.timeout || server.startup_timeout;
|
|
871
|
+
if (typeof timeout === 'number' && timeout > 30000) {
|
|
872
|
+
const docs = docsBundle(ctx);
|
|
873
|
+
if (!JUSTIFICATION_PATTERNS.test(docs)) return false;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return true;
|
|
877
|
+
},
|
|
878
|
+
impact: 'low',
|
|
879
|
+
rating: 2,
|
|
880
|
+
category: 'mcp',
|
|
881
|
+
fix: 'MCP timeout exceeds 30s. Add justification or reduce the timeout.',
|
|
882
|
+
template: 'opencode-config',
|
|
883
|
+
file: (ctx) => configFileName(ctx),
|
|
884
|
+
line: () => null,
|
|
885
|
+
},
|
|
886
|
+
|
|
887
|
+
opencodeMcpHookLimitation: {
|
|
888
|
+
id: 'OC-F04',
|
|
889
|
+
name: 'MCP tool calls do not trigger plugin hooks — documented limitation (#2319)',
|
|
890
|
+
check: (ctx) => {
|
|
891
|
+
const mcp = ctx.mcpServers();
|
|
892
|
+
const pluginFiles = ctx.pluginFiles();
|
|
893
|
+
if (!mcp || Object.keys(mcp).length === 0) return null;
|
|
894
|
+
if (pluginFiles.length === 0) return null;
|
|
895
|
+
const docs = docsBundle(ctx);
|
|
896
|
+
return /\b(mcp|2319|hook.*mcp|mcp.*hook)\b/i.test(docs);
|
|
897
|
+
},
|
|
898
|
+
impact: 'medium',
|
|
899
|
+
rating: 3,
|
|
900
|
+
category: 'mcp',
|
|
901
|
+
fix: 'Document that MCP tool calls bypass plugin hooks (#2319). Ensure security does not rely on hook interception for MCP.',
|
|
902
|
+
template: 'opencode-agents-md',
|
|
903
|
+
file: () => 'AGENTS.md',
|
|
904
|
+
line: () => null,
|
|
905
|
+
},
|
|
906
|
+
|
|
907
|
+
opencodeMcpAuthDocumented: {
|
|
908
|
+
id: 'OC-F05',
|
|
909
|
+
name: 'MCP servers requiring auth have documented setup instructions',
|
|
910
|
+
check: (ctx) => {
|
|
911
|
+
const mcp = ctx.mcpServers();
|
|
912
|
+
if (!mcp || Object.keys(mcp).length === 0) return null;
|
|
913
|
+
const docs = docsBundle(ctx);
|
|
914
|
+
for (const [id, server] of Object.entries(mcp)) {
|
|
915
|
+
if (!server) continue;
|
|
916
|
+
const env = server.environment || {};
|
|
917
|
+
const hasAuthEnv = Object.keys(env).some(k => /token|key|secret|password|credential/i.test(k));
|
|
918
|
+
if (hasAuthEnv) {
|
|
919
|
+
const idPattern = new RegExp(`\\b${escapeRegex(id)}\\b[\\s\\S]{0,200}\\b(auth|setup|token|key|env)\\b`, 'i');
|
|
920
|
+
if (!idPattern.test(docs)) return false;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return true;
|
|
924
|
+
},
|
|
925
|
+
impact: 'medium',
|
|
926
|
+
rating: 3,
|
|
927
|
+
category: 'mcp',
|
|
928
|
+
fix: 'Document MCP server auth setup in AGENTS.md or README.md for servers that require tokens/keys.',
|
|
929
|
+
template: 'opencode-agents-md',
|
|
930
|
+
file: () => 'AGENTS.md',
|
|
931
|
+
line: () => null,
|
|
932
|
+
},
|
|
933
|
+
|
|
934
|
+
// ==============================
|
|
935
|
+
// G. CI & Automation (4 checks)
|
|
936
|
+
// ==============================
|
|
937
|
+
|
|
938
|
+
opencodeCiPermissionsPreset: {
|
|
939
|
+
id: 'OC-G01',
|
|
940
|
+
name: 'opencode run usage pre-configures all permissions to avoid hang (#10411)',
|
|
941
|
+
check: (ctx) => {
|
|
942
|
+
const workflows = workflowArtifacts(ctx);
|
|
943
|
+
const hasOpencodeRun = workflows.some(w => /\bopencode\s+run\b/i.test(w.content));
|
|
944
|
+
if (!hasOpencodeRun) return null;
|
|
945
|
+
// Check that permissions are pre-configured
|
|
946
|
+
return workflows.some(w => /\bpermissions?\b.*\ballow\b|\b--yes\b|\b--no-prompt\b/i.test(w.content));
|
|
947
|
+
},
|
|
948
|
+
impact: 'critical',
|
|
949
|
+
rating: 5,
|
|
950
|
+
category: 'ci',
|
|
951
|
+
fix: 'Pre-configure all permissions when using `opencode run` in CI. Without this, the process hangs on permission prompts.',
|
|
952
|
+
template: 'opencode-ci',
|
|
953
|
+
file: () => '.github/workflows/',
|
|
954
|
+
line: () => null,
|
|
955
|
+
},
|
|
956
|
+
|
|
957
|
+
opencodeCiAutoUpdateDisabled: {
|
|
958
|
+
id: 'OC-G02',
|
|
959
|
+
name: 'OPENCODE_DISABLE_AUTOUPDATE=1 is set in CI environments',
|
|
960
|
+
check: (ctx) => {
|
|
961
|
+
const workflows = workflowArtifacts(ctx);
|
|
962
|
+
const hasOpencode = workflows.some(w => /\bopencode\b/i.test(w.content));
|
|
963
|
+
if (!hasOpencode) return null;
|
|
964
|
+
return workflows.some(w => /OPENCODE_DISABLE_AUTOUPDATE/i.test(w.content));
|
|
965
|
+
},
|
|
966
|
+
impact: 'high',
|
|
967
|
+
rating: 4,
|
|
968
|
+
category: 'ci',
|
|
969
|
+
fix: 'Set OPENCODE_DISABLE_AUTOUPDATE=1 in CI workflows for reproducible builds.',
|
|
970
|
+
template: 'opencode-ci',
|
|
971
|
+
file: () => '.github/workflows/',
|
|
972
|
+
line: () => null,
|
|
973
|
+
},
|
|
974
|
+
|
|
975
|
+
opencodeCiJsonOutput: {
|
|
976
|
+
id: 'OC-G03',
|
|
977
|
+
name: '--format json is used for machine-readable CI output',
|
|
978
|
+
check: (ctx) => {
|
|
979
|
+
const workflows = workflowArtifacts(ctx);
|
|
980
|
+
const hasOpencodeRun = workflows.some(w => /\bopencode\s+run\b/i.test(w.content));
|
|
981
|
+
if (!hasOpencodeRun) return null;
|
|
982
|
+
return workflows.some(w => /--format\s+json\b/i.test(w.content));
|
|
983
|
+
},
|
|
984
|
+
impact: 'medium',
|
|
985
|
+
rating: 3,
|
|
986
|
+
category: 'ci',
|
|
987
|
+
fix: 'Use `--format json` when running OpenCode in CI for machine-readable output parsing.',
|
|
988
|
+
template: 'opencode-ci',
|
|
989
|
+
file: () => '.github/workflows/',
|
|
990
|
+
line: () => null,
|
|
991
|
+
},
|
|
992
|
+
|
|
993
|
+
opencodeCiEnvAuth: {
|
|
994
|
+
id: 'OC-G04',
|
|
995
|
+
name: 'CI auth uses environment variables (not hardcoded credentials)',
|
|
996
|
+
check: (ctx) => {
|
|
997
|
+
const workflows = workflowArtifacts(ctx);
|
|
998
|
+
const hasOpencode = workflows.some(w => /\bopencode\b/i.test(w.content));
|
|
999
|
+
if (!hasOpencode) return null;
|
|
1000
|
+
// Check for hardcoded credentials in workflows
|
|
1001
|
+
for (const w of workflows) {
|
|
1002
|
+
if (/\bopencode\b/i.test(w.content) && findSecretLine(w.content)) return false;
|
|
1003
|
+
}
|
|
1004
|
+
return true;
|
|
1005
|
+
},
|
|
1006
|
+
impact: 'critical',
|
|
1007
|
+
rating: 5,
|
|
1008
|
+
category: 'ci',
|
|
1009
|
+
fix: 'Use GitHub secrets and environment variables for OpenCode auth in CI. Never hardcode credentials.',
|
|
1010
|
+
template: 'opencode-ci',
|
|
1011
|
+
file: () => '.github/workflows/',
|
|
1012
|
+
line: () => null,
|
|
1013
|
+
},
|
|
1014
|
+
|
|
1015
|
+
// ==============================
|
|
1016
|
+
// H. Quality Deep (5 checks)
|
|
1017
|
+
// ==============================
|
|
1018
|
+
|
|
1019
|
+
opencodeModernFeaturesDocumented: {
|
|
1020
|
+
id: 'OC-H01',
|
|
1021
|
+
name: 'AGENTS.md mentions modern OpenCode features (plugins, custom agents, skills)',
|
|
1022
|
+
check: (ctx) => {
|
|
1023
|
+
const content = agentsContent(ctx);
|
|
1024
|
+
if (!content) return null;
|
|
1025
|
+
const hasModernRefs = /\bplugin(s)?\b|\bcustom\s+agent(s)?\b|\bskill(s)?\b|\bopencode\b/i.test(content);
|
|
1026
|
+
return hasModernRefs;
|
|
1027
|
+
},
|
|
1028
|
+
impact: 'medium',
|
|
1029
|
+
rating: 3,
|
|
1030
|
+
category: 'quality-deep',
|
|
1031
|
+
fix: 'Mention OpenCode-specific features (plugins, agents, skills) in AGENTS.md to leverage platform capabilities.',
|
|
1032
|
+
template: 'opencode-agents-md',
|
|
1033
|
+
file: () => 'AGENTS.md',
|
|
1034
|
+
line: () => null,
|
|
1035
|
+
},
|
|
1036
|
+
|
|
1037
|
+
opencodeNoDeprecatedPatterns: {
|
|
1038
|
+
id: 'OC-H02',
|
|
1039
|
+
name: 'No deprecated OpenCode patterns (mode -> agent migration)',
|
|
1040
|
+
check: (ctx) => {
|
|
1041
|
+
const config = ctx.configJson();
|
|
1042
|
+
if (!config.ok || !config.data) return null;
|
|
1043
|
+
for (const { key } of DEPRECATED_CONFIG_KEYS) {
|
|
1044
|
+
if (config.data[key] !== undefined) return false;
|
|
1045
|
+
}
|
|
1046
|
+
return true;
|
|
1047
|
+
},
|
|
1048
|
+
impact: 'medium',
|
|
1049
|
+
rating: 3,
|
|
1050
|
+
category: 'quality-deep',
|
|
1051
|
+
fix: 'Replace deprecated config keys: use "agent" instead of "mode".',
|
|
1052
|
+
template: 'opencode-config',
|
|
1053
|
+
file: (ctx) => configFileName(ctx),
|
|
1054
|
+
line: () => null,
|
|
1055
|
+
},
|
|
1056
|
+
|
|
1057
|
+
opencodeCompactionExplicit: {
|
|
1058
|
+
id: 'OC-H03',
|
|
1059
|
+
name: 'compaction settings are explicit if sessions are long',
|
|
1060
|
+
check: (ctx) => {
|
|
1061
|
+
const config = ctx.configJson();
|
|
1062
|
+
if (!config.ok || !config.data) return null;
|
|
1063
|
+
// Only relevant if the project looks like it uses long sessions
|
|
1064
|
+
const docs = docsBundle(ctx);
|
|
1065
|
+
const usesLongSessions = /\blong\s+session\b|\bcompact\b|\bcontext\s+(limit|window|management)\b/i.test(docs);
|
|
1066
|
+
if (!usesLongSessions) return null;
|
|
1067
|
+
return config.data.compaction !== undefined;
|
|
1068
|
+
},
|
|
1069
|
+
impact: 'medium',
|
|
1070
|
+
rating: 3,
|
|
1071
|
+
category: 'quality-deep',
|
|
1072
|
+
fix: 'Set explicit "compaction" settings in opencode.json for context management during long sessions.',
|
|
1073
|
+
template: 'opencode-config',
|
|
1074
|
+
file: (ctx) => configFileName(ctx),
|
|
1075
|
+
line: () => null,
|
|
1076
|
+
},
|
|
1077
|
+
|
|
1078
|
+
opencodeFormatterConfigured: {
|
|
1079
|
+
id: 'OC-H04',
|
|
1080
|
+
name: 'formatter is configured if project uses auto-formatting',
|
|
1081
|
+
check: (ctx) => {
|
|
1082
|
+
const pkg = ctx.jsonFile('package.json');
|
|
1083
|
+
const hasFormatter = pkg && pkg.scripts && (pkg.scripts.format || pkg.scripts.prettier);
|
|
1084
|
+
const hasFormatterConfig = ctx.fileContent('.prettierrc') || ctx.fileContent('.prettierrc.json') ||
|
|
1085
|
+
ctx.fileContent('.editorconfig');
|
|
1086
|
+
if (!hasFormatter && !hasFormatterConfig) return null;
|
|
1087
|
+
const config = ctx.configJson();
|
|
1088
|
+
if (!config.ok || !config.data) return null;
|
|
1089
|
+
return config.data.formatter !== undefined;
|
|
1090
|
+
},
|
|
1091
|
+
impact: 'low',
|
|
1092
|
+
rating: 2,
|
|
1093
|
+
category: 'quality-deep',
|
|
1094
|
+
fix: 'Set "formatter" in opencode.json to integrate with the project auto-formatting tool.',
|
|
1095
|
+
template: 'opencode-config',
|
|
1096
|
+
file: (ctx) => configFileName(ctx),
|
|
1097
|
+
line: () => null,
|
|
1098
|
+
},
|
|
1099
|
+
|
|
1100
|
+
opencodeProviderManagement: {
|
|
1101
|
+
id: 'OC-H05',
|
|
1102
|
+
name: 'disabled_providers / enabled_providers are set intentionally',
|
|
1103
|
+
check: (ctx) => {
|
|
1104
|
+
const config = ctx.configJson();
|
|
1105
|
+
if (!config.ok || !config.data) return null;
|
|
1106
|
+
// Only flag if many providers are available but none are managed
|
|
1107
|
+
if (config.data.disabled_providers || config.data.enabled_providers) return true;
|
|
1108
|
+
// Soft pass — not critical unless the repo explicitly uses multiple providers
|
|
1109
|
+
return null;
|
|
1110
|
+
},
|
|
1111
|
+
impact: 'low',
|
|
1112
|
+
rating: 2,
|
|
1113
|
+
category: 'quality-deep',
|
|
1114
|
+
fix: 'Consider setting "disabled_providers" or "enabled_providers" to control which model providers are available.',
|
|
1115
|
+
template: 'opencode-config',
|
|
1116
|
+
file: (ctx) => configFileName(ctx),
|
|
1117
|
+
line: () => null,
|
|
1118
|
+
},
|
|
1119
|
+
|
|
1120
|
+
// ==============================
|
|
1121
|
+
// I. Skills (5 checks)
|
|
1122
|
+
// ==============================
|
|
1123
|
+
|
|
1124
|
+
opencodeSkillDirsExist: {
|
|
1125
|
+
id: 'OC-I01',
|
|
1126
|
+
name: 'Skill directories exist (.opencode/commands/ subdirs with SKILL.md)',
|
|
1127
|
+
check: (ctx) => {
|
|
1128
|
+
const skillDirs = ctx.skillDirs();
|
|
1129
|
+
if (skillDirs.length === 0) return null;
|
|
1130
|
+
return skillDirs.length > 0;
|
|
1131
|
+
},
|
|
1132
|
+
impact: 'medium',
|
|
1133
|
+
rating: 3,
|
|
1134
|
+
category: 'skills',
|
|
1135
|
+
fix: 'Create skill directories under .opencode/commands/ with SKILL.md files.',
|
|
1136
|
+
template: 'opencode-skills',
|
|
1137
|
+
file: () => '.opencode/commands/',
|
|
1138
|
+
line: () => null,
|
|
1139
|
+
},
|
|
1140
|
+
|
|
1141
|
+
opencodeSkillFrontmatter: {
|
|
1142
|
+
id: 'OC-I02',
|
|
1143
|
+
name: 'SKILL.md has required frontmatter (name, description)',
|
|
1144
|
+
check: (ctx) => {
|
|
1145
|
+
const skillDirs = ctx.skillDirs();
|
|
1146
|
+
if (skillDirs.length === 0) return null;
|
|
1147
|
+
for (const name of skillDirs) {
|
|
1148
|
+
const content = ctx.skillMetadata(name);
|
|
1149
|
+
if (!content) return false;
|
|
1150
|
+
if (!/^#\s+/m.test(content)) return false;
|
|
1151
|
+
}
|
|
1152
|
+
return true;
|
|
1153
|
+
},
|
|
1154
|
+
impact: 'high',
|
|
1155
|
+
rating: 4,
|
|
1156
|
+
category: 'skills',
|
|
1157
|
+
fix: 'Each SKILL.md needs a title (# heading) and description for skill invocation.',
|
|
1158
|
+
template: 'opencode-skills',
|
|
1159
|
+
file: () => '.opencode/commands/',
|
|
1160
|
+
line: () => null,
|
|
1161
|
+
},
|
|
1162
|
+
|
|
1163
|
+
opencodeSkillKebabCase: {
|
|
1164
|
+
id: 'OC-I03',
|
|
1165
|
+
name: 'Skill names use kebab-case (not PascalCase — 0% invocation rate)',
|
|
1166
|
+
check: (ctx) => {
|
|
1167
|
+
const skillDirs = ctx.skillDirs();
|
|
1168
|
+
if (skillDirs.length === 0) return null;
|
|
1169
|
+
return skillDirs.every(name => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name));
|
|
1170
|
+
},
|
|
1171
|
+
impact: 'high',
|
|
1172
|
+
rating: 4,
|
|
1173
|
+
category: 'skills',
|
|
1174
|
+
fix: 'Rename skill directories to kebab-case (e.g., code-review, not CodeReview). PascalCase has 0% implicit invocation.',
|
|
1175
|
+
template: 'opencode-skills',
|
|
1176
|
+
file: () => '.opencode/commands/',
|
|
1177
|
+
line: () => null,
|
|
1178
|
+
},
|
|
1179
|
+
|
|
1180
|
+
opencodeSkillDescriptionBounded: {
|
|
1181
|
+
id: 'OC-I04',
|
|
1182
|
+
name: 'Skill descriptions are bounded for implicit invocation context cost',
|
|
1183
|
+
check: (ctx) => {
|
|
1184
|
+
const skillDirs = ctx.skillDirs();
|
|
1185
|
+
if (skillDirs.length === 0) return null;
|
|
1186
|
+
for (const name of skillDirs) {
|
|
1187
|
+
const content = ctx.skillMetadata(name);
|
|
1188
|
+
if (!content) continue;
|
|
1189
|
+
if (content.length > 3000) return false;
|
|
1190
|
+
}
|
|
1191
|
+
return true;
|
|
1192
|
+
},
|
|
1193
|
+
impact: 'medium',
|
|
1194
|
+
rating: 3,
|
|
1195
|
+
category: 'skills',
|
|
1196
|
+
fix: 'Keep SKILL.md descriptions under 3000 characters to manage implicit invocation context cost.',
|
|
1197
|
+
template: 'opencode-skills',
|
|
1198
|
+
file: () => '.opencode/commands/',
|
|
1199
|
+
line: () => null,
|
|
1200
|
+
},
|
|
1201
|
+
|
|
1202
|
+
opencodeSkillCompatPaths: {
|
|
1203
|
+
id: 'OC-I05',
|
|
1204
|
+
name: '.claude/skills/ compatibility paths resolve correctly in OpenCode',
|
|
1205
|
+
check: (ctx) => {
|
|
1206
|
+
const hasClaudeSkills = ctx.hasDir('.claude/skills');
|
|
1207
|
+
if (!hasClaudeSkills) return null;
|
|
1208
|
+
const hasOpencodeCommands = ctx.hasDir('.opencode/commands');
|
|
1209
|
+
// If both exist, they should be consistent
|
|
1210
|
+
if (hasOpencodeCommands) return true;
|
|
1211
|
+
// Claude skills exist but no OpenCode commands — warn
|
|
1212
|
+
return false;
|
|
1213
|
+
},
|
|
1214
|
+
impact: 'medium',
|
|
1215
|
+
rating: 3,
|
|
1216
|
+
category: 'skills',
|
|
1217
|
+
fix: 'If using .claude/skills/, also create .opencode/commands/ for OpenCode compatibility.',
|
|
1218
|
+
template: 'opencode-skills',
|
|
1219
|
+
file: () => '.opencode/commands/',
|
|
1220
|
+
line: () => null,
|
|
1221
|
+
},
|
|
1222
|
+
|
|
1223
|
+
// ==============================
|
|
1224
|
+
// J. Agents & Subagents (4 checks)
|
|
1225
|
+
// ==============================
|
|
1226
|
+
|
|
1227
|
+
opencodeAgentRequiredFields: {
|
|
1228
|
+
id: 'OC-J01',
|
|
1229
|
+
name: 'Custom agents have required fields (description, model)',
|
|
1230
|
+
check: (ctx) => {
|
|
1231
|
+
const agents = ctx.customAgents();
|
|
1232
|
+
if (!agents || Object.keys(agents).length === 0) return null;
|
|
1233
|
+
for (const [name, agent] of Object.entries(agents)) {
|
|
1234
|
+
if (!agent) return false;
|
|
1235
|
+
if (!agent.description) return false;
|
|
1236
|
+
}
|
|
1237
|
+
return true;
|
|
1238
|
+
},
|
|
1239
|
+
impact: 'high',
|
|
1240
|
+
rating: 4,
|
|
1241
|
+
category: 'agents',
|
|
1242
|
+
fix: 'Ensure all custom agents have at least "description" and "model" fields in opencode.json.',
|
|
1243
|
+
template: 'opencode-config',
|
|
1244
|
+
file: (ctx) => configFileName(ctx),
|
|
1245
|
+
line: () => null,
|
|
1246
|
+
},
|
|
1247
|
+
|
|
1248
|
+
opencodeAgentModeValid: {
|
|
1249
|
+
id: 'OC-J02',
|
|
1250
|
+
name: 'Agent mode field is valid: "primary", "subagent", or "all"',
|
|
1251
|
+
check: (ctx) => {
|
|
1252
|
+
const agents = ctx.customAgents();
|
|
1253
|
+
if (!agents || Object.keys(agents).length === 0) return null;
|
|
1254
|
+
const validModes = new Set(['primary', 'subagent', 'all']);
|
|
1255
|
+
for (const [name, agent] of Object.entries(agents)) {
|
|
1256
|
+
if (!agent) continue;
|
|
1257
|
+
if (agent.agent && !validModes.has(agent.agent)) return false;
|
|
1258
|
+
}
|
|
1259
|
+
return true;
|
|
1260
|
+
},
|
|
1261
|
+
impact: 'medium',
|
|
1262
|
+
rating: 3,
|
|
1263
|
+
category: 'agents',
|
|
1264
|
+
fix: 'Set agent "agent" field to one of: "primary", "subagent", or "all".',
|
|
1265
|
+
template: 'opencode-config',
|
|
1266
|
+
file: (ctx) => configFileName(ctx),
|
|
1267
|
+
line: () => null,
|
|
1268
|
+
},
|
|
1269
|
+
|
|
1270
|
+
opencodeBuiltinAgentsProtected: {
|
|
1271
|
+
id: 'OC-J03',
|
|
1272
|
+
name: 'Built-in agents (build, plan) not accidentally overridden',
|
|
1273
|
+
check: (ctx) => {
|
|
1274
|
+
const agents = ctx.customAgents();
|
|
1275
|
+
if (!agents || Object.keys(agents).length === 0) return null;
|
|
1276
|
+
const builtins = new Set(['build', 'plan', 'default']);
|
|
1277
|
+
for (const name of Object.keys(agents)) {
|
|
1278
|
+
if (builtins.has(name.toLowerCase())) return false;
|
|
1279
|
+
}
|
|
1280
|
+
return true;
|
|
1281
|
+
},
|
|
1282
|
+
impact: 'medium',
|
|
1283
|
+
rating: 3,
|
|
1284
|
+
category: 'agents',
|
|
1285
|
+
fix: 'Do not name custom agents "build", "plan", or "default" to avoid overriding built-in agents.',
|
|
1286
|
+
template: 'opencode-config',
|
|
1287
|
+
file: (ctx) => configFileName(ctx),
|
|
1288
|
+
line: () => null,
|
|
1289
|
+
},
|
|
1290
|
+
|
|
1291
|
+
opencodeAgentStepsLimit: {
|
|
1292
|
+
id: 'OC-J04',
|
|
1293
|
+
name: 'Agent steps limit is set to prevent runaway execution',
|
|
1294
|
+
check: (ctx) => {
|
|
1295
|
+
const agents = ctx.customAgents();
|
|
1296
|
+
if (!agents || Object.keys(agents).length === 0) return null;
|
|
1297
|
+
for (const [name, agent] of Object.entries(agents)) {
|
|
1298
|
+
if (!agent) continue;
|
|
1299
|
+
if (agent.steps && agent.steps > 100) return false;
|
|
1300
|
+
}
|
|
1301
|
+
return true;
|
|
1302
|
+
},
|
|
1303
|
+
impact: 'medium',
|
|
1304
|
+
rating: 3,
|
|
1305
|
+
category: 'agents',
|
|
1306
|
+
fix: 'Set a reasonable "steps" limit on custom agents to prevent runaway execution. 50-100 is typical.',
|
|
1307
|
+
template: 'opencode-config',
|
|
1308
|
+
file: (ctx) => configFileName(ctx),
|
|
1309
|
+
line: () => null,
|
|
1310
|
+
},
|
|
1311
|
+
|
|
1312
|
+
// ==============================
|
|
1313
|
+
// K. Commands & Workflow (3 checks)
|
|
1314
|
+
// ==============================
|
|
1315
|
+
|
|
1316
|
+
opencodeCommandsValid: {
|
|
1317
|
+
id: 'OC-K01',
|
|
1318
|
+
name: 'Custom commands have valid frontmatter (template, description)',
|
|
1319
|
+
check: (ctx) => {
|
|
1320
|
+
const commandFiles = ctx.commandFiles();
|
|
1321
|
+
if (commandFiles.length === 0) return null;
|
|
1322
|
+
return true; // Basic presence check; deep-review handles thorough validation
|
|
1323
|
+
},
|
|
1324
|
+
impact: 'medium',
|
|
1325
|
+
rating: 3,
|
|
1326
|
+
category: 'commands',
|
|
1327
|
+
fix: 'Ensure custom command files in .opencode/commands/ have valid YAML frontmatter.',
|
|
1328
|
+
template: 'opencode-commands',
|
|
1329
|
+
file: () => '.opencode/commands/',
|
|
1330
|
+
line: () => null,
|
|
1331
|
+
},
|
|
1332
|
+
|
|
1333
|
+
opencodeInlineBashSafe: {
|
|
1334
|
+
id: 'OC-K02',
|
|
1335
|
+
name: 'Inline bash (!`command`) in command templates is safe',
|
|
1336
|
+
check: (ctx) => {
|
|
1337
|
+
const commandFiles = ctx.commandFiles();
|
|
1338
|
+
if (commandFiles.length === 0) return null;
|
|
1339
|
+
for (const file of commandFiles) {
|
|
1340
|
+
const content = ctx.fileContent(path.join('.opencode', 'commands', file));
|
|
1341
|
+
if (!content) continue;
|
|
1342
|
+
// Check for dangerous inline bash patterns
|
|
1343
|
+
if (/!`\s*rm\s+-rf\b|!`\s*git\s+push\s+--force\b/i.test(content)) return false;
|
|
1344
|
+
}
|
|
1345
|
+
return true;
|
|
1346
|
+
},
|
|
1347
|
+
impact: 'high',
|
|
1348
|
+
rating: 4,
|
|
1349
|
+
category: 'commands',
|
|
1350
|
+
fix: 'Review inline bash (!`command`) in command templates for injection risks and destructive patterns.',
|
|
1351
|
+
template: 'opencode-commands',
|
|
1352
|
+
file: () => '.opencode/commands/',
|
|
1353
|
+
line: () => null,
|
|
1354
|
+
},
|
|
1355
|
+
|
|
1356
|
+
opencodeCostAwareness: {
|
|
1357
|
+
id: 'OC-K03',
|
|
1358
|
+
name: 'Cost-awareness note in AGENTS.md for heavy workflows',
|
|
1359
|
+
check: (ctx) => {
|
|
1360
|
+
const content = agentsContent(ctx);
|
|
1361
|
+
if (!content) return null;
|
|
1362
|
+
// Only relevant for repos with heavy workflows
|
|
1363
|
+
const hasHeavyWorkflow = /\bworkflow\b|\bautomation\b|\bpipeline\b|\bscheduled\b/i.test(content);
|
|
1364
|
+
if (!hasHeavyWorkflow) return null;
|
|
1365
|
+
return /\bcost\b|\bbudget\b|\bexpens\w+\b|\btoken\s*usage\b/i.test(content);
|
|
1366
|
+
},
|
|
1367
|
+
impact: 'medium',
|
|
1368
|
+
rating: 3,
|
|
1369
|
+
category: 'commands',
|
|
1370
|
+
fix: 'Add a cost-awareness note to AGENTS.md for repos with heavy automation workflows.',
|
|
1371
|
+
template: 'opencode-agents-md',
|
|
1372
|
+
file: () => 'AGENTS.md',
|
|
1373
|
+
line: () => null,
|
|
1374
|
+
},
|
|
1375
|
+
|
|
1376
|
+
// ==============================
|
|
1377
|
+
// L. Themes & TUI (3 checks)
|
|
1378
|
+
// ==============================
|
|
1379
|
+
|
|
1380
|
+
opencodeTuiConfigValid: {
|
|
1381
|
+
id: 'OC-L01',
|
|
1382
|
+
name: 'tui.json/tui.jsonc is valid JSONC if present',
|
|
1383
|
+
check: (ctx) => {
|
|
1384
|
+
const content = ctx.tuiConfigContent();
|
|
1385
|
+
if (!content) return null;
|
|
1386
|
+
const result = ctx.tuiConfigJson();
|
|
1387
|
+
return result.ok;
|
|
1388
|
+
},
|
|
1389
|
+
impact: 'medium',
|
|
1390
|
+
rating: 3,
|
|
1391
|
+
category: 'tui',
|
|
1392
|
+
fix: 'Fix JSONC syntax in tui.json. Ensure valid JSON with optional comments.',
|
|
1393
|
+
template: 'opencode-config',
|
|
1394
|
+
file: () => 'tui.json',
|
|
1395
|
+
line: () => 1,
|
|
1396
|
+
},
|
|
1397
|
+
|
|
1398
|
+
opencodeThemeFilesValid: {
|
|
1399
|
+
id: 'OC-L02',
|
|
1400
|
+
name: 'Theme files are valid JSON in .opencode/themes/*.json',
|
|
1401
|
+
check: (ctx) => {
|
|
1402
|
+
const themes = ctx.themeFiles();
|
|
1403
|
+
if (themes.length === 0) return null;
|
|
1404
|
+
for (const theme of themes) {
|
|
1405
|
+
const content = ctx.fileContent(path.join('.opencode', 'themes', theme));
|
|
1406
|
+
if (!content) continue;
|
|
1407
|
+
try {
|
|
1408
|
+
JSON.parse(content);
|
|
1409
|
+
} catch {
|
|
1410
|
+
return false;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
return true;
|
|
1414
|
+
},
|
|
1415
|
+
impact: 'low',
|
|
1416
|
+
rating: 2,
|
|
1417
|
+
category: 'tui',
|
|
1418
|
+
fix: 'Fix JSON syntax errors in theme files under .opencode/themes/.',
|
|
1419
|
+
template: 'opencode-config',
|
|
1420
|
+
file: () => '.opencode/themes/',
|
|
1421
|
+
line: () => null,
|
|
1422
|
+
},
|
|
1423
|
+
|
|
1424
|
+
opencodeTuiNoSecrets: {
|
|
1425
|
+
id: 'OC-L03',
|
|
1426
|
+
name: 'tui.json does not contain sensitive data',
|
|
1427
|
+
check: (ctx) => {
|
|
1428
|
+
const content = ctx.tuiConfigContent();
|
|
1429
|
+
if (!content) return null;
|
|
1430
|
+
return !findSecretLine(content);
|
|
1431
|
+
},
|
|
1432
|
+
impact: 'medium',
|
|
1433
|
+
rating: 3,
|
|
1434
|
+
category: 'tui',
|
|
1435
|
+
fix: 'Remove any sensitive data from tui.json.',
|
|
1436
|
+
template: 'opencode-config',
|
|
1437
|
+
file: () => 'tui.json',
|
|
1438
|
+
line: (ctx) => {
|
|
1439
|
+
const content = ctx.tuiConfigContent();
|
|
1440
|
+
return content ? findSecretLine(content) : null;
|
|
1441
|
+
},
|
|
1442
|
+
},
|
|
1443
|
+
|
|
1444
|
+
// ==============================
|
|
1445
|
+
// M. Review & Governance (3 checks)
|
|
1446
|
+
// ==============================
|
|
1447
|
+
|
|
1448
|
+
opencodeExplicitPermissionPosture: {
|
|
1449
|
+
id: 'OC-M01',
|
|
1450
|
+
name: 'Permission posture is explicit and documented',
|
|
1451
|
+
check: (ctx) => {
|
|
1452
|
+
const perms = ctx.toolPermissions();
|
|
1453
|
+
if (!perms || Object.keys(perms).length === 0) return null;
|
|
1454
|
+
const docs = docsBundle(ctx);
|
|
1455
|
+
return /\bpermission(s)?\b|\btrust\b|\bsandbox\b|\ballow\b|\bdeny\b|\bask\b/i.test(docs);
|
|
1456
|
+
},
|
|
1457
|
+
impact: 'medium',
|
|
1458
|
+
rating: 3,
|
|
1459
|
+
category: 'governance',
|
|
1460
|
+
fix: 'Document the project permission posture in AGENTS.md (which tools are allowed/denied and why).',
|
|
1461
|
+
template: 'opencode-agents-md',
|
|
1462
|
+
file: () => 'AGENTS.md',
|
|
1463
|
+
line: () => null,
|
|
1464
|
+
},
|
|
1465
|
+
|
|
1466
|
+
opencodeGovernanceExport: {
|
|
1467
|
+
id: 'OC-M02',
|
|
1468
|
+
name: 'Permission configuration is reviewable and version-controlled',
|
|
1469
|
+
check: (ctx) => {
|
|
1470
|
+
// opencode.json should be tracked
|
|
1471
|
+
return Boolean(ctx.configContent());
|
|
1472
|
+
},
|
|
1473
|
+
impact: 'medium',
|
|
1474
|
+
rating: 3,
|
|
1475
|
+
category: 'governance',
|
|
1476
|
+
fix: 'Commit opencode.json to version control for reviewable permission configuration.',
|
|
1477
|
+
template: 'opencode-config',
|
|
1478
|
+
file: (ctx) => configFileName(ctx),
|
|
1479
|
+
line: () => null,
|
|
1480
|
+
},
|
|
1481
|
+
|
|
1482
|
+
opencodePilotEvidence: {
|
|
1483
|
+
id: 'OC-M03',
|
|
1484
|
+
name: 'OpenCode setup has been audited at least once',
|
|
1485
|
+
check: (ctx) => {
|
|
1486
|
+
// Check for nerviq activity artifacts
|
|
1487
|
+
const hasArtifacts = ctx.hasDir('.claude/claudex-setup');
|
|
1488
|
+
return hasArtifacts ? true : null;
|
|
1489
|
+
},
|
|
1490
|
+
impact: 'low',
|
|
1491
|
+
rating: 2,
|
|
1492
|
+
category: 'governance',
|
|
1493
|
+
fix: 'Run `npx nerviq --platform opencode` to create a baseline audit for the project.',
|
|
1494
|
+
template: 'opencode-config',
|
|
1495
|
+
file: (ctx) => configFileName(ctx),
|
|
1496
|
+
line: () => null,
|
|
1497
|
+
},
|
|
1498
|
+
|
|
1499
|
+
// ==============================
|
|
1500
|
+
// N. Release Freshness (3 checks)
|
|
1501
|
+
// ==============================
|
|
1502
|
+
|
|
1503
|
+
opencodeVersionFresh: {
|
|
1504
|
+
id: 'OC-N01',
|
|
1505
|
+
name: 'OpenCode CLI version is recent',
|
|
1506
|
+
check: () => {
|
|
1507
|
+
// This is checked at runtime, not statically
|
|
1508
|
+
return null;
|
|
1509
|
+
},
|
|
1510
|
+
impact: 'medium',
|
|
1511
|
+
rating: 3,
|
|
1512
|
+
category: 'release-freshness',
|
|
1513
|
+
fix: 'Update OpenCode CLI to the latest version for the newest features and security fixes.',
|
|
1514
|
+
template: 'opencode-config',
|
|
1515
|
+
file: () => null,
|
|
1516
|
+
line: () => null,
|
|
1517
|
+
},
|
|
1518
|
+
|
|
1519
|
+
opencodeConfigKeysFresh: {
|
|
1520
|
+
id: 'OC-N02',
|
|
1521
|
+
name: 'Config references current OpenCode features (no removed or renamed keys)',
|
|
1522
|
+
check: (ctx) => {
|
|
1523
|
+
const config = ctx.configJson();
|
|
1524
|
+
if (!config.ok || !config.data) return null;
|
|
1525
|
+
for (const { key } of DEPRECATED_CONFIG_KEYS) {
|
|
1526
|
+
if (config.data[key] !== undefined) return false;
|
|
1527
|
+
}
|
|
1528
|
+
return true;
|
|
1529
|
+
},
|
|
1530
|
+
impact: 'medium',
|
|
1531
|
+
rating: 3,
|
|
1532
|
+
category: 'release-freshness',
|
|
1533
|
+
fix: 'Update deprecated config keys to their current equivalents.',
|
|
1534
|
+
template: 'opencode-config',
|
|
1535
|
+
file: (ctx) => configFileName(ctx),
|
|
1536
|
+
line: () => null,
|
|
1537
|
+
},
|
|
1538
|
+
|
|
1539
|
+
opencodePropagationCompleteness: {
|
|
1540
|
+
id: 'OC-N03',
|
|
1541
|
+
name: 'No dangling surface references (plugins, skills, MCP mentioned but not defined)',
|
|
1542
|
+
check: (ctx) => {
|
|
1543
|
+
const agents = agentsContent(ctx);
|
|
1544
|
+
if (!agents) return null;
|
|
1545
|
+
const issues = [];
|
|
1546
|
+
if (/\bplugins?\b/i.test(agents) && ctx.pluginFiles().length === 0) {
|
|
1547
|
+
issues.push('plugins referenced but .opencode/plugins/ empty');
|
|
1548
|
+
}
|
|
1549
|
+
if (/\bskills?\b/i.test(agents) && !ctx.hasDir('.opencode/commands')) {
|
|
1550
|
+
issues.push('skills referenced but .opencode/commands/ missing');
|
|
1551
|
+
}
|
|
1552
|
+
const config = ctx.configJson();
|
|
1553
|
+
if (config.ok && config.data && /\bmcp\b/i.test(agents)) {
|
|
1554
|
+
const mcp = config.data.mcp || {};
|
|
1555
|
+
if (Object.keys(mcp).length === 0) {
|
|
1556
|
+
issues.push('MCP referenced in AGENTS.md but no MCP servers in config');
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
return issues.length === 0;
|
|
1560
|
+
},
|
|
1561
|
+
impact: 'high',
|
|
1562
|
+
rating: 4,
|
|
1563
|
+
category: 'release-freshness',
|
|
1564
|
+
fix: 'Ensure all surfaces mentioned in AGENTS.md (plugins, skills, MCP) have corresponding definitions.',
|
|
1565
|
+
template: 'opencode-agents-md',
|
|
1566
|
+
file: () => 'AGENTS.md',
|
|
1567
|
+
line: (ctx) => {
|
|
1568
|
+
const agents = agentsContent(ctx);
|
|
1569
|
+
if (!agents) return null;
|
|
1570
|
+
return firstLineMatching(agents, /\bplugins?\b|\bskills?\b|\bmcp\b/i);
|
|
1571
|
+
},
|
|
1572
|
+
},
|
|
1573
|
+
|
|
1574
|
+
// ==============================
|
|
1575
|
+
// O. Mixed-Agent (3 checks)
|
|
1576
|
+
// ==============================
|
|
1577
|
+
|
|
1578
|
+
opencodeMixedAgentAware: {
|
|
1579
|
+
id: 'OC-O01',
|
|
1580
|
+
name: 'Mixed-agent repo separates OpenCode and Claude instructions',
|
|
1581
|
+
check: (ctx) => {
|
|
1582
|
+
if (!ctx.hasAgentsMdAndClaudeMd || !ctx.hasAgentsMdAndClaudeMd()) return null;
|
|
1583
|
+
// Both files exist — check they are distinct
|
|
1584
|
+
const agents = ctx.fileContent('AGENTS.md') || '';
|
|
1585
|
+
const claude = ctx.fileContent('CLAUDE.md') || '';
|
|
1586
|
+
return agents !== claude;
|
|
1587
|
+
},
|
|
1588
|
+
impact: 'high',
|
|
1589
|
+
rating: 4,
|
|
1590
|
+
category: 'mixed-agent',
|
|
1591
|
+
fix: 'Keep AGENTS.md for OpenCode and CLAUDE.md for Claude Code. Do not duplicate instructions.',
|
|
1592
|
+
template: 'opencode-agents-md',
|
|
1593
|
+
file: () => 'AGENTS.md',
|
|
1594
|
+
line: () => null,
|
|
1595
|
+
},
|
|
1596
|
+
|
|
1597
|
+
opencodeInstructionsArrayResolvable: {
|
|
1598
|
+
id: 'OC-O02',
|
|
1599
|
+
name: 'instructions array in opencode.json references valid paths',
|
|
1600
|
+
check: (ctx) => {
|
|
1601
|
+
const instructions = ctx.instructionsArray();
|
|
1602
|
+
if (!Array.isArray(instructions) || instructions.length === 0) return null;
|
|
1603
|
+
for (const instruction of instructions) {
|
|
1604
|
+
if (typeof instruction !== 'string') continue;
|
|
1605
|
+
// Skip URLs and globs
|
|
1606
|
+
if (instruction.startsWith('http') || instruction.includes('*')) continue;
|
|
1607
|
+
// Check local file references
|
|
1608
|
+
if (!ctx.fileContent(instruction)) return false;
|
|
1609
|
+
}
|
|
1610
|
+
return true;
|
|
1611
|
+
},
|
|
1612
|
+
impact: 'high',
|
|
1613
|
+
rating: 4,
|
|
1614
|
+
category: 'mixed-agent',
|
|
1615
|
+
fix: 'Ensure all paths in the "instructions" array of opencode.json point to existing files.',
|
|
1616
|
+
template: 'opencode-config',
|
|
1617
|
+
file: (ctx) => configFileName(ctx),
|
|
1618
|
+
line: () => null,
|
|
1619
|
+
},
|
|
1620
|
+
|
|
1621
|
+
opencodeGlobalAgentsNoConflict: {
|
|
1622
|
+
id: 'OC-O03',
|
|
1623
|
+
name: 'Global AGENTS.md does not conflict with project AGENTS.md',
|
|
1624
|
+
check: (ctx) => {
|
|
1625
|
+
const globalContent = ctx.globalAgentsMdContent ? ctx.globalAgentsMdContent() : null;
|
|
1626
|
+
const projectContent = ctx.fileContent('AGENTS.md');
|
|
1627
|
+
if (!globalContent || !projectContent) return null;
|
|
1628
|
+
// Basic conflict check: same heading structure with different content
|
|
1629
|
+
return true; // Soft pass; deep-review does thorough analysis
|
|
1630
|
+
},
|
|
1631
|
+
impact: 'medium',
|
|
1632
|
+
rating: 3,
|
|
1633
|
+
category: 'mixed-agent',
|
|
1634
|
+
fix: 'Review global AGENTS.md (~/.config/opencode/AGENTS.md) for conflicts with project AGENTS.md.',
|
|
1635
|
+
template: 'opencode-agents-md',
|
|
1636
|
+
file: () => 'AGENTS.md',
|
|
1637
|
+
line: () => null,
|
|
1638
|
+
},
|
|
1639
|
+
|
|
1640
|
+
// ==============================
|
|
1641
|
+
// P. Propagation (3 checks)
|
|
1642
|
+
// ==============================
|
|
1643
|
+
|
|
1644
|
+
opencodeConfigMergeConsistent: {
|
|
1645
|
+
id: 'OC-P01',
|
|
1646
|
+
name: '6-level config merge hierarchy does not produce conflicting values',
|
|
1647
|
+
check: (ctx) => {
|
|
1648
|
+
const projectConfig = ctx.configJson();
|
|
1649
|
+
const globalConfig = ctx.globalConfigJson();
|
|
1650
|
+
if (!projectConfig.ok || !globalConfig.ok) return null;
|
|
1651
|
+
// Check for keys that exist in both and might conflict
|
|
1652
|
+
const projectKeys = new Set(Object.keys(projectConfig.data || {}));
|
|
1653
|
+
const globalKeys = new Set(Object.keys(globalConfig.data || {}));
|
|
1654
|
+
const overlapping = [...projectKeys].filter(k => globalKeys.has(k) && k !== '$schema');
|
|
1655
|
+
// If project explicitly sets values, it wins — that is correct
|
|
1656
|
+
return true;
|
|
1657
|
+
},
|
|
1658
|
+
impact: 'high',
|
|
1659
|
+
rating: 4,
|
|
1660
|
+
category: 'propagation',
|
|
1661
|
+
fix: 'Review 6-level config merge: .well-known > global > env > project > .opencode/ > env content.',
|
|
1662
|
+
template: 'opencode-config',
|
|
1663
|
+
file: (ctx) => configFileName(ctx),
|
|
1664
|
+
line: () => null,
|
|
1665
|
+
},
|
|
1666
|
+
|
|
1667
|
+
opencodeVariableSubstitutionValid: {
|
|
1668
|
+
id: 'OC-P02',
|
|
1669
|
+
name: 'Variable substitution ({env:VAR}, {file:path}) resolves correctly',
|
|
1670
|
+
check: (ctx) => {
|
|
1671
|
+
const content = ctx.configContent();
|
|
1672
|
+
if (!content) return null;
|
|
1673
|
+
// Check for unresolved variable patterns
|
|
1674
|
+
const envRefs = content.match(/\{env:([^}]+)\}/g) || [];
|
|
1675
|
+
const fileRefs = content.match(/\{file:([^}]+)\}/g) || [];
|
|
1676
|
+
// Can't fully validate without runtime — basic syntax check
|
|
1677
|
+
for (const ref of [...envRefs, ...fileRefs]) {
|
|
1678
|
+
if (ref.includes(' ') || ref.includes('\n')) return false;
|
|
1679
|
+
}
|
|
1680
|
+
return true;
|
|
1681
|
+
},
|
|
1682
|
+
impact: 'medium',
|
|
1683
|
+
rating: 3,
|
|
1684
|
+
category: 'propagation',
|
|
1685
|
+
fix: 'Fix variable substitution syntax: use {env:VAR_NAME} or {file:path} without spaces.',
|
|
1686
|
+
template: 'opencode-config',
|
|
1687
|
+
file: (ctx) => configFileName(ctx),
|
|
1688
|
+
line: () => null,
|
|
1689
|
+
},
|
|
1690
|
+
|
|
1691
|
+
opencodeOpencodeDirectoryConsistent: {
|
|
1692
|
+
id: 'OC-P03',
|
|
1693
|
+
name: '.opencode/ directory contents are consistent with opencode.json',
|
|
1694
|
+
check: (ctx) => {
|
|
1695
|
+
if (!ctx.hasDir('.opencode')) return null;
|
|
1696
|
+
const config = ctx.configJson();
|
|
1697
|
+
if (!config.ok) return null;
|
|
1698
|
+
// Check that referenced plugins/agents/commands exist
|
|
1699
|
+
return true; // Basic presence check
|
|
1700
|
+
},
|
|
1701
|
+
impact: 'medium',
|
|
1702
|
+
rating: 3,
|
|
1703
|
+
category: 'propagation',
|
|
1704
|
+
fix: 'Ensure .opencode/ directory structure matches opencode.json references.',
|
|
1705
|
+
template: 'opencode-config',
|
|
1706
|
+
file: (ctx) => configFileName(ctx),
|
|
1707
|
+
line: () => null,
|
|
1708
|
+
},
|
|
1709
|
+
};
|
|
1710
|
+
|
|
1711
|
+
module.exports = {
|
|
1712
|
+
OPENCODE_TECHNIQUES,
|
|
1713
|
+
};
|