@nerviq/cli 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +216 -124
- package/bin/cli.js +620 -183
- package/package.json +3 -2
- package/src/activity.js +49 -9
- package/src/adoption-advisor.js +299 -0
- package/src/aider/freshness.js +65 -20
- package/src/aider/techniques.js +16 -11
- package/src/analyze.js +128 -0
- package/src/anti-patterns.js +13 -0
- package/src/audit/instruction-files.js +180 -0
- package/src/audit/recommendations.js +531 -0
- package/src/audit.js +53 -681
- package/src/behavioral-drift.js +801 -0
- package/src/codex/freshness.js +84 -25
- package/src/continuous-ops.js +681 -0
- package/src/copilot/freshness.js +57 -20
- package/src/cost-tracking.js +61 -0
- package/src/cursor/freshness.js +65 -20
- package/src/cursor/techniques.js +17 -12
- package/src/deep-review.js +83 -0
- package/src/diff-only.js +280 -0
- package/src/doctor.js +118 -55
- package/src/freshness.js +74 -21
- package/src/gemini/freshness.js +66 -21
- package/src/governance.js +59 -43
- package/src/hook-validation.js +342 -0
- package/src/index.js +5 -0
- package/src/integrations.js +42 -5
- package/src/mcp-server.js +95 -59
- package/src/mcp-validation.js +337 -0
- package/src/opencode/freshness.js +66 -21
- package/src/opencode/techniques.js +12 -7
- package/src/operating-profile.js +574 -0
- package/src/org.js +97 -13
- package/src/plans.js +192 -8
- package/src/platform-change-manifest.js +86 -0
- package/src/policy-layers.js +210 -0
- package/src/profiles.js +4 -1
- package/src/prompt-injection.js +74 -0
- package/src/repo-archetype.js +386 -0
- package/src/setup/analysis.js +619 -0
- package/src/setup/runtime.js +172 -0
- package/src/setup.js +62 -748
- package/src/source-urls.js +132 -132
- package/src/supplemental-checks.js +13 -12
- package/src/techniques/api.js +407 -0
- package/src/techniques/automation.js +316 -0
- package/src/techniques/compliance.js +257 -0
- package/src/techniques/hygiene.js +294 -0
- package/src/techniques/instructions.js +243 -0
- package/src/techniques/observability.js +226 -0
- package/src/techniques/optimization.js +142 -0
- package/src/techniques/quality.js +317 -0
- package/src/techniques/security.js +237 -0
- package/src/techniques/shared.js +443 -0
- package/src/techniques/stacks.js +2294 -0
- package/src/techniques/tools.js +106 -0
- package/src/techniques/workflow.js +413 -0
- package/src/techniques.js +78 -5607
- package/src/watch.js +18 -0
- package/src/windsurf/freshness.js +36 -21
- package/src/windsurf/techniques.js +17 -12
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
const { ProjectContext } = require('./context');
|
|
8
|
+
const { CodexProjectContext } = require('./codex/context');
|
|
9
|
+
const { GeminiProjectContext } = require('./gemini/context');
|
|
10
|
+
const { CopilotProjectContext } = require('./copilot/context');
|
|
11
|
+
const { CursorProjectContext } = require('./cursor/context');
|
|
12
|
+
const { WindsurfProjectContext } = require('./windsurf/context');
|
|
13
|
+
const { OpenCodeProjectContext } = require('./opencode/context');
|
|
14
|
+
|
|
15
|
+
function readJsonFile(filePath) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeSourcePath(filePath) {
|
|
24
|
+
if (!filePath) return null;
|
|
25
|
+
const homeDir = os.homedir();
|
|
26
|
+
if (filePath.startsWith(homeDir)) {
|
|
27
|
+
return filePath.replace(homeDir, '~');
|
|
28
|
+
}
|
|
29
|
+
return filePath;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeServerConfig(rawConfig) {
|
|
33
|
+
const raw = rawConfig && typeof rawConfig === 'object' ? rawConfig : {};
|
|
34
|
+
let command = raw.command || null;
|
|
35
|
+
let args = Array.isArray(raw.args) ? raw.args.map((item) => `${item}`) : [];
|
|
36
|
+
|
|
37
|
+
if (Array.isArray(command)) {
|
|
38
|
+
const commandParts = command.map((item) => `${item}`);
|
|
39
|
+
command = commandParts[0] || null;
|
|
40
|
+
args = [...commandParts.slice(1), ...args];
|
|
41
|
+
} else if (typeof command !== 'string') {
|
|
42
|
+
command = null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const env = raw.env && typeof raw.env === 'object'
|
|
46
|
+
? raw.env
|
|
47
|
+
: raw.environment && typeof raw.environment === 'object'
|
|
48
|
+
? raw.environment
|
|
49
|
+
: {};
|
|
50
|
+
|
|
51
|
+
const urlCandidates = [raw.url, raw.endpoint, raw.serverUrl, raw.sseUrl, raw.httpUrl];
|
|
52
|
+
const url = urlCandidates.find((value) => typeof value === 'string' && value.trim()) || null;
|
|
53
|
+
const transport = typeof raw.transport === 'string'
|
|
54
|
+
? raw.transport.toLowerCase()
|
|
55
|
+
: (url ? 'remote' : 'stdio');
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
command: typeof command === 'string' ? command.trim() : null,
|
|
59
|
+
args,
|
|
60
|
+
env,
|
|
61
|
+
url: typeof url === 'string' ? url.trim() : null,
|
|
62
|
+
transport,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function extractEnvReferences(config) {
|
|
67
|
+
const references = new Set();
|
|
68
|
+
const pattern = /\$\{(?:env:)?([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
69
|
+
|
|
70
|
+
const scan = (value) => {
|
|
71
|
+
if (typeof value !== 'string') return;
|
|
72
|
+
for (const match of value.matchAll(pattern)) {
|
|
73
|
+
references.add(match[1]);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
scan(config.command);
|
|
78
|
+
scan(config.url);
|
|
79
|
+
for (const arg of config.args || []) scan(arg);
|
|
80
|
+
for (const value of Object.values(config.env || {})) scan(value);
|
|
81
|
+
|
|
82
|
+
return [...references];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveExecutable(command, dir) {
|
|
86
|
+
if (!command) {
|
|
87
|
+
return { found: false, resolved: null };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const trimmed = command.trim();
|
|
91
|
+
const isPathLike = /^[A-Za-z]:[\\/]/.test(trimmed) ||
|
|
92
|
+
trimmed.startsWith('.') ||
|
|
93
|
+
trimmed.includes('/') ||
|
|
94
|
+
trimmed.includes('\\');
|
|
95
|
+
|
|
96
|
+
if (isPathLike) {
|
|
97
|
+
const resolved = path.isAbsolute(trimmed) ? trimmed : path.resolve(dir, trimmed);
|
|
98
|
+
return { found: fs.existsSync(resolved), resolved };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const lookup = process.platform === 'win32'
|
|
102
|
+
? spawnSync('where.exe', [trimmed], { encoding: 'utf8' })
|
|
103
|
+
: spawnSync('which', [trimmed], { encoding: 'utf8' });
|
|
104
|
+
const output = `${lookup.stdout || ''}`.trim().split(/\r?\n/).filter(Boolean)[0] || null;
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
found: lookup.status === 0 && Boolean(output),
|
|
108
|
+
resolved: output,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function probeRemoteUrl(targetUrl) {
|
|
113
|
+
try {
|
|
114
|
+
const parsed = new URL(targetUrl);
|
|
115
|
+
if (!/^https?:$/.test(parsed.protocol)) {
|
|
116
|
+
return { status: 'fail', detail: `unsupported protocol ${parsed.protocol}` };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const controller = new AbortController();
|
|
120
|
+
const timer = setTimeout(() => controller.abort(), 2000);
|
|
121
|
+
let response;
|
|
122
|
+
try {
|
|
123
|
+
response = await fetch(targetUrl, { method: 'GET', signal: controller.signal });
|
|
124
|
+
} finally {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (response.status >= 500) {
|
|
129
|
+
return { status: 'warn', detail: `HTTP ${response.status} ${response.statusText}` };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { status: 'pass', detail: `HTTP ${response.status} ${response.statusText}` };
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error && error.name === 'AbortError') {
|
|
135
|
+
return { status: 'warn', detail: 'timed out after 2000ms' };
|
|
136
|
+
}
|
|
137
|
+
return { status: 'warn', detail: error && error.message ? error.message : 'request failed' };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function pushDeclarations(target, platform, scope, source, servers, note = null) {
|
|
142
|
+
if (!servers || typeof servers !== 'object') return;
|
|
143
|
+
|
|
144
|
+
for (const [serverName, rawConfig] of Object.entries(servers)) {
|
|
145
|
+
target.push({
|
|
146
|
+
platform,
|
|
147
|
+
scope,
|
|
148
|
+
source: normalizeSourcePath(source),
|
|
149
|
+
serverName,
|
|
150
|
+
note,
|
|
151
|
+
config: normalizeServerConfig(rawConfig),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function collectDeclaredMcpServers(dir, detectedPlatforms = []) {
|
|
157
|
+
const declarations = [];
|
|
158
|
+
const platformSet = new Set(detectedPlatforms);
|
|
159
|
+
|
|
160
|
+
if (platformSet.has('claude')) {
|
|
161
|
+
const claudeCtx = new ProjectContext(dir);
|
|
162
|
+
const projectConfig = claudeCtx.jsonFile('.mcp.json');
|
|
163
|
+
if (projectConfig && projectConfig.mcpServers) {
|
|
164
|
+
pushDeclarations(declarations, 'claude', 'project', '.mcp.json', projectConfig.mcpServers);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const globalConfigPath = path.join(os.homedir(), '.claude.json');
|
|
168
|
+
const globalConfig = readJsonFile(globalConfigPath);
|
|
169
|
+
if (globalConfig && globalConfig.mcpServers) {
|
|
170
|
+
pushDeclarations(declarations, 'claude', 'global', globalConfigPath, globalConfig.mcpServers);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (platformSet.has('codex')) {
|
|
175
|
+
const ctx = new CodexProjectContext(dir);
|
|
176
|
+
const projectConfig = ctx.configToml();
|
|
177
|
+
if (projectConfig.ok && projectConfig.data && projectConfig.data.mcp_servers) {
|
|
178
|
+
pushDeclarations(declarations, 'codex', 'project', projectConfig.source, projectConfig.data.mcp_servers);
|
|
179
|
+
}
|
|
180
|
+
const globalConfig = ctx.globalConfigToml();
|
|
181
|
+
if (globalConfig.ok && globalConfig.data && globalConfig.data.mcp_servers) {
|
|
182
|
+
pushDeclarations(declarations, 'codex', 'global', globalConfig.source, globalConfig.data.mcp_servers);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (platformSet.has('gemini')) {
|
|
187
|
+
const ctx = new GeminiProjectContext(dir);
|
|
188
|
+
const projectConfig = ctx.settingsJson();
|
|
189
|
+
if (projectConfig.ok && projectConfig.data && projectConfig.data.mcpServers) {
|
|
190
|
+
pushDeclarations(declarations, 'gemini', 'project', projectConfig.source, projectConfig.data.mcpServers);
|
|
191
|
+
}
|
|
192
|
+
const globalConfig = ctx.globalSettingsJson();
|
|
193
|
+
if (globalConfig.ok && globalConfig.data && globalConfig.data.mcpServers) {
|
|
194
|
+
pushDeclarations(declarations, 'gemini', 'global', globalConfig.source, globalConfig.data.mcpServers);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (platformSet.has('copilot')) {
|
|
199
|
+
const ctx = new CopilotProjectContext(dir);
|
|
200
|
+
const projectConfig = ctx.mcpConfig();
|
|
201
|
+
if (projectConfig.ok && projectConfig.data) {
|
|
202
|
+
pushDeclarations(
|
|
203
|
+
declarations,
|
|
204
|
+
'copilot',
|
|
205
|
+
'project',
|
|
206
|
+
projectConfig.source,
|
|
207
|
+
projectConfig.data.servers || projectConfig.data.mcpServers || {}
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (platformSet.has('cursor')) {
|
|
213
|
+
const ctx = new CursorProjectContext(dir);
|
|
214
|
+
const projectConfig = ctx.mcpConfig();
|
|
215
|
+
if (projectConfig.ok && projectConfig.data) {
|
|
216
|
+
pushDeclarations(declarations, 'cursor', 'project', projectConfig.source, projectConfig.data.mcpServers || {});
|
|
217
|
+
}
|
|
218
|
+
const globalConfig = ctx.globalMcpConfig();
|
|
219
|
+
if (globalConfig.ok && globalConfig.data) {
|
|
220
|
+
pushDeclarations(declarations, 'cursor', 'global', globalConfig.source, globalConfig.data.mcpServers || {});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (platformSet.has('windsurf')) {
|
|
225
|
+
const ctx = new WindsurfProjectContext(dir);
|
|
226
|
+
const projectConfig = ctx.mcpConfig();
|
|
227
|
+
if (projectConfig.ok && projectConfig.data) {
|
|
228
|
+
pushDeclarations(
|
|
229
|
+
declarations,
|
|
230
|
+
'windsurf',
|
|
231
|
+
'project',
|
|
232
|
+
projectConfig.source,
|
|
233
|
+
projectConfig.data.mcpServers || {},
|
|
234
|
+
'Current Windsurf runtime may still rely on global MCP config.'
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
const globalConfig = ctx.globalMcpConfig();
|
|
238
|
+
if (globalConfig.ok && globalConfig.data) {
|
|
239
|
+
pushDeclarations(declarations, 'windsurf', 'global', globalConfig.source, globalConfig.data.mcpServers || {});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (platformSet.has('opencode')) {
|
|
244
|
+
const ctx = new OpenCodeProjectContext(dir);
|
|
245
|
+
const projectConfig = ctx.configJson();
|
|
246
|
+
if (projectConfig.ok && projectConfig.data && projectConfig.data.mcp) {
|
|
247
|
+
pushDeclarations(declarations, 'opencode', 'project', projectConfig.source, projectConfig.data.mcp);
|
|
248
|
+
}
|
|
249
|
+
const globalConfig = ctx.globalConfigJson();
|
|
250
|
+
if (globalConfig.ok && globalConfig.data && globalConfig.data.mcp) {
|
|
251
|
+
pushDeclarations(declarations, 'opencode', 'global', globalConfig.source, globalConfig.data.mcp);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return declarations;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function validateDeclaredMcpServers({ dir, detectedPlatforms = [] }) {
|
|
259
|
+
const declarations = collectDeclaredMcpServers(dir, detectedPlatforms);
|
|
260
|
+
const checks = [];
|
|
261
|
+
|
|
262
|
+
for (const declaration of declarations) {
|
|
263
|
+
const missingEnv = extractEnvReferences(declaration.config)
|
|
264
|
+
.filter((name) => !Object.prototype.hasOwnProperty.call(process.env, name));
|
|
265
|
+
const entry = {
|
|
266
|
+
platform: declaration.platform,
|
|
267
|
+
scope: declaration.scope,
|
|
268
|
+
source: declaration.source,
|
|
269
|
+
serverName: declaration.serverName,
|
|
270
|
+
transport: declaration.config.transport,
|
|
271
|
+
command: declaration.config.command,
|
|
272
|
+
args: declaration.config.args,
|
|
273
|
+
url: declaration.config.url,
|
|
274
|
+
missingEnv,
|
|
275
|
+
status: 'fail',
|
|
276
|
+
mode: 'unknown',
|
|
277
|
+
detail: '',
|
|
278
|
+
fix: null,
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
if (declaration.config.command) {
|
|
282
|
+
entry.mode = 'command';
|
|
283
|
+
const resolution = resolveExecutable(declaration.config.command, dir);
|
|
284
|
+
if (!resolution.found) {
|
|
285
|
+
entry.status = 'fail';
|
|
286
|
+
entry.detail = `Command '${declaration.config.command}' was not found in PATH or at the declared path.`;
|
|
287
|
+
entry.fix = 'Install the MCP server command or correct the configured command path.';
|
|
288
|
+
} else if (missingEnv.length > 0) {
|
|
289
|
+
entry.status = 'warn';
|
|
290
|
+
entry.detail = `Command '${declaration.config.command}' resolves to ${resolution.resolved}, but referenced env vars are missing: ${missingEnv.join(', ')}.`;
|
|
291
|
+
entry.fix = `Set the missing env vars before starting this MCP server: ${missingEnv.join(', ')}.`;
|
|
292
|
+
} else {
|
|
293
|
+
entry.status = 'pass';
|
|
294
|
+
entry.detail = `Command '${declaration.config.command}' resolves to ${resolution.resolved}.`;
|
|
295
|
+
}
|
|
296
|
+
} else if (declaration.config.url) {
|
|
297
|
+
entry.mode = 'remote';
|
|
298
|
+
const probe = await probeRemoteUrl(declaration.config.url);
|
|
299
|
+
entry.status = probe.status;
|
|
300
|
+
entry.detail = `URL ${declaration.config.url} ${probe.status === 'pass' ? 'responded' : 'was not confirmed'} (${probe.detail}).`;
|
|
301
|
+
entry.fix = probe.status === 'pass'
|
|
302
|
+
? null
|
|
303
|
+
: 'Start the remote MCP endpoint locally or update the configured URL.';
|
|
304
|
+
if (missingEnv.length > 0) {
|
|
305
|
+
entry.status = entry.status === 'fail' ? 'fail' : 'warn';
|
|
306
|
+
entry.detail += ` Missing env vars: ${missingEnv.join(', ')}.`;
|
|
307
|
+
if (!entry.fix) {
|
|
308
|
+
entry.fix = `Set the missing env vars before starting this MCP server: ${missingEnv.join(', ')}.`;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
entry.mode = 'unknown';
|
|
313
|
+
entry.status = 'fail';
|
|
314
|
+
entry.detail = 'Declared MCP server has neither a command nor a URL to validate.';
|
|
315
|
+
entry.fix = 'Add a valid command/args pair or a reachable URL-based transport definition.';
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (declaration.note) {
|
|
319
|
+
entry.detail += ` ${declaration.note}`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
checks.push(entry);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
checks,
|
|
327
|
+
declared: checks.length,
|
|
328
|
+
pass: checks.filter((item) => item.status === 'pass').length,
|
|
329
|
+
warn: checks.filter((item) => item.status === 'warn').length,
|
|
330
|
+
fail: checks.filter((item) => item.status === 'fail').length,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = {
|
|
335
|
+
collectDeclaredMcpServers,
|
|
336
|
+
validateDeclaredMcpServers,
|
|
337
|
+
};
|
|
@@ -29,18 +29,39 @@ const P0_SOURCES = [
|
|
|
29
29
|
stalenessThresholdDays: 14,
|
|
30
30
|
verifiedAt: '2026-04-07',
|
|
31
31
|
},
|
|
32
|
-
{
|
|
33
|
-
key: 'opencode-plugin-api',
|
|
34
|
-
label: 'OpenCode Plugin API',
|
|
35
|
-
url: 'https://opencode.ai/docs/plugins/',
|
|
36
|
-
stalenessThresholdDays: 30,
|
|
37
|
-
verifiedAt: '2026-04-07',
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
key: 'opencode-
|
|
41
|
-
label: 'OpenCode
|
|
42
|
-
url: 'https://opencode.ai/docs/
|
|
43
|
-
stalenessThresholdDays:
|
|
32
|
+
{
|
|
33
|
+
key: 'opencode-plugin-api',
|
|
34
|
+
label: 'OpenCode Plugin API',
|
|
35
|
+
url: 'https://opencode.ai/docs/plugins/',
|
|
36
|
+
stalenessThresholdDays: 30,
|
|
37
|
+
verifiedAt: '2026-04-07',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
key: 'opencode-agents-docs',
|
|
41
|
+
label: 'OpenCode Agents',
|
|
42
|
+
url: 'https://opencode.ai/docs/agents/',
|
|
43
|
+
stalenessThresholdDays: 14,
|
|
44
|
+
verifiedAt: '2026-04-10',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: 'opencode-models-docs',
|
|
48
|
+
label: 'OpenCode Models',
|
|
49
|
+
url: 'https://opencode.ai/docs/models',
|
|
50
|
+
stalenessThresholdDays: 14,
|
|
51
|
+
verifiedAt: '2026-04-10',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
key: 'opencode-github-docs',
|
|
55
|
+
label: 'OpenCode GitHub Integration',
|
|
56
|
+
url: 'https://opencode.ai/docs/github/',
|
|
57
|
+
stalenessThresholdDays: 14,
|
|
58
|
+
verifiedAt: '2026-04-10',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: 'opencode-permissions-docs',
|
|
62
|
+
label: 'OpenCode Permissions Documentation',
|
|
63
|
+
url: 'https://opencode.ai/docs/permissions/',
|
|
64
|
+
stalenessThresholdDays: 30,
|
|
44
65
|
verifiedAt: '2026-04-07',
|
|
45
66
|
},
|
|
46
67
|
];
|
|
@@ -78,15 +99,39 @@ const PROPAGATION_CHECKLIST = [
|
|
|
78
99
|
'src/opencode/techniques.js — update MCP checks',
|
|
79
100
|
],
|
|
80
101
|
},
|
|
81
|
-
{
|
|
82
|
-
trigger: 'Known security bug fixed or new bug reported',
|
|
83
|
-
targets: [
|
|
84
|
-
'src/opencode/techniques.js — update security checks (E02, E03, D05)',
|
|
85
|
-
'src/opencode/governance.js — update platformCaveats',
|
|
86
|
-
'src/opencode/freshness.js — verify against latest release',
|
|
87
|
-
],
|
|
88
|
-
},
|
|
89
|
-
|
|
102
|
+
{
|
|
103
|
+
trigger: 'Known security bug fixed or new bug reported',
|
|
104
|
+
targets: [
|
|
105
|
+
'src/opencode/techniques.js — update security checks (E02, E03, D05)',
|
|
106
|
+
'src/opencode/governance.js — update platformCaveats',
|
|
107
|
+
'src/opencode/freshness.js — verify against latest release',
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
trigger: 'OpenCode agent or subagent behavior change',
|
|
112
|
+
targets: [
|
|
113
|
+
'src/opencode/techniques.js — update agent and multi-session checks',
|
|
114
|
+
'src/opencode/governance.js — update permission guidance for plan/build/agent surfaces',
|
|
115
|
+
'src/source-urls.js — refresh OpenCode agent source mappings',
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
trigger: 'OpenCode model catalog or provider-option change',
|
|
120
|
+
targets: [
|
|
121
|
+
'src/opencode/techniques.js — update model-awareness and provider-option assumptions',
|
|
122
|
+
'src/opencode/setup.js — update starter config guidance for model selection',
|
|
123
|
+
'src/source-urls.js — refresh OpenCode model source mappings',
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
trigger: 'OpenCode GitHub integration or workflow contract change',
|
|
128
|
+
targets: [
|
|
129
|
+
'src/opencode/techniques.js — update GitHub/workflow checks',
|
|
130
|
+
'src/opencode/setup.js — update GitHub starter guidance',
|
|
131
|
+
'src/source-urls.js — refresh OpenCode GitHub source mappings',
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
];
|
|
90
135
|
|
|
91
136
|
function checkReleaseGate(sourceVerifications = {}) {
|
|
92
137
|
const now = new Date();
|
|
@@ -26,6 +26,7 @@ const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-
|
|
|
26
26
|
const { attachSourceUrls } = require('../source-urls');
|
|
27
27
|
const { buildStackChecks } = require('../stack-checks');
|
|
28
28
|
const { isApiProject, isDatabaseProject, isAuthProject, isMonitoringRelevant } = require('../supplemental-checks');
|
|
29
|
+
const { hasCostBudgetOrUsageTracking } = require('../cost-tracking');
|
|
29
30
|
const { resolveProjectStateReadPath } = require('../state-paths');
|
|
30
31
|
|
|
31
32
|
const DEFAULT_PROJECT_DOC_MAX_BYTES = 32768;
|
|
@@ -2016,13 +2017,17 @@ const OPENCODE_TECHNIQUES = {
|
|
|
2016
2017
|
fix: 'Document prompt caching in opencode.json or AGENTS.md.',
|
|
2017
2018
|
template: null, file: () => 'opencode.json', line: () => null,
|
|
2018
2019
|
},
|
|
2019
|
-
ocCostBudgetDefined: {
|
|
2020
|
-
id: 'OC-T48', name: 'AI cost budget or usage
|
|
2021
|
-
check: (ctx) => {
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2020
|
+
ocCostBudgetDefined: {
|
|
2021
|
+
id: 'OC-T48', name: 'AI cost budget or per-run usage tracking documented',
|
|
2022
|
+
check: (ctx) => {
|
|
2023
|
+
const docs = docsBundle(ctx) + (ctx.fileContent('README.md') || '');
|
|
2024
|
+
if (!docs.trim() && !hasCostBudgetOrUsageTracking('', ctx)) return null;
|
|
2025
|
+
return hasCostBudgetOrUsageTracking(docs, ctx);
|
|
2026
|
+
},
|
|
2027
|
+
impact: 'low', rating: 2, category: 'cost-optimization',
|
|
2028
|
+
fix: 'Document AI cost guardrails or per-run usage tracking so OpenCode usage is measurable over time.',
|
|
2029
|
+
template: null, file: () => 'README.md', line: () => null,
|
|
2030
|
+
},
|
|
2026
2031
|
|
|
2027
2032
|
// ============================================================
|
|
2028
2033
|
// === PYTHON STACK CHECKS (category: 'python') ===============
|