@lousy-agents/lint 5.11.3 → 5.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +192 -13
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -895,7 +895,8 @@ __webpack_require__.d(classic_schemas_namespaceObject, {
|
|
|
895
895
|
"instruction/parse-error": "warn",
|
|
896
896
|
"instruction/command-not-in-code-block": "warn",
|
|
897
897
|
"instruction/command-outside-section": "warn",
|
|
898
|
-
"instruction/missing-error-handling": "warn"
|
|
898
|
+
"instruction/missing-error-handling": "warn",
|
|
899
|
+
"instruction/missing-structural-heading": "warn"
|
|
899
900
|
},
|
|
900
901
|
skills: {
|
|
901
902
|
"skill/invalid-frontmatter": "error",
|
|
@@ -905,7 +906,8 @@ __webpack_require__.d(classic_schemas_namespaceObject, {
|
|
|
905
906
|
"skill/name-mismatch": "error",
|
|
906
907
|
"skill/missing-description": "error",
|
|
907
908
|
"skill/invalid-description": "error",
|
|
908
|
-
"skill/missing-allowed-tools": "warn"
|
|
909
|
+
"skill/missing-allowed-tools": "warn",
|
|
910
|
+
"skill/missing-argument-hint": "warn"
|
|
909
911
|
}
|
|
910
912
|
};
|
|
911
913
|
|
|
@@ -37010,6 +37012,24 @@ core_config(en());
|
|
|
37010
37012
|
"Validation Suite",
|
|
37011
37013
|
"Commands"
|
|
37012
37014
|
];
|
|
37015
|
+
/**
|
|
37016
|
+
* Descriptions for each default structural heading pattern explaining why it is recommended.
|
|
37017
|
+
* Keyed by Record<StructuralHeadingPattern, string> to ensure compile-time coverage — every
|
|
37018
|
+
* entry in DEFAULT_STRUCTURAL_HEADING_PATTERNS must have a description here.
|
|
37019
|
+
*/ const HEADING_PATTERN_DESCRIPTION_RECORD = {
|
|
37020
|
+
// biome-ignore lint/style/useNamingConvention: heading pattern keys use PascalCase by design
|
|
37021
|
+
Validation: "Agents need this section to understand how to validate that their changes meet quality standards.",
|
|
37022
|
+
// biome-ignore lint/style/useNamingConvention: heading pattern keys use PascalCase by design
|
|
37023
|
+
Verification: "Agents need this section to understand how to verify their implementation is correct.",
|
|
37024
|
+
"Feedback Loop": "Agents need this section to understand the iterative improvement process to follow.",
|
|
37025
|
+
// biome-ignore lint/style/useNamingConvention: heading pattern keys use PascalCase by design
|
|
37026
|
+
Mandatory: "Agents need this section to understand which steps are required and cannot be skipped.",
|
|
37027
|
+
"Before Commit": "Agents need this section to know what checks to run before committing changes.",
|
|
37028
|
+
"Validation Suite": "Agents need this section to know which validation commands to run against the codebase.",
|
|
37029
|
+
// biome-ignore lint/style/useNamingConvention: heading pattern keys use PascalCase by design
|
|
37030
|
+
Commands: "Agents need this section to know which commands and tools are available in the project."
|
|
37031
|
+
};
|
|
37032
|
+
const HEADING_PATTERN_DESCRIPTIONS = new Map(Object.entries(HEADING_PATTERN_DESCRIPTION_RECORD));
|
|
37013
37033
|
/** Conditional keywords indicating error handling near code blocks */ const CONDITIONAL_KEYWORDS = [
|
|
37014
37034
|
"if",
|
|
37015
37035
|
"fail",
|
|
@@ -37029,6 +37049,24 @@ core_config(en());
|
|
|
37029
37049
|
* Use case for analyzing instruction quality across a repository.
|
|
37030
37050
|
* Orchestrates discovery, AST parsing, and scoring of feedback loop documentation.
|
|
37031
37051
|
*/
|
|
37052
|
+
|
|
37053
|
+
/** Maximum number of raw heading pattern entries accepted before deduplication. */ const MAX_RAW_HEADING_PATTERNS = 1000;
|
|
37054
|
+
/** Maximum number of heading patterns accepted in a single execute() call. */ const MAX_HEADING_PATTERNS = 50;
|
|
37055
|
+
/** Maximum character length for a single heading pattern. */ const MAX_PATTERN_LENGTH = 200;
|
|
37056
|
+
/**
|
|
37057
|
+
* Lowercase-keyed lookup built once from HEADING_PATTERN_DESCRIPTIONS.
|
|
37058
|
+
* Enables description lookup regardless of the casing callers supply for patterns.
|
|
37059
|
+
*/ const HEADING_DESCRIPTIONS_BY_LOWER = new Map(Array.from(HEADING_PATTERN_DESCRIPTIONS, ([k, v])=>[
|
|
37060
|
+
k.toLowerCase(),
|
|
37061
|
+
v
|
|
37062
|
+
]));
|
|
37063
|
+
/**
|
|
37064
|
+
* Input for the analyze instruction quality use case.
|
|
37065
|
+
*/ const AnalyzeInstructionQualityInputSchema = schemas_object({
|
|
37066
|
+
targetDir: schemas_string().min(1, "Target directory is required"),
|
|
37067
|
+
headingPatterns: schemas_array(schemas_string().max(MAX_PATTERN_LENGTH * 2)).max(MAX_RAW_HEADING_PATTERNS).optional(),
|
|
37068
|
+
proximityWindow: schemas_number().int().positive().optional()
|
|
37069
|
+
});
|
|
37032
37070
|
/**
|
|
37033
37071
|
* Use case for analyzing instruction quality.
|
|
37034
37072
|
*/ class AnalyzeInstructionQualityUseCase {
|
|
@@ -37040,16 +37078,107 @@ core_config(en());
|
|
|
37040
37078
|
this.astGateway = astGateway;
|
|
37041
37079
|
this.commandsGateway = commandsGateway;
|
|
37042
37080
|
}
|
|
37081
|
+
/**
|
|
37082
|
+
* Returns true if the given heading-pattern string contains any characters
|
|
37083
|
+
* that could cause terminal injection or garbled output when the pattern
|
|
37084
|
+
* is embedded in a diagnostic message.
|
|
37085
|
+
*
|
|
37086
|
+
* Rejects: ASCII C0 (0x00–0x1F), DEL (0x7F), C1 (0x80–0x9F), **lone**
|
|
37087
|
+
* Unicode surrogates (U+D800–U+DFFF — but valid surrogate pairs like emoji
|
|
37088
|
+
* are accepted), line/paragraph separators (U+2028–U+2029), and bidi
|
|
37089
|
+
* override characters (U+202A–U+202E, U+2066–U+2069).
|
|
37090
|
+
*/ static patternHasControlCharacters(value) {
|
|
37091
|
+
for(let i = 0; i < value.length; i++){
|
|
37092
|
+
const code = value.charCodeAt(i);
|
|
37093
|
+
if (code <= 0x1f) return true;
|
|
37094
|
+
if (code === 0x7f) return true;
|
|
37095
|
+
if (code >= 0x80 && code <= 0x9f) return true;
|
|
37096
|
+
// Detect lone surrogates only; valid surrogate pairs (e.g., emoji)
|
|
37097
|
+
// should be accepted. A high surrogate (0xD800–0xDBFF) is lone if
|
|
37098
|
+
// it is not immediately followed by a low surrogate (0xDC00–0xDFFF).
|
|
37099
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
37100
|
+
const next = value.charCodeAt(i + 1);
|
|
37101
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
37102
|
+
i++; // valid pair — skip the low surrogate
|
|
37103
|
+
} else {
|
|
37104
|
+
return true; // lone high surrogate
|
|
37105
|
+
}
|
|
37106
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
37107
|
+
return true; // lone low surrogate
|
|
37108
|
+
}
|
|
37109
|
+
if (code === 0x2028 || code === 0x2029) return true;
|
|
37110
|
+
if (code >= 0x202a && code <= 0x202e) return true;
|
|
37111
|
+
if (code >= 0x2066 && code <= 0x2069) return true;
|
|
37112
|
+
}
|
|
37113
|
+
return false;
|
|
37114
|
+
}
|
|
37115
|
+
/**
|
|
37116
|
+
* Serializes a heading pattern for safe inclusion in error messages.
|
|
37117
|
+
* Produces a quoted, unambiguous representation by:
|
|
37118
|
+
* - Escaping `"` as `\"` and `\` as `\\` within printable ASCII (0x20–0x7E)
|
|
37119
|
+
* - Escaping every code unit outside that range to `\uXXXX`
|
|
37120
|
+
*
|
|
37121
|
+
* The \uXXXX escaping prevents terminal injection via bidi override
|
|
37122
|
+
* characters and other non-printable code points that JSON.stringify does
|
|
37123
|
+
* not escape.
|
|
37124
|
+
*/ static serializePatternForError(value) {
|
|
37125
|
+
let result = '"';
|
|
37126
|
+
for(let i = 0; i < value.length; i++){
|
|
37127
|
+
const code = value.charCodeAt(i);
|
|
37128
|
+
if (code >= 0x20 && code <= 0x7e) {
|
|
37129
|
+
if (code === 0x22) {
|
|
37130
|
+
result += '\\"';
|
|
37131
|
+
} else if (code === 0x5c) {
|
|
37132
|
+
result += "\\\\";
|
|
37133
|
+
} else {
|
|
37134
|
+
result += value[i];
|
|
37135
|
+
}
|
|
37136
|
+
} else {
|
|
37137
|
+
result += `\\u${code.toString(16).padStart(4, "0")}`;
|
|
37138
|
+
}
|
|
37139
|
+
}
|
|
37140
|
+
return `${result}"`;
|
|
37141
|
+
}
|
|
37043
37142
|
async execute(input) {
|
|
37044
|
-
|
|
37045
|
-
|
|
37143
|
+
let parsed;
|
|
37144
|
+
try {
|
|
37145
|
+
parsed = AnalyzeInstructionQualityInputSchema.parse(input);
|
|
37146
|
+
} catch (error) {
|
|
37147
|
+
if (error instanceof ZodError) {
|
|
37148
|
+
throw new Error(error.issues[0]?.message ?? "Validation failed: unable to parse error details");
|
|
37149
|
+
}
|
|
37150
|
+
throw error;
|
|
37046
37151
|
}
|
|
37047
|
-
const
|
|
37152
|
+
const seenLower = new Set();
|
|
37153
|
+
const headingPatterns = [];
|
|
37154
|
+
for (const raw of parsed.headingPatterns ?? [
|
|
37048
37155
|
...DEFAULT_STRUCTURAL_HEADING_PATTERNS
|
|
37049
|
-
]
|
|
37050
|
-
|
|
37051
|
-
|
|
37052
|
-
|
|
37156
|
+
]){
|
|
37157
|
+
// Validate BEFORE trim() — U+2028/U+2029 are JavaScript whitespace
|
|
37158
|
+
// and would be silently stripped by trim(), bypassing the check.
|
|
37159
|
+
// Filter before transform.
|
|
37160
|
+
if (AnalyzeInstructionQualityUseCase.patternHasControlCharacters(raw)) {
|
|
37161
|
+
throw new Error(`headingPatterns must not contain control characters, bidi override characters, or lone surrogate code points: ${AnalyzeInstructionQualityUseCase.serializePatternForError(raw)}`);
|
|
37162
|
+
}
|
|
37163
|
+
const trimmed = raw.trim();
|
|
37164
|
+
if (trimmed.length === 0) {
|
|
37165
|
+
throw new Error("headingPatterns must not contain empty or whitespace-only entries");
|
|
37166
|
+
}
|
|
37167
|
+
if (trimmed.length > MAX_PATTERN_LENGTH) {
|
|
37168
|
+
throw new Error(`headingPatterns entries must not exceed ${MAX_PATTERN_LENGTH} characters`);
|
|
37169
|
+
}
|
|
37170
|
+
const lower = trimmed.toLowerCase();
|
|
37171
|
+
if (!seenLower.has(lower)) {
|
|
37172
|
+
seenLower.add(lower);
|
|
37173
|
+
if (seenLower.size > MAX_HEADING_PATTERNS) {
|
|
37174
|
+
throw new Error(`headingPatterns must contain at most ${MAX_HEADING_PATTERNS} unique entries (case-insensitive)`);
|
|
37175
|
+
}
|
|
37176
|
+
headingPatterns.push(trimmed);
|
|
37177
|
+
}
|
|
37178
|
+
}
|
|
37179
|
+
const proximityWindow = parsed.proximityWindow ?? 3;
|
|
37180
|
+
const discoveredFiles = await this.discoveryGateway.discoverInstructionFiles(parsed.targetDir);
|
|
37181
|
+
const mandatoryCommands = await this.commandsGateway.getMandatoryCommands(parsed.targetDir);
|
|
37053
37182
|
if (discoveredFiles.length === 0) {
|
|
37054
37183
|
return {
|
|
37055
37184
|
result: {
|
|
@@ -37082,7 +37211,7 @@ core_config(en());
|
|
|
37082
37211
|
}
|
|
37083
37212
|
}
|
|
37084
37213
|
// Sort parsing errors for deterministic output
|
|
37085
|
-
parsingErrors.sort((a, b)=>a.filePath.
|
|
37214
|
+
parsingErrors.sort((a, b)=>a.filePath < b.filePath ? -1 : a.filePath > b.filePath ? 1 : 0);
|
|
37086
37215
|
// Score each mandatory command across all files
|
|
37087
37216
|
const commandScores = [];
|
|
37088
37217
|
const diagnostics = [];
|
|
@@ -37097,6 +37226,16 @@ core_config(en());
|
|
|
37097
37226
|
target: "instruction"
|
|
37098
37227
|
});
|
|
37099
37228
|
}
|
|
37229
|
+
// Check each successfully parsed file for missing structural headings
|
|
37230
|
+
const sortedFilePaths = Array.from(fileStructures.keys()).sort((a, b)=>a < b ? -1 : a > b ? 1 : 0);
|
|
37231
|
+
for (const filePath of sortedFilePaths){
|
|
37232
|
+
const structure = fileStructures.get(filePath);
|
|
37233
|
+
if (!structure) {
|
|
37234
|
+
continue;
|
|
37235
|
+
}
|
|
37236
|
+
const missingDiagnostics = this.checkMissingHeadings(filePath, structure, headingPatterns);
|
|
37237
|
+
diagnostics.push(...missingDiagnostics);
|
|
37238
|
+
}
|
|
37100
37239
|
for (const command of mandatoryCommands){
|
|
37101
37240
|
const bestAnalysis = this.findBestScore(command, discoveredFiles, fileStructures, headingPatterns, proximityWindow, diagnostics);
|
|
37102
37241
|
if (bestAnalysis) {
|
|
@@ -37338,6 +37477,43 @@ core_config(en());
|
|
|
37338
37477
|
}
|
|
37339
37478
|
return false;
|
|
37340
37479
|
}
|
|
37480
|
+
/**
|
|
37481
|
+
* Emits a warning diagnostic for each heading pattern that is absent from the file.
|
|
37482
|
+
* Headings are matched case-insensitively using substring inclusion, matching the
|
|
37483
|
+
* same logic used elsewhere in the use case.
|
|
37484
|
+
*/ checkMissingHeadings(filePath, structure, headingPatterns) {
|
|
37485
|
+
// Precondition: `headingPatterns` must already be deduplicated
|
|
37486
|
+
// (case-insensitively) and validated. This invariant is enforced by
|
|
37487
|
+
// `execute()` before calling this method.
|
|
37488
|
+
const diagnostics = [];
|
|
37489
|
+
const fileHeadingTexts = structure.headings.map((h)=>h.text.toLowerCase());
|
|
37490
|
+
for (const pattern of headingPatterns){
|
|
37491
|
+
const patternLower = pattern.toLowerCase();
|
|
37492
|
+
const hasHeading = fileHeadingTexts.some((text)=>{
|
|
37493
|
+
if (!text.includes(patternLower)) {
|
|
37494
|
+
return false;
|
|
37495
|
+
}
|
|
37496
|
+
// Don't let a heading satisfy a shorter pattern when it also satisfies
|
|
37497
|
+
// a longer, more-specific pattern that starts with the shorter one.
|
|
37498
|
+
// E.g., a "Validation Suite" heading satisfies "Validation Suite" but
|
|
37499
|
+
// should NOT also satisfy "Validation".
|
|
37500
|
+
const supersededByMoreSpecific = headingPatterns.some((other)=>other !== pattern && other.toLowerCase().startsWith(patternLower) && text.includes(other.toLowerCase()));
|
|
37501
|
+
return !supersededByMoreSpecific;
|
|
37502
|
+
});
|
|
37503
|
+
if (!hasHeading) {
|
|
37504
|
+
const description = HEADING_DESCRIPTIONS_BY_LOWER.get(patternLower) ?? "This heading helps guide coding agents through structured workflows.";
|
|
37505
|
+
diagnostics.push({
|
|
37506
|
+
filePath,
|
|
37507
|
+
line: 1,
|
|
37508
|
+
severity: "warning",
|
|
37509
|
+
message: `Missing '${pattern}' heading section. ${description}`,
|
|
37510
|
+
ruleId: "instruction/missing-structural-heading",
|
|
37511
|
+
target: "instruction"
|
|
37512
|
+
});
|
|
37513
|
+
}
|
|
37514
|
+
}
|
|
37515
|
+
return diagnostics;
|
|
37516
|
+
}
|
|
37341
37517
|
generateSuggestions(commandScores) {
|
|
37342
37518
|
const suggestions = [];
|
|
37343
37519
|
const lowStructural = commandScores.filter((s)=>s.structuralContext === 0 && s.bestSourceFile !== "");
|
|
@@ -37794,13 +37970,16 @@ const AgentSkillFrontmatterSchema = schemas_object({
|
|
|
37794
37970
|
license: schemas_string().optional(),
|
|
37795
37971
|
compatibility: schemas_string().max(500, "Compatibility must be 500 characters or fewer").optional(),
|
|
37796
37972
|
metadata: record(schemas_string(), schemas_string()).optional(),
|
|
37797
|
-
"allowed-tools": schemas_string().optional()
|
|
37973
|
+
"allowed-tools": schemas_string().optional(),
|
|
37974
|
+
"argument-hint": schemas_string().optional()
|
|
37798
37975
|
});
|
|
37799
37976
|
const RECOMMENDED_FIELDS = [
|
|
37800
|
-
"allowed-tools"
|
|
37977
|
+
"allowed-tools",
|
|
37978
|
+
"argument-hint"
|
|
37801
37979
|
];
|
|
37802
37980
|
const RECOMMENDED_FIELD_RULE_IDS = {
|
|
37803
|
-
"allowed-tools": "skill/missing-allowed-tools"
|
|
37981
|
+
"allowed-tools": "skill/missing-allowed-tools",
|
|
37982
|
+
"argument-hint": "skill/missing-argument-hint"
|
|
37804
37983
|
};
|
|
37805
37984
|
class LintSkillFrontmatterUseCase {
|
|
37806
37985
|
gateway;
|
package/package.json
CHANGED