@nerviq/cli 1.27.0 → 1.29.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +1493 -0
  2. package/README.md +2 -2
  3. package/SECURITY.md +82 -0
  4. package/contracts/audit-webhook-event.schema.json +138 -0
  5. package/contracts/pack-contract.schema.json +15 -0
  6. package/contracts/technique-contract.schema.json +18 -0
  7. package/docs/ARCHITECTURE.md +74 -0
  8. package/docs/api-reference.md +356 -0
  9. package/docs/autofix.md +64 -0
  10. package/docs/bitbucket-pipe.yml +57 -0
  11. package/docs/case-studies.md +149 -0
  12. package/docs/category-definition-kit.md +56 -0
  13. package/docs/ci-integration.md +127 -0
  14. package/docs/claude-code-style.md +24 -0
  15. package/docs/claude-maintainer-ops.md +19 -0
  16. package/docs/external-validation.md +78 -0
  17. package/docs/first-tier-integration-gate.md +59 -0
  18. package/docs/getting-started.md +119 -0
  19. package/docs/gitlab-ci-template.yml +54 -0
  20. package/docs/index.html +597 -0
  21. package/docs/integration-contracts.md +287 -0
  22. package/docs/license-faq.md +53 -0
  23. package/docs/maintenance.md +155 -0
  24. package/docs/methodology.md +236 -0
  25. package/docs/new-platform-guide.md +202 -0
  26. package/docs/open-vsx-publishing.md +46 -0
  27. package/docs/platform-change-ingestion.md +46 -0
  28. package/docs/plugins.md +101 -0
  29. package/docs/pre-commit.md +58 -0
  30. package/docs/security-model.md +63 -0
  31. package/docs/shallow-risk.md +246 -0
  32. package/docs/versioning-policy.md +63 -0
  33. package/docs/why-nerviq.md +82 -0
  34. package/package.json +7 -2
  35. package/sdk/README.md +190 -0
  36. package/src/codex/setup.js +3 -2
  37. package/src/gemini/setup.js +3 -2
  38. package/src/init.js +4 -3
  39. package/src/opencode/context.js +42 -3
  40. package/src/opencode/techniques.js +198 -142
  41. package/src/output-icons.js +44 -0
  42. package/src/setup/runtime.js +6 -5
  43. package/src/setup.js +4 -3
  44. package/src/shallow-risk/patterns/agent-config-missing-file.js +254 -9
  45. package/src/shallow-risk/shared.js +135 -7
@@ -1,11 +1,15 @@
1
1
  'use strict';
2
2
 
3
+ const path = require('path');
4
+
3
5
  const {
4
6
  SHALLOW_RISK_DOC_URL,
5
7
  escapeRegExp,
8
+ findFirstRepoPath,
6
9
  getAgentConfigEntries,
7
10
  getScannableLines,
8
11
  isKnownConventionPath,
12
+ lineHasExampleContext,
9
13
  looksLikeRelativeFileReference,
10
14
  normalizeCandidatePath,
11
15
  resolveRepoPath,
@@ -13,6 +17,236 @@ const {
13
17
  } = require('../shared');
14
18
 
15
19
  const POINTER_RE = /(?:^|[\s([`'"])(@?(?:\.{1,2}\/)?[A-Za-z0-9._/-]+)(?=$|[\s)\]`'",:;!?])/g;
20
+ const MARKDOWN_LINK_RE = /\[[^\]]+\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
21
+ const BACKTICK_TOKEN_RE = /`([^`]+)`/g;
22
+ const PLACEHOLDER_PATH_RE = /(?:^|\/)(?:path(?:_to)?|to)(?:\/|$)|(?:^|\/)test_file\.py$|(?:^|\/)path_to_test\.py$|(?:^|\/)module_name\.[A-Za-z0-9._-]+$/i;
23
+ const ENV_POLICY_RE = /\b(?:dotenv|environment variables?|api keys?|secrets?|credential|gitignore|removed\s+\.env|look for\s+\.env|via\s+`?\.env|defaults?\s+to|do not commit)\b/i;
24
+ const OWNERSHIP_CONTEXT_RE = /\b(?:subdirectory|integration|folder|workspace|extension|module|package|component|app|generated file|composition root|entrypoint|directory structure|utility functions|updated in|register feature|build from)s?(?:['’]s)?\b/i;
25
+ const SOFT_REFERENCE_CONTEXT_RE = /\b(?:can be deleted afterwards|quality scale|search result|scrape the web page content)\b/i;
26
+ const ALWAYS_AMBIGUOUS_BASENAMES = new Set([
27
+ 'findings.md',
28
+ 'manifest.json',
29
+ 'progress.md',
30
+ 'quality_scale.yaml',
31
+ 'task_plan.md',
32
+ 'todo.md',
33
+ ]);
34
+
35
+ function repoHasBasename(ctx, basename, state) {
36
+ if (!basename) {
37
+ return false;
38
+ }
39
+ if (state.basenameCache.has(basename)) {
40
+ return state.basenameCache.get(basename);
41
+ }
42
+
43
+ const match = findFirstRepoPath(ctx, (_relPath, entryName) => entryName === basename, { maxDepth: 10 });
44
+ const exists = Boolean(match);
45
+ state.basenameCache.set(basename, exists);
46
+ return exists;
47
+ }
48
+
49
+ function repoHasPathSuffix(ctx, candidate, state) {
50
+ const normalized = toPosix(candidate || '').replace(/^\.?\//, '');
51
+ if (!normalized) {
52
+ return false;
53
+ }
54
+ if (state.suffixCache.has(normalized)) {
55
+ return state.suffixCache.get(normalized);
56
+ }
57
+
58
+ const match = findFirstRepoPath(
59
+ ctx,
60
+ (relPath) => {
61
+ const normalizedPath = toPosix(relPath);
62
+ return normalizedPath === normalized || normalizedPath.endsWith(`/${normalized}`);
63
+ },
64
+ { maxDepth: 10 },
65
+ );
66
+ const exists = Boolean(match);
67
+ state.suffixCache.set(normalized, exists);
68
+ return exists;
69
+ }
70
+
71
+ function lineHasEnvPolicyContext(line) {
72
+ return ENV_POLICY_RE.test(String(line || ''));
73
+ }
74
+
75
+ function lineHasScopedOwnershipContext(line) {
76
+ const text = String(line || '');
77
+ return OWNERSHIP_CONTEXT_RE.test(text) || SOFT_REFERENCE_CONTEXT_RE.test(text) || /<[^>]+>/.test(text);
78
+ }
79
+
80
+ function extractLineAnchors(line) {
81
+ const anchors = new Set();
82
+ const text = String(line || '');
83
+
84
+ BACKTICK_TOKEN_RE.lastIndex = 0;
85
+ let match = BACKTICK_TOKEN_RE.exec(text);
86
+ while (match) {
87
+ const rawToken = String(match[1] || '');
88
+ const token = normalizeCandidatePath(rawToken)
89
+ .replace(/<[^>]+>/g, '')
90
+ .replace(/^\/+/, '')
91
+ .replace(/\/+$/, '');
92
+ if (!token || !rawToken.includes('/')) {
93
+ match = BACKTICK_TOKEN_RE.exec(text);
94
+ continue;
95
+ }
96
+ anchors.add(token);
97
+ match = BACKTICK_TOKEN_RE.exec(text);
98
+ }
99
+
100
+ MARKDOWN_LINK_RE.lastIndex = 0;
101
+ match = MARKDOWN_LINK_RE.exec(text);
102
+ while (match) {
103
+ const rawToken = String(match[1] || '');
104
+ const token = normalizeCandidatePath(rawToken)
105
+ .replace(/<[^>]+>/g, '')
106
+ .replace(/^\/+/, '')
107
+ .replace(/\/+$/, '');
108
+ if (!token || !rawToken.includes('/')) {
109
+ match = MARKDOWN_LINK_RE.exec(text);
110
+ continue;
111
+ }
112
+ anchors.add(token);
113
+ match = MARKDOWN_LINK_RE.exec(text);
114
+ }
115
+
116
+ return [...anchors];
117
+ }
118
+
119
+ function anchorDirsForToken(token) {
120
+ if (!token) {
121
+ return [];
122
+ }
123
+
124
+ const normalized = normalizeCandidatePath(token)
125
+ .replace(/<[^>]+>/g, '')
126
+ .replace(/^\/+/, '')
127
+ .replace(/\/+$/, '');
128
+ if (!normalized) {
129
+ return [];
130
+ }
131
+
132
+ const dirs = new Set();
133
+ const looksFileLike = looksLikeRelativeFileReference(normalized);
134
+ const direct = normalized.includes('/')
135
+ ? (looksFileLike ? path.posix.dirname(normalized) : normalized)
136
+ : normalized;
137
+ if (direct && direct !== '.') {
138
+ dirs.add(direct);
139
+ }
140
+
141
+ const parent = path.posix.dirname(direct || normalized);
142
+ if (parent && parent !== '.' && parent !== direct) {
143
+ dirs.add(parent);
144
+ }
145
+
146
+ return [...dirs];
147
+ }
148
+
149
+ function lineResolvesBareCandidate(ctx, line, candidate, state) {
150
+ const base = path.posix.basename(candidate);
151
+ const anchors = extractLineAnchors(line);
152
+
153
+ for (const anchor of anchors) {
154
+ const normalizedAnchor = normalizeCandidatePath(anchor);
155
+ if (path.posix.basename(normalizedAnchor) === base && (ctx.fileContent(normalizedAnchor) !== null || repoHasPathSuffix(ctx, normalizedAnchor, state))) {
156
+ return true;
157
+ }
158
+
159
+ for (const dir of anchorDirsForToken(anchor)) {
160
+ const match = findFirstRepoPath(
161
+ ctx,
162
+ (relPath, entryName) => entryName === base && toPosix(relPath).startsWith(`${dir}/`),
163
+ { maxDepth: 10 },
164
+ );
165
+ if (match) {
166
+ return true;
167
+ }
168
+ }
169
+ }
170
+
171
+ if (anchors.length > 0 && repoHasBasename(ctx, base, state)) {
172
+ return true;
173
+ }
174
+
175
+ if (lineHasScopedOwnershipContext(line) && repoHasBasename(ctx, base, state)) {
176
+ return true;
177
+ }
178
+
179
+ return false;
180
+ }
181
+
182
+ function lineHasAnchorContext(line) {
183
+ return extractLineAnchors(line).length > 0;
184
+ }
185
+
186
+ function lineResolvesPathSuffix(ctx, line, candidate, state) {
187
+ if (!candidate || !candidate.includes('/')) {
188
+ return false;
189
+ }
190
+ if (!lineHasAnchorContext(line) && !lineHasScopedOwnershipContext(line)) {
191
+ return false;
192
+ }
193
+ return repoHasPathSuffix(ctx, candidate, state);
194
+ }
195
+
196
+ function shouldIgnoreCandidate(ctx, line, candidate, state) {
197
+ const normalized = String(candidate || '');
198
+ const base = path.posix.basename(normalized);
199
+ if (!normalized) {
200
+ return true;
201
+ }
202
+ if (PLACEHOLDER_PATH_RE.test(normalized)) {
203
+ return true;
204
+ }
205
+ if (ALWAYS_AMBIGUOUS_BASENAMES.has(base) && repoHasBasename(ctx, base, state)) {
206
+ return true;
207
+ }
208
+ if (SOFT_REFERENCE_CONTEXT_RE.test(String(line || '')) && (base === 'PLAN.md' || base === 'web_scraper.py')) {
209
+ return true;
210
+ }
211
+ if (normalized === '.env' && lineHasEnvPolicyContext(line)) {
212
+ return true;
213
+ }
214
+ if (lineResolvesPathSuffix(ctx, line, normalized, state)) {
215
+ return true;
216
+ }
217
+ if (!normalized.includes('/') && lineResolvesBareCandidate(ctx, line, normalized, state)) {
218
+ return true;
219
+ }
220
+ return false;
221
+ }
222
+
223
+ function resolveMissingCandidate(ctx, fromFile, candidate) {
224
+ const isNestedAgentDoc = toPosix(fromFile).includes('/');
225
+ const prefersRepoRoot = isNestedAgentDoc && !candidate.startsWith('../');
226
+ const modes = prefersRepoRoot
227
+ ? ['repo-root', 'relative-to-file']
228
+ : ['relative-to-file', 'repo-root'];
229
+
230
+ let firstMissing = null;
231
+ for (const mode of modes) {
232
+ const resolvedPath = resolveRepoPath(ctx, fromFile, candidate, mode);
233
+ if (!resolvedPath || isKnownConventionPath(resolvedPath)) {
234
+ continue;
235
+ }
236
+ if (!firstMissing) {
237
+ firstMissing = resolvedPath;
238
+ }
239
+ if (ctx.fileContent(resolvedPath) !== null) {
240
+ return { exists: true, resolvedPath };
241
+ }
242
+ }
243
+
244
+ return { exists: false, resolvedPath: firstMissing };
245
+ }
246
+
247
+ function rewriteMarkdownLinksForScanning(text) {
248
+ return String(text || '').replace(MARKDOWN_LINK_RE, (_match, target) => ` ${target} `);
249
+ }
16
250
 
17
251
  module.exports = {
18
252
  key: 'agent-config-missing-file',
@@ -23,35 +257,46 @@ module.exports = {
23
257
  run(ctx) {
24
258
  const findings = [];
25
259
  const seen = new Set();
260
+ const state = {
261
+ basenameCache: new Map(),
262
+ suffixCache: new Map(),
263
+ };
26
264
 
27
265
  for (const entry of getAgentConfigEntries(ctx)) {
28
266
  if (!/\.(?:md|mdc|txt|rst)$/i.test(entry.path) && !/\.cursorrules$|\.windsurfrules$/i.test(entry.path)) {
29
267
  continue;
30
268
  }
31
269
  for (const { lineNumber, text } of getScannableLines(entry.content)) {
270
+ const scanText = rewriteMarkdownLinksForScanning(text);
32
271
  POINTER_RE.lastIndex = 0;
33
- let match = POINTER_RE.exec(text);
272
+ let match = POINTER_RE.exec(scanText);
34
273
  while (match) {
35
274
  const candidate = normalizeCandidatePath(match[1]);
36
275
  if (!looksLikeRelativeFileReference(candidate)) {
37
- match = POINTER_RE.exec(text);
276
+ match = POINTER_RE.exec(scanText);
277
+ continue;
278
+ }
279
+
280
+ if (lineHasExampleContext(text)) {
281
+ match = POINTER_RE.exec(scanText);
38
282
  continue;
39
283
  }
40
284
 
41
- const resolvedPath = resolveRepoPath(ctx, entry.path, candidate, 'relative-to-file');
42
- if (!resolvedPath || isKnownConventionPath(resolvedPath)) {
43
- match = POINTER_RE.exec(text);
285
+ if (shouldIgnoreCandidate(ctx, text, candidate, state)) {
286
+ match = POINTER_RE.exec(scanText);
44
287
  continue;
45
288
  }
46
289
 
47
- if (ctx.fileContent(resolvedPath) !== null) {
48
- match = POINTER_RE.exec(text);
290
+ const resolution = resolveMissingCandidate(ctx, entry.path, candidate);
291
+ if (!resolution.resolvedPath || resolution.exists) {
292
+ match = POINTER_RE.exec(scanText);
49
293
  continue;
50
294
  }
295
+ const resolvedPath = resolution.resolvedPath;
51
296
 
52
297
  const dedupeKey = `${entry.path}:${toPosix(resolvedPath)}`;
53
298
  if (seen.has(dedupeKey)) {
54
- match = POINTER_RE.exec(text);
299
+ match = POINTER_RE.exec(scanText);
55
300
  continue;
56
301
  }
57
302
  seen.add(dedupeKey);
@@ -62,7 +307,7 @@ module.exports = {
62
307
  fix: `${entry.path} references \`${toPosix(resolvedPath)}\`, but the file is missing. Create the file or update the agent guidance to point at a real repo path.`,
63
308
  });
64
309
 
65
- match = POINTER_RE.exec(text);
310
+ match = POINTER_RE.exec(scanText);
66
311
  }
67
312
  }
68
313
  }
@@ -74,17 +74,71 @@ const SPECIAL_FILE_BASENAMES = new Set([
74
74
  'Dockerfile',
75
75
  'Makefile',
76
76
  'justfile',
77
+ 'manifest.json',
77
78
  'package.json',
78
79
  'pyproject.toml',
79
80
  'go.mod',
80
81
  'Cargo.toml',
81
82
  ]);
82
83
 
84
+ const COMMON_DOTFILE_BASENAMES = new Set([
85
+ '.editorconfig',
86
+ '.env',
87
+ '.env.example',
88
+ '.env.sample',
89
+ '.env.template',
90
+ '.gitattributes',
91
+ '.gitignore',
92
+ '.npmrc',
93
+ '.nvmrc',
94
+ '.prettierrc',
95
+ '.python-version',
96
+ '.tool-versions',
97
+ ]);
98
+
83
99
  const KNOWN_CONVENTION_PATHS = new Set([
84
100
  'CODEOWNERS',
85
101
  '.github/CODEOWNERS',
86
102
  ]);
87
103
 
104
+ const FILE_REFERENCE_EXTENSION_RE = /\.(?:md|mdc|txt|rst|json|jsonc|ya?ml|toml|conf|sh|ps1|js|cjs|mjs|ts|tsx|jsx|cts|mts|py|go|rs|java|kt|kts|gradle|cs|rb|php|swift|pbxproj|xcconfig|xcworkspace|xcodeproj|h|hpp|c|cc|cpp|m|mm|sql|ini|cfg|properties|xml|html|css|scss|sass|lock)$/i;
105
+ const KNOWN_DOMAIN_TLDS = new Set([
106
+ 'ai',
107
+ 'app',
108
+ 'co',
109
+ 'com',
110
+ 'dev',
111
+ 'io',
112
+ 'net',
113
+ 'org',
114
+ 'sh',
115
+ ]);
116
+ const KNOWN_HIDDEN_PATH_SEGMENTS = new Set([
117
+ '.claude',
118
+ '.codex',
119
+ '.cursor',
120
+ '.gemini',
121
+ '.github',
122
+ '.opencode',
123
+ '.vscode',
124
+ '.windsurf',
125
+ ]);
126
+ const FRAMEWORK_LABEL_TOKENS = new Set([
127
+ 'd3.js',
128
+ 'go',
129
+ 'golang',
130
+ 'javascript',
131
+ 'kotlin',
132
+ 'next',
133
+ 'next.js',
134
+ 'node',
135
+ 'node.js',
136
+ 'python',
137
+ 'rust',
138
+ 'swift',
139
+ 'typescript',
140
+ ]);
141
+
88
142
  const LOCAL_MCP_BINARIES = new Set([
89
143
  'context7-mcp',
90
144
  'nerviq-mcp',
@@ -127,8 +181,9 @@ function existsSyncSafe(targetPath) {
127
181
  function isLikelyTextFile(relPath) {
128
182
  const base = path.posix.basename(toPosix(relPath));
129
183
  if (SPECIAL_FILE_BASENAMES.has(base)) return true;
184
+ if (COMMON_DOTFILE_BASENAMES.has(base)) return true;
130
185
  if (base === '.cursorrules' || base === '.windsurfrules') return true;
131
- return /\.(?:md|mdc|txt|rst|json|jsonc|ya?ml|toml|conf|sh|ps1|js|cjs|mjs|ts|tsx|jsx|py|go|rs|java|kt|cs|rb|php|swift)$/i.test(base);
186
+ return hasKnownFileExtension(base);
132
187
  }
133
188
 
134
189
  function fileExists(ctx, relPath) {
@@ -235,27 +290,69 @@ function stripWrapperChars(value) {
235
290
  function normalizeCandidatePath(rawValue) {
236
291
  let value = stripWrapperChars(rawValue);
237
292
  if (value.startsWith('@')) value = value.slice(1);
293
+ if (/^mdc:/i.test(value)) value = value.slice(4);
238
294
  return value;
239
295
  }
240
296
 
297
+ function hasKnownFileExtension(baseName) {
298
+ return FILE_REFERENCE_EXTENSION_RE.test(baseName || '');
299
+ }
300
+
301
+ function isVersionLikeToken(candidate) {
302
+ return /^v?\d+(?:\.\d+)+(?:[a-z]+\d*|\.[xX*])?$/i.test(candidate || '');
303
+ }
304
+
305
+ function isFrameworkLabelToken(candidate) {
306
+ return FRAMEWORK_LABEL_TOKENS.has(String(candidate || '').toLowerCase());
307
+ }
308
+
309
+ function isDomainLikeToken(candidate) {
310
+ if (!candidate || candidate.includes('/')) return false;
311
+ const parts = String(candidate).split('.');
312
+ if (parts.length < 2) return false;
313
+ const tld = parts[parts.length - 1].toLowerCase();
314
+ if (!KNOWN_DOMAIN_TLDS.has(tld)) return false;
315
+ return parts.slice(0, -1).every((part) => /^[A-Za-z0-9-]+$/.test(part));
316
+ }
317
+
318
+ function lineHasExampleContext(line) {
319
+ const text = String(line || '');
320
+ if (/^\s*\|/.test(text)) return true;
321
+ if (/^\s*#{1,6}\s+/.test(text)) return true;
322
+ return /\b(?:e\.g\.?|for example|examples?|sample|placeholder|template|snippet|user request|problem|solution)\b/i.test(text);
323
+ }
324
+
241
325
  function looksLikeRelativeFileReference(candidate) {
242
326
  if (!candidate) return false;
243
327
  if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(candidate)) return false;
244
- if (/^[A-Za-z0-9-]+\.[A-Za-z]{2,}\//.test(candidate)) return false;
245
328
  if (candidate.startsWith('#')) return false;
329
+ if (/[<>{}|]/.test(candidate)) return false;
246
330
 
247
331
  const normalized = candidate.replace(/^\.\//, '');
248
332
  const base = path.posix.basename(normalized);
333
+ const lowered = normalized.toLowerCase();
249
334
 
250
- if (KNOWN_CONVENTION_PATHS.has(normalized) || SPECIAL_FILE_BASENAMES.has(base)) {
251
- return true;
335
+ if (isDomainLikeToken(normalized)) return false;
336
+ if (isVersionLikeToken(normalized)) return false;
337
+ if (isFrameworkLabelToken(normalized)) return false;
338
+ if (base.startsWith('.') && !COMMON_DOTFILE_BASENAMES.has(base) && !COMMON_DOTFILE_BASENAMES.has(lowered)) {
339
+ return false;
340
+ }
341
+ if (normalized.split('/').some((segment) => /^\.[A-Za-z0-9_-]+$/.test(segment) && !COMMON_DOTFILE_BASENAMES.has(segment.toLowerCase()) && !KNOWN_HIDDEN_PATH_SEGMENTS.has(segment.toLowerCase()))) {
342
+ return false;
343
+ }
344
+ if (/^[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z][A-Za-z0-9_-]*){2,}$/i.test(normalized) && !hasKnownFileExtension(base)) {
345
+ return false;
346
+ }
347
+ if (/^\.[A-Za-z0-9_-]+\.[A-Za-z0-9._-]+$/.test(base) && !COMMON_DOTFILE_BASENAMES.has(base) && !COMMON_DOTFILE_BASENAMES.has(lowered)) {
348
+ return false;
252
349
  }
253
350
 
254
- if (normalized.includes('/')) {
255
- return /\.(?:md|mdc|txt|rst|json|jsonc|ya?ml|toml|conf|sh|ps1|js|cjs|mjs|ts|tsx|jsx|py|go|rs|java|kt|cs|rb|php|swift)$/i.test(base);
351
+ if (KNOWN_CONVENTION_PATHS.has(normalized) || SPECIAL_FILE_BASENAMES.has(base) || COMMON_DOTFILE_BASENAMES.has(base) || COMMON_DOTFILE_BASENAMES.has(lowered)) {
352
+ return true;
256
353
  }
257
354
 
258
- return /^(?:\.?[A-Za-z0-9_-]+\.)[A-Za-z0-9._-]+$/i.test(normalized);
355
+ return hasKnownFileExtension(base);
259
356
  }
260
357
 
261
358
  function resolveRepoPath(ctx, fromFile, candidate, mode = 'relative-to-file') {
@@ -277,11 +374,34 @@ function getScannableLines(content) {
277
374
  const lines = String(content || '').split(/\r?\n/);
278
375
  const output = [];
279
376
  let fence = null;
377
+ let htmlComment = false;
378
+ let frontmatter = false;
379
+ let frontmatterConsumed = false;
280
380
 
281
381
  for (let index = 0; index < lines.length; index++) {
282
382
  const line = lines[index];
283
383
  const trimmed = line.trim();
284
384
 
385
+ if (!frontmatterConsumed && index === 0 && /^(---|\+\+\+)$/.test(trimmed)) {
386
+ frontmatter = true;
387
+ frontmatterConsumed = true;
388
+ continue;
389
+ }
390
+
391
+ if (frontmatter) {
392
+ if (/^(---|\+\+\+)$/.test(trimmed)) {
393
+ frontmatter = false;
394
+ }
395
+ continue;
396
+ }
397
+
398
+ if (!fence && htmlComment) {
399
+ if (trimmed.includes('-->')) {
400
+ htmlComment = false;
401
+ }
402
+ continue;
403
+ }
404
+
285
405
  if (!fence && /^(```|~~~)/.test(trimmed)) {
286
406
  fence = trimmed.slice(0, 3);
287
407
  continue;
@@ -294,6 +414,13 @@ function getScannableLines(content) {
294
414
  continue;
295
415
  }
296
416
 
417
+ if (/^<!--/.test(trimmed)) {
418
+ if (!trimmed.includes('-->')) {
419
+ htmlComment = true;
420
+ }
421
+ continue;
422
+ }
423
+
297
424
  output.push({ lineNumber: index + 1, text: line });
298
425
  }
299
426
 
@@ -512,6 +639,7 @@ module.exports = {
512
639
  hasLegacyAiderPin,
513
640
  isClearlyLocalMcpBinary,
514
641
  isKnownConventionPath,
642
+ lineHasExampleContext,
515
643
  looksLikeRelativeFileReference,
516
644
  normalizeCandidatePath,
517
645
  platformForFile,