@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,101 @@
1
+ /**
2
+ * Relentless Agent Adapter Types
3
+ *
4
+ * Defines the interface that all AI coding agents must implement
5
+ * to work with the Relentless orchestrator.
6
+ */
7
+
8
+ export type AgentName = "claude" | "amp" | "opencode" | "codex" | "droid" | "gemini";
9
+
10
+ export interface InvokeOptions {
11
+ /** Working directory for the agent */
12
+ workingDirectory?: string;
13
+ /** Timeout in milliseconds */
14
+ timeout?: number;
15
+ /** Model to use (agent-specific) */
16
+ model?: string;
17
+ /** Skip all permission prompts */
18
+ dangerouslyAllowAll?: boolean;
19
+ }
20
+
21
+ /**
22
+ * Rate limit detection result
23
+ */
24
+ export interface RateLimitInfo {
25
+ /** Whether the agent is rate limited */
26
+ limited: boolean;
27
+ /** When the rate limit resets (if known) */
28
+ resetTime?: Date;
29
+ /** Raw error message */
30
+ message?: string;
31
+ }
32
+
33
+ export interface AgentResult {
34
+ /** Raw output from the agent */
35
+ output: string;
36
+ /** Exit code of the process */
37
+ exitCode: number;
38
+ /** Whether the agent signaled completion */
39
+ isComplete: boolean;
40
+ /** Duration in milliseconds */
41
+ duration: number;
42
+ /** Whether the agent hit rate limits */
43
+ rateLimited?: boolean;
44
+ /** When the rate limit resets */
45
+ resetTime?: Date;
46
+ }
47
+
48
+ export interface AgentAdapter {
49
+ /** Agent identifier */
50
+ name: AgentName;
51
+
52
+ /** Human-readable display name */
53
+ displayName: string;
54
+
55
+ /** Check if the agent is installed on this system */
56
+ isInstalled(): Promise<boolean>;
57
+
58
+ /** Get the path to the agent executable */
59
+ getExecutablePath(): Promise<string | null>;
60
+
61
+ /** Invoke the agent with a prompt */
62
+ invoke(prompt: string, options?: InvokeOptions): Promise<AgentResult>;
63
+
64
+ /** Invoke the agent with streaming output */
65
+ invokeStream?(
66
+ prompt: string,
67
+ options?: InvokeOptions
68
+ ): AsyncGenerator<string, AgentResult, unknown>;
69
+
70
+ /** Detect if the output indicates completion */
71
+ detectCompletion(output: string): boolean;
72
+
73
+ /** Detect if the output indicates rate limiting */
74
+ detectRateLimit(output: string): RateLimitInfo;
75
+
76
+ /** Whether this agent supports skills/plugins */
77
+ hasSkillSupport: boolean;
78
+
79
+ /** Command to install skills for this agent (if supported) */
80
+ skillInstallCommand?: string;
81
+
82
+ /** Install skills to the agent (if supported) */
83
+ installSkills?(projectPath: string): Promise<void>;
84
+ }
85
+
86
+ /**
87
+ * Story types for smart routing
88
+ */
89
+ export type StoryType = "database" | "ui" | "api" | "test" | "refactor" | "docs" | "general";
90
+
91
+ /**
92
+ * Agent specializations for smart routing
93
+ */
94
+ export const AGENT_SPECIALIZATIONS: Record<AgentName, StoryType[]> = {
95
+ claude: ["database", "refactor", "api", "general"],
96
+ amp: ["ui", "test", "general"],
97
+ opencode: ["api", "general"],
98
+ codex: ["api", "database", "general"],
99
+ droid: ["refactor", "test", "general"],
100
+ gemini: ["docs", "general"],
101
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Configuration Module
3
+ *
4
+ * Re-exports configuration types and functions
5
+ */
6
+
7
+ export * from "./schema";
8
+ export * from "./loader";
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Configuration Loader
3
+ *
4
+ * Loads and validates relentless/config.json and constitution.md
5
+ */
6
+
7
+ import { existsSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { RelentlessConfigSchema, DEFAULT_CONFIG, type RelentlessConfig } from "./schema";
10
+
11
+ const CONFIG_FILENAME = "config.json";
12
+ const CONSTITUTION_FILENAME = "constitution.md";
13
+ const RELENTLESS_DIR = "relentless";
14
+
15
+ /**
16
+ * Find the relentless directory in the current or parent directories
17
+ */
18
+ export function findRelentlessDir(startDir: string = process.cwd()): string | null {
19
+ let dir = startDir;
20
+
21
+ while (dir !== "/") {
22
+ const relentlessPath = join(dir, RELENTLESS_DIR);
23
+ if (existsSync(relentlessPath)) {
24
+ return relentlessPath;
25
+ }
26
+ dir = join(dir, "..");
27
+ }
28
+
29
+ return null;
30
+ }
31
+
32
+ /**
33
+ * Find the config file in the relentless directory
34
+ */
35
+ export function findConfigFile(startDir: string = process.cwd()): string | null {
36
+ const relentlessDir = findRelentlessDir(startDir);
37
+ if (!relentlessDir) {
38
+ return null;
39
+ }
40
+
41
+ const configPath = join(relentlessDir, CONFIG_FILENAME);
42
+ if (existsSync(configPath)) {
43
+ return configPath;
44
+ }
45
+
46
+ return null;
47
+ }
48
+
49
+ /**
50
+ * Load configuration from file
51
+ */
52
+ export async function loadConfig(configPath?: string): Promise<RelentlessConfig> {
53
+ const path = configPath ?? findConfigFile();
54
+
55
+ if (!path) {
56
+ console.warn(`No relentless/${CONFIG_FILENAME} found, using defaults`);
57
+ return DEFAULT_CONFIG;
58
+ }
59
+
60
+ try {
61
+ const content = await Bun.file(path).text();
62
+ const json = JSON.parse(content);
63
+ const validated = RelentlessConfigSchema.parse(json);
64
+ return validated;
65
+ } catch (error) {
66
+ if (error instanceof SyntaxError) {
67
+ throw new Error(`Invalid JSON in ${path}: ${error.message}`);
68
+ }
69
+ throw error;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Save configuration to file
75
+ */
76
+ export async function saveConfig(
77
+ config: RelentlessConfig,
78
+ path: string = join(process.cwd(), RELENTLESS_DIR, CONFIG_FILENAME)
79
+ ): Promise<void> {
80
+ const validated = RelentlessConfigSchema.parse(config);
81
+ const content = JSON.stringify(validated, null, 2);
82
+ await Bun.write(path, content);
83
+ }
84
+
85
+ /**
86
+ * Create a default configuration file
87
+ */
88
+ export async function createDefaultConfig(
89
+ dir: string = process.cwd()
90
+ ): Promise<string> {
91
+ const path = join(dir, RELENTLESS_DIR, CONFIG_FILENAME);
92
+ await saveConfig(DEFAULT_CONFIG, path);
93
+ return path;
94
+ }
95
+
96
+ /**
97
+ * Find the constitution file in the relentless directory
98
+ */
99
+ export function findConstitutionFile(startDir: string = process.cwd()): string | null {
100
+ const relentlessDir = findRelentlessDir(startDir);
101
+ if (!relentlessDir) {
102
+ return null;
103
+ }
104
+
105
+ const constitutionPath = join(relentlessDir, CONSTITUTION_FILENAME);
106
+ if (existsSync(constitutionPath)) {
107
+ return constitutionPath;
108
+ }
109
+
110
+ return null;
111
+ }
112
+
113
+ /**
114
+ * Constitution principle level
115
+ */
116
+ export type PrincipleLevel = "MUST" | "SHOULD";
117
+
118
+ /**
119
+ * Parsed constitution principle
120
+ */
121
+ export interface Principle {
122
+ level: PrincipleLevel;
123
+ text: string;
124
+ section: string;
125
+ }
126
+
127
+ /**
128
+ * Parsed constitution
129
+ */
130
+ export interface Constitution {
131
+ raw: string;
132
+ principles: Principle[];
133
+ sections: string[];
134
+ }
135
+
136
+ /**
137
+ * Parse constitution markdown to extract MUST/SHOULD principles
138
+ */
139
+ export function parseConstitution(content: string): Constitution {
140
+ const principles: Principle[] = [];
141
+ const sections = new Set<string>();
142
+
143
+ let currentSection = "";
144
+ const lines = content.split("\n");
145
+
146
+ for (const line of lines) {
147
+ // Track current section
148
+ if (line.startsWith("## ")) {
149
+ currentSection = line.replace("## ", "").trim();
150
+ sections.add(currentSection);
151
+ } else if (line.startsWith("### ")) {
152
+ currentSection = line.replace("### ", "").trim();
153
+ sections.add(currentSection);
154
+ }
155
+
156
+ // Extract MUST principles
157
+ const mustMatch = line.match(/\*\*MUST\*\*\s+(.+)/);
158
+ if (mustMatch) {
159
+ principles.push({
160
+ level: "MUST",
161
+ text: mustMatch[1].trim(),
162
+ section: currentSection,
163
+ });
164
+ }
165
+
166
+ // Extract SHOULD principles
167
+ const shouldMatch = line.match(/\*\*SHOULD\*\*\s+(.+)/);
168
+ if (shouldMatch) {
169
+ principles.push({
170
+ level: "SHOULD",
171
+ text: shouldMatch[1].trim(),
172
+ section: currentSection,
173
+ });
174
+ }
175
+ }
176
+
177
+ return {
178
+ raw: content,
179
+ principles,
180
+ sections: Array.from(sections),
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Load constitution from file
186
+ */
187
+ export async function loadConstitution(constitutionPath?: string): Promise<Constitution | null> {
188
+ const path = constitutionPath ?? findConstitutionFile();
189
+
190
+ if (!path || !existsSync(path)) {
191
+ return null;
192
+ }
193
+
194
+ try {
195
+ const content = await Bun.file(path).text();
196
+ return parseConstitution(content);
197
+ } catch (error) {
198
+ console.warn(`Failed to load constitution: ${error}`);
199
+ return null;
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Validate constitution format
205
+ */
206
+ export function validateConstitution(constitution: Constitution): { valid: boolean; errors: string[] } {
207
+ const errors: string[] = [];
208
+
209
+ // Check that we have at least some principles
210
+ if (constitution.principles.length === 0) {
211
+ errors.push("Constitution has no MUST or SHOULD principles defined");
212
+ }
213
+
214
+ // Check that we have both MUST and SHOULD principles
215
+ const hasMust = constitution.principles.some(p => p.level === "MUST");
216
+ const hasShould = constitution.principles.some(p => p.level === "SHOULD");
217
+
218
+ if (!hasMust) {
219
+ errors.push("Constitution has no MUST principles (required directives)");
220
+ }
221
+
222
+ if (!hasShould) {
223
+ errors.push("Constitution has no SHOULD principles (recommended guidelines)");
224
+ }
225
+
226
+ // Check for empty principle text
227
+ for (const principle of constitution.principles) {
228
+ if (!principle.text || principle.text.trim().length === 0) {
229
+ errors.push(`Empty ${principle.level} principle in section: ${principle.section}`);
230
+ }
231
+ }
232
+
233
+ return {
234
+ valid: errors.length === 0,
235
+ errors,
236
+ };
237
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Relentless Configuration Schema
3
+ *
4
+ * Defines the structure and validation for relentless.config.json
5
+ */
6
+
7
+ import { z } from "zod";
8
+ // Agent and story types defined inline in Zod schemas below
9
+
10
+ /**
11
+ * Agent-specific configuration
12
+ */
13
+ export const AgentConfigSchema = z.object({
14
+ model: z.string().optional(),
15
+ dangerouslyAllowAll: z.boolean().default(true),
16
+ });
17
+
18
+ export type AgentConfig = z.infer<typeof AgentConfigSchema>;
19
+
20
+ /**
21
+ * Routing rule for smart agent selection
22
+ */
23
+ export const RoutingRuleSchema = z.object({
24
+ storyType: z.enum(["database", "ui", "api", "test", "refactor", "docs", "general"]),
25
+ agent: z.enum(["claude", "amp", "opencode", "codex", "droid", "gemini"]),
26
+ });
27
+
28
+ export type RoutingRule = z.infer<typeof RoutingRuleSchema>;
29
+
30
+ /**
31
+ * Fallback configuration for automatic agent switching
32
+ */
33
+ export const FallbackConfigSchema = z.object({
34
+ /** Enable automatic fallback when rate limited */
35
+ enabled: z.boolean().default(true),
36
+ /** Priority order for agents (first = preferred) */
37
+ priority: z.array(z.enum(["claude", "amp", "opencode", "codex", "droid", "gemini"]))
38
+ .default(["claude", "codex", "amp", "opencode", "gemini"]),
39
+ /** Automatically switch back when limits reset */
40
+ autoRecovery: z.boolean().default(true),
41
+ /** Delay (ms) before trying fallback agent */
42
+ retryDelay: z.number().int().nonnegative().default(2000),
43
+ });
44
+
45
+ export type FallbackConfig = z.infer<typeof FallbackConfigSchema>;
46
+
47
+ /**
48
+ * Execution configuration
49
+ */
50
+ export const ExecutionConfigSchema = z.object({
51
+ maxIterations: z.number().int().positive().default(20),
52
+ iterationDelay: z.number().int().nonnegative().default(2000),
53
+ timeout: z.number().int().positive().default(600000), // 10 minutes
54
+ });
55
+
56
+ export type ExecutionConfig = z.infer<typeof ExecutionConfigSchema>;
57
+
58
+ /**
59
+ * Routing configuration
60
+ */
61
+ export const RoutingConfigSchema = z.object({
62
+ rules: z.array(RoutingRuleSchema).default([]),
63
+ default: z.enum(["claude", "amp", "opencode", "codex", "droid", "gemini"]).default("claude"),
64
+ });
65
+
66
+ export type RoutingConfig = z.infer<typeof RoutingConfigSchema>;
67
+
68
+ /**
69
+ * Complete Relentless configuration
70
+ */
71
+ export const RelentlessConfigSchema = z.object({
72
+ defaultAgent: z.enum(["claude", "amp", "opencode", "codex", "droid", "gemini"]).default("claude"),
73
+ agents: z.record(
74
+ z.enum(["claude", "amp", "opencode", "codex", "droid", "gemini"]),
75
+ AgentConfigSchema
76
+ ).default({}),
77
+ routing: RoutingConfigSchema.default({}),
78
+ fallback: FallbackConfigSchema.default({}),
79
+ execution: ExecutionConfigSchema.default({}),
80
+ prompt: z.object({
81
+ path: z.string().default("prompt.md"),
82
+ }).default({}),
83
+ });
84
+
85
+ export type RelentlessConfig = z.infer<typeof RelentlessConfigSchema>;
86
+
87
+ /**
88
+ * Default configuration
89
+ */
90
+ export const DEFAULT_CONFIG: RelentlessConfig = {
91
+ defaultAgent: "claude",
92
+ agents: {
93
+ claude: { dangerouslyAllowAll: true },
94
+ amp: { dangerouslyAllowAll: true },
95
+ gemini: { dangerouslyAllowAll: true },
96
+ },
97
+ routing: {
98
+ rules: [],
99
+ default: "claude",
100
+ },
101
+ fallback: {
102
+ enabled: true,
103
+ priority: ["claude", "codex", "amp", "opencode", "gemini"],
104
+ autoRecovery: true,
105
+ retryDelay: 2000,
106
+ },
107
+ execution: {
108
+ maxIterations: 20,
109
+ iterationDelay: 2000,
110
+ timeout: 600000,
111
+ },
112
+ prompt: {
113
+ path: "prompt.md",
114
+ },
115
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Execution Module
3
+ *
4
+ * Re-exports execution-related functions
5
+ */
6
+
7
+ export * from "./runner";
8
+ export * from "./router";
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Smart Agent Router
3
+ *
4
+ * Routes stories to the best-suited agent based on story type
5
+ */
6
+
7
+ import type { AgentName, StoryType } from "../agents/types";
8
+ import { AGENT_SPECIALIZATIONS } from "../agents/types";
9
+ import type { RoutingConfig } from "../config/schema";
10
+ import { inferStoryType, type UserStory } from "../prd/types";
11
+
12
+ /**
13
+ * Route a story to the best agent
14
+ */
15
+ export function routeStory(story: UserStory, routing: RoutingConfig): AgentName {
16
+ const storyType = inferStoryType(story) as StoryType;
17
+
18
+ // Check explicit routing rules first
19
+ const rule = routing.rules.find((r) => r.storyType === storyType);
20
+ if (rule) {
21
+ return rule.agent;
22
+ }
23
+
24
+ // Find best agent based on specializations
25
+ const scores = Object.entries(AGENT_SPECIALIZATIONS).map(([agent, specializations]) => {
26
+ const score = specializations.includes(storyType) ? 2 : 0;
27
+ const generalScore = specializations.includes("general") ? 1 : 0;
28
+ return { agent: agent as AgentName, score: score + generalScore };
29
+ });
30
+
31
+ // Sort by score descending
32
+ scores.sort((a, b) => b.score - a.score);
33
+
34
+ // If no good match, use default
35
+ if (scores[0].score === 0) {
36
+ return routing.default;
37
+ }
38
+
39
+ return scores[0].agent;
40
+ }
41
+
42
+ /**
43
+ * Get recommended agent for a story type
44
+ */
45
+ export function getRecommendedAgent(storyType: StoryType): AgentName[] {
46
+ return Object.entries(AGENT_SPECIALIZATIONS)
47
+ .filter(([, specializations]) => specializations.includes(storyType))
48
+ .map(([agent]) => agent as AgentName);
49
+ }