@nerviq/cli 0.9.2 → 0.9.4

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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * OpenCode Techniques — 68+ checks (OC-A01 through OC-P03)
2
+ * OpenCode Techniques — 73 checks (OC-A01 through OC-P03)
3
3
  *
4
4
  * Categories:
5
5
  * A. Instructions (7 checks)
@@ -23,6 +23,7 @@
23
23
  const os = require('os');
24
24
  const path = require('path');
25
25
  const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
26
+ const { attachSourceUrls } = require('../source-urls');
26
27
 
27
28
  const DEFAULT_PROJECT_DOC_MAX_BYTES = 32768;
28
29
 
@@ -62,18 +63,14 @@ const VALID_PLUGIN_EVENTS = new Set([
62
63
  'error', 'warning',
63
64
  ]);
64
65
 
65
- const DEPRECATED_CONFIG_KEYS = [
66
- { key: 'mode', replacement: 'agent', note: 'Use `agent` field instead of deprecated `mode`.' },
67
- ];
68
-
69
66
  // --- Helpers ---
70
67
 
71
68
  function agentsPath(ctx) {
72
- return ctx.agentsMdPath ? ctx.agentsMdPath() : null;
69
+ return ctx.fileContent('AGENTS.md') ? 'AGENTS.md' : null;
73
70
  }
74
71
 
75
72
  function agentsContent(ctx) {
76
- return ctx.agentsMdContent ? (ctx.agentsMdContent() || '') : '';
73
+ return ctx.fileContent('AGENTS.md') || '';
77
74
  }
78
75
 
79
76
  function configFileName(ctx) {
@@ -262,17 +259,16 @@ const OPENCODE_TECHNIQUES = {
262
259
 
263
260
  opencodeNoCoexistenceConflict: {
264
261
  id: 'OC-A05',
265
- name: 'No CLAUDE.md coexistence conflict (AGENTS.md wins if both exist in same dir)',
262
+ name: 'Mixed AGENTS.md + CLAUDE.md repos keep OpenCode guidance in AGENTS.md',
266
263
  check: (ctx) => {
267
264
  if (!ctx.hasAgentsMdAndClaudeMd || !ctx.hasAgentsMdAndClaudeMd()) return true;
268
- // Both exist: check that AGENTS.md is primary, warn about potential confusion
269
265
  const agentsMd = ctx.fileContent('AGENTS.md') || '';
270
266
  return agentsMd.length > 0;
271
267
  },
272
268
  impact: 'high',
273
269
  rating: 4,
274
270
  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.',
271
+ fix: 'Keep OpenCode instructions in `AGENTS.md` when both files exist. Current runtime evidence did not validate a clean `CLAUDE.md` fallback, so do not rely on `CLAUDE.md` as the primary OpenCode instruction surface.',
276
272
  template: 'opencode-agents-md',
277
273
  file: () => 'AGENTS.md',
278
274
  line: () => null,
@@ -291,7 +287,7 @@ const OPENCODE_TECHNIQUES = {
291
287
  category: 'instructions',
292
288
  fix: 'Remove generic filler ("Be helpful", "Write clean code") and replace with specific, actionable project instructions.',
293
289
  template: 'opencode-agents-md',
294
- file: () => agentsPath,
290
+ file: (ctx) => agentsPath(ctx),
295
291
  line: (ctx) => findFillerLine(agentsContent(ctx)),
296
292
  },
297
293
 
@@ -571,7 +567,7 @@ const OPENCODE_TECHNIQUES = {
571
567
 
572
568
  opencodeAllToolsCovered: {
573
569
  id: 'OC-C08',
574
- name: 'All 15 permissioned tools have explicit state',
570
+ name: 'Critical tool permissions are explicit',
575
571
  check: (ctx) => {
576
572
  const perms = ctx.toolPermissions();
577
573
  if (!perms || Object.keys(perms).length === 0) return null;
@@ -582,7 +578,7 @@ const OPENCODE_TECHNIQUES = {
582
578
  impact: 'high',
583
579
  rating: 4,
584
580
  category: 'permissions',
585
- fix: 'Set explicit permissions for at least the critical tools: bash, edit, read, task.',
581
+ fix: 'Set explicit permissions for at least the critical tools: bash, edit, read, and task. The old fixed "15 tools" framing no longer matches current CLI/runtime surfaces.',
586
582
  template: 'opencode-permissions',
587
583
  file: (ctx) => configFileName(ctx),
588
584
  line: () => null,
@@ -670,21 +666,17 @@ const OPENCODE_TECHNIQUES = {
670
666
 
671
667
  opencodePluginHookGapAware: {
672
668
  id: 'OC-D05',
673
- name: 'No plugins relying on tool.execute.before for subagent/MCP coverage (broken: #5894, #2319)',
669
+ name: 'Plugin docs do not rely on stale hook-gap claims',
674
670
  check: (ctx) => {
675
671
  const pluginFiles = ctx.pluginFiles();
676
672
  if (pluginFiles.length === 0) return null;
677
673
  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;
674
+ return !/\btool\.execute\.before\b[\s\S]{0,80}\b(subagent|mcp)\b[\s\S]{0,80}\b(bypass|gap|broken|2319|5894)\b/i.test(docs);
683
675
  },
684
676
  impact: 'high',
685
677
  rating: 4,
686
678
  category: 'plugins',
687
- fix: 'Document that tool.execute.before hooks do not intercept subagent/MCP calls (known bugs #5894, #2319).',
679
+ fix: 'Remove blanket claims that subagent or MCP calls bypass plugin visibility. On current runtime, hook coverage was observed for direct, subagent, and MCP paths, so any caveat should be version-specific and evidence-backed.',
688
680
  template: 'opencode-agents-md',
689
681
  file: () => 'AGENTS.md',
690
682
  line: () => null,
@@ -713,20 +705,17 @@ const OPENCODE_TECHNIQUES = {
713
705
 
714
706
  opencodeToolInterceptionGap: {
715
707
  id: 'OC-E02',
716
- name: 'tool.execute.before hook gap documented (#5894, #2319)',
708
+ name: 'Security docs do not overstate plugin hook bypass gaps',
717
709
  check: (ctx) => {
718
710
  const pluginFiles = ctx.pluginFiles();
719
711
  if (pluginFiles.length === 0) return null;
720
- // Check if the project uses plugins and has documented the gap
721
712
  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);
713
+ return !/\b(subagent|mcp)\b[\s\S]{0,80}\b(bypass|gap|broken|2319|5894)\b/i.test(docs);
725
714
  },
726
715
  impact: 'high',
727
716
  rating: 4,
728
717
  category: 'security',
729
- fix: 'Document that subagent and MCP tool calls bypass tool.execute.before hooks (bugs #5894, #2319).',
718
+ fix: 'Do not treat historical hook-gap bug reports as a current security guarantee. If you mention plugin coverage limits, mark them as version-sensitive and pair them with fresh runtime evidence.',
730
719
  template: 'opencode-agents-md',
731
720
  file: () => 'AGENTS.md',
732
721
  line: () => null,
@@ -734,19 +723,19 @@ const OPENCODE_TECHNIQUES = {
734
723
 
735
724
  opencodeAgentDenyNotBypassable: {
736
725
  id: 'OC-E03',
737
- name: 'Agent deny permissions not bypassable via SDK (#6396)',
726
+ name: 'Agent-permission docs do not rely on stale SDK bypass claims',
738
727
  check: (ctx) => {
739
728
  const agents = ctx.customAgents();
740
729
  if (!agents || Object.keys(agents).length === 0) return null;
741
730
  const docs = docsBundle(ctx);
742
731
  const usesAgentPerms = Object.values(agents).some(a => a && a.permissions);
743
732
  if (!usesAgentPerms) return true;
744
- return /\b(bypass|gap|limitation|6396)\b/i.test(docs);
733
+ return !/\b6396\b|\bagent\b[\s\S]{0,80}\b(bypass|gap|broken)\b/i.test(docs);
745
734
  },
746
735
  impact: 'high',
747
736
  rating: 4,
748
737
  category: 'security',
749
- fix: 'Document that agent deny permissions can be bypassed via SDK (bug #6396). Add compensating controls.',
738
+ fix: 'Remove blanket claims that agent deny permissions are bypassed via SDK unless you have fresh version-specific proof. The older `#6396` framing did not reproduce in the current CLI harness.',
750
739
  template: 'opencode-agents-md',
751
740
  file: () => 'AGENTS.md',
752
741
  line: () => null,
@@ -781,7 +770,7 @@ const OPENCODE_TECHNIQUES = {
781
770
  impact: 'critical',
782
771
  rating: 5,
783
772
  category: 'security',
784
- fix: 'Use {env:VAR_NAME} substitution for secrets in opencode.json instead of hardcoded values.',
773
+ fix: 'Do not hardcode secrets in `opencode.json`, and do not assume `{env:VAR}` keeps values invisible. Current runtime exposed resolved env substitutions in `debug config`, so treat that surface as sensitive too.',
785
774
  template: 'opencode-config',
786
775
  file: (ctx) => configFileName(ctx),
787
776
  line: (ctx) => {
@@ -821,9 +810,7 @@ const OPENCODE_TECHNIQUES = {
821
810
  if (!mcp || Object.keys(mcp).length === 0) return null;
822
811
  for (const [id, server] of Object.entries(mcp)) {
823
812
  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
813
+ if (server.command && !Array.isArray(server.command)) return false;
827
814
  if (server.env && !server.environment) return false;
828
815
  }
829
816
  return true;
@@ -831,7 +818,7 @@ const OPENCODE_TECHNIQUES = {
831
818
  impact: 'critical',
832
819
  rating: 5,
833
820
  category: 'mcp',
834
- fix: 'Fix MCP config schema: use "command": ["npx", ...] (array) and "environment": {} (not "env").',
821
+ fix: 'Fix MCP config schema: use `command` as a string array and `environment` as the env-var object. Current runtime rejected string commands and the legacy `env` key.',
835
822
  template: 'opencode-config',
836
823
  file: (ctx) => configFileName(ctx),
837
824
  line: () => null,
@@ -843,17 +830,13 @@ const OPENCODE_TECHNIQUES = {
843
830
  check: (ctx) => {
844
831
  const mcp = ctx.mcpServers();
845
832
  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('*'));
833
+ const hasMcpToolRestrictions = Object.values(mcp).some((server) => server && server.tools && Object.keys(server.tools).length > 0);
851
834
  return hasMcpToolRestrictions || Object.keys(mcp).length <= 2;
852
835
  },
853
836
  impact: 'high',
854
837
  rating: 4,
855
838
  category: 'mcp',
856
- fix: 'Add tool whitelisting for MCP servers: { "tools": { "my-mcp*": false } } to limit tool access.',
839
+ fix: 'Add MCP tool restrictions with per-tool globs such as `{ "tools": { "my-mcp*": false } }`. This limits only those MCP tools; other available tools like `webfetch` may still satisfy the same intent unless you restrict them too.',
857
840
  template: 'opencode-config',
858
841
  file: (ctx) => configFileName(ctx),
859
842
  line: () => null,
@@ -886,19 +869,19 @@ const OPENCODE_TECHNIQUES = {
886
869
 
887
870
  opencodeMcpHookLimitation: {
888
871
  id: 'OC-F04',
889
- name: 'MCP tool calls do not trigger plugin hooks — documented limitation (#2319)',
872
+ name: 'MCP hook caveats are treated as version-sensitive',
890
873
  check: (ctx) => {
891
874
  const mcp = ctx.mcpServers();
892
875
  const pluginFiles = ctx.pluginFiles();
893
876
  if (!mcp || Object.keys(mcp).length === 0) return null;
894
877
  if (pluginFiles.length === 0) return null;
895
878
  const docs = docsBundle(ctx);
896
- return /\b(mcp|2319|hook.*mcp|mcp.*hook)\b/i.test(docs);
879
+ return !/\bmcp\b[\s\S]{0,80}\b(hook|plugin)\b[\s\S]{0,80}\b(bypass|gap|broken|2319)\b/i.test(docs);
897
880
  },
898
881
  impact: 'medium',
899
882
  rating: 3,
900
883
  category: 'mcp',
901
- fix: 'Document that MCP tool calls bypass plugin hooks (#2319). Ensure security does not rely on hook interception for MCP.',
884
+ fix: 'Do not hard-code an MCP hook-bypass warning as if it were universal. Current runtime showed MCP plugin events firing, so keep any caveat version-sensitive and backed by fresh evidence.',
902
885
  template: 'opencode-agents-md',
903
886
  file: () => 'AGENTS.md',
904
887
  line: () => null,
@@ -937,18 +920,17 @@ const OPENCODE_TECHNIQUES = {
937
920
 
938
921
  opencodeCiPermissionsPreset: {
939
922
  id: 'OC-G01',
940
- name: 'opencode run usage pre-configures all permissions to avoid hang (#10411)',
923
+ name: 'opencode run usage pre-configures permissions to avoid silent auto-rejects',
941
924
  check: (ctx) => {
942
925
  const workflows = workflowArtifacts(ctx);
943
926
  const hasOpencodeRun = workflows.some(w => /\bopencode\s+run\b/i.test(w.content));
944
927
  if (!hasOpencodeRun) return null;
945
- // Check that permissions are pre-configured
946
928
  return workflows.some(w => /\bpermissions?\b.*\ballow\b|\b--yes\b|\b--no-prompt\b/i.test(w.content));
947
929
  },
948
930
  impact: 'critical',
949
931
  rating: 5,
950
932
  category: 'ci',
951
- fix: 'Pre-configure all permissions when using `opencode run` in CI. Without this, the process hangs on permission prompts.',
933
+ fix: 'Pre-configure permissions when using `opencode run` in CI. In the current harness, permission requests auto-rejected instead of hanging, which still breaks tasks that expected tool access.',
952
934
  template: 'opencode-ci',
953
935
  file: () => '.github/workflows/',
954
936
  line: () => null,
@@ -984,7 +966,7 @@ const OPENCODE_TECHNIQUES = {
984
966
  impact: 'medium',
985
967
  rating: 3,
986
968
  category: 'ci',
987
- fix: 'Use `--format json` when running OpenCode in CI for machine-readable output parsing.',
969
+ fix: 'Use `--format json` when running OpenCode in CI, and parse it as JSONL/event frames rather than expecting one monolithic JSON document.',
988
970
  template: 'opencode-ci',
989
971
  file: () => '.github/workflows/',
990
972
  line: () => null,
@@ -1036,19 +1018,16 @@ const OPENCODE_TECHNIQUES = {
1036
1018
 
1037
1019
  opencodeNoDeprecatedPatterns: {
1038
1020
  id: 'OC-H02',
1039
- name: 'No deprecated OpenCode patterns (mode -> agent migration)',
1021
+ name: 'Repo docs do not push the stale mode -> agent migration claim',
1040
1022
  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;
1023
+ const docs = docsBundle(ctx);
1024
+ if (!docs.trim()) return null;
1025
+ return !/\bmode\b[\s\S]{0,60}\bdeprecated\b|\buse\b[\s\S]{0,40}\bagent\b[\s\S]{0,40}\binstead of\b[\s\S]{0,20}\bmode\b/i.test(docs);
1047
1026
  },
1048
1027
  impact: 'medium',
1049
1028
  rating: 3,
1050
1029
  category: 'quality-deep',
1051
- fix: 'Replace deprecated config keys: use "agent" instead of "mode".',
1030
+ fix: 'Do not tell users that `mode` has been replaced by `agent` across the board. Current runtime still validated `mode` for markdown custom agents, so any migration guidance should be explicitly version-scoped.',
1052
1031
  template: 'opencode-config',
1053
1032
  file: (ctx) => configFileName(ctx),
1054
1033
  line: () => null,
@@ -1162,16 +1141,16 @@ const OPENCODE_TECHNIQUES = {
1162
1141
 
1163
1142
  opencodeSkillKebabCase: {
1164
1143
  id: 'OC-I03',
1165
- name: 'Skill names use kebab-case (not PascalCase — 0% invocation rate)',
1144
+ name: 'Skill names preferably use kebab-case',
1166
1145
  check: (ctx) => {
1167
1146
  const skillDirs = ctx.skillDirs();
1168
1147
  if (skillDirs.length === 0) return null;
1169
1148
  return skillDirs.every(name => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name));
1170
1149
  },
1171
- impact: 'high',
1172
- rating: 4,
1150
+ impact: 'medium',
1151
+ rating: 2,
1173
1152
  category: 'skills',
1174
- fix: 'Rename skill directories to kebab-case (e.g., code-review, not CodeReview). PascalCase has 0% implicit invocation.',
1153
+ fix: 'Prefer kebab-case for skill names, but treat it as a style recommendation rather than a hard runtime requirement. Current runtime still discovered underscore-based names.',
1175
1154
  template: 'opencode-skills',
1176
1155
  file: () => '.opencode/commands/',
1177
1156
  line: () => null,
@@ -1201,20 +1180,17 @@ const OPENCODE_TECHNIQUES = {
1201
1180
 
1202
1181
  opencodeSkillCompatPaths: {
1203
1182
  id: 'OC-I05',
1204
- name: '.claude/skills/ compatibility paths resolve correctly in OpenCode',
1183
+ name: 'OpenCode skill discovery accepts either .opencode/commands or .claude/skills',
1205
1184
  check: (ctx) => {
1206
1185
  const hasClaudeSkills = ctx.hasDir('.claude/skills');
1207
- if (!hasClaudeSkills) return null;
1208
1186
  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;
1187
+ if (!hasClaudeSkills && !hasOpencodeCommands) return null;
1188
+ return hasClaudeSkills || hasOpencodeCommands;
1213
1189
  },
1214
1190
  impact: 'medium',
1215
1191
  rating: 3,
1216
1192
  category: 'skills',
1217
- fix: 'If using .claude/skills/, also create .opencode/commands/ for OpenCode compatibility.',
1193
+ fix: 'Use `.opencode/commands/` for native OpenCode skills when you need them, but do not require a duplicate tree just to mirror `.claude/skills/`. Current runtime discovered `.claude/skills/` compatibility successfully.',
1218
1194
  template: 'opencode-skills',
1219
1195
  file: () => '.opencode/commands/',
1220
1196
  line: () => null,
@@ -1247,21 +1223,22 @@ const OPENCODE_TECHNIQUES = {
1247
1223
 
1248
1224
  opencodeAgentModeValid: {
1249
1225
  id: 'OC-J02',
1250
- name: 'Agent mode field is valid: "primary", "subagent", or "all"',
1226
+ name: 'Custom agent mode is valid when declared',
1251
1227
  check: (ctx) => {
1252
1228
  const agents = ctx.customAgents();
1253
1229
  if (!agents || Object.keys(agents).length === 0) return null;
1254
1230
  const validModes = new Set(['primary', 'subagent', 'all']);
1255
1231
  for (const [name, agent] of Object.entries(agents)) {
1256
1232
  if (!agent) continue;
1257
- if (agent.agent && !validModes.has(agent.agent)) return false;
1233
+ const mode = agent.mode || agent.agent;
1234
+ if (mode && !validModes.has(mode)) return false;
1258
1235
  }
1259
1236
  return true;
1260
1237
  },
1261
1238
  impact: 'medium',
1262
1239
  rating: 3,
1263
1240
  category: 'agents',
1264
- fix: 'Set agent "agent" field to one of: "primary", "subagent", or "all".',
1241
+ fix: 'Use a valid mode value (`primary`, `subagent`, or `all`) when declaring custom agents. Current runtime still validated `mode` for markdown agents, so do not rename to `agent` solely because of stale docs.',
1265
1242
  template: 'opencode-config',
1266
1243
  file: (ctx) => configFileName(ctx),
1267
1244
  line: () => null,
@@ -1269,20 +1246,20 @@ const OPENCODE_TECHNIQUES = {
1269
1246
 
1270
1247
  opencodeBuiltinAgentsProtected: {
1271
1248
  id: 'OC-J03',
1272
- name: 'Built-in agents (build, plan) not accidentally overridden',
1249
+ name: 'Built-in agent overrides are intentional and documented',
1273
1250
  check: (ctx) => {
1274
1251
  const agents = ctx.customAgents();
1275
1252
  if (!agents || Object.keys(agents).length === 0) return null;
1276
1253
  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;
1254
+ const overriding = Object.keys(agents).filter((name) => builtins.has(name.toLowerCase()));
1255
+ if (overriding.length === 0) return true;
1256
+ const docs = docsBundle(ctx);
1257
+ return /override|intentional|customized|replace/i.test(docs);
1281
1258
  },
1282
1259
  impact: 'medium',
1283
1260
  rating: 3,
1284
1261
  category: 'agents',
1285
- fix: 'Do not name custom agents "build", "plan", or "default" to avoid overriding built-in agents.',
1262
+ fix: 'Built-in agents appear overrideable in current runtime. If you intentionally override `build`, `plan`, or `default`, document why; otherwise rename the custom agent to avoid surprising behavior.',
1286
1263
  template: 'opencode-config',
1287
1264
  file: (ctx) => configFileName(ctx),
1288
1265
  line: () => null,
@@ -1389,7 +1366,7 @@ const OPENCODE_TECHNIQUES = {
1389
1366
  impact: 'medium',
1390
1367
  rating: 3,
1391
1368
  category: 'tui',
1392
- fix: 'Fix JSONC syntax in tui.json. Ensure valid JSON with optional comments.',
1369
+ fix: 'Fix JSONC syntax in `tui.json`, then validate the behavioral effect in the real TUI/UI. Headless CLI surfaces did not provide enough evidence for TUI behavior on their own.',
1393
1370
  template: 'opencode-config',
1394
1371
  file: () => 'tui.json',
1395
1372
  line: () => 1,
@@ -1415,7 +1392,7 @@ const OPENCODE_TECHNIQUES = {
1415
1392
  impact: 'low',
1416
1393
  rating: 2,
1417
1394
  category: 'tui',
1418
- fix: 'Fix JSON syntax errors in theme files under .opencode/themes/.',
1395
+ fix: 'Fix JSON syntax errors in `.opencode/themes/`, then verify the theme in an actual UI/TUI session. Headless `run` did not give reliable theme evidence.',
1419
1396
  template: 'opencode-config',
1420
1397
  file: () => '.opencode/themes/',
1421
1398
  line: () => null,
@@ -1432,7 +1409,7 @@ const OPENCODE_TECHNIQUES = {
1432
1409
  impact: 'medium',
1433
1410
  rating: 3,
1434
1411
  category: 'tui',
1435
- fix: 'Remove any sensitive data from tui.json.',
1412
+ fix: 'Remove any sensitive data from `tui.json`, and remember that `tui.json` was not meaningfully observable through headless `run` alone.',
1436
1413
  template: 'opencode-config',
1437
1414
  file: () => 'tui.json',
1438
1415
  line: (ctx) => {
@@ -1520,17 +1497,16 @@ const OPENCODE_TECHNIQUES = {
1520
1497
  id: 'OC-N02',
1521
1498
  name: 'Config references current OpenCode features (no removed or renamed keys)',
1522
1499
  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;
1500
+ const docs = docsBundle(ctx);
1501
+ const config = ctx.configContent();
1502
+ if (!docs.trim() && !config) return null;
1503
+ const combined = `${docs}\n${config || ''}`;
1504
+ return !/\bconfig\.json\b|\.well-known\/opencode|mode\s*->\s*agent|CLAUDE\.md fallback/i.test(combined);
1529
1505
  },
1530
1506
  impact: 'medium',
1531
1507
  rating: 3,
1532
1508
  category: 'release-freshness',
1533
- fix: 'Update deprecated config keys to their current equivalents.',
1509
+ fix: 'Update stale OpenCode references. Use `opencode.json`/`opencode.jsonc`, keep `mode` guidance version-scoped, and treat `.well-known/opencode` plus `CLAUDE.md` fallback claims as unvalidated until you have fresh runtime proof.',
1534
1510
  template: 'opencode-config',
1535
1511
  file: (ctx) => configFileName(ctx),
1536
1512
  line: () => null,
@@ -1596,15 +1572,13 @@ const OPENCODE_TECHNIQUES = {
1596
1572
 
1597
1573
  opencodeInstructionsArrayResolvable: {
1598
1574
  id: 'OC-O02',
1599
- name: 'instructions array in opencode.json references valid paths',
1575
+ name: 'instructions array uses validated local file paths',
1600
1576
  check: (ctx) => {
1601
1577
  const instructions = ctx.instructionsArray();
1602
1578
  if (!Array.isArray(instructions) || instructions.length === 0) return null;
1603
1579
  for (const instruction of instructions) {
1604
1580
  if (typeof instruction !== 'string') continue;
1605
- // Skip URLs and globs
1606
- if (instruction.startsWith('http') || instruction.includes('*')) continue;
1607
- // Check local file references
1581
+ if (instruction.startsWith('http') || instruction.includes('*')) return false;
1608
1582
  if (!ctx.fileContent(instruction)) return false;
1609
1583
  }
1610
1584
  return true;
@@ -1612,7 +1586,7 @@ const OPENCODE_TECHNIQUES = {
1612
1586
  impact: 'high',
1613
1587
  rating: 4,
1614
1588
  category: 'mixed-agent',
1615
- fix: 'Ensure all paths in the "instructions" array of opencode.json point to existing files.',
1589
+ fix: 'Prefer direct local file paths in the `instructions` array. Current runtime clearly validated direct files, but glob and URL sources were not visibly applied in `run`, so treat them as experimental until reproduced.',
1616
1590
  template: 'opencode-config',
1617
1591
  file: (ctx) => configFileName(ctx),
1618
1592
  line: () => null,
@@ -1620,18 +1594,16 @@ const OPENCODE_TECHNIQUES = {
1620
1594
 
1621
1595
  opencodeGlobalAgentsNoConflict: {
1622
1596
  id: 'OC-O03',
1623
- name: 'Global AGENTS.md does not conflict with project AGENTS.md',
1597
+ name: 'Project docs do not depend on global AGENTS.md behavior',
1624
1598
  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
1599
+ const docs = `${ctx.fileContent('AGENTS.md') || ''}\n${ctx.fileContent('README.md') || ''}`;
1600
+ if (!docs.trim()) return null;
1601
+ return !/~\/\.config\/opencode\/AGENTS\.md|global AGENTS/i.test(docs);
1630
1602
  },
1631
1603
  impact: 'medium',
1632
1604
  rating: 3,
1633
1605
  category: 'mixed-agent',
1634
- fix: 'Review global AGENTS.md (~/.config/opencode/AGENTS.md) for conflicts with project AGENTS.md.',
1606
+ fix: 'Do not rely on `~/.config/opencode/AGENTS.md` as a guaranteed project behavior. Current Windows runtime did not show global AGENTS loading in `run`, so keep project-critical guidance in repo files.',
1635
1607
  template: 'opencode-agents-md',
1636
1608
  file: () => 'AGENTS.md',
1637
1609
  line: () => null,
@@ -1643,7 +1615,7 @@ const OPENCODE_TECHNIQUES = {
1643
1615
 
1644
1616
  opencodeConfigMergeConsistent: {
1645
1617
  id: 'OC-P01',
1646
- name: '6-level config merge hierarchy does not produce conflicting values',
1618
+ name: 'Observed config merge hierarchy does not produce conflicting values',
1647
1619
  check: (ctx) => {
1648
1620
  const projectConfig = ctx.configJson();
1649
1621
  const globalConfig = ctx.globalConfigJson();
@@ -1658,7 +1630,7 @@ const OPENCODE_TECHNIQUES = {
1658
1630
  impact: 'high',
1659
1631
  rating: 4,
1660
1632
  category: 'propagation',
1661
- fix: 'Review 6-level config merge: .well-known > global > env > project > .opencode/ > env content.',
1633
+ fix: 'Review the observed precedence chain: global `opencode.json` < `OPENCODE_CONFIG` < project `opencode.json` < `.opencode/opencode.json` < `OPENCODE_CONFIG_CONTENT`. Treat `.well-known/opencode` as remote-only until you have runtime proof.',
1662
1634
  template: 'opencode-config',
1663
1635
  file: (ctx) => configFileName(ctx),
1664
1636
  line: () => null,
@@ -1708,6 +1680,8 @@ const OPENCODE_TECHNIQUES = {
1708
1680
  },
1709
1681
  };
1710
1682
 
1683
+ attachSourceUrls('opencode', OPENCODE_TECHNIQUES);
1684
+
1711
1685
  module.exports = {
1712
1686
  OPENCODE_TECHNIQUES,
1713
1687
  };