@lousy-agents/cli 2.10.0 → 2.11.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 CHANGED
@@ -55118,6 +55118,102 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
55118
55118
  return new RemarkMarkdownAstGateway();
55119
55119
  }
55120
55120
 
55121
+ ;// CONCATENATED MODULE: ./src/entities/lint-rules.ts
55122
+ /**
55123
+ * Lint rule registry entity.
55124
+ * Defines all known lint rule IDs with their default severities, organized by target.
55125
+ */ /** Valid severity values for rule configuration */ /** Default severity levels for all known lint rules */ const DEFAULT_LINT_RULES = {
55126
+ agents: {
55127
+ "agent/missing-frontmatter": "error",
55128
+ "agent/invalid-frontmatter": "error",
55129
+ "agent/missing-name": "error",
55130
+ "agent/invalid-name-format": "error",
55131
+ "agent/name-mismatch": "error",
55132
+ "agent/missing-description": "error",
55133
+ "agent/invalid-description": "error",
55134
+ "agent/invalid-field": "warn"
55135
+ },
55136
+ instructions: {
55137
+ "instruction/parse-error": "warn",
55138
+ "instruction/command-not-in-code-block": "warn",
55139
+ "instruction/command-outside-section": "warn",
55140
+ "instruction/missing-error-handling": "warn"
55141
+ },
55142
+ skills: {
55143
+ "skill/invalid-frontmatter": "error",
55144
+ "skill/missing-frontmatter": "error",
55145
+ "skill/missing-name": "error",
55146
+ "skill/invalid-name-format": "error",
55147
+ "skill/name-mismatch": "error",
55148
+ "skill/missing-description": "error",
55149
+ "skill/invalid-description": "error",
55150
+ "skill/missing-allowed-tools": "warn"
55151
+ }
55152
+ };
55153
+
55154
+ ;// CONCATENATED MODULE: ./src/lib/lint-config.ts
55155
+ /**
55156
+ * Lint configuration loader.
55157
+ * Loads lint rule severity overrides from c12 config and merges with defaults.
55158
+ */
55159
+
55160
+
55161
+ /** Zod schema for a rule config map: rule IDs validated with regex to prevent prototype pollution */ const RuleConfigMapSchema = record(schemas_string().regex(/^[a-z]+\/[a-z]+(?:-[a-z]+)*$/), schemas_enum([
55162
+ "error",
55163
+ "warn",
55164
+ "off"
55165
+ ]));
55166
+ /** Zod schema for the lint.rules section of the config */ const LintRulesConfigSchema = schemas_object({
55167
+ agents: RuleConfigMapSchema.optional(),
55168
+ instructions: RuleConfigMapSchema.optional(),
55169
+ skills: RuleConfigMapSchema.optional()
55170
+ });
55171
+ /** Zod schema for the lint section of the config */ const LintConfigSchema = schemas_object({
55172
+ lint: schemas_object({
55173
+ rules: LintRulesConfigSchema.optional()
55174
+ }).optional()
55175
+ });
55176
+ /**
55177
+ * Merges user overrides with defaults for a single target.
55178
+ * Only known rule IDs (present in defaults) are applied; unknown IDs are discarded.
55179
+ */ function mergeTargetRules(defaults, overrides) {
55180
+ if (!overrides) {
55181
+ return defaults;
55182
+ }
55183
+ const merged = {
55184
+ ...defaults
55185
+ };
55186
+ for (const [ruleId, severity] of Object.entries(overrides)){
55187
+ if (Object.hasOwn(defaults, ruleId)) {
55188
+ merged[ruleId] = severity;
55189
+ }
55190
+ }
55191
+ return merged;
55192
+ }
55193
+ /**
55194
+ * Loads lint configuration from the target directory using c12.
55195
+ * Merges user overrides with default rule severities.
55196
+ * Throws on config load failures (syntax errors, permission denied, validation errors).
55197
+ */ async function loadLintConfig(targetDir) {
55198
+ const { config } = await loadConfig({
55199
+ name: "lousy-agents",
55200
+ cwd: targetDir
55201
+ });
55202
+ if (!config) {
55203
+ return DEFAULT_LINT_RULES;
55204
+ }
55205
+ const parsed = LintConfigSchema.parse(config);
55206
+ const rules = parsed.lint?.rules;
55207
+ if (!rules) {
55208
+ return DEFAULT_LINT_RULES;
55209
+ }
55210
+ return {
55211
+ agents: mergeTargetRules(DEFAULT_LINT_RULES.agents, rules.agents),
55212
+ instructions: mergeTargetRules(DEFAULT_LINT_RULES.instructions, rules.instructions),
55213
+ skills: mergeTargetRules(DEFAULT_LINT_RULES.skills, rules.skills)
55214
+ };
55215
+ }
55216
+
55121
55217
  ;// CONCATENATED MODULE: ./src/entities/instruction-quality.ts
55122
55218
  /**
55123
55219
  * Core domain entities for instruction quality analysis.
@@ -55178,7 +55274,9 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
55178
55274
  commandScores: [],
55179
55275
  overallQualityScore: 0,
55180
55276
  suggestions: [
55181
- "No agent instruction files found. Supported formats: .github/copilot-instructions.md, .github/instructions/*.md, .github/agents/*.md, AGENTS.md, CLAUDE.md"
55277
+ {
55278
+ message: "No agent instruction files found. Supported formats: .github/copilot-instructions.md, .github/instructions/*.md, .github/agents/*.md, AGENTS.md, CLAUDE.md"
55279
+ }
55182
55280
  ],
55183
55281
  parsingErrors: []
55184
55282
  },
@@ -55245,7 +55343,10 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
55245
55343
  const suggestions = this.generateSuggestions(commandScores);
55246
55344
  if (parsingErrors.length > 0) {
55247
55345
  const skippedFiles = parsingErrors.map((pe)=>pe.filePath).join(", ");
55248
- suggestions.push(`${parsingErrors.length} file(s) could not be parsed and were skipped: ${skippedFiles}. Analysis may be incomplete.`);
55346
+ suggestions.push({
55347
+ message: `${parsingErrors.length} file(s) could not be parsed and were skipped: ${skippedFiles}. Analysis may be incomplete.`,
55348
+ ruleId: "instruction/parse-error"
55349
+ });
55249
55350
  }
55250
55351
  return {
55251
55352
  result: {
@@ -55459,22 +55560,33 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
55459
55560
  const lowStructural = commandScores.filter((s)=>s.structuralContext === 0 && s.bestSourceFile !== "");
55460
55561
  if (lowStructural.length > 0) {
55461
55562
  const names = lowStructural.map((s)=>s.commandName).join(", ");
55462
- suggestions.push(`Commands not under a dedicated section: ${names}. Add a heading like "## Validation" or "## Feedback Loop" above these commands.`);
55563
+ suggestions.push({
55564
+ message: `Commands not under a dedicated section: ${names}. Add a heading like "## Validation" or "## Feedback Loop" above these commands.`,
55565
+ ruleId: "instruction/command-outside-section"
55566
+ });
55463
55567
  }
55464
55568
  const lowExecution = commandScores.filter((s)=>s.executionClarity === 0 && s.bestSourceFile !== "");
55465
55569
  if (lowExecution.length > 0) {
55466
55570
  const names = lowExecution.map((s)=>s.commandName).join(", ");
55467
- suggestions.push(`Commands not in code blocks: ${names}. Document these commands in fenced code blocks for clarity.`);
55571
+ suggestions.push({
55572
+ message: `Commands not in code blocks: ${names}. Document these commands in fenced code blocks for clarity.`,
55573
+ ruleId: "instruction/command-not-in-code-block"
55574
+ });
55468
55575
  }
55469
55576
  const lowLoop = commandScores.filter((s)=>s.loopCompleteness === 0 && s.executionClarity === 1 && s.bestSourceFile !== "");
55470
55577
  if (lowLoop.length > 0) {
55471
55578
  const names = lowLoop.map((s)=>s.commandName).join(", ");
55472
- suggestions.push(`Commands missing error handling guidance: ${names}. Add instructions for what to do if the command fails.`);
55579
+ suggestions.push({
55580
+ message: `Commands missing error handling guidance: ${names}. Add instructions for what to do if the command fails.`,
55581
+ ruleId: "instruction/missing-error-handling"
55582
+ });
55473
55583
  }
55474
55584
  const notFound = commandScores.filter((s)=>s.bestSourceFile === "");
55475
55585
  if (notFound.length > 0) {
55476
55586
  const names = notFound.map((s)=>s.commandName).join(", ");
55477
- suggestions.push(`Commands not found in any instruction file: ${names}. Document these feedback loop commands in your instruction files.`);
55587
+ suggestions.push({
55588
+ message: `Commands not found in any instruction file: ${names}. Document these feedback loop commands in your instruction files.`
55589
+ });
55478
55590
  }
55479
55591
  return suggestions;
55480
55592
  }
@@ -55612,14 +55724,8 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
55612
55724
  }
55613
55725
 
55614
55726
  ;// CONCATENATED MODULE: ./src/use-cases/lint-skill-frontmatter.ts
55615
- /**
55616
- * Use case for linting GitHub Copilot Agent Skill frontmatter.
55617
- * Validates required and recommended fields, name format, and directory naming.
55618
- */
55619
- /**
55620
- * Zod schema for validating agent skill frontmatter.
55621
- * Based on the agentskills.io specification.
55622
- */ const AgentSkillFrontmatterSchema = schemas_object({
55727
+
55728
+ const AgentSkillFrontmatterSchema = schemas_object({
55623
55729
  name: schemas_string().min(1, "Name is required").max(64, "Name must be 64 characters or fewer").regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Name must contain only lowercase letters, numbers, and hyphens. It cannot start/end with a hyphen or contain consecutive hyphens."),
55624
55730
  description: schemas_string().min(1, "Description is required").max(1024, "Description must be 1024 characters or fewer").refine((s)=>s.trim().length > 0, {
55625
55731
  message: "Description cannot be empty or whitespace-only"
@@ -55629,14 +55735,13 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
55629
55735
  metadata: record(schemas_string(), schemas_string()).optional(),
55630
55736
  "allowed-tools": schemas_string().optional()
55631
55737
  });
55632
- /**
55633
- * Recommended (optional) fields that produce warnings when missing.
55634
- */ const RECOMMENDED_FIELDS = [
55738
+ const RECOMMENDED_FIELDS = [
55635
55739
  "allowed-tools"
55636
55740
  ];
55637
- /**
55638
- * Use case for linting skill frontmatter across a repository.
55639
- */ class LintSkillFrontmatterUseCase {
55741
+ const RECOMMENDED_FIELD_RULE_IDS = {
55742
+ "allowed-tools": "skill/missing-allowed-tools"
55743
+ };
55744
+ class LintSkillFrontmatterUseCase {
55640
55745
  gateway;
55641
55746
  constructor(gateway){
55642
55747
  this.gateway = gateway;
@@ -55672,16 +55777,20 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
55672
55777
  diagnostics.push({
55673
55778
  line: 1,
55674
55779
  severity: "error",
55675
- message: errorMessage
55780
+ message: errorMessage,
55781
+ ruleId: "skill/invalid-frontmatter"
55676
55782
  });
55677
55783
  }
55678
55784
  if (!parsed) {
55679
55785
  if (diagnostics.length === 0) {
55680
- const message = hasFrontmatterDelimiters(content) ? "Invalid YAML frontmatter. The content between --- delimiters could not be parsed as valid YAML." : "Missing YAML frontmatter. Skill files must begin with --- delimited YAML frontmatter.";
55786
+ const hasDelimiters = hasFrontmatterDelimiters(content);
55787
+ const message = hasDelimiters ? "Invalid YAML frontmatter. The content between --- delimiters could not be parsed as valid YAML." : "Missing YAML frontmatter. Skill files must begin with --- delimited YAML frontmatter.";
55788
+ const ruleId = hasDelimiters ? "skill/invalid-frontmatter" : "skill/missing-frontmatter";
55681
55789
  diagnostics.push({
55682
55790
  line: 1,
55683
55791
  severity: "error",
55684
- message
55792
+ message,
55793
+ ruleId
55685
55794
  });
55686
55795
  }
55687
55796
  return {
@@ -55702,47 +55811,58 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
55702
55811
  }
55703
55812
  validateFrontmatter(parsed, parentDirName) {
55704
55813
  const diagnostics = [];
55705
- // Validate against Zod schema
55706
55814
  const result = AgentSkillFrontmatterSchema.safeParse(parsed.data);
55707
55815
  if (!result.success) {
55708
55816
  for (const issue of result.error.issues){
55709
55817
  const fieldName = issue.path[0]?.toString();
55710
55818
  const line = fieldName ? parsed.fieldLines.get(fieldName) ?? parsed.frontmatterStartLine : parsed.frontmatterStartLine;
55819
+ const ruleId = this.getRuleIdForField(fieldName, issue.code, parsed.data);
55711
55820
  diagnostics.push({
55712
55821
  line,
55713
55822
  severity: "error",
55714
55823
  message: issue.message,
55715
- field: fieldName
55824
+ field: fieldName,
55825
+ ruleId
55716
55826
  });
55717
55827
  }
55718
55828
  }
55719
- // Check name matches parent directory
55720
55829
  if (result.success && result.data.name !== parentDirName) {
55721
55830
  const nameLine = parsed.fieldLines.get("name") ?? parsed.frontmatterStartLine;
55722
55831
  diagnostics.push({
55723
55832
  line: nameLine,
55724
55833
  severity: "error",
55725
55834
  message: `Frontmatter name '${result.data.name}' must match parent directory name '${parentDirName}'`,
55726
- field: "name"
55835
+ field: "name",
55836
+ ruleId: "skill/name-mismatch"
55727
55837
  });
55728
55838
  }
55729
- // Check recommended fields
55730
55839
  for (const field of RECOMMENDED_FIELDS){
55731
55840
  if (parsed.data[field] === undefined) {
55732
55841
  diagnostics.push({
55733
55842
  line: parsed.frontmatterStartLine,
55734
55843
  severity: "warning",
55735
55844
  message: `Recommended field '${field}' is missing`,
55736
- field
55845
+ field,
55846
+ ruleId: RECOMMENDED_FIELD_RULE_IDS[field]
55737
55847
  });
55738
55848
  }
55739
55849
  }
55740
55850
  return diagnostics;
55741
55851
  }
55852
+ getRuleIdForField(fieldName, issueCode, inputData) {
55853
+ // Check the actual input data for field presence rather than
55854
+ // relying on Zod message text which can change across versions.
55855
+ const isMissing = issueCode === "invalid_type" && (fieldName === undefined || !Object.hasOwn(inputData, fieldName));
55856
+ if (fieldName === "name") {
55857
+ return isMissing ? "skill/missing-name" : "skill/invalid-name-format";
55858
+ }
55859
+ if (fieldName === "description") {
55860
+ return isMissing ? "skill/missing-description" : "skill/invalid-description";
55861
+ }
55862
+ return "skill/invalid-frontmatter";
55863
+ }
55742
55864
  }
55743
- /**
55744
- * Checks whether content has opening and closing --- frontmatter delimiters.
55745
- */ function hasFrontmatterDelimiters(content) {
55865
+ function hasFrontmatterDelimiters(content) {
55746
55866
  const lines = content.split("\n");
55747
55867
  if (lines[0]?.trim() !== "---") {
55748
55868
  return false;
@@ -55772,6 +55892,7 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
55772
55892
 
55773
55893
 
55774
55894
 
55895
+
55775
55896
  /** Schema for validating target directory */ const TargetDirSchema = schemas_string().min(1, "Target directory is required");
55776
55897
  /**
55777
55898
  * Validates the target directory.
@@ -55796,6 +55917,7 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
55796
55917
  severity: d.severity,
55797
55918
  message: d.message,
55798
55919
  field: d.field,
55920
+ ruleId: d.ruleId,
55799
55921
  target: "skill"
55800
55922
  });
55801
55923
  }
@@ -55932,8 +56054,80 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
55932
56054
  consola.info(`Overall instruction quality score: ${result.overallQualityScore}%`);
55933
56055
  }
55934
56056
  for (const suggestion of result.suggestions){
55935
- consola.warn(suggestion);
56057
+ consola.warn(suggestion.message);
56058
+ }
56059
+ }
56060
+ /** Maps a lint target to its config key */ const TARGET_TO_CONFIG_KEY = {
56061
+ skill: "skills",
56062
+ agent: "agents",
56063
+ instruction: "instructions"
56064
+ };
56065
+ /**
56066
+ * Maps config-facing severity to diagnostic-facing severity.
56067
+ * "warn" → "warning", "error" → "error", "off" → null (drop).
56068
+ */ function lint_mapSeverity(configSeverity) {
56069
+ if (configSeverity === "off") {
56070
+ return null;
56071
+ }
56072
+ if (configSeverity === "warn") {
56073
+ return "warning";
56074
+ }
56075
+ return configSeverity;
56076
+ }
56077
+ /**
56078
+ * Filters instruction suggestions based on rule severity configuration.
56079
+ * Drops suggestions whose corresponding rule is "off".
56080
+ * Suggestions without a ruleId pass through unchanged.
56081
+ */ function filterInstructionSuggestions(suggestions, rules) {
56082
+ return suggestions.filter((suggestion)=>{
56083
+ if (!suggestion.ruleId) {
56084
+ return true;
56085
+ }
56086
+ return rules[suggestion.ruleId] !== "off";
56087
+ });
56088
+ }
56089
+ /**
56090
+ * Applies severity filtering to a LintOutput based on rule configuration.
56091
+ * Drops diagnostics for "off" rules, remaps severity for "warn"/"error" rules.
56092
+ * Diagnostics without a ruleId pass through unchanged.
56093
+ * For instruction targets, also filters qualityResult.suggestions.
56094
+ */ function applySeverityFilter(output, rulesConfig) {
56095
+ const configKey = TARGET_TO_CONFIG_KEY[output.target];
56096
+ const targetRules = rulesConfig[configKey];
56097
+ const filteredDiagnostics = [];
56098
+ for (const diagnostic of output.diagnostics){
56099
+ const configuredSeverity = diagnostic.ruleId ? targetRules[diagnostic.ruleId] : undefined;
56100
+ if (!configuredSeverity) {
56101
+ filteredDiagnostics.push(diagnostic);
56102
+ continue;
56103
+ }
56104
+ const mappedSeverity = lint_mapSeverity(configuredSeverity);
56105
+ if (mappedSeverity === null) {
56106
+ continue;
56107
+ }
56108
+ filteredDiagnostics.push({
56109
+ ...diagnostic,
56110
+ severity: mappedSeverity
56111
+ });
55936
56112
  }
56113
+ const totalErrors = filteredDiagnostics.filter((d)=>d.severity === "error").length;
56114
+ const totalWarnings = filteredDiagnostics.filter((d)=>d.severity === "warning").length;
56115
+ const totalInfos = filteredDiagnostics.filter((d)=>d.severity === "info").length;
56116
+ const filteredQualityResult = output.qualityResult && configKey === "instructions" ? {
56117
+ ...output.qualityResult,
56118
+ suggestions: filterInstructionSuggestions(output.qualityResult.suggestions, targetRules)
56119
+ } : output.qualityResult;
56120
+ return {
56121
+ ...output,
56122
+ diagnostics: filteredDiagnostics,
56123
+ qualityResult: filteredQualityResult,
56124
+ summary: {
56125
+ ...output.summary,
56126
+ totalErrors,
56127
+ totalWarnings,
56128
+ totalInfos
56129
+ }
56130
+ };
55937
56131
  }
55938
56132
  /**
55939
56133
  * The `lint` command for validating agent skills, custom agents, and instruction files.
@@ -55967,6 +56161,15 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
55967
56161
  run: async (context)=>{
55968
56162
  const rawTargetDir = typeof context.data?.targetDir === "string" ? context.data.targetDir : process.cwd();
55969
56163
  const targetDir = validateTargetDir(rawTargetDir);
56164
+ let rulesConfig;
56165
+ try {
56166
+ rulesConfig = await loadLintConfig(targetDir);
56167
+ } catch (error) {
56168
+ const message = error instanceof Error ? error.message : String(error);
56169
+ consola.error(`Failed to load lint configuration: ${message}`);
56170
+ process.exitCode = 1;
56171
+ return;
56172
+ }
55970
56173
  const lintSkillsFlag = context.args?.skills === true || context.data?.skills === true;
55971
56174
  const lintAgentsFlag = context.args?.agents === true || context.data?.agents === true;
55972
56175
  const lintInstructionsFlag = context.args?.instructions === true || context.data?.instructions === true;
@@ -55981,19 +56184,22 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
55981
56184
  let totalWarnings = 0;
55982
56185
  const allOutputs = [];
55983
56186
  if (noFlagProvided || lintSkillsFlag) {
55984
- const skillOutput = await lintSkills(targetDir);
56187
+ const rawOutput = await lintSkills(targetDir);
56188
+ const skillOutput = applySeverityFilter(rawOutput, rulesConfig);
55985
56189
  allOutputs.push(skillOutput);
55986
56190
  totalErrors += skillOutput.summary.totalErrors;
55987
56191
  totalWarnings += skillOutput.summary.totalWarnings;
55988
56192
  }
55989
56193
  if (noFlagProvided || lintAgentsFlag) {
55990
- const agentOutput = await lintAgents(targetDir);
56194
+ const rawOutput = await lintAgents(targetDir);
56195
+ const agentOutput = applySeverityFilter(rawOutput, rulesConfig);
55991
56196
  allOutputs.push(agentOutput);
55992
56197
  totalErrors += agentOutput.summary.totalErrors;
55993
56198
  totalWarnings += agentOutput.summary.totalWarnings;
55994
56199
  }
55995
56200
  if (noFlagProvided || lintInstructionsFlag) {
55996
- const instructionOutput = await lintInstructions(targetDir);
56201
+ const rawOutput = await lintInstructions(targetDir);
56202
+ const instructionOutput = applySeverityFilter(rawOutput, rulesConfig);
55997
56203
  allOutputs.push(instructionOutput);
55998
56204
  totalErrors += instructionOutput.summary.totalErrors;
55999
56205
  totalWarnings += instructionOutput.summary.totalWarnings;