@arvorco/relentless 0.1.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 (107) hide show
  1. package/.claude/commands/relentless.analyze.md +20 -0
  2. package/.claude/commands/relentless.checklist.md +15 -0
  3. package/.claude/commands/relentless.clarify.md +19 -0
  4. package/.claude/commands/relentless.constitution.md +78 -0
  5. package/.claude/commands/relentless.implement.md +15 -0
  6. package/.claude/commands/relentless.plan.md +22 -0
  7. package/.claude/commands/relentless.plan.old.md +89 -0
  8. package/.claude/commands/relentless.specify.md +254 -0
  9. package/.claude/commands/relentless.tasks.md +25 -0
  10. package/.claude/commands/relentless.taskstoissues.md +15 -0
  11. package/.claude/settings.local.json +23 -0
  12. package/.claude/skills/analyze/SKILL.md +149 -0
  13. package/.claude/skills/checklist/SKILL.md +173 -0
  14. package/.claude/skills/checklist/templates/checklist-template.md +40 -0
  15. package/.claude/skills/clarify/SKILL.md +174 -0
  16. package/.claude/skills/constitution/SKILL.md +150 -0
  17. package/.claude/skills/constitution/templates/constitution-template.md +228 -0
  18. package/.claude/skills/implement/SKILL.md +141 -0
  19. package/.claude/skills/plan/SKILL.md +179 -0
  20. package/.claude/skills/plan/templates/plan-template.md +104 -0
  21. package/.claude/skills/prd/SKILL.md +242 -0
  22. package/.claude/skills/relentless/SKILL.md +265 -0
  23. package/.claude/skills/specify/SKILL.md +220 -0
  24. package/.claude/skills/specify/scripts/bash/check-prerequisites.sh +166 -0
  25. package/.claude/skills/specify/scripts/bash/common.sh +156 -0
  26. package/.claude/skills/specify/scripts/bash/create-new-feature.sh +305 -0
  27. package/.claude/skills/specify/scripts/bash/setup-plan.sh +61 -0
  28. package/.claude/skills/specify/scripts/bash/update-agent-context.sh +799 -0
  29. package/.claude/skills/specify/templates/spec-template.md +115 -0
  30. package/.claude/skills/tasks/SKILL.md +202 -0
  31. package/.claude/skills/tasks/templates/tasks-template.md +251 -0
  32. package/.claude/skills/taskstoissues/SKILL.md +97 -0
  33. package/.specify/memory/constitution.md +50 -0
  34. package/.specify/scripts/bash/check-prerequisites.sh +166 -0
  35. package/.specify/scripts/bash/common.sh +156 -0
  36. package/.specify/scripts/bash/create-new-feature.sh +297 -0
  37. package/.specify/scripts/bash/setup-plan.sh +61 -0
  38. package/.specify/scripts/bash/update-agent-context.sh +799 -0
  39. package/.specify/templates/agent-file-template.md +28 -0
  40. package/.specify/templates/checklist-template.md +40 -0
  41. package/.specify/templates/plan-template.md +104 -0
  42. package/.specify/templates/spec-template.md +115 -0
  43. package/.specify/templates/tasks-template.md +251 -0
  44. package/CHANGES_SUMMARY.md +255 -0
  45. package/CLAUDE.md +92 -0
  46. package/GEMINI_SETUP.md +256 -0
  47. package/LICENSE +21 -0
  48. package/README.md +1171 -0
  49. package/REFACTOR_SUMMARY.md +267 -0
  50. package/bin/relentless.ts +536 -0
  51. package/bun.lock +352 -0
  52. package/eslint.config.js +37 -0
  53. package/package.json +61 -0
  54. package/prd.json.example +64 -0
  55. package/prompt.md +108 -0
  56. package/ralph.sh +80 -0
  57. package/relentless/config.json +38 -0
  58. package/relentless/features/.gitkeep +0 -0
  59. package/relentless/features/ghsk-ideas/prd.json +229 -0
  60. package/relentless/features/ghsk-ideas/prd.md +191 -0
  61. package/relentless/features/ghsk-ideas/progress.txt +408 -0
  62. package/relentless/prompt.md +79 -0
  63. package/skills/checklist/SKILL.md +349 -0
  64. package/skills/clarify/SKILL.md +476 -0
  65. package/skills/prd/SKILL.md +242 -0
  66. package/skills/relentless/SKILL.md +268 -0
  67. package/skills/tasks/SKILL.md +577 -0
  68. package/src/agents/amp.ts +115 -0
  69. package/src/agents/claude.ts +185 -0
  70. package/src/agents/codex.ts +89 -0
  71. package/src/agents/droid.ts +90 -0
  72. package/src/agents/gemini.ts +109 -0
  73. package/src/agents/index.ts +16 -0
  74. package/src/agents/opencode.ts +88 -0
  75. package/src/agents/registry.ts +95 -0
  76. package/src/agents/types.ts +101 -0
  77. package/src/config/index.ts +8 -0
  78. package/src/config/loader.ts +237 -0
  79. package/src/config/schema.ts +115 -0
  80. package/src/execution/index.ts +8 -0
  81. package/src/execution/router.ts +49 -0
  82. package/src/execution/runner.ts +512 -0
  83. package/src/index.ts +11 -0
  84. package/src/init/index.ts +7 -0
  85. package/src/init/scaffolder.ts +377 -0
  86. package/src/prd/analyzer.ts +512 -0
  87. package/src/prd/index.ts +11 -0
  88. package/src/prd/issues.ts +249 -0
  89. package/src/prd/parser.ts +281 -0
  90. package/src/prd/progress.ts +198 -0
  91. package/src/prd/types.ts +170 -0
  92. package/src/tui/App.tsx +85 -0
  93. package/src/tui/TUIRunner.tsx +400 -0
  94. package/src/tui/components/AgentOutput.tsx +45 -0
  95. package/src/tui/components/AgentStatus.tsx +64 -0
  96. package/src/tui/components/CurrentStory.tsx +66 -0
  97. package/src/tui/components/Header.tsx +49 -0
  98. package/src/tui/components/ProgressBar.tsx +39 -0
  99. package/src/tui/components/StoryGrid.tsx +86 -0
  100. package/src/tui/hooks/useTUI.ts +147 -0
  101. package/src/tui/hooks/useTimer.ts +51 -0
  102. package/src/tui/index.tsx +17 -0
  103. package/src/tui/theme.ts +41 -0
  104. package/src/tui/types.ts +77 -0
  105. package/templates/constitution.md +228 -0
  106. package/templates/plan.md +273 -0
  107. package/tsconfig.json +27 -0
@@ -0,0 +1,400 @@
1
+ /**
2
+ * TUI Runner
3
+ *
4
+ * Wraps the execution runner with a beautiful terminal interface
5
+ */
6
+
7
+ import React, { useState, useEffect, useCallback } from "react";
8
+ import { render, useApp } from "ink";
9
+ import { App } from "./App.js";
10
+ import type { TUIState, Story, AgentState } from "./types.js";
11
+ import type { AgentName } from "../agents/types.js";
12
+ import type { PRD } from "../prd/types.js";
13
+ import type { RelentlessConfig } from "../config/schema.js";
14
+ import { getAgent, getInstalledAgents } from "../agents/registry.js";
15
+ import { loadPRD, getNextStory, isComplete, countStories } from "../prd/index.js";
16
+ import { routeStory } from "../execution/router.js";
17
+ import { existsSync } from "node:fs";
18
+
19
+ export interface TUIRunnerOptions {
20
+ agent: AgentName | "auto";
21
+ maxIterations: number;
22
+ workingDirectory: string;
23
+ prdPath: string;
24
+ promptPath: string;
25
+ feature: string;
26
+ config: RelentlessConfig;
27
+ dryRun?: boolean;
28
+ }
29
+
30
+ interface TUIRunnerProps extends TUIRunnerOptions {
31
+ onComplete: (success: boolean) => void;
32
+ }
33
+
34
+ function TUIRunnerComponent({
35
+ agent: preferredAgent,
36
+ maxIterations,
37
+ workingDirectory,
38
+ prdPath,
39
+ promptPath,
40
+ feature,
41
+ config,
42
+ dryRun = false,
43
+ onComplete,
44
+ }: TUIRunnerProps): React.ReactElement {
45
+ const { exit } = useApp();
46
+ const [state, setState] = useState<TUIState>({
47
+ feature,
48
+ project: "",
49
+ branchName: "",
50
+ stories: [],
51
+ iteration: 0,
52
+ maxIterations,
53
+ currentStory: null,
54
+ currentAgent: null,
55
+ agents: [],
56
+ outputLines: [],
57
+ elapsedSeconds: 0,
58
+ isRunning: false,
59
+ isComplete: false,
60
+ });
61
+
62
+ // Timer for elapsed time
63
+ useEffect(() => {
64
+ if (!state.isRunning) return;
65
+
66
+ const interval = setInterval(() => {
67
+ setState((prev) => ({
68
+ ...prev,
69
+ elapsedSeconds: prev.elapsedSeconds + 1,
70
+ }));
71
+ }, 1000);
72
+
73
+ return () => clearInterval(interval);
74
+ }, [state.isRunning]);
75
+
76
+ // Add output line
77
+ const addOutput = useCallback((line: string) => {
78
+ setState((prev) => ({
79
+ ...prev,
80
+ outputLines: [...prev.outputLines.slice(-100), line], // Keep last 100 lines
81
+ }));
82
+ }, []);
83
+
84
+ // Main execution effect
85
+ useEffect(() => {
86
+ let cancelled = false;
87
+
88
+ async function run() {
89
+ try {
90
+ // Load PRD
91
+ const prd = await loadPRD(prdPath);
92
+
93
+ // Get installed agents
94
+ const installed = await getInstalledAgents();
95
+ const agentStates: AgentState[] = installed.map((a) => ({
96
+ name: a.name,
97
+ displayName: a.displayName,
98
+ active: false,
99
+ rateLimited: false,
100
+ }));
101
+
102
+ // Initial state
103
+ setState((prev) => ({
104
+ ...prev,
105
+ project: prd.project,
106
+ branchName: prd.branchName,
107
+ stories: prd.userStories.map((s) => ({
108
+ id: s.id,
109
+ title: s.title,
110
+ passes: s.passes,
111
+ })),
112
+ agents: agentStates,
113
+ }));
114
+
115
+ addOutput(`Starting Relentless for feature: ${feature}`);
116
+ addOutput(`Project: ${prd.project}`);
117
+ addOutput(`Max iterations: ${maxIterations}`);
118
+
119
+ if (dryRun) {
120
+ addOutput("DRY RUN - not executing agents");
121
+ }
122
+
123
+ // Track rate-limited agents
124
+ const limitedAgents = new Map<AgentName, { resetTime?: Date; detectedAt: Date }>();
125
+
126
+ // Main loop
127
+ for (let i = 1; i <= maxIterations && !cancelled; i++) {
128
+ setState((prev) => ({ ...prev, iteration: i }));
129
+
130
+ // Reload PRD
131
+ const currentPRD = await loadPRD(prdPath);
132
+
133
+ // Check completion
134
+ if (isComplete(currentPRD)) {
135
+ addOutput("All stories complete!");
136
+ setState((prev) => ({ ...prev, isComplete: true, isRunning: false }));
137
+ break;
138
+ }
139
+
140
+ // Get next story
141
+ const story = getNextStory(currentPRD);
142
+ if (!story) {
143
+ addOutput("No more stories to work on!");
144
+ setState((prev) => ({ ...prev, isComplete: true, isRunning: false }));
145
+ break;
146
+ }
147
+
148
+ // Update current story
149
+ setState((prev) => ({
150
+ ...prev,
151
+ currentStory: { id: story.id, title: story.title, passes: story.passes },
152
+ elapsedSeconds: 0,
153
+ }));
154
+
155
+ addOutput(`\n--- Iteration ${i}/${maxIterations} ---`);
156
+ addOutput(`Story: ${story.id} - ${story.title}`);
157
+
158
+ // Select agent
159
+ let agentName: AgentName;
160
+ if (preferredAgent === "auto") {
161
+ agentName = routeStory(story, config.routing);
162
+ } else {
163
+ agentName = preferredAgent;
164
+ }
165
+
166
+ // Check if agent is rate-limited
167
+ const limitState = limitedAgents.get(agentName);
168
+ if (limitState) {
169
+ const hasReset = limitState.resetTime
170
+ ? new Date() >= limitState.resetTime
171
+ : new Date().getTime() - limitState.detectedAt.getTime() > 3600000;
172
+
173
+ if (hasReset) {
174
+ limitedAgents.delete(agentName);
175
+ } else {
176
+ // Try fallback
177
+ for (const fallbackName of config.fallback.priority) {
178
+ if (!limitedAgents.has(fallbackName)) {
179
+ const installed = agentStates.find((a) => a.name === fallbackName);
180
+ if (installed) {
181
+ agentName = fallbackName;
182
+ addOutput(`Switched to fallback agent: ${fallbackName}`);
183
+ break;
184
+ }
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ const agent = getAgent(agentName);
191
+
192
+ // Update agent state
193
+ setState((prev) => ({
194
+ ...prev,
195
+ currentAgent: {
196
+ name: agent.name,
197
+ displayName: agent.displayName,
198
+ active: true,
199
+ rateLimited: false,
200
+ },
201
+ agents: prev.agents.map((a) => ({
202
+ ...a,
203
+ active: a.name === agent.name,
204
+ })),
205
+ isRunning: true,
206
+ }));
207
+
208
+ addOutput(`Agent: ${agent.displayName}`);
209
+
210
+ if (dryRun) {
211
+ addOutput("[Dry run - skipping execution]");
212
+ await sleep(1000);
213
+ continue;
214
+ }
215
+
216
+ // Load prompt
217
+ const prompt = await Bun.file(promptPath).text();
218
+
219
+ // Invoke agent with streaming if available
220
+ if (agent.invokeStream) {
221
+ const stream = agent.invokeStream(prompt, {
222
+ workingDirectory,
223
+ dangerouslyAllowAll: config.agents[agent.name]?.dangerouslyAllowAll ?? true,
224
+ model: config.agents[agent.name]?.model,
225
+ });
226
+
227
+ let result;
228
+ for await (const chunk of stream) {
229
+ if (cancelled) break;
230
+ // Split chunk into lines and add each
231
+ const lines = chunk.split("\n");
232
+ for (const line of lines) {
233
+ if (line.trim()) {
234
+ addOutput(line);
235
+ }
236
+ }
237
+ result = chunk; // Will be overwritten by return value
238
+ }
239
+
240
+ // Get the final result
241
+ const finalResult = await stream.next();
242
+ if (finalResult.done && finalResult.value) {
243
+ const agentResult = finalResult.value;
244
+
245
+ // Check rate limit
246
+ const rateLimit = agent.detectRateLimit(agentResult.output);
247
+ if (rateLimit.limited) {
248
+ addOutput(`⚠️ ${agent.displayName} rate limited!`);
249
+ limitedAgents.set(agent.name, {
250
+ resetTime: rateLimit.resetTime,
251
+ detectedAt: new Date(),
252
+ });
253
+
254
+ setState((prev) => ({
255
+ ...prev,
256
+ agents: prev.agents.map((a) =>
257
+ a.name === agent.name
258
+ ? { ...a, rateLimited: true, resetTime: rateLimit.resetTime }
259
+ : a
260
+ ),
261
+ }));
262
+
263
+ i--; // Retry iteration
264
+ await sleep(config.fallback.retryDelay);
265
+ continue;
266
+ }
267
+
268
+ if (agentResult.isComplete) {
269
+ addOutput("🎉 Agent signaled COMPLETE!");
270
+ }
271
+
272
+ addOutput(`Duration: ${(agentResult.duration / 1000).toFixed(1)}s`);
273
+ }
274
+ } else {
275
+ // Non-streaming fallback
276
+ addOutput("Running agent (non-streaming)...");
277
+ const result = await agent.invoke(prompt, {
278
+ workingDirectory,
279
+ dangerouslyAllowAll: config.agents[agent.name]?.dangerouslyAllowAll ?? true,
280
+ model: config.agents[agent.name]?.model,
281
+ });
282
+
283
+ // Add output preview
284
+ const lines = result.output.split("\n").slice(0, 10);
285
+ for (const line of lines) {
286
+ if (line.trim()) {
287
+ addOutput(line);
288
+ }
289
+ }
290
+ if (result.output.split("\n").length > 10) {
291
+ addOutput(`... (${result.output.split("\n").length - 10} more lines)`);
292
+ }
293
+
294
+ // Check rate limit
295
+ const rateLimit = agent.detectRateLimit(result.output);
296
+ if (rateLimit.limited) {
297
+ addOutput(`⚠️ ${agent.displayName} rate limited!`);
298
+ limitedAgents.set(agent.name, {
299
+ resetTime: rateLimit.resetTime,
300
+ detectedAt: new Date(),
301
+ });
302
+
303
+ setState((prev) => ({
304
+ ...prev,
305
+ agents: prev.agents.map((a) =>
306
+ a.name === agent.name
307
+ ? { ...a, rateLimited: true, resetTime: rateLimit.resetTime }
308
+ : a
309
+ ),
310
+ }));
311
+
312
+ i--; // Retry iteration
313
+ await sleep(config.fallback.retryDelay);
314
+ continue;
315
+ }
316
+
317
+ if (result.isComplete) {
318
+ addOutput("🎉 Agent signaled COMPLETE!");
319
+ }
320
+
321
+ addOutput(`Duration: ${(result.duration / 1000).toFixed(1)}s`);
322
+ }
323
+
324
+ // Update stories from PRD
325
+ const updatedPRD = await loadPRD(prdPath);
326
+ setState((prev) => ({
327
+ ...prev,
328
+ stories: updatedPRD.userStories.map((s) => ({
329
+ id: s.id,
330
+ title: s.title,
331
+ passes: s.passes,
332
+ })),
333
+ }));
334
+
335
+ const counts = countStories(updatedPRD);
336
+ addOutput(`Progress: ${counts.completed}/${counts.total} complete`);
337
+
338
+ // Delay between iterations
339
+ await sleep(config.execution.iterationDelay);
340
+ }
341
+
342
+ // Final state
343
+ const finalPRD = await loadPRD(prdPath);
344
+ const success = isComplete(finalPRD);
345
+
346
+ setState((prev) => ({
347
+ ...prev,
348
+ isRunning: false,
349
+ isComplete: success,
350
+ }));
351
+
352
+ if (!success) {
353
+ addOutput(`\n⚠️ Reached max iterations (${maxIterations}) without completing all stories.`);
354
+ }
355
+
356
+ onComplete(success);
357
+ } catch (error) {
358
+ addOutput(`Error: ${error}`);
359
+ setState((prev) => ({
360
+ ...prev,
361
+ isRunning: false,
362
+ error: String(error),
363
+ }));
364
+ onComplete(false);
365
+ }
366
+ }
367
+
368
+ run();
369
+
370
+ return () => {
371
+ cancelled = true;
372
+ };
373
+ }, []);
374
+
375
+ return <App state={state} />;
376
+ }
377
+
378
+ function sleep(ms: number): Promise<void> {
379
+ return new Promise((resolve) => setTimeout(resolve, ms));
380
+ }
381
+
382
+ /**
383
+ * Run the TUI
384
+ */
385
+ export async function runTUI(options: TUIRunnerOptions): Promise<boolean> {
386
+ return new Promise((resolve) => {
387
+ const { unmount } = render(
388
+ <TUIRunnerComponent
389
+ {...options}
390
+ onComplete={(success) => {
391
+ // Wait a bit to show final state
392
+ setTimeout(() => {
393
+ unmount();
394
+ resolve(success);
395
+ }, 2000);
396
+ }}
397
+ />
398
+ );
399
+ });
400
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * AgentOutput Component
3
+ *
4
+ * Scrolling window showing agent output
5
+ */
6
+
7
+ import React from "react";
8
+ import { Box, Text } from "ink";
9
+ import { colors, borders } from "../theme.js";
10
+
11
+ interface AgentOutputProps {
12
+ lines: string[];
13
+ maxLines?: number;
14
+ }
15
+
16
+ export function AgentOutput({
17
+ lines,
18
+ maxLines = 8,
19
+ }: AgentOutputProps): React.ReactElement {
20
+ // Take last N lines for display
21
+ const displayLines = lines.slice(-maxLines);
22
+
23
+ // Pad to maintain consistent height
24
+ const paddedLines = [...displayLines];
25
+ while (paddedLines.length < maxLines) {
26
+ paddedLines.push("");
27
+ }
28
+
29
+ return (
30
+ <Box flexDirection="column" borderStyle="single" borderColor={colors.dim}>
31
+ <Box paddingX={1} borderBottom borderColor={colors.dim}>
32
+ <Text color={colors.dim} bold>
33
+ Agent Output
34
+ </Text>
35
+ </Box>
36
+ <Box flexDirection="column" paddingX={1} paddingY={0}>
37
+ {paddedLines.map((line, i) => (
38
+ <Text key={i} color={colors.dim} wrap="truncate">
39
+ {line || " "}
40
+ </Text>
41
+ ))}
42
+ </Box>
43
+ </Box>
44
+ );
45
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * AgentStatus Component
3
+ *
4
+ * Footer showing agent availability and rate limit status
5
+ */
6
+
7
+ import React from "react";
8
+ import { Box, Text } from "ink";
9
+ import { colors } from "../theme.js";
10
+ import type { AgentState } from "../types.js";
11
+
12
+ interface AgentStatusProps {
13
+ agents: AgentState[];
14
+ iteration: number;
15
+ maxIterations: number;
16
+ }
17
+
18
+ function formatResetTime(date: Date): string {
19
+ return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
20
+ }
21
+
22
+ export function AgentStatus({
23
+ agents,
24
+ iteration,
25
+ maxIterations,
26
+ }: AgentStatusProps): React.ReactElement {
27
+ // Find next reset time
28
+ const nextReset = agents
29
+ .filter((a) => a.rateLimited && a.resetTime)
30
+ .sort((a, b) => (a.resetTime?.getTime() ?? 0) - (b.resetTime?.getTime() ?? 0))[0];
31
+
32
+ return (
33
+ <Box
34
+ borderStyle="single"
35
+ borderColor={colors.dim}
36
+ paddingX={1}
37
+ flexDirection="row"
38
+ justifyContent="space-between"
39
+ >
40
+ <Box>
41
+ <Text color={colors.dim}>Agents: </Text>
42
+ {agents.map((agent, i) => (
43
+ <React.Fragment key={agent.name}>
44
+ <Text color={colors.dim}>{agent.name} </Text>
45
+ <Text color={agent.active ? colors.success : agent.rateLimited ? colors.warning : colors.dim}>
46
+ {agent.active ? "●" : agent.rateLimited ? "○" : "○"}
47
+ </Text>
48
+ {i < agents.length - 1 && <Text color={colors.dim}> </Text>}
49
+ </React.Fragment>
50
+ ))}
51
+ </Box>
52
+ <Box>
53
+ <Text color={colors.dim}>
54
+ Iteration: {iteration}/{maxIterations}
55
+ </Text>
56
+ {nextReset?.resetTime && (
57
+ <Text color={colors.dim}>
58
+ {" "}Next reset: {nextReset.name} @ {formatResetTime(nextReset.resetTime)}
59
+ </Text>
60
+ )}
61
+ </Box>
62
+ </Box>
63
+ );
64
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * CurrentStory Component
3
+ *
4
+ * Shows the current story being worked on with elapsed time
5
+ */
6
+
7
+ import React from "react";
8
+ import { Box, Text } from "ink";
9
+ import Spinner from "ink-spinner";
10
+ import { colors, symbols } from "../theme.js";
11
+ import type { Story } from "../types.js";
12
+
13
+ interface CurrentStoryProps {
14
+ story: Story | null;
15
+ elapsedSeconds: number;
16
+ isRunning: boolean;
17
+ }
18
+
19
+ function formatTime(seconds: number): string {
20
+ const mins = Math.floor(seconds / 60);
21
+ const secs = seconds % 60;
22
+ if (mins > 0) {
23
+ return `${mins}m ${secs}s`;
24
+ }
25
+ return `${secs}s`;
26
+ }
27
+
28
+ export function CurrentStory({
29
+ story,
30
+ elapsedSeconds,
31
+ isRunning,
32
+ }: CurrentStoryProps): React.ReactElement {
33
+ if (!story) {
34
+ return (
35
+ <Box paddingY={1}>
36
+ <Text color={colors.dim}>No story in progress</Text>
37
+ </Box>
38
+ );
39
+ }
40
+
41
+ return (
42
+ <Box flexDirection="column" paddingY={1}>
43
+ <Box>
44
+ <Text color={colors.dim}>Current Story: </Text>
45
+ <Text color={colors.warning} bold>
46
+ {story.id}
47
+ </Text>
48
+ <Text color={colors.dim}> - </Text>
49
+ <Text>{story.title}</Text>
50
+ </Box>
51
+ <Box>
52
+ {isRunning ? (
53
+ <>
54
+ <Text color={colors.success}>
55
+ <Spinner type="dots" />
56
+ </Text>
57
+ <Text color={colors.success}> Working...</Text>
58
+ </>
59
+ ) : (
60
+ <Text color={colors.dim}>{symbols.pending} Waiting</Text>
61
+ )}
62
+ <Text color={colors.dim}> [elapsed: {formatTime(elapsedSeconds)}]</Text>
63
+ </Box>
64
+ </Box>
65
+ );
66
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Header Component
3
+ *
4
+ * Displays title and current agent status
5
+ */
6
+
7
+ import React from "react";
8
+ import { Box, Text } from "ink";
9
+ import { symbols, colors } from "../theme.js";
10
+ import type { AgentState } from "../types.js";
11
+
12
+ interface HeaderProps {
13
+ agent: AgentState | null;
14
+ }
15
+
16
+ export function Header({ agent }: HeaderProps): React.ReactElement {
17
+ return (
18
+ <Box
19
+ borderStyle="single"
20
+ borderColor={colors.primary}
21
+ paddingX={1}
22
+ flexDirection="row"
23
+ justifyContent="space-between"
24
+ >
25
+ <Box>
26
+ <Text color={colors.primary} bold>
27
+ {symbols.lightning} RELENTLESS
28
+ </Text>
29
+ <Text color={colors.dim}> Universal AI Agent Orchestrator</Text>
30
+ </Box>
31
+ <Box>
32
+ <Text color={colors.dim}>Agent: </Text>
33
+ {agent ? (
34
+ <>
35
+ <Text color={agent.rateLimited ? colors.warning : colors.success} bold>
36
+ {agent.displayName}
37
+ </Text>
38
+ <Text color={colors.dim}>
39
+ {" "}
40
+ ({agent.rateLimited ? "limited" : "active"})
41
+ </Text>
42
+ </>
43
+ ) : (
44
+ <Text color={colors.dim}>none</Text>
45
+ )}
46
+ </Box>
47
+ </Box>
48
+ );
49
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * ProgressBar Component
3
+ *
4
+ * Visual progress bar for story completion
5
+ */
6
+
7
+ import React from "react";
8
+ import { Box, Text } from "ink";
9
+ import { colors } from "../theme.js";
10
+
11
+ interface ProgressBarProps {
12
+ completed: number;
13
+ total: number;
14
+ width?: number;
15
+ }
16
+
17
+ export function ProgressBar({
18
+ completed,
19
+ total,
20
+ width = 40,
21
+ }: ProgressBarProps): React.ReactElement {
22
+ const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
23
+ const filledWidth = total > 0 ? Math.round((completed / total) * width) : 0;
24
+ const emptyWidth = width - filledWidth;
25
+
26
+ const filled = "█".repeat(filledWidth);
27
+ const empty = "░".repeat(emptyWidth);
28
+
29
+ return (
30
+ <Box>
31
+ <Text color={colors.success}>{filled}</Text>
32
+ <Text color={colors.dim}>{empty}</Text>
33
+ <Text color={colors.dim}>
34
+ {" "}
35
+ {completed}/{total} ({percentage}%)
36
+ </Text>
37
+ </Box>
38
+ );
39
+ }