@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,185 @@
1
+ /**
2
+ * Claude Code Agent Adapter
3
+ *
4
+ * Adapter for Anthropic's Claude Code CLI
5
+ * https://docs.anthropic.com/claude-code
6
+ */
7
+
8
+ import type { AgentAdapter, AgentResult, InvokeOptions, RateLimitInfo } from "./types.js";
9
+
10
+ export const claudeAdapter: AgentAdapter = {
11
+ name: "claude",
12
+ displayName: "Claude Code",
13
+ hasSkillSupport: true,
14
+ skillInstallCommand: "/plugin install github:ArvorCo/Relentless",
15
+
16
+ async isInstalled(): Promise<boolean> {
17
+ try {
18
+ const path = await this.getExecutablePath();
19
+ return path !== null;
20
+ } catch {
21
+ return false;
22
+ }
23
+ },
24
+
25
+ async getExecutablePath(): Promise<string | null> {
26
+ try {
27
+ const proc = Bun.spawn(["which", "claude"], { stdout: "pipe" });
28
+ const output = await new Response(proc.stdout).text();
29
+ const exitCode = await proc.exited;
30
+ if (exitCode === 0 && output.trim()) {
31
+ return output.trim();
32
+ }
33
+ return null;
34
+ } catch {
35
+ return null;
36
+ }
37
+ },
38
+
39
+ async invoke(prompt: string, options?: InvokeOptions): Promise<AgentResult> {
40
+ const startTime = Date.now();
41
+ const args = ["-p"];
42
+
43
+ if (options?.dangerouslyAllowAll) {
44
+ args.push("--dangerously-skip-permissions");
45
+ }
46
+
47
+ if (options?.model) {
48
+ args.push("--model", options.model);
49
+ }
50
+
51
+ const proc = Bun.spawn(["claude", ...args], {
52
+ cwd: options?.workingDirectory,
53
+ stdin: new Blob([prompt]),
54
+ stdout: "pipe",
55
+ stderr: "pipe",
56
+ });
57
+
58
+ // Collect output
59
+ const stdout = await new Response(proc.stdout).text();
60
+ const stderr = await new Response(proc.stderr).text();
61
+ const exitCode = await proc.exited;
62
+
63
+ const output = stdout + (stderr ? `\n${stderr}` : "");
64
+ const duration = Date.now() - startTime;
65
+
66
+ return {
67
+ output,
68
+ exitCode,
69
+ isComplete: this.detectCompletion(output),
70
+ duration,
71
+ };
72
+ },
73
+
74
+ async *invokeStream(
75
+ prompt: string,
76
+ options?: InvokeOptions
77
+ ): AsyncGenerator<string, AgentResult, unknown> {
78
+ const startTime = Date.now();
79
+ const args = ["-p"];
80
+
81
+ if (options?.dangerouslyAllowAll) {
82
+ args.push("--dangerously-skip-permissions");
83
+ }
84
+
85
+ if (options?.model) {
86
+ args.push("--model", options.model);
87
+ }
88
+
89
+ const proc = Bun.spawn(["claude", ...args], {
90
+ cwd: options?.workingDirectory,
91
+ stdin: new Blob([prompt]),
92
+ stdout: "pipe",
93
+ stderr: "pipe",
94
+ });
95
+
96
+ const decoder = new TextDecoder();
97
+ let fullOutput = "";
98
+
99
+ // Stream stdout
100
+ const reader = proc.stdout.getReader();
101
+ try {
102
+ while (true) {
103
+ const { done, value } = await reader.read();
104
+ if (done) break;
105
+ const chunk = decoder.decode(value, { stream: true });
106
+ fullOutput += chunk;
107
+ yield chunk;
108
+ }
109
+ } finally {
110
+ reader.releaseLock();
111
+ }
112
+
113
+ // Collect any stderr
114
+ const stderr = await new Response(proc.stderr).text();
115
+ const exitCode = await proc.exited;
116
+
117
+ const output = fullOutput + (stderr ? `\n${stderr}` : "");
118
+ const duration = Date.now() - startTime;
119
+
120
+ return {
121
+ output,
122
+ exitCode,
123
+ isComplete: this.detectCompletion(output),
124
+ duration,
125
+ };
126
+ },
127
+
128
+ detectCompletion(output: string): boolean {
129
+ return output.includes("<promise>COMPLETE</promise>");
130
+ },
131
+
132
+ detectRateLimit(output: string): RateLimitInfo {
133
+ // Pattern: "You've hit your limit · resets 12am (America/Sao_Paulo)"
134
+ if (output.includes("You've hit your limit") || output.includes("you've hit your limit")) {
135
+ const resetMatch = output.match(/resets\s+(\d{1,2})(am|pm)/i);
136
+ let resetTime: Date | undefined;
137
+
138
+ if (resetMatch) {
139
+ const hour = parseInt(resetMatch[1], 10);
140
+ const isPM = resetMatch[2].toLowerCase() === "pm";
141
+ const now = new Date();
142
+
143
+ resetTime = new Date(now);
144
+ resetTime.setHours(isPM && hour !== 12 ? hour + 12 : hour === 12 && !isPM ? 0 : hour);
145
+ resetTime.setMinutes(0);
146
+ resetTime.setSeconds(0);
147
+ resetTime.setMilliseconds(0);
148
+
149
+ // If reset time is in the past, move to tomorrow
150
+ if (resetTime <= now) {
151
+ resetTime.setDate(resetTime.getDate() + 1);
152
+ }
153
+ }
154
+
155
+ return {
156
+ limited: true,
157
+ resetTime,
158
+ message: "Claude Code rate limit exceeded",
159
+ };
160
+ }
161
+
162
+ return { limited: false };
163
+ },
164
+
165
+ async installSkills(projectPath: string): Promise<void> {
166
+ // Claude Code reads skills from .claude/skills/ in the project
167
+ const skillsDir = `${projectPath}/.claude/skills`;
168
+ await Bun.spawn(["mkdir", "-p", skillsDir]).exited;
169
+
170
+ // Copy our skills to the project
171
+ const relentlessRoot = import.meta.dir.replace("/src/agents", "");
172
+ await Bun.spawn([
173
+ "cp",
174
+ "-r",
175
+ `${relentlessRoot}/skills/prd`,
176
+ `${skillsDir}/`,
177
+ ]).exited;
178
+ await Bun.spawn([
179
+ "cp",
180
+ "-r",
181
+ `${relentlessRoot}/skills/relentless`,
182
+ `${skillsDir}/`,
183
+ ]).exited;
184
+ },
185
+ };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Codex Agent Adapter
3
+ *
4
+ * Adapter for OpenAI's Codex CLI
5
+ * https://developers.openai.com/codex/cli/
6
+ */
7
+
8
+ import type { AgentAdapter, AgentResult, InvokeOptions, RateLimitInfo } from "./types";
9
+
10
+ export const codexAdapter: AgentAdapter = {
11
+ name: "codex",
12
+ displayName: "OpenAI Codex",
13
+ hasSkillSupport: false, // Uses SKILL.md but requires manual setup
14
+
15
+ async isInstalled(): Promise<boolean> {
16
+ try {
17
+ const path = await this.getExecutablePath();
18
+ return path !== null;
19
+ } catch {
20
+ return false;
21
+ }
22
+ },
23
+
24
+ async getExecutablePath(): Promise<string | null> {
25
+ try {
26
+ const proc = Bun.spawn(["which", "codex"], { stdout: "pipe" });
27
+ const output = await new Response(proc.stdout).text();
28
+ const exitCode = await proc.exited;
29
+ if (exitCode === 0 && output.trim()) {
30
+ return output.trim();
31
+ }
32
+ return null;
33
+ } catch {
34
+ return null;
35
+ }
36
+ },
37
+
38
+ async invoke(prompt: string, options?: InvokeOptions): Promise<AgentResult> {
39
+ const startTime = Date.now();
40
+
41
+ // Codex uses `codex exec -` to read from stdin
42
+ const proc = Bun.spawn(["codex", "exec", "-"], {
43
+ cwd: options?.workingDirectory,
44
+ stdin: new Blob([prompt]),
45
+ stdout: "pipe",
46
+ stderr: "pipe",
47
+ });
48
+
49
+ // Collect output
50
+ const stdout = await new Response(proc.stdout).text();
51
+ const stderr = await new Response(proc.stderr).text();
52
+ const exitCode = await proc.exited;
53
+
54
+ const output = stdout + (stderr ? `\n${stderr}` : "");
55
+ const duration = Date.now() - startTime;
56
+
57
+ return {
58
+ output,
59
+ exitCode,
60
+ isComplete: this.detectCompletion(output),
61
+ duration,
62
+ };
63
+ },
64
+
65
+ detectCompletion(output: string): boolean {
66
+ return output.includes("<promise>COMPLETE</promise>");
67
+ },
68
+
69
+ detectRateLimit(output: string): RateLimitInfo {
70
+ // Codex/OpenAI rate limit patterns
71
+ const patterns = [
72
+ /rate limit exceeded/i,
73
+ /\b429\b/,
74
+ /too many requests/i,
75
+ /quota exceeded/i,
76
+ ];
77
+
78
+ for (const pattern of patterns) {
79
+ if (pattern.test(output)) {
80
+ return {
81
+ limited: true,
82
+ message: "OpenAI Codex rate limit exceeded",
83
+ };
84
+ }
85
+ }
86
+
87
+ return { limited: false };
88
+ },
89
+ };
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Droid Agent Adapter
3
+ *
4
+ * Adapter for Factory's Droid CLI
5
+ * https://factory.ai
6
+ */
7
+
8
+ import type { AgentAdapter, AgentResult, InvokeOptions, RateLimitInfo } from "./types";
9
+
10
+ export const droidAdapter: AgentAdapter = {
11
+ name: "droid",
12
+ displayName: "Factory Droid",
13
+ hasSkillSupport: false, // No skill system, uses prompting
14
+
15
+ async isInstalled(): Promise<boolean> {
16
+ try {
17
+ const path = await this.getExecutablePath();
18
+ return path !== null;
19
+ } catch {
20
+ return false;
21
+ }
22
+ },
23
+
24
+ async getExecutablePath(): Promise<string | null> {
25
+ try {
26
+ const proc = Bun.spawn(["which", "droid"], { stdout: "pipe" });
27
+ const output = await new Response(proc.stdout).text();
28
+ const exitCode = await proc.exited;
29
+ if (exitCode === 0 && output.trim()) {
30
+ return output.trim();
31
+ }
32
+ return null;
33
+ } catch {
34
+ return null;
35
+ }
36
+ },
37
+
38
+ async invoke(prompt: string, options?: InvokeOptions): Promise<AgentResult> {
39
+ const startTime = Date.now();
40
+
41
+ // Droid reads from stdin when piped or redirected.
42
+ // Use --auto high for high risk tolerance by default
43
+ const proc = Bun.spawn(["droid", "exec", "--auto", "high"], {
44
+ cwd: options?.workingDirectory,
45
+ stdin: new Blob([prompt]),
46
+ stdout: "pipe",
47
+ stderr: "pipe",
48
+ });
49
+
50
+ // Collect output
51
+ const stdout = await new Response(proc.stdout).text();
52
+ const stderr = await new Response(proc.stderr).text();
53
+ const exitCode = await proc.exited;
54
+
55
+ const output = stdout + (stderr ? `\n${stderr}` : "");
56
+ const duration = Date.now() - startTime;
57
+
58
+ return {
59
+ output,
60
+ exitCode,
61
+ isComplete: this.detectCompletion(output),
62
+ duration,
63
+ };
64
+ },
65
+
66
+ detectCompletion(output: string): boolean {
67
+ return output.includes("<promise>COMPLETE</promise>");
68
+ },
69
+
70
+ detectRateLimit(output: string): RateLimitInfo {
71
+ // Droid rate limit patterns
72
+ const patterns = [
73
+ /rate limit/i,
74
+ /\b429\b/,
75
+ /too many requests/i,
76
+ /quota exceeded/i,
77
+ ];
78
+
79
+ for (const pattern of patterns) {
80
+ if (pattern.test(output)) {
81
+ return {
82
+ limited: true,
83
+ message: "Factory Droid rate limit exceeded",
84
+ };
85
+ }
86
+ }
87
+
88
+ return { limited: false };
89
+ },
90
+ };
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Gemini Agent Adapter
3
+ *
4
+ * Adapter for Google's Gemini CLI
5
+ * https://github.com/google-gemini/gemini-cli
6
+ */
7
+
8
+ import type { AgentAdapter, AgentResult, InvokeOptions, RateLimitInfo } from "./types";
9
+
10
+ export const geminiAdapter: AgentAdapter = {
11
+ name: "gemini",
12
+ displayName: "Gemini CLI",
13
+ hasSkillSupport: true, // Uses extension system
14
+ skillInstallCommand: "gemini extensions install https://github.com/ArvorCo/Relentless",
15
+
16
+ async isInstalled(): Promise<boolean> {
17
+ try {
18
+ const path = await this.getExecutablePath();
19
+ return path !== null;
20
+ } catch {
21
+ return false;
22
+ }
23
+ },
24
+
25
+ async getExecutablePath(): Promise<string | null> {
26
+ try {
27
+ const proc = Bun.spawn(["which", "gemini"], { stdout: "pipe" });
28
+ const output = await new Response(proc.stdout).text();
29
+ const exitCode = await proc.exited;
30
+ if (exitCode === 0 && output.trim()) {
31
+ return output.trim();
32
+ }
33
+ return null;
34
+ } catch {
35
+ return null;
36
+ }
37
+ },
38
+
39
+ async invoke(prompt: string, options?: InvokeOptions): Promise<AgentResult> {
40
+ const startTime = Date.now();
41
+ const args: string[] = [];
42
+
43
+ if (options?.dangerouslyAllowAll) {
44
+ args.push("--yolo");
45
+ }
46
+
47
+ if (options?.model) {
48
+ args.push("--model", options.model);
49
+ }
50
+
51
+ // Gemini CLI accepts prompt as positional argument
52
+ args.push(prompt);
53
+
54
+ const proc = Bun.spawn(["gemini", ...args], {
55
+ cwd: options?.workingDirectory,
56
+ stdout: "pipe",
57
+ stderr: "pipe",
58
+ });
59
+
60
+ // Collect output
61
+ const stdout = await new Response(proc.stdout).text();
62
+ const stderr = await new Response(proc.stderr).text();
63
+ const exitCode = await proc.exited;
64
+
65
+ const output = stdout + (stderr ? `\n${stderr}` : "");
66
+ const duration = Date.now() - startTime;
67
+
68
+ return {
69
+ output,
70
+ exitCode,
71
+ isComplete: this.detectCompletion(output),
72
+ duration,
73
+ };
74
+ },
75
+
76
+ detectCompletion(output: string): boolean {
77
+ return output.includes("<promise>COMPLETE</promise>");
78
+ },
79
+
80
+ detectRateLimit(output: string): RateLimitInfo {
81
+ // Gemini rate limit patterns
82
+ const patterns = [
83
+ /quota exceeded/i,
84
+ /resource exhausted/i,
85
+ /rate limit/i,
86
+ /\b429\b/,
87
+ /too many requests/i,
88
+ ];
89
+
90
+ for (const pattern of patterns) {
91
+ if (pattern.test(output)) {
92
+ return {
93
+ limited: true,
94
+ message: "Gemini rate limit exceeded",
95
+ };
96
+ }
97
+ }
98
+
99
+ return { limited: false };
100
+ },
101
+
102
+ async installSkills(_projectPath: string): Promise<void> {
103
+ // Gemini uses extensions, we can't install project-local
104
+ // Users need to run: gemini extensions install <url>
105
+ console.log(
106
+ `To install Relentless skills for Gemini, run:\n ${this.skillInstallCommand}`
107
+ );
108
+ },
109
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Agent Adapters Module
3
+ *
4
+ * Re-exports all agent-related types and functions
5
+ */
6
+
7
+ export * from "./types";
8
+ export * from "./registry";
9
+
10
+ // Individual adapters (for direct access if needed)
11
+ export { claudeAdapter } from "./claude";
12
+ export { ampAdapter } from "./amp";
13
+ export { opencodeAdapter } from "./opencode";
14
+ export { codexAdapter } from "./codex";
15
+ export { droidAdapter } from "./droid";
16
+ export { geminiAdapter } from "./gemini";
@@ -0,0 +1,88 @@
1
+ /**
2
+ * OpenCode Agent Adapter
3
+ *
4
+ * Adapter for the OpenCode CLI
5
+ * https://opencode.ai
6
+ */
7
+
8
+ import type { AgentAdapter, AgentResult, InvokeOptions, RateLimitInfo } from "./types";
9
+
10
+ export const opencodeAdapter: AgentAdapter = {
11
+ name: "opencode",
12
+ displayName: "OpenCode",
13
+ hasSkillSupport: false, // Uses agent system, different from skills
14
+
15
+ async isInstalled(): Promise<boolean> {
16
+ try {
17
+ const path = await this.getExecutablePath();
18
+ return path !== null;
19
+ } catch {
20
+ return false;
21
+ }
22
+ },
23
+
24
+ async getExecutablePath(): Promise<string | null> {
25
+ try {
26
+ const proc = Bun.spawn(["which", "opencode"], { stdout: "pipe" });
27
+ const output = await new Response(proc.stdout).text();
28
+ const exitCode = await proc.exited;
29
+ if (exitCode === 0 && output.trim()) {
30
+ return output.trim();
31
+ }
32
+ return null;
33
+ } catch {
34
+ return null;
35
+ }
36
+ },
37
+
38
+ async invoke(prompt: string, options?: InvokeOptions): Promise<AgentResult> {
39
+ const startTime = Date.now();
40
+
41
+ // OpenCode uses `opencode run "message"` for non-interactive mode
42
+ const proc = Bun.spawn(["opencode", "run", prompt], {
43
+ cwd: options?.workingDirectory,
44
+ stdout: "pipe",
45
+ stderr: "pipe",
46
+ });
47
+
48
+ // Collect output
49
+ const stdout = await new Response(proc.stdout).text();
50
+ const stderr = await new Response(proc.stderr).text();
51
+ const exitCode = await proc.exited;
52
+
53
+ const output = stdout + (stderr ? `\n${stderr}` : "");
54
+ const duration = Date.now() - startTime;
55
+
56
+ return {
57
+ output,
58
+ exitCode,
59
+ isComplete: this.detectCompletion(output),
60
+ duration,
61
+ };
62
+ },
63
+
64
+ detectCompletion(output: string): boolean {
65
+ return output.includes("<promise>COMPLETE</promise>");
66
+ },
67
+
68
+ detectRateLimit(output: string): RateLimitInfo {
69
+ // OpenCode rate limit patterns
70
+ const patterns = [
71
+ /rate limited/i,
72
+ /try again later/i,
73
+ /quota exceeded/i,
74
+ /\b429\b/,
75
+ ];
76
+
77
+ for (const pattern of patterns) {
78
+ if (pattern.test(output)) {
79
+ return {
80
+ limited: true,
81
+ message: "OpenCode rate limit exceeded",
82
+ };
83
+ }
84
+ }
85
+
86
+ return { limited: false };
87
+ },
88
+ };
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Agent Registry
3
+ *
4
+ * Central registry for all supported AI coding agents.
5
+ * Provides agent discovery, selection, and health checks.
6
+ */
7
+
8
+ import type { AgentAdapter, AgentName } from "./types";
9
+ import { claudeAdapter } from "./claude";
10
+ import { ampAdapter } from "./amp";
11
+ import { opencodeAdapter } from "./opencode";
12
+ import { codexAdapter } from "./codex";
13
+ import { droidAdapter } from "./droid";
14
+ import { geminiAdapter } from "./gemini";
15
+
16
+ /**
17
+ * Map of all registered agent adapters
18
+ */
19
+ export const AGENTS: Record<AgentName, AgentAdapter> = {
20
+ claude: claudeAdapter,
21
+ amp: ampAdapter,
22
+ opencode: opencodeAdapter,
23
+ codex: codexAdapter,
24
+ droid: droidAdapter,
25
+ gemini: geminiAdapter,
26
+ };
27
+
28
+ /**
29
+ * Get an agent adapter by name
30
+ */
31
+ export function getAgent(name: AgentName): AgentAdapter {
32
+ const agent = AGENTS[name];
33
+ if (!agent) {
34
+ throw new Error(`Unknown agent: ${name}`);
35
+ }
36
+ return agent;
37
+ }
38
+
39
+ /**
40
+ * Get all agent names
41
+ */
42
+ export function getAllAgentNames(): AgentName[] {
43
+ return Object.keys(AGENTS) as AgentName[];
44
+ }
45
+
46
+ /**
47
+ * Check which agents are installed on the system
48
+ */
49
+ export async function getInstalledAgents(): Promise<AgentAdapter[]> {
50
+ const results = await Promise.all(
51
+ Object.values(AGENTS).map(async (agent) => ({
52
+ agent,
53
+ installed: await agent.isInstalled(),
54
+ }))
55
+ );
56
+
57
+ return results.filter((r) => r.installed).map((r) => r.agent);
58
+ }
59
+
60
+ /**
61
+ * Agent health check result
62
+ */
63
+ export interface AgentHealthResult {
64
+ name: AgentName;
65
+ displayName: string;
66
+ installed: boolean;
67
+ executablePath: string | null;
68
+ hasSkillSupport: boolean;
69
+ skillInstallCommand?: string;
70
+ }
71
+
72
+ /**
73
+ * Run health checks on all agents
74
+ */
75
+ export async function checkAgentHealth(): Promise<AgentHealthResult[]> {
76
+ const results = await Promise.all(
77
+ Object.values(AGENTS).map(async (agent) => ({
78
+ name: agent.name,
79
+ displayName: agent.displayName,
80
+ installed: await agent.isInstalled(),
81
+ executablePath: await agent.getExecutablePath(),
82
+ hasSkillSupport: agent.hasSkillSupport,
83
+ skillInstallCommand: agent.skillInstallCommand,
84
+ }))
85
+ );
86
+
87
+ return results;
88
+ }
89
+
90
+ /**
91
+ * Validate that an agent name is valid
92
+ */
93
+ export function isValidAgentName(name: string): name is AgentName {
94
+ return name in AGENTS;
95
+ }