@arvorco/relentless 0.3.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.claude/commands/relentless.constitution.md +1 -1
  2. package/.claude/commands/relentless.convert.md +25 -0
  3. package/.claude/commands/relentless.specify.md +1 -1
  4. package/.claude/skills/analyze/SKILL.md +113 -40
  5. package/.claude/skills/analyze/templates/analysis-report.md +138 -0
  6. package/.claude/skills/checklist/SKILL.md +143 -51
  7. package/.claude/skills/checklist/templates/checklist.md +43 -11
  8. package/.claude/skills/clarify/SKILL.md +70 -11
  9. package/.claude/skills/constitution/SKILL.md +61 -3
  10. package/.claude/skills/constitution/templates/constitution.md +241 -160
  11. package/.claude/skills/constitution/templates/prompt.md +150 -20
  12. package/.claude/skills/convert/SKILL.md +248 -0
  13. package/.claude/skills/implement/SKILL.md +82 -34
  14. package/.claude/skills/plan/SKILL.md +136 -27
  15. package/.claude/skills/plan/templates/plan.md +92 -9
  16. package/.claude/skills/specify/SKILL.md +110 -19
  17. package/.claude/skills/specify/scripts/bash/create-new-feature.sh +2 -2
  18. package/.claude/skills/specify/scripts/bash/setup-plan.sh +1 -1
  19. package/.claude/skills/specify/templates/spec.md +40 -5
  20. package/.claude/skills/tasks/SKILL.md +75 -1
  21. package/.claude/skills/tasks/templates/tasks.md +5 -4
  22. package/CHANGELOG.md +63 -1
  23. package/MANUAL.md +40 -0
  24. package/README.md +263 -11
  25. package/bin/relentless.ts +292 -5
  26. package/package.json +2 -2
  27. package/relentless/config.json +46 -2
  28. package/relentless/constitution.md +2 -2
  29. package/relentless/prompt.md +97 -18
  30. package/src/agents/amp.ts +53 -13
  31. package/src/agents/claude.ts +70 -15
  32. package/src/agents/codex.ts +73 -14
  33. package/src/agents/droid.ts +68 -14
  34. package/src/agents/exec.ts +96 -0
  35. package/src/agents/gemini.ts +59 -16
  36. package/src/agents/opencode.ts +188 -9
  37. package/src/cli/fallback-order.ts +210 -0
  38. package/src/cli/index.ts +63 -0
  39. package/src/cli/mode-flag.ts +198 -0
  40. package/src/cli/review-flags.ts +192 -0
  41. package/src/config/loader.ts +16 -1
  42. package/src/config/schema.ts +157 -2
  43. package/src/execution/runner.ts +144 -21
  44. package/src/init/scaffolder.ts +285 -25
  45. package/src/prd/parser.ts +92 -1
  46. package/src/prd/types.ts +136 -0
  47. package/src/review/index.ts +92 -0
  48. package/src/review/prompt.ts +293 -0
  49. package/src/review/runner.ts +337 -0
  50. package/src/review/tasks/docs.ts +529 -0
  51. package/src/review/tasks/index.ts +80 -0
  52. package/src/review/tasks/lint.ts +436 -0
  53. package/src/review/tasks/quality.ts +760 -0
  54. package/src/review/tasks/security.ts +452 -0
  55. package/src/review/tasks/test.ts +456 -0
  56. package/src/review/tasks/typecheck.ts +323 -0
  57. package/src/review/types.ts +139 -0
  58. package/src/routing/cascade.ts +310 -0
  59. package/src/routing/classifier.ts +338 -0
  60. package/src/routing/estimate.ts +270 -0
  61. package/src/routing/fallback.ts +512 -0
  62. package/src/routing/index.ts +124 -0
  63. package/src/routing/registry.ts +501 -0
  64. package/src/routing/report.ts +570 -0
  65. package/src/routing/router.ts +287 -0
  66. package/src/tui/App.tsx +2 -0
  67. package/src/tui/TUIRunner.tsx +103 -8
  68. package/src/tui/components/CurrentStory.tsx +23 -1
  69. package/src/tui/hooks/useTUI.ts +1 -0
  70. package/src/tui/types.ts +9 -0
  71. package/.claude/skills/specify/scripts/bash/update-agent-context.sh +0 -799
@@ -0,0 +1,436 @@
1
+ /**
2
+ * Lint Micro-Task
3
+ *
4
+ * Runs `bun run lint --format json` in the project's working directory,
5
+ * parses ESLint issues, and generates fix tasks.
6
+ *
7
+ * Features:
8
+ * - Parses ESLint JSON output with file, line, column, severity, rule, message
9
+ * - Groups issues by file for efficient fixing
10
+ * - Generates fix tasks with priority "high" for errors only
11
+ * - Warnings are logged but NOT added to fixTasks
12
+ * - Reports parsing errors separately from lint violations
13
+ * - Includes autoFixable count for issues fixable with --fix
14
+ * - Includes disabledRulesCount for eslint-disable comments
15
+ * - Provides summary with total files scanned and breakdown by severity
16
+ * - Falls back to standard output parsing if JSON unavailable
17
+ *
18
+ * @module src/review/tasks/lint
19
+ */
20
+
21
+ import type { ReviewTaskResult, FixTask } from "../types";
22
+
23
+ /**
24
+ * Severity levels for lint issues
25
+ */
26
+ export type LintSeverity = "error" | "warning";
27
+
28
+ /**
29
+ * A parsed ESLint issue
30
+ */
31
+ export interface LintIssue {
32
+ /** File path */
33
+ file: string;
34
+ /** Line number (1-based) */
35
+ line: number;
36
+ /** Column number (1-based) */
37
+ column: number;
38
+ /** Issue severity */
39
+ severity: LintSeverity;
40
+ /** ESLint rule ID (e.g., "no-unused-vars") */
41
+ rule: string;
42
+ /** Issue message text */
43
+ message: string;
44
+ }
45
+
46
+ /**
47
+ * Summary of lint results
48
+ */
49
+ export interface LintSummary {
50
+ /** Total files scanned */
51
+ totalFiles: number;
52
+ /** Total error count */
53
+ errorCount: number;
54
+ /** Total warning count */
55
+ warningCount: number;
56
+ /** Number of issues that can be auto-fixed with --fix */
57
+ autoFixable: number;
58
+ /** Number of parsing errors (invalid JS/TS) */
59
+ parsingErrors: number;
60
+ /** Count of eslint-disable comments */
61
+ disabledRulesCount: number;
62
+ }
63
+
64
+ /**
65
+ * Result from parsing lint output
66
+ */
67
+ export interface LintParseResult {
68
+ /** Parsed lint issues */
69
+ issues: LintIssue[];
70
+ /** Summary statistics */
71
+ summary: LintSummary;
72
+ /** Parsing error message if JSON parsing failed */
73
+ parsingError?: string;
74
+ }
75
+
76
+ /**
77
+ * Extended result type for lint micro-task
78
+ */
79
+ export interface LintResult extends ReviewTaskResult {
80
+ /** The command that was executed */
81
+ command: string;
82
+ /** Warning count (non-blocking) */
83
+ warningCount: number;
84
+ /** Number of issues that can be auto-fixed */
85
+ autoFixable?: number;
86
+ /** Number of parsing errors */
87
+ parsingErrors?: number;
88
+ /** Count of eslint-disable comments */
89
+ disabledRulesCount?: number;
90
+ /** Human-readable summary */
91
+ summary?: string;
92
+ }
93
+
94
+ /**
95
+ * Options for running lint
96
+ */
97
+ export interface LintOptions {
98
+ /** Working directory for the command */
99
+ cwd?: string;
100
+ }
101
+
102
+ /**
103
+ * ESLint JSON message format
104
+ */
105
+ interface ESLintMessage {
106
+ ruleId: string | null;
107
+ severity: 1 | 2;
108
+ message: string;
109
+ line: number;
110
+ column: number;
111
+ fatal?: boolean;
112
+ fix?: { range: [number, number]; text: string };
113
+ }
114
+
115
+ /**
116
+ * ESLint JSON file result format
117
+ */
118
+ interface ESLintFileResult {
119
+ filePath: string;
120
+ messages: ESLintMessage[];
121
+ errorCount: number;
122
+ warningCount: number;
123
+ fixableErrorCount?: number;
124
+ fixableWarningCount?: number;
125
+ usedDeprecatedRules?: unknown[];
126
+ }
127
+
128
+ /**
129
+ * Parse ESLint JSON output into structured issues
130
+ *
131
+ * @param output - Raw ESLint JSON output
132
+ * @returns Parsed issues and summary
133
+ */
134
+ export function parseLintOutput(output: string): LintParseResult {
135
+ const summary: LintSummary = {
136
+ totalFiles: 0,
137
+ errorCount: 0,
138
+ warningCount: 0,
139
+ autoFixable: 0,
140
+ parsingErrors: 0,
141
+ disabledRulesCount: 0,
142
+ };
143
+
144
+ const issues: LintIssue[] = [];
145
+
146
+ try {
147
+ const results: ESLintFileResult[] = JSON.parse(output);
148
+
149
+ summary.totalFiles = results.length;
150
+
151
+ for (const fileResult of results) {
152
+ summary.errorCount += fileResult.errorCount;
153
+ summary.warningCount += fileResult.warningCount;
154
+ summary.autoFixable +=
155
+ (fileResult.fixableErrorCount || 0) +
156
+ (fileResult.fixableWarningCount || 0);
157
+
158
+ for (const message of fileResult.messages) {
159
+ // Track parsing errors
160
+ if (message.fatal || message.ruleId === null) {
161
+ summary.parsingErrors++;
162
+ }
163
+
164
+ const severity: LintSeverity = message.severity === 2 ? "error" : "warning";
165
+ const rule = message.ruleId || (message.fatal ? "parsing-error" : "unknown");
166
+
167
+ issues.push({
168
+ file: fileResult.filePath,
169
+ line: message.line,
170
+ column: message.column,
171
+ severity,
172
+ rule,
173
+ message: message.message,
174
+ });
175
+ }
176
+ }
177
+
178
+ return { issues, summary };
179
+ } catch (error) {
180
+ // JSON parsing failed - return empty with error
181
+ return {
182
+ issues: [],
183
+ summary,
184
+ parsingError:
185
+ error instanceof Error ? error.message : "Failed to parse JSON output",
186
+ };
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Parse standard ESLint text output (fallback when JSON unavailable)
192
+ *
193
+ * @param output - Raw ESLint text output
194
+ * @returns Parsed issues and summary
195
+ */
196
+ export function parseFallbackLintOutput(output: string): LintParseResult {
197
+ const summary: LintSummary = {
198
+ totalFiles: 0,
199
+ errorCount: 0,
200
+ warningCount: 0,
201
+ autoFixable: 0,
202
+ parsingErrors: 0,
203
+ disabledRulesCount: 0,
204
+ };
205
+
206
+ const issues: LintIssue[] = [];
207
+ const lines = output.split("\n");
208
+ let currentFile = "";
209
+ const filesSet = new Set<string>();
210
+
211
+ for (const line of lines) {
212
+ // Check for file path (starts with /)
213
+ if (line.startsWith("/") && !line.includes(":")) {
214
+ currentFile = line.trim();
215
+ filesSet.add(currentFile);
216
+ continue;
217
+ }
218
+
219
+ // Parse issue line: " 10:5 error message rule-id"
220
+ const match = line.match(/^\s+(\d+):(\d+)\s+(error|warning)\s+(.+?)\s{2,}(\S+)\s*$/);
221
+ if (match && currentFile) {
222
+ const severity = match[3] as LintSeverity;
223
+ issues.push({
224
+ file: currentFile,
225
+ line: parseInt(match[1], 10),
226
+ column: parseInt(match[2], 10),
227
+ severity,
228
+ rule: match[5],
229
+ message: match[4].trim(),
230
+ });
231
+
232
+ if (severity === "error") {
233
+ summary.errorCount++;
234
+ } else {
235
+ summary.warningCount++;
236
+ }
237
+ }
238
+ }
239
+
240
+ summary.totalFiles = filesSet.size;
241
+
242
+ return { issues, summary };
243
+ }
244
+
245
+ /**
246
+ * Group lint issues by file path
247
+ *
248
+ * @param issues - Array of parsed lint issues
249
+ * @returns Record mapping file paths to their issues
250
+ */
251
+ export function groupIssuesByFile(
252
+ issues: LintIssue[]
253
+ ): Record<string, LintIssue[]> {
254
+ const grouped: Record<string, LintIssue[]> = {};
255
+
256
+ for (const issue of issues) {
257
+ if (!grouped[issue.file]) {
258
+ grouped[issue.file] = [];
259
+ }
260
+ grouped[issue.file].push(issue);
261
+ }
262
+
263
+ return grouped;
264
+ }
265
+
266
+ /**
267
+ * Create a fix task from a lint issue
268
+ *
269
+ * @param issue - The parsed lint issue
270
+ * @returns A fix task for the review system
271
+ */
272
+ function createFixTask(issue: LintIssue): FixTask {
273
+ return {
274
+ type: "lint_fix",
275
+ file: issue.file,
276
+ line: issue.line,
277
+ column: issue.column,
278
+ description: `Fix lint error ${issue.rule} at line ${issue.line}, column ${issue.column}: ${issue.message}`,
279
+ priority: "high",
280
+ rule: issue.rule,
281
+ };
282
+ }
283
+
284
+ /**
285
+ * Generate human-readable summary
286
+ *
287
+ * @param summary - Lint summary statistics
288
+ * @returns Human-readable summary string
289
+ */
290
+ function generateSummary(summary: LintSummary): string {
291
+ const parts: string[] = [];
292
+
293
+ parts.push(`${summary.totalFiles} file${summary.totalFiles !== 1 ? "s" : ""} scanned`);
294
+
295
+ if (summary.errorCount > 0 || summary.warningCount > 0) {
296
+ const issueParts: string[] = [];
297
+ if (summary.errorCount > 0) {
298
+ issueParts.push(`${summary.errorCount} error${summary.errorCount !== 1 ? "s" : ""}`);
299
+ }
300
+ if (summary.warningCount > 0) {
301
+ issueParts.push(`${summary.warningCount} warning${summary.warningCount !== 1 ? "s" : ""}`);
302
+ }
303
+ parts.push(issueParts.join(", "));
304
+ } else {
305
+ parts.push("no issues found");
306
+ }
307
+
308
+ if (summary.autoFixable > 0) {
309
+ parts.push(`${summary.autoFixable} auto-fixable`);
310
+ }
311
+
312
+ if (summary.parsingErrors > 0) {
313
+ parts.push(`${summary.parsingErrors} parsing error${summary.parsingErrors !== 1 ? "s" : ""}`);
314
+ }
315
+
316
+ return parts.join(", ");
317
+ }
318
+
319
+ /**
320
+ * Run the lint micro-task
321
+ *
322
+ * Executes `bun run lint --format json` in the specified working directory,
323
+ * parses the output, and generates fix tasks for any errors found.
324
+ *
325
+ * @param options - Options including working directory
326
+ * @returns LintResult with success status, issues, and fix tasks
327
+ *
328
+ * @example
329
+ * ```typescript
330
+ * const result = await runLint({ cwd: "/path/to/project" });
331
+ * if (!result.success) {
332
+ * console.log(`${result.errorCount} errors found`);
333
+ * result.fixTasks.forEach(task => console.log(task.description));
334
+ * }
335
+ * ```
336
+ */
337
+ export async function runLint(options: LintOptions = {}): Promise<LintResult> {
338
+ const cwd = options.cwd || process.cwd();
339
+ const command = "bun run lint --format json";
340
+ const startTime = Date.now();
341
+
342
+ try {
343
+ // Spawn the lint process with JSON format
344
+ const proc = Bun.spawn(["bun", "run", "lint", "--format", "json"], {
345
+ cwd,
346
+ stdout: "pipe",
347
+ stderr: "pipe",
348
+ });
349
+
350
+ // Wait for completion
351
+ const exitCode = await proc.exited;
352
+ const stdout = await proc.stdout.text();
353
+ const stderr = await proc.stderr.text();
354
+ const duration = Date.now() - startTime;
355
+
356
+ // Check for ESLint configuration error
357
+ if (
358
+ stderr.includes("configuration") ||
359
+ stderr.includes("ESLint") ||
360
+ stderr.includes("Config")
361
+ ) {
362
+ return {
363
+ taskType: "lint",
364
+ success: false,
365
+ errorCount: 1,
366
+ warningCount: 0,
367
+ fixTasks: [],
368
+ duration,
369
+ command,
370
+ error: stderr.includes("configuration")
371
+ ? `ESLint configuration error: ${stderr}`
372
+ : stderr,
373
+ };
374
+ }
375
+
376
+ // Try parsing as JSON first
377
+ let parseResult = parseLintOutput(stdout);
378
+
379
+ // If JSON parsing failed, try fallback format
380
+ if (parseResult.parsingError) {
381
+ parseResult = parseFallbackLintOutput(stdout);
382
+ }
383
+
384
+ const { issues, summary } = parseResult;
385
+
386
+ // Generate fix tasks only for errors (not warnings)
387
+ const errorIssues = issues.filter((issue) => issue.severity === "error");
388
+ const fixTasks = errorIssues.map(createFixTask);
389
+
390
+ // Check for general command failure (stderr has content but no parsed issues)
391
+ if (exitCode !== 0 && issues.length === 0 && stderr.trim()) {
392
+ return {
393
+ taskType: "lint",
394
+ success: false,
395
+ errorCount: 1,
396
+ warningCount: 0,
397
+ fixTasks: [],
398
+ duration,
399
+ command,
400
+ error: stderr.trim(),
401
+ };
402
+ }
403
+
404
+ // Success if no errors (warnings are OK)
405
+ const success = summary.errorCount === 0 && exitCode === 0;
406
+
407
+ return {
408
+ taskType: "lint",
409
+ success,
410
+ errorCount: summary.errorCount,
411
+ warningCount: summary.warningCount,
412
+ fixTasks,
413
+ duration,
414
+ command,
415
+ autoFixable: summary.autoFixable,
416
+ parsingErrors: summary.parsingErrors,
417
+ disabledRulesCount: summary.disabledRulesCount,
418
+ summary: generateSummary(summary),
419
+ };
420
+ } catch (error) {
421
+ const duration = Date.now() - startTime;
422
+ const errorMessage =
423
+ error instanceof Error ? error.message : String(error);
424
+
425
+ return {
426
+ taskType: "lint",
427
+ success: false,
428
+ errorCount: 1,
429
+ warningCount: 0,
430
+ fixTasks: [],
431
+ duration,
432
+ command,
433
+ error: `Command execution failed: ${errorMessage}`,
434
+ };
435
+ }
436
+ }