@serdaraytac/agentsitter 1.0.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.
- package/LICENSE +91 -0
- package/README.md +349 -0
- package/dist/analyzer.d.ts +78 -0
- package/dist/analyzer.d.ts.map +1 -0
- package/dist/analyzer.js +815 -0
- package/dist/analyzer.js.map +1 -0
- package/dist/browser.d.ts +5 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +5 -0
- package/dist/browser.js.map +1 -0
- package/dist/mcp-server.d.ts +3 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +323 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/optimizer.d.ts +9 -0
- package/dist/optimizer.d.ts.map +1 -0
- package/dist/optimizer.js +345 -0
- package/dist/optimizer.js.map +1 -0
- package/dist/parser.d.ts +20 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +115 -0
- package/dist/parser.js.map +1 -0
- package/dist/platforms.d.ts +12 -0
- package/dist/platforms.d.ts.map +1 -0
- package/dist/platforms.js +143 -0
- package/dist/platforms.js.map +1 -0
- package/dist/scorer.d.ts +14 -0
- package/dist/scorer.d.ts.map +1 -0
- package/dist/scorer.js +124 -0
- package/dist/scorer.js.map +1 -0
- package/package.json +56 -0
package/dist/analyzer.js
ADDED
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
import { getProfile } from "./platforms.js";
|
|
2
|
+
const CHARS_PER_TOKEN = 4;
|
|
3
|
+
// Patterns derived from Anthropic context engineering article, The Prompt Report (arXiv:2406.06608),
|
|
4
|
+
// and prompt knowledge-gap research (arXiv:2501.11709).
|
|
5
|
+
const VAGUE_PATTERNS = [
|
|
6
|
+
// --- unmeasurable-quality: subjective adjectives with no measurable criterion ---
|
|
7
|
+
{ pattern: /\bwrite\s+(good|great|better|clean|quality|nice)\b/i, reason: "Subjective quality term — specify measurable criteria (e.g. passes linting, all tests green)", category: "unmeasurable-quality" },
|
|
8
|
+
{ pattern: /\b(elegant|robust)\b/i, reason: "Subjective attribute — define what this means concretely for this codebase", category: "unmeasurable-quality" },
|
|
9
|
+
{ pattern: /\bwell[\s-]?(written|structured|organized|documented)\b/i, reason: "Unmeasurable quality claim — replace with a specific convention or checklist item", category: "unmeasurable-quality" },
|
|
10
|
+
{ pattern: /\bhigh[\s-]?quality\b/i, reason: "\"High quality\" is not measurable — specify the criteria (test coverage, lint, types)", category: "unmeasurable-quality" },
|
|
11
|
+
{ pattern: /\bmaintainable\b/i, reason: "\"Maintainable\" is subjective — describe concrete patterns (e.g. max function length, no side effects)", category: "unmeasurable-quality" },
|
|
12
|
+
{ pattern: /\breadable\b/i, reason: "\"Readable\" is subjective — specify conventions (naming rules, comment policy, line length)", category: "unmeasurable-quality" },
|
|
13
|
+
{ pattern: /\bproper(ly)?\b/i, reason: "\"Proper\" is undefined — specify what the correct approach is in this context", category: "unmeasurable-quality" },
|
|
14
|
+
// --- false-shared-context: rules that assume unstated shared knowledge ---
|
|
15
|
+
{ pattern: /\bfollow\s+best\s+practices\b/i, reason: "\"Best practices\" is undefined — name the specific practices or link a reference", category: "false-shared-context" },
|
|
16
|
+
{ pattern: /\buse\s+common\s+sense\b/i, reason: "\"Common sense\" assumes shared context the model may not have — make the rule explicit", category: "false-shared-context" },
|
|
17
|
+
{ pattern: /\buse\s+(your\s+)?judgment\b/i, reason: "Delegating to judgment provides no actionable guidance — define the decision criteria", category: "false-shared-context" },
|
|
18
|
+
{ pattern: /\buse\s+standard\s+patterns?\b/i, reason: "\"Standard patterns\" is ambiguous — specify which patterns apply (e.g. repository pattern, factory)", category: "false-shared-context" },
|
|
19
|
+
{ pattern: /\bindustry\s+standards?\b/i, reason: "\"Industry standard\" varies by context — name the specific standard or spec", category: "false-shared-context" },
|
|
20
|
+
{ pattern: /\bfollow\s+(the\s+)?conventions?\b/i, reason: "\"Conventions\" is undefined here — specify which file or section defines them", category: "false-shared-context" },
|
|
21
|
+
{ pattern: /\bconventional\s+(approach|way|method)\b/i, reason: "\"Conventional\" assumes shared knowledge — describe the expected approach explicitly", category: "false-shared-context" },
|
|
22
|
+
{ pattern: /\bstandard\s+(way|approach|practice)\b/i, reason: "\"Standard\" is undefined without a reference — name the specific practice", category: "false-shared-context" },
|
|
23
|
+
// --- passive-voice: passive constructions that omit the responsible agent ---
|
|
24
|
+
{ pattern: /\bshould\s+be\s+(done|handled|implemented|addressed|considered|reviewed|tested)\b/i, reason: "Passive voice omits who is responsible — rewrite as an active directive (e.g. \"You must handle X by...\")", category: "passive-voice" },
|
|
25
|
+
{ pattern: /\bneeds?\s+to\s+be\s+(handled|done|checked|fixed|resolved|addressed)\b/i, reason: "Passive construction — specify who does this and how", category: "passive-voice" },
|
|
26
|
+
{ pattern: /\bmust\s+be\s+considered\b/i, reason: "\"Must be considered\" has no action — specify what action to take when this applies", category: "passive-voice" },
|
|
27
|
+
{ pattern: /\bis\s+expected\s+to\b/i, reason: "Passive expectation — rewrite as an explicit directive with a clear subject", category: "passive-voice" },
|
|
28
|
+
// --- weak-obligation: soft modals that dilute the rule's force ---
|
|
29
|
+
{ pattern: /\btry\s+to\b/i, reason: "\"Try to\" is a soft obligation — use \"must\" or \"always\" if the rule is mandatory, or remove if truly optional", category: "weak-obligation" },
|
|
30
|
+
{ pattern: /\battempt\s+to\b/i, reason: "\"Attempt to\" signals optional behavior — state clearly whether this is required or not", category: "weak-obligation" },
|
|
31
|
+
{ pattern: /\bconsider\s+(using|adding|implementing|making|doing)\b/i, reason: "\"Consider\" leaves the decision open — either make it a rule or remove it", category: "weak-obligation" },
|
|
32
|
+
{ pattern: /\bmight\s+want\s+to\b/i, reason: "\"Might want to\" is not a directive — state the rule clearly", category: "weak-obligation" },
|
|
33
|
+
{ pattern: /\bit\s+would\s+be\s+(good|nice|helpful|better)\s+to\b/i, reason: "Suggestion rather than a rule — convert to a clear directive or omit", category: "weak-obligation" },
|
|
34
|
+
{ pattern: /\bideally\b/i, reason: "\"Ideally\" implies the rule is aspirational, not enforced — clarify the actual expectation", category: "weak-obligation" },
|
|
35
|
+
// --- vague-condition: conditions without measurable thresholds ---
|
|
36
|
+
{ pattern: /\bappropriate(ly)?\b/i, reason: "\"Appropriate\" is subjective — define the criteria that determine appropriateness", category: "vague-condition" },
|
|
37
|
+
{ pattern: /\bas\s+needed\b/i, reason: "\"As needed\" is ambiguous — define the condition that triggers this action", category: "vague-condition" },
|
|
38
|
+
{ pattern: /\bwhen\s+(necessary|applicable|possible|appropriate)\b/i, reason: "Conditional without a defined trigger — specify when exactly this applies", category: "vague-condition" },
|
|
39
|
+
{ pattern: /\bif\s+(necessary|needed|required|applicable)\b/i, reason: "Conditional without defined criteria — make the condition explicit", category: "vague-condition" },
|
|
40
|
+
{ pattern: /\bin\s+most\s+cases\b/i, reason: "\"In most cases\" is undefined — either make the rule universal or list the exceptions explicitly", category: "vague-condition" },
|
|
41
|
+
{ pattern: /\bin\s+general\b/i, reason: "\"In general\" allows uncontrolled exceptions — state the rule and its explicit exceptions separately", category: "vague-condition" },
|
|
42
|
+
{ pattern: /\bwhere\s+(possible|feasible)\b/i, reason: "\"Where possible\" creates an escape hatch without criteria — define what makes it impossible", category: "vague-condition" },
|
|
43
|
+
{ pattern: /\bfor\s+(large|complex|small)\s+(files?|functions?|classes?|components?)\b/i, reason: "Threshold is undefined — specify a measurable limit (e.g. functions over 50 lines, files over 300 lines)", category: "vague-condition" },
|
|
44
|
+
// --- comparative-without-baseline: comparisons with no reference point ---
|
|
45
|
+
{ pattern: /\bas\s+\w+\s+as\s+possible\b/i, reason: "Relative goal without a stopping criterion — specify the concrete target (e.g. under 200ms, under 50 lines)", category: "comparative-without-baseline" },
|
|
46
|
+
{ pattern: /\bimprove\s+(performance|readability|maintainability|quality)\b/i, reason: "\"Improve\" without a baseline or target is not actionable — specify the current state and goal", category: "comparative-without-baseline" },
|
|
47
|
+
{ pattern: /\b(better|cleaner|simpler|faster)\s+(code|approach|solution|implementation)\b/i, reason: "Comparative without a reference point — define what the acceptable baseline looks like", category: "comparative-without-baseline" },
|
|
48
|
+
{ pattern: /\befficient(ly)?\s+as\s+possible\b/i, reason: "Relative efficiency without a benchmark — define a measurable performance target", category: "comparative-without-baseline" },
|
|
49
|
+
{ pattern: /\boptimize\s+(for\s+)?(performance|speed|memory|readability)\b/i, reason: "Optimization without a target metric — specify the threshold or trade-off criteria", category: "comparative-without-baseline" },
|
|
50
|
+
// --- outcome-without-criterion: desired outcomes with no success definition ---
|
|
51
|
+
{ pattern: /\bensure\s+(quality|correctness|accuracy|consistency)\b/i, reason: "Vague outcome directive — define how quality/correctness is verified (e.g. all tests pass, no type errors)", category: "outcome-without-criterion" },
|
|
52
|
+
{ pattern: /\bmaintain\s+(standards?|quality|consistency)\b/i, reason: "\"Maintain\" without a definition is not enforceable — reference the specific standard or checklist", category: "outcome-without-criterion" },
|
|
53
|
+
{ pattern: /\bbe\s+(thorough|careful|diligent|mindful|consistent)\b/i, reason: "Behavioral directive without success criteria — describe what thoroughness/care means in this context", category: "outcome-without-criterion" },
|
|
54
|
+
{ pattern: /\bpay\s+attention\s+to\b/i, reason: "\"Pay attention to\" has no defined action — replace with a concrete check or rule", category: "outcome-without-criterion" },
|
|
55
|
+
{ pattern: /\bhandle\s+(errors?|edge\s+cases?)\s+properly\b/i, reason: "\"Properly\" is undefined — specify the error handling strategy (e.g. log and rethrow, return Result type)", category: "outcome-without-criterion" },
|
|
56
|
+
{ pattern: /\bsimple(r|ly)?\b/i, reason: "\"Simple\" is relative — describe what simplicity means here (e.g. cyclomatic complexity ≤ 5)", category: "outcome-without-criterion" },
|
|
57
|
+
];
|
|
58
|
+
export function analyzeTokenCost(config, profile) {
|
|
59
|
+
profile ??= getProfile(config.platform);
|
|
60
|
+
const tokenCount = Math.ceil(config.charCount / CHARS_PER_TOKEN);
|
|
61
|
+
const costPerSession = profile.subscriptionBased
|
|
62
|
+
? 0
|
|
63
|
+
: tokenCount * (profile.costPerMillion / 1_000_000);
|
|
64
|
+
return {
|
|
65
|
+
charCount: config.charCount,
|
|
66
|
+
tokenCount,
|
|
67
|
+
estimatedCostUsd: costPerSession,
|
|
68
|
+
costPerSession,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function analyzeVagueRules(config) {
|
|
72
|
+
const vagueLines = [];
|
|
73
|
+
for (let i = 0; i < config.lines.length; i++) {
|
|
74
|
+
const line = config.lines[i];
|
|
75
|
+
if (!line.trim() || line.startsWith("#"))
|
|
76
|
+
continue;
|
|
77
|
+
for (const { pattern, reason, category } of VAGUE_PATTERNS) {
|
|
78
|
+
if (pattern.test(line)) {
|
|
79
|
+
vagueLines.push({ line: i + 1, text: line.trim(), reason, category });
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { vagueLines };
|
|
85
|
+
}
|
|
86
|
+
export function analyzeMissingSections(config, profile) {
|
|
87
|
+
profile ??= getProfile(config.platform);
|
|
88
|
+
const expected = profile.expectedSections;
|
|
89
|
+
const headings = config.sections.map((s) => s.heading.toLowerCase());
|
|
90
|
+
const present = [];
|
|
91
|
+
const missing = [];
|
|
92
|
+
for (const section of expected) {
|
|
93
|
+
const found = headings.some((h) => h.includes(section));
|
|
94
|
+
if (found) {
|
|
95
|
+
present.push(section);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
missing.push(section);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { missing, present };
|
|
102
|
+
}
|
|
103
|
+
export function analyzeDuplicates(config) {
|
|
104
|
+
const phraseMap = new Map();
|
|
105
|
+
for (let i = 0; i < config.lines.length; i++) {
|
|
106
|
+
const line = config.lines[i].trim().toLowerCase();
|
|
107
|
+
if (line.length < 20 || line.startsWith("#"))
|
|
108
|
+
continue;
|
|
109
|
+
// Extract 5-word ngrams to find duplicate content
|
|
110
|
+
const words = line.split(/\s+/);
|
|
111
|
+
if (words.length < 5)
|
|
112
|
+
continue;
|
|
113
|
+
for (let j = 0; j <= words.length - 5; j++) {
|
|
114
|
+
const phrase = words.slice(j, j + 5).join(" ");
|
|
115
|
+
if (!phraseMap.has(phrase)) {
|
|
116
|
+
phraseMap.set(phrase, []);
|
|
117
|
+
}
|
|
118
|
+
phraseMap.get(phrase).push(i + 1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const duplicatePhrases = Array.from(phraseMap.entries())
|
|
122
|
+
.filter(([, lines]) => {
|
|
123
|
+
// Only flag if it appears on different lines
|
|
124
|
+
const uniqueLines = [...new Set(lines)];
|
|
125
|
+
return uniqueLines.length > 1;
|
|
126
|
+
})
|
|
127
|
+
.map(([phrase, lines]) => ({
|
|
128
|
+
phrase,
|
|
129
|
+
occurrences: [...new Set(lines)].length,
|
|
130
|
+
lines: [...new Set(lines)],
|
|
131
|
+
}))
|
|
132
|
+
// Deduplicate overlapping ngrams — keep only the first representative
|
|
133
|
+
.slice(0, 10);
|
|
134
|
+
return { duplicatePhrases };
|
|
135
|
+
}
|
|
136
|
+
export function analyzeAttentionPlacement(config) {
|
|
137
|
+
const totalLines = config.lines.length;
|
|
138
|
+
const headSize = Math.min(10, Math.floor(totalLines * 0.15));
|
|
139
|
+
const tailSize = Math.min(10, Math.floor(totalLines * 0.15));
|
|
140
|
+
const headLines = config.lines.slice(0, headSize);
|
|
141
|
+
const tailLines = config.lines.slice(Math.max(0, totalLines - tailSize));
|
|
142
|
+
const criticalKeywords = /\b(important|critical|never|always|must|required|forbidden|do not|don't)\b/i;
|
|
143
|
+
const criticalInHead = headLines.some((l) => criticalKeywords.test(l));
|
|
144
|
+
const criticalInTail = tailLines.some((l) => criticalKeywords.test(l));
|
|
145
|
+
const suggestions = [];
|
|
146
|
+
if (!criticalInHead && !criticalInTail) {
|
|
147
|
+
// Check if critical content exists anywhere
|
|
148
|
+
const hasCritical = config.lines.some((l) => criticalKeywords.test(l));
|
|
149
|
+
if (hasCritical) {
|
|
150
|
+
suggestions.push("Move critical rules (never/always/must) to the first or last 15% of the file for better LLM attention");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!criticalInHead && criticalInTail) {
|
|
154
|
+
suggestions.push("Critical rules found only at the tail — consider also adding a brief summary at the top");
|
|
155
|
+
}
|
|
156
|
+
return { criticalInHead, criticalInTail, headLines, tailLines, suggestions };
|
|
157
|
+
}
|
|
158
|
+
export function analyzeStructure(config) {
|
|
159
|
+
const hasHeadings = config.sections.length > 0;
|
|
160
|
+
const headingCount = config.sections.length;
|
|
161
|
+
const maxDepth = config.sections.reduce((max, s) => Math.max(max, s.level), 0);
|
|
162
|
+
// Lines that are long prose paragraphs (>120 chars, not a heading/list)
|
|
163
|
+
const longParagraphLines = [];
|
|
164
|
+
for (let i = 0; i < config.lines.length; i++) {
|
|
165
|
+
const line = config.lines[i];
|
|
166
|
+
if (line.length > 120 && !line.startsWith("#") && !line.startsWith("-") && !line.startsWith("*")) {
|
|
167
|
+
longParagraphLines.push(i + 1);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Rules not under any heading (appear before the first heading)
|
|
171
|
+
const firstHeadingLine = config.sections[0]?.startLine ?? config.lines.length;
|
|
172
|
+
const unorganizedRuleCount = config.lines
|
|
173
|
+
.slice(0, firstHeadingLine)
|
|
174
|
+
.filter((l) => l.trim() && !l.startsWith("#")).length;
|
|
175
|
+
return { hasHeadings, headingCount, maxDepth, longParagraphLines, unorganizedRuleCount };
|
|
176
|
+
}
|
|
177
|
+
// Copilot: repository-wide instructions at .github/copilot-instructions.md; path-specific rules at
|
|
178
|
+
// .github/instructions/*.instructions.md with applyTo frontmatter. excludeAgent accepts only
|
|
179
|
+
// "code-review" or "cloud-agent". Source: docs.github.com/en/copilot/customizing-copilot/
|
|
180
|
+
function checkCopilotFormat(config) {
|
|
181
|
+
const issues = [];
|
|
182
|
+
const normalized = config.filename.toLowerCase().replace(/\\/g, "/");
|
|
183
|
+
const basename = normalized.split("/").pop() ?? normalized;
|
|
184
|
+
const isPathSpecific = normalized.includes(".github/instructions/");
|
|
185
|
+
// Repository-wide instructions.md must live inside .github/ — files elsewhere are not recognized.
|
|
186
|
+
if (basename === "copilot-instructions.md" && !normalized.includes(".github/")) {
|
|
187
|
+
issues.push({
|
|
188
|
+
code: "COPILOT_WRONG_LOCATION",
|
|
189
|
+
severity: "critical",
|
|
190
|
+
message: "copilot-instructions.md must be placed inside the .github/ directory — GitHub Copilot does not recognize it at any other location (docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot)",
|
|
191
|
+
});
|
|
192
|
+
return issues;
|
|
193
|
+
}
|
|
194
|
+
// Path-specific instruction files must end in .instructions.md and require applyTo frontmatter.
|
|
195
|
+
if (isPathSpecific) {
|
|
196
|
+
if (!basename.endsWith(".instructions.md")) {
|
|
197
|
+
issues.push({
|
|
198
|
+
code: "COPILOT_INSTRUCTIONS_WRONG_EXTENSION",
|
|
199
|
+
severity: "warning",
|
|
200
|
+
message: "Files in .github/instructions/ must end with .instructions.md to be recognized by GitHub Copilot — rename this file accordingly",
|
|
201
|
+
});
|
|
202
|
+
return issues;
|
|
203
|
+
}
|
|
204
|
+
const hasFrontmatter = config.content.trimStart().startsWith("---");
|
|
205
|
+
if (!hasFrontmatter) {
|
|
206
|
+
issues.push({
|
|
207
|
+
code: "COPILOT_INSTRUCTIONS_MISSING_APPLY_TO",
|
|
208
|
+
severity: "warning",
|
|
209
|
+
message: "Path-specific instruction files in .github/instructions/ should have an 'applyTo:' frontmatter field to scope them to matching files (e.g. applyTo: '**/*.rb') — without it the file is applied repository-wide",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
const closeIdx = config.content.indexOf("---", 3);
|
|
214
|
+
const frontmatter = closeIdx > -1 ? config.content.slice(3, closeIdx) : "";
|
|
215
|
+
if (!frontmatter.includes("applyTo")) {
|
|
216
|
+
issues.push({
|
|
217
|
+
code: "COPILOT_INSTRUCTIONS_MISSING_APPLY_TO",
|
|
218
|
+
severity: "warning",
|
|
219
|
+
message: "Frontmatter is present but missing 'applyTo:' — add a glob pattern (e.g. applyTo: '**/*.rb') to scope this instruction file to specific paths",
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
// excludeAgent only accepts "code-review" or "cloud-agent"
|
|
223
|
+
const excludeMatch = frontmatter.match(/excludeAgent:\s*["']?([^"'\n]+)["']?/);
|
|
224
|
+
if (excludeMatch) {
|
|
225
|
+
const val = excludeMatch[1].trim();
|
|
226
|
+
if (val !== "code-review" && val !== "cloud-agent") {
|
|
227
|
+
issues.push({
|
|
228
|
+
code: "COPILOT_INVALID_EXCLUDE_AGENT",
|
|
229
|
+
severity: "warning",
|
|
230
|
+
message: `excludeAgent: "${val}" is not a valid value — GitHub Copilot only accepts "code-review" or "cloud-agent"`,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return issues;
|
|
236
|
+
}
|
|
237
|
+
// Repository-wide copilot-instructions.md size checks (existing documented limits).
|
|
238
|
+
if (config.charCount > 4_000) {
|
|
239
|
+
issues.push({
|
|
240
|
+
code: "COPILOT_SIZE_LIMIT",
|
|
241
|
+
severity: "warning",
|
|
242
|
+
message: `File is ${config.charCount} characters — GitHub Copilot's code review feature reads only the first 4,000 characters; content beyond that is ignored during reviews`,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
if (config.lines.length > 1_000) {
|
|
246
|
+
issues.push({
|
|
247
|
+
code: "COPILOT_LINE_LIMIT",
|
|
248
|
+
severity: "warning",
|
|
249
|
+
message: `File has ${config.lines.length} lines — GitHub documentation states response quality may deteriorate beyond ~1,000 lines; split into multiple instruction files`,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return issues;
|
|
253
|
+
}
|
|
254
|
+
// Cursor: .cursor/rules/*.md files require YAML frontmatter with valid keys.
|
|
255
|
+
// Valid frontmatter keys: description, globs, alwaysApply — any others are silently ignored.
|
|
256
|
+
// .cursorrules is the legacy format; the current system is .cursor/rules/*.md (or *.mdc).
|
|
257
|
+
// Best practice: keep rules under 500 lines; split large rules into composable files.
|
|
258
|
+
// Source: cursor.com/docs/context/rules
|
|
259
|
+
function checkCursorFormat(config) {
|
|
260
|
+
const issues = [];
|
|
261
|
+
const normalized = config.filename.toLowerCase().replace(/\\/g, "/");
|
|
262
|
+
const basename = normalized.split("/").pop() ?? normalized;
|
|
263
|
+
// .cursorrules is the legacy format — current Cursor docs no longer mention it
|
|
264
|
+
if (basename === ".cursorrules") {
|
|
265
|
+
issues.push({
|
|
266
|
+
code: "CURSOR_CURSORRULES_LEGACY",
|
|
267
|
+
severity: "info",
|
|
268
|
+
message: ".cursorrules is the legacy Cursor rules format — migrate to .cursor/rules/*.md (or *.mdc) files with YAML frontmatter for full scope control; the old format is no longer documented at cursor.com/docs/context/rules",
|
|
269
|
+
});
|
|
270
|
+
return issues;
|
|
271
|
+
}
|
|
272
|
+
if (!normalized.includes(".cursor/rules/"))
|
|
273
|
+
return issues;
|
|
274
|
+
// 500-line best practice (explicitly documented at cursor.com/docs/context/rules)
|
|
275
|
+
if (config.lines.length > 500) {
|
|
276
|
+
issues.push({
|
|
277
|
+
code: "CURSOR_RULE_TOO_LONG",
|
|
278
|
+
severity: "warning",
|
|
279
|
+
message: `Rule file has ${config.lines.length} lines — Cursor documentation recommends keeping rules under 500 lines; split into multiple composable rule files`,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
const hasFrontmatter = config.content.trimStart().startsWith("---");
|
|
283
|
+
if (!hasFrontmatter) {
|
|
284
|
+
issues.push({
|
|
285
|
+
code: "CURSOR_MISSING_FRONTMATTER",
|
|
286
|
+
severity: "warning",
|
|
287
|
+
message: "Cursor rule files in .cursor/rules/ should have YAML frontmatter — add 'globs' (file patterns) or 'alwaysApply: true' to control when the rule applies",
|
|
288
|
+
});
|
|
289
|
+
return issues;
|
|
290
|
+
}
|
|
291
|
+
const closeIdx = config.content.indexOf("---", 3);
|
|
292
|
+
const frontmatter = closeIdx > -1 ? config.content.slice(3, closeIdx) : "";
|
|
293
|
+
// Detect unknown frontmatter keys — only description, globs, alwaysApply are valid
|
|
294
|
+
const VALID_CURSOR_KEYS = ["description", "globs", "alwaysApply"];
|
|
295
|
+
const keyMatches = frontmatter.matchAll(/^(\w[\w-]*):/gm);
|
|
296
|
+
for (const match of keyMatches) {
|
|
297
|
+
const key = match[1];
|
|
298
|
+
if (!VALID_CURSOR_KEYS.includes(key)) {
|
|
299
|
+
issues.push({
|
|
300
|
+
code: "CURSOR_UNKNOWN_FRONTMATTER_KEY",
|
|
301
|
+
severity: "info",
|
|
302
|
+
message: `Unknown frontmatter key '${key}' — Cursor only recognizes 'description', 'globs', and 'alwaysApply'; unknown keys are silently ignored`,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (!frontmatter.includes("globs") && !frontmatter.includes("alwaysApply") && !frontmatter.includes("description")) {
|
|
307
|
+
issues.push({
|
|
308
|
+
code: "CURSOR_FRONTMATTER_INCOMPLETE",
|
|
309
|
+
severity: "info",
|
|
310
|
+
message: "Cursor rule frontmatter is present but missing scope — add 'globs: [\"**/*.ts\"]' or 'alwaysApply: true'; without these the rule requires manual @-mention to activate",
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return issues;
|
|
314
|
+
}
|
|
315
|
+
// Shared: detect build/test commands that appear outside fenced code blocks.
|
|
316
|
+
const COMMAND_PATTERN = /\b(npm run|npm test|npm start|yarn run|yarn test|npx |pnpm run|pnpm test|make |cargo run|go run|python |pytest|jest|vitest)\b/;
|
|
317
|
+
function findCommandOutsideBlock(config, code, severity, message) {
|
|
318
|
+
let inCodeBlock = false;
|
|
319
|
+
for (let i = 0; i < config.lines.length; i++) {
|
|
320
|
+
const line = config.lines[i];
|
|
321
|
+
if (line.startsWith("```")) {
|
|
322
|
+
inCodeBlock = !inCodeBlock;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (!inCodeBlock && !line.startsWith("#") && COMMAND_PATTERN.test(line)) {
|
|
326
|
+
return [{ code, severity, message, line: i + 1 }];
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
// Claude Code: CLAUDE.md is injected into every context window — completeness and token efficiency both matter.
|
|
332
|
+
// Commands must live in fenced code blocks so Claude Code can parse and run them without reading package.json.
|
|
333
|
+
// Supports @-import syntax (@./path.md) to split large files; imports inside code blocks are not resolved.
|
|
334
|
+
// Unfilled [TODO:] placeholders left in production configs are worse than omission — the model cannot act on them.
|
|
335
|
+
// Source: docs.anthropic.com/en/docs/claude-code/memory
|
|
336
|
+
function checkClaudeFormat(config) {
|
|
337
|
+
const issues = [];
|
|
338
|
+
// Commands outside code blocks
|
|
339
|
+
const commandIssues = findCommandOutsideBlock(config, "CLAUDE_COMMANDS_NOT_IN_BLOCK", "info", "Claude Code reads build/test commands from fenced code blocks — wrap them in ``` for reliable parsing");
|
|
340
|
+
issues.push(...commandIssues);
|
|
341
|
+
// No build/test commands anywhere — highest single-value addition to any CLAUDE.md
|
|
342
|
+
if (commandIssues.length === 0 && !COMMAND_PATTERN.test(config.content)) {
|
|
343
|
+
issues.push({
|
|
344
|
+
code: "CLAUDE_MISSING_BUILD_COMMANDS",
|
|
345
|
+
severity: "warning",
|
|
346
|
+
message: "No build, test, or lint commands found — add a fenced code block with runnable commands (e.g. npm test, npm run build) so Claude Code can execute them without reading package.json on every request",
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
// Unfilled [TODO:] placeholders — incomplete directives the model cannot act on
|
|
350
|
+
const todoCount = (config.content.match(/\[TODO:/g) ?? []).length;
|
|
351
|
+
if (todoCount > 0) {
|
|
352
|
+
issues.push({
|
|
353
|
+
code: "CLAUDE_PLACEHOLDER_FOUND",
|
|
354
|
+
severity: "warning",
|
|
355
|
+
message: `${todoCount} unfilled [TODO:] placeholder(s) — the model cannot act on incomplete directives; fill them in or remove them`,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
// @-imports inside fenced code blocks are treated as literal text, not resolved
|
|
359
|
+
let insideBlock = false;
|
|
360
|
+
const blockedImports = [];
|
|
361
|
+
for (let i = 0; i < config.lines.length; i++) {
|
|
362
|
+
const line = config.lines[i];
|
|
363
|
+
if (line.trimStart().startsWith("```")) {
|
|
364
|
+
insideBlock = !insideBlock;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (insideBlock && /@(?:\.\/|\.\.\/|\/)/.test(line)) {
|
|
368
|
+
blockedImports.push(i + 1);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (blockedImports.length > 0) {
|
|
372
|
+
issues.push({
|
|
373
|
+
code: "CLAUDE_IMPORT_IN_CODE_BLOCK",
|
|
374
|
+
severity: "info",
|
|
375
|
+
message: `@-import on line${blockedImports.length > 1 ? "s" : ""} ${blockedImports.join(", ")} is inside a fenced code block — Claude Code does not resolve @-imports within code blocks; move outside to activate`,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
// Large file with no @-imports — splitting into subdirectory CLAUDE.md files reduces per-context cost
|
|
379
|
+
if (config.charCount > 10_000 && !/@(?:\.\/|\.\.\/|\/)/.test(config.content)) {
|
|
380
|
+
issues.push({
|
|
381
|
+
code: "CLAUDE_SUBDIR_SPLIT_RECOMMENDED",
|
|
382
|
+
severity: "info",
|
|
383
|
+
message: `File is ${config.charCount} characters — Claude Code loads CLAUDE.md from every directory it navigates to; split into subdirectory CLAUDE.md files to reduce per-context token cost and keep rules close to the code they govern`,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
return issues;
|
|
387
|
+
}
|
|
388
|
+
// Cline: .clinerules/ directory supports YAML frontmatter with 'paths:' for glob scoping.
|
|
389
|
+
// Critical distinction: Cline uses 'paths:' (not 'globs:' — that is Cursor's key, silently ignored by Cline).
|
|
390
|
+
// Empty paths: [] means the rule never activates. Only .md and .txt files are processed.
|
|
391
|
+
// No @-import syntax — composition is done via the .clinerules/ directory itself (merge all files).
|
|
392
|
+
// Source: docs.cline.bot/customization/cline-rules
|
|
393
|
+
function checkClineFormat(config) {
|
|
394
|
+
const issues = [];
|
|
395
|
+
const normalized = config.filename.toLowerCase().replace(/\\/g, "/");
|
|
396
|
+
const isInRulesDir = normalized.includes(".clinerules/");
|
|
397
|
+
if (isInRulesDir) {
|
|
398
|
+
const ext = normalized.split(".").pop() ?? "";
|
|
399
|
+
if (ext !== "md" && ext !== "txt") {
|
|
400
|
+
issues.push({
|
|
401
|
+
code: "CLINE_UNSUPPORTED_EXTENSION",
|
|
402
|
+
severity: "warning",
|
|
403
|
+
message: `File has .${ext} extension — Cline only processes .md and .txt files from the .clinerules/ directory; this file will be silently ignored`,
|
|
404
|
+
});
|
|
405
|
+
return issues;
|
|
406
|
+
}
|
|
407
|
+
if (config.content.trimStart().startsWith("---")) {
|
|
408
|
+
const closeIdx = config.content.indexOf("---", 3);
|
|
409
|
+
const frontmatter = closeIdx > -1 ? config.content.slice(3, closeIdx) : "";
|
|
410
|
+
// 'globs:' is Cursor's key — silently ignored by Cline, rule activates unconditionally
|
|
411
|
+
if (frontmatter.includes("globs:") && !frontmatter.includes("paths:")) {
|
|
412
|
+
issues.push({
|
|
413
|
+
code: "CLINE_WRONG_FRONTMATTER_KEY",
|
|
414
|
+
severity: "warning",
|
|
415
|
+
message: "Frontmatter uses 'globs:' which is Cursor's key — Cline uses 'paths:' for glob scoping; 'globs:' is silently ignored and the rule will activate on every request",
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
// Empty paths list means the rule never fires
|
|
419
|
+
if (/paths:\s*\[\s*\]/.test(frontmatter) || /paths:\s*\n\s*\n/.test(frontmatter)) {
|
|
420
|
+
issues.push({
|
|
421
|
+
code: "CLINE_EMPTY_PATHS",
|
|
422
|
+
severity: "warning",
|
|
423
|
+
message: "Frontmatter has an empty 'paths:' list — this rule will never activate; add glob patterns (e.g. 'src/**/*.ts') or remove the paths key to make it always active",
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// @ import syntax is not supported in Cline — unlike Gemini CLI or Amp
|
|
429
|
+
let insideBlock = false;
|
|
430
|
+
for (let i = 0; i < config.lines.length; i++) {
|
|
431
|
+
const line = config.lines[i];
|
|
432
|
+
if (line.trimStart().startsWith("```")) {
|
|
433
|
+
insideBlock = !insideBlock;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (!insideBlock && /@[./~]/.test(line)) {
|
|
437
|
+
issues.push({
|
|
438
|
+
code: "CLINE_AT_IMPORT_NOT_SUPPORTED",
|
|
439
|
+
severity: "warning",
|
|
440
|
+
message: `Line ${i + 1}: @-import syntax is not supported in Cline — split content into separate files under .clinerules/ directory instead; all .md and .txt files there are merged automatically`,
|
|
441
|
+
line: i + 1,
|
|
442
|
+
});
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const bulletLines = config.lines.filter((l) => /^\s*[-*]\s/.test(l)).length;
|
|
447
|
+
const contentLines = config.lines.filter((l) => l.trim() && !l.startsWith("#")).length;
|
|
448
|
+
if (contentLines > 5 && bulletLines === 0) {
|
|
449
|
+
issues.push({
|
|
450
|
+
code: "CLINE_UNSTRUCTURED_RULES",
|
|
451
|
+
severity: "info",
|
|
452
|
+
message: "Cline documentation recommends using bullet points and headers to organize rules — \"Bullet points make individual requirements clear\" (docs.cline.bot)",
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
return issues;
|
|
456
|
+
}
|
|
457
|
+
// Codex (AGENTS.md): 32 KiB limit confirmed in source (codex-rs/config/src/config_toml.rs line 67).
|
|
458
|
+
// AGENTS.override.md at the same directory level takes precedence over AGENTS.md.
|
|
459
|
+
// Codex reads hierarchically from Git root down; files concatenate root-first, closer files override.
|
|
460
|
+
// Source: developers.openai.com/codex/guides/agents-md + github.com/openai/codex source
|
|
461
|
+
function checkCodexFormat(config) {
|
|
462
|
+
const issues = [];
|
|
463
|
+
if (config.charCount > 32_768) {
|
|
464
|
+
issues.push({
|
|
465
|
+
code: "CODEX_SIZE_LIMIT",
|
|
466
|
+
severity: "warning",
|
|
467
|
+
message: `File is ${config.charCount} characters — Codex enforces a default 32 KiB (32,768 byte) limit on AGENTS.md via project_doc_max_bytes; content beyond this limit is silently truncated (configurable via ~/.codex/config.toml)`,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
// AGENTS.override.md takes precedence at the same directory level — surface this only when the file
|
|
471
|
+
// is large enough that splitting or overriding becomes relevant (>50% of the 32 KiB limit).
|
|
472
|
+
const basename = config.filename.toLowerCase().split("/").pop() ?? "";
|
|
473
|
+
if (basename === "agents.md" && config.charCount > 16_384) {
|
|
474
|
+
issues.push({
|
|
475
|
+
code: "CODEX_OVERRIDE_FILE_AVAILABLE",
|
|
476
|
+
severity: "info",
|
|
477
|
+
message: "Codex checks for AGENTS.override.md at the same directory level before AGENTS.md — use AGENTS.override.md when you need this directory's rules to take precedence without modifying the shared AGENTS.md",
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
return issues;
|
|
481
|
+
}
|
|
482
|
+
// Windsurf: Windsurf now reads AGENTS.md files dynamically — .windsurfrules is the legacy format.
|
|
483
|
+
// Source: docs.windsurf.com/windsurf/cascade/agents-md
|
|
484
|
+
function checkWindsurfFormat(config) {
|
|
485
|
+
const issues = [];
|
|
486
|
+
if (config.filename.toLowerCase().endsWith(".windsurfrules")) {
|
|
487
|
+
issues.push({
|
|
488
|
+
code: "WINDSURF_PREFER_AGENTS_MD",
|
|
489
|
+
severity: "info",
|
|
490
|
+
message: "Windsurf now reads AGENTS.md files dynamically as it navigates the repo — consider migrating from .windsurfrules to AGENTS.md for better compatibility with the current Windsurf Cascade agent",
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
return issues;
|
|
494
|
+
}
|
|
495
|
+
// Gemini CLI: supports @-import syntax (@./path.md, @../path, @/abs, @~/home) for splitting large files.
|
|
496
|
+
// Imports inside fenced code blocks are silently ignored. Max import depth: 5 levels.
|
|
497
|
+
// Source: geminicli.com/docs/reference/memport (Memory Import Processor)
|
|
498
|
+
function checkGeminiFormat(config) {
|
|
499
|
+
const issues = [];
|
|
500
|
+
let insideBlock = false;
|
|
501
|
+
const activeImports = [];
|
|
502
|
+
const skippedImports = [];
|
|
503
|
+
for (let i = 0; i < config.lines.length; i++) {
|
|
504
|
+
const line = config.lines[i];
|
|
505
|
+
if (line.trimStart().startsWith("```")) {
|
|
506
|
+
insideBlock = !insideBlock;
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
const match = line.match(/@([^\s]+)/);
|
|
510
|
+
if (!match)
|
|
511
|
+
continue;
|
|
512
|
+
if (insideBlock) {
|
|
513
|
+
skippedImports.push(i + 1);
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
const path = match[1];
|
|
517
|
+
if (path.startsWith("./") || path.startsWith("../") || path.startsWith("/") || path.startsWith("~/")) {
|
|
518
|
+
activeImports.push(i + 1);
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
issues.push({
|
|
522
|
+
code: "GEMINI_IMPORT_INVALID_PATH",
|
|
523
|
+
severity: "warning",
|
|
524
|
+
message: `Line ${i + 1}: @${path} — Gemini CLI requires a path prefix: @./relative, @../parent, @/absolute, or @~/home-relative; bare @word is not a valid import`,
|
|
525
|
+
line: i + 1,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// @ imports inside code blocks are silently ignored by Gemini CLI
|
|
531
|
+
if (skippedImports.length > 0) {
|
|
532
|
+
issues.push({
|
|
533
|
+
code: "GEMINI_IMPORT_IN_CODE_BLOCK",
|
|
534
|
+
severity: "info",
|
|
535
|
+
message: `@-import on line${skippedImports.length > 1 ? "s" : ""} ${skippedImports.join(", ")} is inside a fenced code block — Gemini CLI ignores @-imports within code blocks; move outside to activate`,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
// Large file with no active imports — suggest splitting
|
|
539
|
+
if (config.charCount > 10_000 && activeImports.length === 0) {
|
|
540
|
+
issues.push({
|
|
541
|
+
code: "GEMINI_LARGE_NO_IMPORTS",
|
|
542
|
+
severity: "info",
|
|
543
|
+
message: `File is ${config.charCount} characters — Gemini CLI supports @./path.md import syntax to split content across multiple files; imports resolve recursively up to 5 levels deep (geminicli.com/docs/reference/memport)`,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
return issues;
|
|
547
|
+
}
|
|
548
|
+
// OpenCode: AGENTS.md at project root is the primary config file.
|
|
549
|
+
// CLAUDE.md is accepted as a legacy fallback but AGENTS.md is preferred for clarity.
|
|
550
|
+
// No @-import syntax inside AGENTS.md — file composition is handled via opencode.json "instructions" field.
|
|
551
|
+
// Source: opencode.ai/docs/rules
|
|
552
|
+
function checkOpenCodeFormat(config) {
|
|
553
|
+
const issues = [];
|
|
554
|
+
const lower = config.filename.toLowerCase();
|
|
555
|
+
const basename = lower.split("/").pop() ?? lower;
|
|
556
|
+
// CLAUDE.md works as fallback but signals Claude Code intent, not OpenCode
|
|
557
|
+
if (basename === "claude.md") {
|
|
558
|
+
issues.push({
|
|
559
|
+
code: "OPENCODE_PREFERS_AGENTS_MD",
|
|
560
|
+
severity: "info",
|
|
561
|
+
message: "OpenCode reads CLAUDE.md as a legacy fallback — rename to AGENTS.md at the project root to make OpenCode intent explicit (opencode.ai/docs/rules)",
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
// @ import syntax is not supported within AGENTS.md for OpenCode
|
|
565
|
+
// File composition is done via the "instructions" field in opencode.json
|
|
566
|
+
let insideBlock = false;
|
|
567
|
+
for (let i = 0; i < config.lines.length; i++) {
|
|
568
|
+
const line = config.lines[i];
|
|
569
|
+
if (line.trimStart().startsWith("```")) {
|
|
570
|
+
insideBlock = !insideBlock;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if (!insideBlock && /@[./~]/.test(line)) {
|
|
574
|
+
issues.push({
|
|
575
|
+
code: "OPENCODE_AT_IMPORT_NOT_SUPPORTED",
|
|
576
|
+
severity: "warning",
|
|
577
|
+
message: `Line ${i + 1}: @-import syntax is not supported within AGENTS.md for OpenCode — use the "instructions" field in opencode.json to compose multiple files or reference remote URLs (5-second fetch timeout applies)`,
|
|
578
|
+
line: i + 1,
|
|
579
|
+
});
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return issues;
|
|
584
|
+
}
|
|
585
|
+
// Amp: AGENTS.md with @-mention syntax for including other files.
|
|
586
|
+
// Paths not starting with ./ or ../ get **/ prepended implicitly (recursive project-wide match).
|
|
587
|
+
// @-mentions inside fenced code blocks are silently ignored.
|
|
588
|
+
// Source: ampcode.com/manual
|
|
589
|
+
function checkAmpFormat(config) {
|
|
590
|
+
const issues = [];
|
|
591
|
+
if (config.filename.toLowerCase().includes(".amp/instructions")) {
|
|
592
|
+
issues.push({
|
|
593
|
+
code: "AMP_INCORRECT_FILENAME",
|
|
594
|
+
severity: "info",
|
|
595
|
+
message: "Amp's official config file is AGENTS.md (also AGENT.md or CLAUDE.md) placed at the project root — .amp/instructions.md is not recognized by Amp per official documentation (ampcode.com/manual)",
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
let insideBlock = false;
|
|
599
|
+
const implicitRecursive = [];
|
|
600
|
+
const ignoredInBlock = [];
|
|
601
|
+
for (let i = 0; i < config.lines.length; i++) {
|
|
602
|
+
const line = config.lines[i];
|
|
603
|
+
if (line.trimStart().startsWith("```")) {
|
|
604
|
+
insideBlock = !insideBlock;
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
const match = line.match(/@([^\s,]+)/);
|
|
608
|
+
if (!match)
|
|
609
|
+
continue;
|
|
610
|
+
if (insideBlock) {
|
|
611
|
+
ignoredInBlock.push(i + 1);
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
const path = match[1];
|
|
615
|
+
// Paths without ./ or ../ prefix get **/ prepended — may match more files than intended
|
|
616
|
+
if (!path.startsWith("./") && !path.startsWith("../") && !path.startsWith("/") && !path.startsWith("~/") && !path.startsWith("*")) {
|
|
617
|
+
implicitRecursive.push(i + 1);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (ignoredInBlock.length > 0) {
|
|
621
|
+
issues.push({
|
|
622
|
+
code: "AMP_IMPORT_IN_CODE_BLOCK",
|
|
623
|
+
severity: "info",
|
|
624
|
+
message: `@-mention on line${ignoredInBlock.length > 1 ? "s" : ""} ${ignoredInBlock.join(", ")} is inside a fenced code block — Amp ignores @-mentions within code blocks; move outside to activate`,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
for (const lineNum of implicitRecursive) {
|
|
628
|
+
const line = config.lines[lineNum - 1];
|
|
629
|
+
const match = line.match(/@([^\s,]+)/);
|
|
630
|
+
if (!match)
|
|
631
|
+
continue;
|
|
632
|
+
issues.push({
|
|
633
|
+
code: "AMP_IMPORT_IMPLICIT_RECURSIVE",
|
|
634
|
+
severity: "info",
|
|
635
|
+
message: `Line ${lineNum}: @${match[1]} — Amp implicitly prepends **/ to paths not starting with ./ or ../; this matches recursively across the entire project; use @./${match[1]} to pin to the current directory`,
|
|
636
|
+
line: lineNum,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
return issues;
|
|
640
|
+
}
|
|
641
|
+
// Warp: official config file is AGENTS.md (or legacy WARP.md). Filename must be ALL CAPS.
|
|
642
|
+
// .warp/instructions.md does not appear in official Warp documentation (docs.warp.dev/agent-platform/capabilities/rules/).
|
|
643
|
+
function checkWarpFormat(config) {
|
|
644
|
+
const issues = [];
|
|
645
|
+
if (config.filename.toLowerCase().includes(".warp/instructions")) {
|
|
646
|
+
issues.push({
|
|
647
|
+
code: "WARP_INCORRECT_FILENAME",
|
|
648
|
+
severity: "info",
|
|
649
|
+
message: "Warp's official config file is AGENTS.md (or legacy WARP.md) — .warp/instructions.md is not documented; Warp also requires the filename to be ALL CAPS (AGENTS.md, not agents.md)",
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
return issues;
|
|
653
|
+
}
|
|
654
|
+
// Gemini CLI: official docs specify no ordering requirement for GEMINI.md sections.
|
|
655
|
+
// File supports @-import syntax for referencing other files — no platform-specific format checks apply.
|
|
656
|
+
// Firebender: config must be XML — markdown headings and plain text are invalid.
|
|
657
|
+
function checkFirebenderFormat(config) {
|
|
658
|
+
const issues = [];
|
|
659
|
+
const trimmed = config.content.trim();
|
|
660
|
+
if (config.lines.some((l) => /^#{1,6}\s/.test(l))) {
|
|
661
|
+
issues.push({
|
|
662
|
+
code: "FIREBENDER_MARKDOWN_IN_XML",
|
|
663
|
+
severity: "warning",
|
|
664
|
+
message: "firebender.xml expects XML format — markdown headings (#) are not valid XML and will be ignored by Firebender",
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
if (trimmed.length > 0 && !trimmed.startsWith("<?xml") && !/<[a-zA-Z]/.test(trimmed)) {
|
|
668
|
+
issues.push({
|
|
669
|
+
code: "FIREBENDER_MISSING_XML",
|
|
670
|
+
severity: "critical",
|
|
671
|
+
message: "firebender.xml contains no XML markup — Firebender requires properly tagged XML, not plain text or markdown",
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
return issues;
|
|
675
|
+
}
|
|
676
|
+
export function analyzeFormatCompliance(config) {
|
|
677
|
+
let issues = [];
|
|
678
|
+
switch (config.platform) {
|
|
679
|
+
case "claude":
|
|
680
|
+
issues = checkClaudeFormat(config);
|
|
681
|
+
break;
|
|
682
|
+
case "cursor":
|
|
683
|
+
issues = checkCursorFormat(config);
|
|
684
|
+
break;
|
|
685
|
+
case "cline":
|
|
686
|
+
issues = checkClineFormat(config);
|
|
687
|
+
break;
|
|
688
|
+
case "codex":
|
|
689
|
+
issues = checkCodexFormat(config);
|
|
690
|
+
break;
|
|
691
|
+
case "copilot":
|
|
692
|
+
issues = checkCopilotFormat(config);
|
|
693
|
+
break;
|
|
694
|
+
case "windsurf":
|
|
695
|
+
issues = checkWindsurfFormat(config);
|
|
696
|
+
break;
|
|
697
|
+
case "gemini":
|
|
698
|
+
issues = checkGeminiFormat(config);
|
|
699
|
+
break;
|
|
700
|
+
case "opencode":
|
|
701
|
+
issues = checkOpenCodeFormat(config);
|
|
702
|
+
break;
|
|
703
|
+
case "amp":
|
|
704
|
+
issues = checkAmpFormat(config);
|
|
705
|
+
break;
|
|
706
|
+
case "warp":
|
|
707
|
+
issues = checkWarpFormat(config);
|
|
708
|
+
break;
|
|
709
|
+
case "firebender":
|
|
710
|
+
issues = checkFirebenderFormat(config);
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
return { issues };
|
|
714
|
+
}
|
|
715
|
+
function buildIssues(tokenCost, vagueRules, missingSections, duplicates, attention, structure, formatCompliance) {
|
|
716
|
+
const issues = [];
|
|
717
|
+
if (tokenCost.tokenCount > 4000) {
|
|
718
|
+
issues.push({
|
|
719
|
+
code: "TOKEN_COST_HIGH",
|
|
720
|
+
severity: "critical",
|
|
721
|
+
message: `File uses ~${tokenCost.tokenCount} tokens per session (~$${(tokenCost.costPerSession * 100).toFixed(4)}/100 sessions). Consider trimming.`,
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
else if (tokenCost.tokenCount > 2000) {
|
|
725
|
+
issues.push({
|
|
726
|
+
code: "TOKEN_COST_MEDIUM",
|
|
727
|
+
severity: "warning",
|
|
728
|
+
message: `File uses ~${tokenCost.tokenCount} tokens per session. Moderate cost.`,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
for (const { line, text, reason, category } of vagueRules.vagueLines) {
|
|
732
|
+
issues.push({
|
|
733
|
+
code: "VAGUE_RULE",
|
|
734
|
+
severity: "warning",
|
|
735
|
+
message: `[${category}] ${reason}`,
|
|
736
|
+
line,
|
|
737
|
+
context: text,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
for (const section of missingSections.missing) {
|
|
741
|
+
issues.push({
|
|
742
|
+
code: "MISSING_SECTION",
|
|
743
|
+
severity: "info",
|
|
744
|
+
message: `Consider adding a "${section}" section for this platform`,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
if (duplicates.duplicatePhrases.length > 0) {
|
|
748
|
+
const top = duplicates.duplicatePhrases[0];
|
|
749
|
+
issues.push({
|
|
750
|
+
code: "DUPLICATE_CONTENT",
|
|
751
|
+
severity: "warning",
|
|
752
|
+
message: `Duplicate content detected (${duplicates.duplicatePhrases.length} repeated phrase(s)). Example: "${top.phrase}" appears on lines ${top.lines.join(", ")}.`,
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
for (const suggestion of attention.suggestions) {
|
|
756
|
+
issues.push({
|
|
757
|
+
code: "ATTENTION_PLACEMENT",
|
|
758
|
+
severity: "warning",
|
|
759
|
+
message: suggestion,
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
if (!structure.hasHeadings && (structure.unorganizedRuleCount > 5)) {
|
|
763
|
+
issues.push({
|
|
764
|
+
code: "NO_STRUCTURE",
|
|
765
|
+
severity: "warning",
|
|
766
|
+
message: "File has no markdown headings — group rules under descriptive headings for better clarity",
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
if (structure.unorganizedRuleCount > 3) {
|
|
770
|
+
issues.push({
|
|
771
|
+
code: "UNORGANIZED_RULES",
|
|
772
|
+
severity: "info",
|
|
773
|
+
message: `${structure.unorganizedRuleCount} lines appear before any heading — move them under a section`,
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
if (structure.longParagraphLines.length > 0) {
|
|
777
|
+
issues.push({
|
|
778
|
+
code: "LONG_PARAGRAPHS",
|
|
779
|
+
severity: "info",
|
|
780
|
+
message: `${structure.longParagraphLines.length} line(s) exceed 120 chars — break into shorter rules or bullet points`,
|
|
781
|
+
line: structure.longParagraphLines[0],
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
for (const issue of formatCompliance.issues) {
|
|
785
|
+
issues.push(issue);
|
|
786
|
+
}
|
|
787
|
+
return issues;
|
|
788
|
+
}
|
|
789
|
+
export function analyze(config) {
|
|
790
|
+
const profile = getProfile(config.platform);
|
|
791
|
+
const tokenCost = analyzeTokenCost(config, profile);
|
|
792
|
+
const vagueRules = analyzeVagueRules(config);
|
|
793
|
+
const missingSections = analyzeMissingSections(config, profile);
|
|
794
|
+
const duplicates = analyzeDuplicates(config);
|
|
795
|
+
const attentionPlacement = analyzeAttentionPlacement(config);
|
|
796
|
+
const structure = analyzeStructure(config);
|
|
797
|
+
const formatCompliance = analyzeFormatCompliance(config);
|
|
798
|
+
const issues = buildIssues(tokenCost, vagueRules, missingSections, duplicates, attentionPlacement, structure, formatCompliance);
|
|
799
|
+
return {
|
|
800
|
+
platform: config.platform,
|
|
801
|
+
tokenCount: tokenCost.tokenCount,
|
|
802
|
+
estimatedCostUsd: tokenCost.costPerSession,
|
|
803
|
+
issues,
|
|
804
|
+
checks: {
|
|
805
|
+
tokenCost,
|
|
806
|
+
vagueRules,
|
|
807
|
+
missingSections,
|
|
808
|
+
duplicates,
|
|
809
|
+
attentionPlacement,
|
|
810
|
+
structure,
|
|
811
|
+
formatCompliance,
|
|
812
|
+
},
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
//# sourceMappingURL=analyzer.js.map
|