@arvorco/relentless 0.6.1 → 0.8.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.
Files changed (64) hide show
  1. package/.claude/commands/relentless.learn.md +52 -0
  2. package/.claude/skills/checklist/SKILL.md +9 -2
  3. package/.claude/skills/constitution/SKILL.md +11 -0
  4. package/.claude/skills/constitution/templates/constitution.md +0 -3
  5. package/.claude/skills/learn/SKILL.md +361 -0
  6. package/.claude/skills/learn/scripts/common.sh +46 -0
  7. package/.claude/skills/learn/scripts/extract-costs.sh +48 -0
  8. package/.claude/skills/learn/scripts/extract-errors.sh +47 -0
  9. package/.claude/skills/learn/scripts/extract-failures.sh +47 -0
  10. package/.claude/skills/learn/scripts/extract-learnings.sh +66 -0
  11. package/.claude/skills/learn/scripts/extract-patterns.sh +72 -0
  12. package/.claude/skills/learn/scripts/extract-session-context.sh +77 -0
  13. package/.claude/skills/learn/scripts/generate-stats.sh +129 -0
  14. package/.claude/skills/learn/templates/learning-proposal.md +105 -0
  15. package/.claude/skills/learn/templates/stats.md +69 -0
  16. package/.claude/skills/plan/SKILL.md +10 -3
  17. package/.claude/skills/specify/SKILL.md +10 -4
  18. package/.claude/skills/tasks/SKILL.md +10 -3
  19. package/.claude/skills/validators/SKILL.md +174 -0
  20. package/.claude/skills/validators/scripts/common.sh +182 -0
  21. package/.claude/skills/validators/scripts/validate-checklist.sh +190 -0
  22. package/.claude/skills/validators/scripts/validate-constitution.sh +111 -0
  23. package/.claude/skills/validators/scripts/validate-plan.sh +163 -0
  24. package/.claude/skills/validators/scripts/validate-prompt.sh +125 -0
  25. package/.claude/skills/validators/scripts/validate-spec.sh +214 -0
  26. package/.claude/skills/validators/scripts/validate-tasks.sh +179 -0
  27. package/.claude/skills/validators/scripts/validate.sh +295 -0
  28. package/CHANGELOG.md +112 -1
  29. package/package.json +1 -1
  30. package/relentless/constitution.md +0 -3
  31. package/src/agents/amp.ts +3 -6
  32. package/src/agents/claude.ts +4 -7
  33. package/src/agents/codex.ts +3 -6
  34. package/src/agents/droid.ts +3 -6
  35. package/src/agents/exec.ts +56 -10
  36. package/src/agents/gemini.ts +3 -6
  37. package/src/agents/opencode.ts +3 -6
  38. package/src/agents/types.ts +2 -0
  39. package/src/config/schema.ts +2 -2
  40. package/src/execution/runner.ts +4 -0
  41. package/src/init/scaffolder.ts +2 -0
  42. package/src/tui/App.tsx +132 -16
  43. package/src/tui/TUIRunner.tsx +68 -4
  44. package/src/tui/components/CostBadge.tsx +59 -0
  45. package/src/tui/components/MessageItem.tsx +113 -0
  46. package/src/tui/components/MessageQueuePanel.tsx +126 -0
  47. package/src/tui/components/OutputPanel.tsx +270 -0
  48. package/src/tui/components/QueueInput.tsx +28 -11
  49. package/src/tui/components/RateLimitIndicator.tsx +97 -0
  50. package/src/tui/components/StatusBar.tsx +188 -0
  51. package/src/tui/components/TaskItem.tsx +131 -0
  52. package/src/tui/components/TaskPanel.tsx +189 -0
  53. package/src/tui/components/TokenCounter.tsx +48 -0
  54. package/src/tui/hooks/useAnimation.ts +220 -0
  55. package/src/tui/hooks/useCostTracking.ts +199 -0
  56. package/src/tui/hooks/useResponsiveLayout.ts +94 -0
  57. package/src/tui/hooks/useTUI.ts +57 -1
  58. package/src/tui/index.tsx +24 -0
  59. package/src/tui/layouts/LayoutSwitcher.tsx +95 -0
  60. package/src/tui/layouts/ThreeColumnLayout.tsx +97 -0
  61. package/src/tui/layouts/VerticalLayout.tsx +69 -0
  62. package/src/tui/layouts/index.ts +9 -0
  63. package/src/tui/theme.ts +152 -21
  64. package/src/tui/types.ts +95 -0
@@ -0,0 +1,126 @@
1
+ /**
2
+ * MessageQueuePanel Component
3
+ *
4
+ * mIRC-style message queue panel for the right column
5
+ */
6
+
7
+ import React from "react";
8
+ import { Box, Text } from "ink";
9
+ import { colors, borders, symbols } from "../theme.js";
10
+ import { MessageItem } from "./MessageItem.js";
11
+ import type { MessageItem as MessageItemType } from "../types.js";
12
+ import type { QueueItem } from "../../queue/types.js";
13
+
14
+ interface MessageQueuePanelProps {
15
+ /** Messages to display (mIRC-style) */
16
+ messages: MessageItemType[];
17
+ /** Queue items (pending commands) */
18
+ queueItems: QueueItem[];
19
+ /** Maximum messages to show */
20
+ maxMessages?: number;
21
+ /** Maximum queue items to show */
22
+ maxQueueItems?: number;
23
+ /** Panel title */
24
+ title?: string;
25
+ }
26
+
27
+ /**
28
+ * Format queue item for display
29
+ */
30
+ function formatQueueItem(item: QueueItem, index: number): string {
31
+ return `${index + 1}. ${item.content}`;
32
+ }
33
+
34
+ export function MessageQueuePanel({
35
+ messages,
36
+ queueItems,
37
+ maxMessages = 10,
38
+ maxQueueItems = 3,
39
+ title = "Messages",
40
+ }: MessageQueuePanelProps): React.ReactElement {
41
+ // Get visible messages (most recent)
42
+ const visibleMessages = messages.slice(-maxMessages);
43
+ const hasMoreMessages = messages.length > maxMessages;
44
+
45
+ // Get visible queue items
46
+ const visibleQueueItems = queueItems.slice(0, maxQueueItems);
47
+ const hasMoreQueueItems = queueItems.length > maxQueueItems;
48
+
49
+ return (
50
+ <Box flexDirection="column" paddingX={1} width="100%">
51
+ {/* Messages section */}
52
+ <Box flexDirection="column" marginBottom={1}>
53
+ <Box>
54
+ <Text color={colors.accent} bold>
55
+ {title}
56
+ </Text>
57
+ {messages.length > 0 && (
58
+ <Text color={colors.dim}> ({messages.length})</Text>
59
+ )}
60
+ </Box>
61
+
62
+ {/* Scroll indicator */}
63
+ {hasMoreMessages && (
64
+ <Text color={colors.dim}>
65
+ {symbols.priority.high} {messages.length - maxMessages} more
66
+ </Text>
67
+ )}
68
+
69
+ {/* Messages list */}
70
+ <Box flexDirection="column">
71
+ {visibleMessages.length > 0 ? (
72
+ visibleMessages.map((msg) => (
73
+ <MessageItem key={msg.id} message={msg} maxWidth={25} />
74
+ ))
75
+ ) : (
76
+ <Text color={colors.dim} dimColor>
77
+ No messages yet
78
+ </Text>
79
+ )}
80
+ </Box>
81
+ </Box>
82
+
83
+ {/* Queue section */}
84
+ <Box flexDirection="column">
85
+ <Box>
86
+ <Text color={colors.dim} bold>
87
+ {borders.horizontal}
88
+ {borders.horizontal} Queue {borders.horizontal}
89
+ </Text>
90
+ {queueItems.length > 0 && (
91
+ <Text color={colors.warning}> ({queueItems.length})</Text>
92
+ )}
93
+ </Box>
94
+
95
+ {/* Queue items */}
96
+ <Box flexDirection="column">
97
+ {visibleQueueItems.length > 0 ? (
98
+ visibleQueueItems.map((item, i) => (
99
+ <Text key={item.addedAt} color={colors.dim} wrap="truncate">
100
+ {formatQueueItem(item, i)}
101
+ </Text>
102
+ ))
103
+ ) : (
104
+ <Text color={colors.dim} dimColor>
105
+ Empty
106
+ </Text>
107
+ )}
108
+
109
+ {/* More items indicator */}
110
+ {hasMoreQueueItems && (
111
+ <Text color={colors.dim}>
112
+ +{queueItems.length - maxQueueItems} more
113
+ </Text>
114
+ )}
115
+ </Box>
116
+
117
+ {/* Help text */}
118
+ <Box marginTop={1}>
119
+ <Text color={colors.dim} dimColor>
120
+ Items sent to agent each iteration
121
+ </Text>
122
+ </Box>
123
+ </Box>
124
+ </Box>
125
+ );
126
+ }
@@ -0,0 +1,270 @@
1
+ /**
2
+ * OutputPanel Component
3
+ *
4
+ * Enhanced central output panel with context header,
5
+ * code block detection, and fullscreen support
6
+ */
7
+
8
+ import React, { useMemo } from "react";
9
+ import { Box, Text } from "ink";
10
+ import { colors, borders } from "../theme.js";
11
+ import type { Story, OutputMode } from "../types.js";
12
+ import type { AgentName } from "../../agents/types.js";
13
+
14
+ interface OutputPanelProps {
15
+ /** Output lines to display */
16
+ lines: string[];
17
+ /** Maximum lines to show */
18
+ maxLines?: number;
19
+ /** Current story being worked on */
20
+ currentStory?: Story | null;
21
+ /** Current agent name */
22
+ currentAgent?: string;
23
+ /** Current model being used */
24
+ currentModel?: string;
25
+ /** Routing information */
26
+ routing?: {
27
+ mode: "free" | "cheap" | "good" | "genius";
28
+ complexity: "simple" | "medium" | "complex" | "expert";
29
+ harness: AgentName;
30
+ model: string;
31
+ };
32
+ /** Display mode (normal or fullscreen) */
33
+ displayMode?: OutputMode;
34
+ /** Panel title */
35
+ title?: string;
36
+ }
37
+
38
+ interface ParsedLine {
39
+ text: string;
40
+ type: "normal" | "code" | "code-start" | "code-end" | "header" | "success" | "error" | "warning";
41
+ language?: string;
42
+ }
43
+
44
+ /**
45
+ * Parse output lines to detect code blocks and special formatting
46
+ */
47
+ function parseLines(lines: string[]): ParsedLine[] {
48
+ const parsed: ParsedLine[] = [];
49
+ let inCodeBlock = false;
50
+
51
+ for (const line of lines) {
52
+ // Check for code block markers
53
+ if (line.startsWith("```")) {
54
+ if (inCodeBlock) {
55
+ parsed.push({ text: line, type: "code-end" });
56
+ inCodeBlock = false;
57
+ } else {
58
+ const language = line.substring(3).trim();
59
+ parsed.push({ text: line, type: "code-start", language });
60
+ inCodeBlock = true;
61
+ }
62
+ continue;
63
+ }
64
+
65
+ // Inside code block
66
+ if (inCodeBlock) {
67
+ parsed.push({ text: line, type: "code" });
68
+ continue;
69
+ }
70
+
71
+ // Check for special line types
72
+ if (line.startsWith("---") || line.startsWith("===")) {
73
+ parsed.push({ text: line, type: "header" });
74
+ } else if (
75
+ line.includes("success") ||
76
+ line.includes("complete") ||
77
+ line.includes("\u2713") ||
78
+ line.includes("\uD83C\uDF89")
79
+ ) {
80
+ parsed.push({ text: line, type: "success" });
81
+ } else if (
82
+ line.includes("error") ||
83
+ line.includes("Error") ||
84
+ line.includes("failed") ||
85
+ line.includes("\u274C")
86
+ ) {
87
+ parsed.push({ text: line, type: "error" });
88
+ } else if (
89
+ line.includes("warning") ||
90
+ line.includes("Warning") ||
91
+ line.includes("\u26A0")
92
+ ) {
93
+ parsed.push({ text: line, type: "warning" });
94
+ } else {
95
+ parsed.push({ text: line, type: "normal" });
96
+ }
97
+ }
98
+
99
+ return parsed;
100
+ }
101
+
102
+ /**
103
+ * Get color for a parsed line type
104
+ */
105
+ function getLineColor(type: ParsedLine["type"]): string {
106
+ switch (type) {
107
+ case "code":
108
+ case "code-start":
109
+ case "code-end":
110
+ return colors.accent;
111
+ case "header":
112
+ return colors.primary;
113
+ case "success":
114
+ return colors.success;
115
+ case "error":
116
+ return colors.error;
117
+ case "warning":
118
+ return colors.warning;
119
+ default:
120
+ return colors.dim;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Format mode badge text
126
+ */
127
+ function getModeBadge(mode: string): { text: string; color: string } {
128
+ switch (mode) {
129
+ case "free":
130
+ return { text: "FREE", color: colors.success };
131
+ case "cheap":
132
+ return { text: "CHEAP", color: colors.primary };
133
+ case "good":
134
+ return { text: "GOOD", color: colors.warning };
135
+ case "genius":
136
+ return { text: "GENIUS", color: colors.accent };
137
+ default:
138
+ return { text: mode.toUpperCase(), color: colors.dim };
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Format complexity badge
144
+ */
145
+ function getComplexityBadge(complexity: string): { text: string; color: string } {
146
+ switch (complexity) {
147
+ case "simple":
148
+ return { text: "S", color: colors.success };
149
+ case "medium":
150
+ return { text: "M", color: colors.warning };
151
+ case "complex":
152
+ return { text: "C", color: colors.error };
153
+ case "expert":
154
+ return { text: "E", color: colors.accent };
155
+ default:
156
+ return { text: complexity[0]?.toUpperCase() ?? "?", color: colors.dim };
157
+ }
158
+ }
159
+
160
+ export function OutputPanel({
161
+ lines,
162
+ maxLines = 12,
163
+ currentStory,
164
+ currentAgent,
165
+ currentModel,
166
+ routing,
167
+ displayMode = "normal",
168
+ title = "Output",
169
+ }: OutputPanelProps): React.ReactElement {
170
+ // Parse lines for syntax highlighting
171
+ const parsedLines = useMemo(() => parseLines(lines), [lines]);
172
+
173
+ // Get visible lines based on max
174
+ const clampedMaxLines = Math.max(0, maxLines);
175
+ const displayLines =
176
+ clampedMaxLines > 0 ? parsedLines.slice(-clampedMaxLines) : [];
177
+
178
+ // Build context header
179
+ const hasContext = currentStory || currentAgent || routing;
180
+
181
+ return (
182
+ <Box flexDirection="column" paddingX={1} width="100%">
183
+ {/* Context header */}
184
+ {hasContext && (
185
+ <Box flexDirection="row" marginBottom={1}>
186
+ {/* Story ID */}
187
+ {currentStory && (
188
+ <>
189
+ <Text color={colors.warning} bold>
190
+ {currentStory.id}
191
+ </Text>
192
+ <Text color={colors.dim}> {borders.vertical} </Text>
193
+ </>
194
+ )}
195
+
196
+ {/* Agent/Model */}
197
+ {(currentAgent || currentModel) && (
198
+ <>
199
+ <Text color={colors.primary}>
200
+ {currentAgent ?? ""}
201
+ {currentModel ? `/${currentModel}` : ""}
202
+ </Text>
203
+ </>
204
+ )}
205
+
206
+ {/* Routing info */}
207
+ {routing && (
208
+ <>
209
+ <Text color={colors.dim}> {borders.vertical} </Text>
210
+ <Text color={getModeBadge(routing.mode).color}>
211
+ {getModeBadge(routing.mode).text}
212
+ </Text>
213
+ <Text color={colors.dim}>/</Text>
214
+ <Text color={getComplexityBadge(routing.complexity).color}>
215
+ {getComplexityBadge(routing.complexity).text}
216
+ </Text>
217
+ </>
218
+ )}
219
+
220
+ {/* Fullscreen indicator */}
221
+ {displayMode === "fullscreen" && (
222
+ <>
223
+ <Text color={colors.dim}> {borders.vertical} </Text>
224
+ <Text color={colors.accent}>[F] Fullscreen</Text>
225
+ </>
226
+ )}
227
+ </Box>
228
+ )}
229
+
230
+ {/* Title bar */}
231
+ <Box>
232
+ <Text color={colors.dim} bold>
233
+ {borders.horizontal}
234
+ {borders.horizontal} {title} {borders.horizontal}
235
+ {borders.horizontal}
236
+ </Text>
237
+ </Box>
238
+
239
+ {/* Output content */}
240
+ <Box flexDirection="column">
241
+ {displayLines.length > 0 ? (
242
+ displayLines.map((line, i) => (
243
+ <Text
244
+ key={i}
245
+ color={getLineColor(line.type)}
246
+ dimColor={line.type === "normal"}
247
+ wrap="truncate"
248
+ >
249
+ {line.text}
250
+ </Text>
251
+ ))
252
+ ) : (
253
+ <Text color={colors.dim} dimColor>
254
+ Waiting for agent output...
255
+ </Text>
256
+ )}
257
+ </Box>
258
+
259
+ {/* Auto-scroll indicator */}
260
+ {lines.length > clampedMaxLines && (
261
+ <Box marginTop={1}>
262
+ <Text color={colors.dim}>
263
+ {borders.vertical} {lines.length - clampedMaxLines} lines above{" "}
264
+ {borders.vertical}
265
+ </Text>
266
+ </Box>
267
+ )}
268
+ </Box>
269
+ );
270
+ }
@@ -138,23 +138,40 @@ interface QueueInputProps {
138
138
  /**
139
139
  * Queue Input Component
140
140
  *
141
- * Displays an input field at the bottom of the TUI for adding items to the queue.
142
- * Only visible when active is true.
141
+ * Displays a full-width input field at the bottom of the TUI for adding items to the queue.
142
+ * Styled like Claude Code's input area with horizontal borders.
143
143
  */
144
144
  export function QueueInput({ active, value }: QueueInputProps): React.ReactElement | null {
145
145
  if (!active) {
146
- return null;
146
+ // Show hint when not active
147
+ return (
148
+ <Box flexDirection="column" width="100%">
149
+ <Box width="100%">
150
+ <Text color={colors.dim}>{"─".repeat(80)}</Text>
151
+ </Box>
152
+ <Box paddingX={1}>
153
+ <Text color={colors.dim}>Press 'q' to add to queue, 'd' to delete, 'D' to clear</Text>
154
+ </Box>
155
+ <Box width="100%">
156
+ <Text color={colors.dim}>{"─".repeat(80)}</Text>
157
+ </Box>
158
+ </Box>
159
+ );
147
160
  }
148
161
 
149
- const formatted = formatQueueInput(value);
150
-
151
162
  return (
152
- <Box paddingX={1}>
153
- <Text color={colors.accent} bold>
154
- {formatted.prompt}{" "}
155
- </Text>
156
- <Text>{formatted.value}</Text>
157
- <Text color={colors.dim}>{formatted.cursor}</Text>
163
+ <Box flexDirection="column" width="100%">
164
+ <Box width="100%">
165
+ <Text color={colors.accent}>{"".repeat(80)}</Text>
166
+ </Box>
167
+ <Box paddingX={1} width="100%">
168
+ <Text color={colors.accent} bold>❯ </Text>
169
+ <Text>{value}</Text>
170
+ <Text color={colors.accent}>█</Text>
171
+ </Box>
172
+ <Box width="100%">
173
+ <Text color={colors.accent}>{"─".repeat(80)}</Text>
174
+ </Box>
158
175
  </Box>
159
176
  );
160
177
  }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * RateLimitIndicator Component
3
+ *
4
+ * Rate limit countdown display for agents
5
+ */
6
+
7
+ import React from "react";
8
+ import { Box, Text } from "ink";
9
+ import { colors, symbols } from "../theme.js";
10
+ import { useCountdown } from "../hooks/useAnimation.js";
11
+ import type { AgentState } from "../types.js";
12
+
13
+ interface RateLimitIndicatorProps {
14
+ /** All agent states */
15
+ agents: AgentState[];
16
+ /** Whether to show compact view */
17
+ compact?: boolean;
18
+ /** Maximum agents to show */
19
+ maxAgents?: number;
20
+ }
21
+
22
+ interface AgentCountdownProps {
23
+ agent: AgentState;
24
+ compact: boolean;
25
+ }
26
+
27
+ function AgentCountdown({ agent, compact }: AgentCountdownProps): React.ReactElement | null {
28
+ const countdown = useCountdown(agent.resetTime, agent.rateLimited);
29
+
30
+ if (!agent.rateLimited || !countdown) {
31
+ return null;
32
+ }
33
+
34
+ if (compact) {
35
+ return (
36
+ <Text color={colors.status.rateLimited}>
37
+ {agent.name.substring(0, 3)}: {countdown.display}
38
+ </Text>
39
+ );
40
+ }
41
+
42
+ return (
43
+ <Box>
44
+ <Text color={colors.status.rateLimited}>
45
+ {symbols.clock} {agent.displayName}: {countdown.display}
46
+ </Text>
47
+ </Box>
48
+ );
49
+ }
50
+
51
+ export function RateLimitIndicator({
52
+ agents,
53
+ compact = false,
54
+ maxAgents = 3,
55
+ }: RateLimitIndicatorProps): React.ReactElement | null {
56
+ // Filter to rate-limited agents only
57
+ const limitedAgents = agents.filter((a) => a.rateLimited);
58
+
59
+ if (limitedAgents.length === 0) {
60
+ return null;
61
+ }
62
+
63
+ const visibleAgents = limitedAgents.slice(0, maxAgents);
64
+ const hasMore = limitedAgents.length > maxAgents;
65
+
66
+ if (compact) {
67
+ return (
68
+ <Box>
69
+ {visibleAgents.map((agent, i) => (
70
+ <React.Fragment key={agent.name}>
71
+ {i > 0 && <Text color={colors.dim}> </Text>}
72
+ <AgentCountdown agent={agent} compact />
73
+ </React.Fragment>
74
+ ))}
75
+ {hasMore && (
76
+ <Text color={colors.dim}> +{limitedAgents.length - maxAgents}</Text>
77
+ )}
78
+ </Box>
79
+ );
80
+ }
81
+
82
+ return (
83
+ <Box flexDirection="column">
84
+ <Text color={colors.status.rateLimited} bold>
85
+ Rate Limited:
86
+ </Text>
87
+ {visibleAgents.map((agent) => (
88
+ <AgentCountdown key={agent.name} agent={agent} compact={false} />
89
+ ))}
90
+ {hasMore && (
91
+ <Text color={colors.dim}>
92
+ +{limitedAgents.length - maxAgents} more
93
+ </Text>
94
+ )}
95
+ </Box>
96
+ );
97
+ }