@lousy-agents/cli 5.11.4 → 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.
@@ -18,11 +18,11 @@
18
18
  },
19
19
  "devDependencies": {
20
20
  "@biomejs/biome": "2.4.13",
21
- "@lousy-agents/mcp": "5.11.3",
21
+ "@lousy-agents/mcp": "5.11.4",
22
22
  "@modelcontextprotocol/server-sequential-thinking": "2025.12.18",
23
23
  "@testcontainers/postgresql": "11.14.0",
24
24
  "@types/node": "24.12.2",
25
- "@upstash/context7-mcp": "2.2.0",
25
+ "@upstash/context7-mcp": "2.2.1",
26
26
  "chance": "1.1.13",
27
27
  "testcontainers": "11.14.0",
28
28
  "tsx": "4.21.0",
@@ -956,9 +956,9 @@
956
956
  }
957
957
  },
958
958
  "node_modules/@lousy-agents/mcp": {
959
- "version": "5.11.3",
960
- "resolved": "https://registry.npmjs.org/@lousy-agents/mcp/-/mcp-5.11.3.tgz",
961
- "integrity": "sha512-J/XUOmJwpArU99A4ObeE63XEhyrga8L3kIFex1Jl2mbUkcIhz7bakvZ1SCgj34FI02ygxk0Z/K0QKBTDTWgqsQ==",
959
+ "version": "5.11.4",
960
+ "resolved": "https://registry.npmjs.org/@lousy-agents/mcp/-/mcp-5.11.4.tgz",
961
+ "integrity": "sha512-3hhhpJcopMWdXBT8cR4gHF9ieCNzDtwQDGWidKeSzbaq8+dW2t0zU0Znow+JW1lxjx3QuI68S09J4id6FpZqow==",
962
962
  "dev": true,
963
963
  "license": "BSD-2-Clause-Patent",
964
964
  "dependencies": {
@@ -1649,9 +1649,9 @@
1649
1649
  "license": "MIT"
1650
1650
  },
1651
1651
  "node_modules/@upstash/context7-mcp": {
1652
- "version": "2.2.0",
1653
- "resolved": "https://registry.npmjs.org/@upstash/context7-mcp/-/context7-mcp-2.2.0.tgz",
1654
- "integrity": "sha512-AYcvmMWaf2HsZMdp8eURiHwGB+0MF4IDkG9IB22Th9OfoCWL5rldeSL7pEFxxaWBcRl0yOga1exo+1Ga4d2vMA==",
1652
+ "version": "2.2.1",
1653
+ "resolved": "https://registry.npmjs.org/@upstash/context7-mcp/-/context7-mcp-2.2.1.tgz",
1654
+ "integrity": "sha512-0Uu7GekKYaP/sgSCeoClYFqCm00rqehMFfHhj14krs0cVkVVz9vrChhZdV5kmkYDNNnQ7Vd6F5b57evina2+5w==",
1655
1655
  "dev": true,
1656
1656
  "license": "MIT",
1657
1657
  "dependencies": {
@@ -26,11 +26,11 @@
26
26
  },
27
27
  "devDependencies": {
28
28
  "@biomejs/biome": "2.4.13",
29
- "@lousy-agents/mcp": "5.11.3",
29
+ "@lousy-agents/mcp": "5.11.4",
30
30
  "@modelcontextprotocol/server-sequential-thinking": "2025.12.18",
31
31
  "@testcontainers/postgresql": "11.14.0",
32
32
  "@types/node": "24.12.2",
33
- "@upstash/context7-mcp": "2.2.0",
33
+ "@upstash/context7-mcp": "2.2.1",
34
34
  "chance": "1.1.13",
35
35
  "testcontainers": "11.14.0",
36
36
  "tsx": "4.21.0",
@@ -20,10 +20,10 @@
20
20
  },
21
21
  "devDependencies": {
22
22
  "@biomejs/biome": "2.4.13",
23
- "@lousy-agents/mcp": "5.11.3",
23
+ "@lousy-agents/mcp": "5.11.4",
24
24
  "@modelcontextprotocol/server-sequential-thinking": "2025.12.18",
25
25
  "@types/node": "24.12.2",
26
- "@upstash/context7-mcp": "2.2.0",
26
+ "@upstash/context7-mcp": "2.2.1",
27
27
  "chance": "1.1.13",
28
28
  "tsx": "4.21.0",
29
29
  "typescript": "6.0.3",
package/dist/index.js CHANGED
@@ -47095,7 +47095,8 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
47095
47095
  "instruction/parse-error": "warn",
47096
47096
  "instruction/command-not-in-code-block": "warn",
47097
47097
  "instruction/command-outside-section": "warn",
47098
- "instruction/missing-error-handling": "warn"
47098
+ "instruction/missing-error-handling": "warn",
47099
+ "instruction/missing-structural-heading": "warn"
47099
47100
  },
47100
47101
  skills: {
47101
47102
  "skill/invalid-frontmatter": "error",
@@ -47188,6 +47189,24 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
47188
47189
  "Validation Suite",
47189
47190
  "Commands"
47190
47191
  ];
47192
+ /**
47193
+ * Descriptions for each default structural heading pattern explaining why it is recommended.
47194
+ * Keyed by Record<StructuralHeadingPattern, string> to ensure compile-time coverage — every
47195
+ * entry in DEFAULT_STRUCTURAL_HEADING_PATTERNS must have a description here.
47196
+ */ const HEADING_PATTERN_DESCRIPTION_RECORD = {
47197
+ // biome-ignore lint/style/useNamingConvention: heading pattern keys use PascalCase by design
47198
+ Validation: "Agents need this section to understand how to validate that their changes meet quality standards.",
47199
+ // biome-ignore lint/style/useNamingConvention: heading pattern keys use PascalCase by design
47200
+ Verification: "Agents need this section to understand how to verify their implementation is correct.",
47201
+ "Feedback Loop": "Agents need this section to understand the iterative improvement process to follow.",
47202
+ // biome-ignore lint/style/useNamingConvention: heading pattern keys use PascalCase by design
47203
+ Mandatory: "Agents need this section to understand which steps are required and cannot be skipped.",
47204
+ "Before Commit": "Agents need this section to know what checks to run before committing changes.",
47205
+ "Validation Suite": "Agents need this section to know which validation commands to run against the codebase.",
47206
+ // biome-ignore lint/style/useNamingConvention: heading pattern keys use PascalCase by design
47207
+ Commands: "Agents need this section to know which commands and tools are available in the project."
47208
+ };
47209
+ const HEADING_PATTERN_DESCRIPTIONS = new Map(Object.entries(HEADING_PATTERN_DESCRIPTION_RECORD));
47191
47210
  /** Conditional keywords indicating error handling near code blocks */ const CONDITIONAL_KEYWORDS = [
47192
47211
  "if",
47193
47212
  "fail",
@@ -47207,6 +47226,24 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
47207
47226
  * Use case for analyzing instruction quality across a repository.
47208
47227
  * Orchestrates discovery, AST parsing, and scoring of feedback loop documentation.
47209
47228
  */
47229
+
47230
+ /** Maximum number of raw heading pattern entries accepted before deduplication. */ const MAX_RAW_HEADING_PATTERNS = 1000;
47231
+ /** Maximum number of heading patterns accepted in a single execute() call. */ const MAX_HEADING_PATTERNS = 50;
47232
+ /** Maximum character length for a single heading pattern. */ const MAX_PATTERN_LENGTH = 200;
47233
+ /**
47234
+ * Lowercase-keyed lookup built once from HEADING_PATTERN_DESCRIPTIONS.
47235
+ * Enables description lookup regardless of the casing callers supply for patterns.
47236
+ */ const HEADING_DESCRIPTIONS_BY_LOWER = new Map(Array.from(HEADING_PATTERN_DESCRIPTIONS, ([k, v])=>[
47237
+ k.toLowerCase(),
47238
+ v
47239
+ ]));
47240
+ /**
47241
+ * Input for the analyze instruction quality use case.
47242
+ */ const AnalyzeInstructionQualityInputSchema = schemas_object({
47243
+ targetDir: schemas_string().min(1, "Target directory is required"),
47244
+ headingPatterns: schemas_array(schemas_string().max(MAX_PATTERN_LENGTH * 2)).max(MAX_RAW_HEADING_PATTERNS).optional(),
47245
+ proximityWindow: schemas_number().int().positive().optional()
47246
+ });
47210
47247
  /**
47211
47248
  * Use case for analyzing instruction quality.
47212
47249
  */ class AnalyzeInstructionQualityUseCase {
@@ -47218,16 +47255,107 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
47218
47255
  this.astGateway = astGateway;
47219
47256
  this.commandsGateway = commandsGateway;
47220
47257
  }
47258
+ /**
47259
+ * Returns true if the given heading-pattern string contains any characters
47260
+ * that could cause terminal injection or garbled output when the pattern
47261
+ * is embedded in a diagnostic message.
47262
+ *
47263
+ * Rejects: ASCII C0 (0x00–0x1F), DEL (0x7F), C1 (0x80–0x9F), **lone**
47264
+ * Unicode surrogates (U+D800–U+DFFF — but valid surrogate pairs like emoji
47265
+ * are accepted), line/paragraph separators (U+2028–U+2029), and bidi
47266
+ * override characters (U+202A–U+202E, U+2066–U+2069).
47267
+ */ static patternHasControlCharacters(value) {
47268
+ for(let i = 0; i < value.length; i++){
47269
+ const code = value.charCodeAt(i);
47270
+ if (code <= 0x1f) return true;
47271
+ if (code === 0x7f) return true;
47272
+ if (code >= 0x80 && code <= 0x9f) return true;
47273
+ // Detect lone surrogates only; valid surrogate pairs (e.g., emoji)
47274
+ // should be accepted. A high surrogate (0xD800–0xDBFF) is lone if
47275
+ // it is not immediately followed by a low surrogate (0xDC00–0xDFFF).
47276
+ if (code >= 0xd800 && code <= 0xdbff) {
47277
+ const next = value.charCodeAt(i + 1);
47278
+ if (next >= 0xdc00 && next <= 0xdfff) {
47279
+ i++; // valid pair — skip the low surrogate
47280
+ } else {
47281
+ return true; // lone high surrogate
47282
+ }
47283
+ } else if (code >= 0xdc00 && code <= 0xdfff) {
47284
+ return true; // lone low surrogate
47285
+ }
47286
+ if (code === 0x2028 || code === 0x2029) return true;
47287
+ if (code >= 0x202a && code <= 0x202e) return true;
47288
+ if (code >= 0x2066 && code <= 0x2069) return true;
47289
+ }
47290
+ return false;
47291
+ }
47292
+ /**
47293
+ * Serializes a heading pattern for safe inclusion in error messages.
47294
+ * Produces a quoted, unambiguous representation by:
47295
+ * - Escaping `"` as `\"` and `\` as `\\` within printable ASCII (0x20–0x7E)
47296
+ * - Escaping every code unit outside that range to `\uXXXX`
47297
+ *
47298
+ * The \uXXXX escaping prevents terminal injection via bidi override
47299
+ * characters and other non-printable code points that JSON.stringify does
47300
+ * not escape.
47301
+ */ static serializePatternForError(value) {
47302
+ let result = '"';
47303
+ for(let i = 0; i < value.length; i++){
47304
+ const code = value.charCodeAt(i);
47305
+ if (code >= 0x20 && code <= 0x7e) {
47306
+ if (code === 0x22) {
47307
+ result += '\\"';
47308
+ } else if (code === 0x5c) {
47309
+ result += "\\\\";
47310
+ } else {
47311
+ result += value[i];
47312
+ }
47313
+ } else {
47314
+ result += `\\u${code.toString(16).padStart(4, "0")}`;
47315
+ }
47316
+ }
47317
+ return `${result}"`;
47318
+ }
47221
47319
  async execute(input) {
47222
- if (!input.targetDir) {
47223
- throw new Error("Target directory is required");
47320
+ let parsed;
47321
+ try {
47322
+ parsed = AnalyzeInstructionQualityInputSchema.parse(input);
47323
+ } catch (error) {
47324
+ if (error instanceof ZodError) {
47325
+ throw new Error(error.issues[0]?.message ?? "Validation failed: unable to parse error details");
47326
+ }
47327
+ throw error;
47224
47328
  }
47225
- const headingPatterns = input.headingPatterns ?? [
47329
+ const seenLower = new Set();
47330
+ const headingPatterns = [];
47331
+ for (const raw of parsed.headingPatterns ?? [
47226
47332
  ...DEFAULT_STRUCTURAL_HEADING_PATTERNS
47227
- ];
47228
- const proximityWindow = input.proximityWindow ?? 3;
47229
- const discoveredFiles = await this.discoveryGateway.discoverInstructionFiles(input.targetDir);
47230
- const mandatoryCommands = await this.commandsGateway.getMandatoryCommands(input.targetDir);
47333
+ ]){
47334
+ // Validate BEFORE trim() U+2028/U+2029 are JavaScript whitespace
47335
+ // and would be silently stripped by trim(), bypassing the check.
47336
+ // Filter before transform.
47337
+ if (AnalyzeInstructionQualityUseCase.patternHasControlCharacters(raw)) {
47338
+ throw new Error(`headingPatterns must not contain control characters, bidi override characters, or lone surrogate code points: ${AnalyzeInstructionQualityUseCase.serializePatternForError(raw)}`);
47339
+ }
47340
+ const trimmed = raw.trim();
47341
+ if (trimmed.length === 0) {
47342
+ throw new Error("headingPatterns must not contain empty or whitespace-only entries");
47343
+ }
47344
+ if (trimmed.length > MAX_PATTERN_LENGTH) {
47345
+ throw new Error(`headingPatterns entries must not exceed ${MAX_PATTERN_LENGTH} characters`);
47346
+ }
47347
+ const lower = trimmed.toLowerCase();
47348
+ if (!seenLower.has(lower)) {
47349
+ seenLower.add(lower);
47350
+ if (seenLower.size > MAX_HEADING_PATTERNS) {
47351
+ throw new Error(`headingPatterns must contain at most ${MAX_HEADING_PATTERNS} unique entries (case-insensitive)`);
47352
+ }
47353
+ headingPatterns.push(trimmed);
47354
+ }
47355
+ }
47356
+ const proximityWindow = parsed.proximityWindow ?? 3;
47357
+ const discoveredFiles = await this.discoveryGateway.discoverInstructionFiles(parsed.targetDir);
47358
+ const mandatoryCommands = await this.commandsGateway.getMandatoryCommands(parsed.targetDir);
47231
47359
  if (discoveredFiles.length === 0) {
47232
47360
  return {
47233
47361
  result: {
@@ -47260,7 +47388,7 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
47260
47388
  }
47261
47389
  }
47262
47390
  // Sort parsing errors for deterministic output
47263
- parsingErrors.sort((a, b)=>a.filePath.localeCompare(b.filePath));
47391
+ parsingErrors.sort((a, b)=>a.filePath < b.filePath ? -1 : a.filePath > b.filePath ? 1 : 0);
47264
47392
  // Score each mandatory command across all files
47265
47393
  const commandScores = [];
47266
47394
  const diagnostics = [];
@@ -47275,6 +47403,16 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
47275
47403
  target: "instruction"
47276
47404
  });
47277
47405
  }
47406
+ // Check each successfully parsed file for missing structural headings
47407
+ const sortedFilePaths = Array.from(fileStructures.keys()).sort((a, b)=>a < b ? -1 : a > b ? 1 : 0);
47408
+ for (const filePath of sortedFilePaths){
47409
+ const structure = fileStructures.get(filePath);
47410
+ if (!structure) {
47411
+ continue;
47412
+ }
47413
+ const missingDiagnostics = this.checkMissingHeadings(filePath, structure, headingPatterns);
47414
+ diagnostics.push(...missingDiagnostics);
47415
+ }
47278
47416
  for (const command of mandatoryCommands){
47279
47417
  const bestAnalysis = this.findBestScore(command, discoveredFiles, fileStructures, headingPatterns, proximityWindow, diagnostics);
47280
47418
  if (bestAnalysis) {
@@ -47516,6 +47654,43 @@ const remark = unified().use(remarkParse).use(remarkStringify).freeze()
47516
47654
  }
47517
47655
  return false;
47518
47656
  }
47657
+ /**
47658
+ * Emits a warning diagnostic for each heading pattern that is absent from the file.
47659
+ * Headings are matched case-insensitively using substring inclusion, matching the
47660
+ * same logic used elsewhere in the use case.
47661
+ */ checkMissingHeadings(filePath, structure, headingPatterns) {
47662
+ // Precondition: `headingPatterns` must already be deduplicated
47663
+ // (case-insensitively) and validated. This invariant is enforced by
47664
+ // `execute()` before calling this method.
47665
+ const diagnostics = [];
47666
+ const fileHeadingTexts = structure.headings.map((h)=>h.text.toLowerCase());
47667
+ for (const pattern of headingPatterns){
47668
+ const patternLower = pattern.toLowerCase();
47669
+ const hasHeading = fileHeadingTexts.some((text)=>{
47670
+ if (!text.includes(patternLower)) {
47671
+ return false;
47672
+ }
47673
+ // Don't let a heading satisfy a shorter pattern when it also satisfies
47674
+ // a longer, more-specific pattern that starts with the shorter one.
47675
+ // E.g., a "Validation Suite" heading satisfies "Validation Suite" but
47676
+ // should NOT also satisfy "Validation".
47677
+ const supersededByMoreSpecific = headingPatterns.some((other)=>other !== pattern && other.toLowerCase().startsWith(patternLower) && text.includes(other.toLowerCase()));
47678
+ return !supersededByMoreSpecific;
47679
+ });
47680
+ if (!hasHeading) {
47681
+ const description = HEADING_DESCRIPTIONS_BY_LOWER.get(patternLower) ?? "This heading helps guide coding agents through structured workflows.";
47682
+ diagnostics.push({
47683
+ filePath,
47684
+ line: 1,
47685
+ severity: "warning",
47686
+ message: `Missing '${pattern}' heading section. ${description}`,
47687
+ ruleId: "instruction/missing-structural-heading",
47688
+ target: "instruction"
47689
+ });
47690
+ }
47691
+ }
47692
+ return diagnostics;
47693
+ }
47519
47694
  generateSuggestions(commandScores) {
47520
47695
  const suggestions = [];
47521
47696
  const lowStructural = commandScores.filter((s)=>s.structuralContext === 0 && s.bestSourceFile !== "");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lousy-agents/cli",
3
- "version": "5.11.4",
3
+ "version": "5.12.0",
4
4
  "description": "CLI scaffolding tool that sets up projects with structure, instructions, and feedback loops for AI coding assistants",
5
5
  "type": "module",
6
6
  "repository": {
@@ -23,20 +23,20 @@
23
23
  "devDependencies": {
24
24
  "@biomejs/biome": "2.4.13",
25
25
  "@eslint/eslintrc": "3.3.5",
26
- "@lousy-agents/mcp": "5.11.3",
26
+ "@lousy-agents/mcp": "5.11.4",
27
27
  "@modelcontextprotocol/server-sequential-thinking": "2025.12.18",
28
28
  "@testing-library/jest-dom": "6.9.1",
29
29
  "@testing-library/react": "16.3.2",
30
30
  "@types/node": "24.12.2",
31
31
  "@types/react": "19.2.14",
32
32
  "@types/react-dom": "19.2.3",
33
- "@upstash/context7-mcp": "2.2.0",
33
+ "@upstash/context7-mcp": "2.2.1",
34
34
  "@vitejs/plugin-react": "6.0.1",
35
35
  "@vitest/ui": "4.1.5",
36
36
  "eslint": "10.2.1",
37
37
  "eslint-config-next": "16.2.4",
38
38
  "happy-dom": "20.9.0",
39
- "jsdom": "29.0.2",
39
+ "jsdom": "29.1.0",
40
40
  "typescript": "6.0.3",
41
41
  "vitest": "4.1.5",
42
42
  "whatwg-fetch": "3.6.20"