@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.
- package/.claude/commands/relentless.analyze.md +20 -0
- package/.claude/commands/relentless.checklist.md +15 -0
- package/.claude/commands/relentless.clarify.md +19 -0
- package/.claude/commands/relentless.constitution.md +78 -0
- package/.claude/commands/relentless.implement.md +15 -0
- package/.claude/commands/relentless.plan.md +22 -0
- package/.claude/commands/relentless.plan.old.md +89 -0
- package/.claude/commands/relentless.specify.md +254 -0
- package/.claude/commands/relentless.tasks.md +25 -0
- package/.claude/commands/relentless.taskstoissues.md +15 -0
- package/.claude/settings.local.json +23 -0
- package/.claude/skills/analyze/SKILL.md +149 -0
- package/.claude/skills/checklist/SKILL.md +173 -0
- package/.claude/skills/checklist/templates/checklist-template.md +40 -0
- package/.claude/skills/clarify/SKILL.md +174 -0
- package/.claude/skills/constitution/SKILL.md +150 -0
- package/.claude/skills/constitution/templates/constitution-template.md +228 -0
- package/.claude/skills/implement/SKILL.md +141 -0
- package/.claude/skills/plan/SKILL.md +179 -0
- package/.claude/skills/plan/templates/plan-template.md +104 -0
- package/.claude/skills/prd/SKILL.md +242 -0
- package/.claude/skills/relentless/SKILL.md +265 -0
- package/.claude/skills/specify/SKILL.md +220 -0
- package/.claude/skills/specify/scripts/bash/check-prerequisites.sh +166 -0
- package/.claude/skills/specify/scripts/bash/common.sh +156 -0
- package/.claude/skills/specify/scripts/bash/create-new-feature.sh +305 -0
- package/.claude/skills/specify/scripts/bash/setup-plan.sh +61 -0
- package/.claude/skills/specify/scripts/bash/update-agent-context.sh +799 -0
- package/.claude/skills/specify/templates/spec-template.md +115 -0
- package/.claude/skills/tasks/SKILL.md +202 -0
- package/.claude/skills/tasks/templates/tasks-template.md +251 -0
- package/.claude/skills/taskstoissues/SKILL.md +97 -0
- package/.specify/memory/constitution.md +50 -0
- package/.specify/scripts/bash/check-prerequisites.sh +166 -0
- package/.specify/scripts/bash/common.sh +156 -0
- package/.specify/scripts/bash/create-new-feature.sh +297 -0
- package/.specify/scripts/bash/setup-plan.sh +61 -0
- package/.specify/scripts/bash/update-agent-context.sh +799 -0
- package/.specify/templates/agent-file-template.md +28 -0
- package/.specify/templates/checklist-template.md +40 -0
- package/.specify/templates/plan-template.md +104 -0
- package/.specify/templates/spec-template.md +115 -0
- package/.specify/templates/tasks-template.md +251 -0
- package/CHANGES_SUMMARY.md +255 -0
- package/CLAUDE.md +92 -0
- package/GEMINI_SETUP.md +256 -0
- package/LICENSE +21 -0
- package/README.md +1171 -0
- package/REFACTOR_SUMMARY.md +267 -0
- package/bin/relentless.ts +536 -0
- package/bun.lock +352 -0
- package/eslint.config.js +37 -0
- package/package.json +61 -0
- package/prd.json.example +64 -0
- package/prompt.md +108 -0
- package/ralph.sh +80 -0
- package/relentless/config.json +38 -0
- package/relentless/features/.gitkeep +0 -0
- package/relentless/features/ghsk-ideas/prd.json +229 -0
- package/relentless/features/ghsk-ideas/prd.md +191 -0
- package/relentless/features/ghsk-ideas/progress.txt +408 -0
- package/relentless/prompt.md +79 -0
- package/skills/checklist/SKILL.md +349 -0
- package/skills/clarify/SKILL.md +476 -0
- package/skills/prd/SKILL.md +242 -0
- package/skills/relentless/SKILL.md +268 -0
- package/skills/tasks/SKILL.md +577 -0
- package/src/agents/amp.ts +115 -0
- package/src/agents/claude.ts +185 -0
- package/src/agents/codex.ts +89 -0
- package/src/agents/droid.ts +90 -0
- package/src/agents/gemini.ts +109 -0
- package/src/agents/index.ts +16 -0
- package/src/agents/opencode.ts +88 -0
- package/src/agents/registry.ts +95 -0
- package/src/agents/types.ts +101 -0
- package/src/config/index.ts +8 -0
- package/src/config/loader.ts +237 -0
- package/src/config/schema.ts +115 -0
- package/src/execution/index.ts +8 -0
- package/src/execution/router.ts +49 -0
- package/src/execution/runner.ts +512 -0
- package/src/index.ts +11 -0
- package/src/init/index.ts +7 -0
- package/src/init/scaffolder.ts +377 -0
- package/src/prd/analyzer.ts +512 -0
- package/src/prd/index.ts +11 -0
- package/src/prd/issues.ts +249 -0
- package/src/prd/parser.ts +281 -0
- package/src/prd/progress.ts +198 -0
- package/src/prd/types.ts +170 -0
- package/src/tui/App.tsx +85 -0
- package/src/tui/TUIRunner.tsx +400 -0
- package/src/tui/components/AgentOutput.tsx +45 -0
- package/src/tui/components/AgentStatus.tsx +64 -0
- package/src/tui/components/CurrentStory.tsx +66 -0
- package/src/tui/components/Header.tsx +49 -0
- package/src/tui/components/ProgressBar.tsx +39 -0
- package/src/tui/components/StoryGrid.tsx +86 -0
- package/src/tui/hooks/useTUI.ts +147 -0
- package/src/tui/hooks/useTimer.ts +51 -0
- package/src/tui/index.tsx +17 -0
- package/src/tui/theme.ts +41 -0
- package/src/tui/types.ts +77 -0
- package/templates/constitution.md +228 -0
- package/templates/plan.md +273 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Relentless CLI
|
|
4
|
+
*
|
|
5
|
+
* Universal AI agent orchestrator
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
12
|
+
|
|
13
|
+
import { checkAgentHealth, getAllAgentNames, isValidAgentName } from "../src/agents";
|
|
14
|
+
import type { AgentName } from "../src/agents/types";
|
|
15
|
+
import { loadConfig, findRelentlessDir } from "../src/config";
|
|
16
|
+
import { loadPRD, savePRD, parsePRDMarkdown, createPRD, analyzeConsistency, formatReport, generateGitHubIssues } from "../src/prd";
|
|
17
|
+
import { run } from "../src/execution/runner";
|
|
18
|
+
import { runTUI } from "../src/tui/TUIRunner";
|
|
19
|
+
import { initProject, createFeature, listFeatures, createProgressTemplate } from "../src/init/scaffolder";
|
|
20
|
+
|
|
21
|
+
const program = new Command();
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.name("relentless")
|
|
25
|
+
.description("Universal AI agent orchestrator - works with Claude Code, Amp, OpenCode, Codex, Droid, and Gemini")
|
|
26
|
+
.version("0.1.0");
|
|
27
|
+
|
|
28
|
+
// Init command
|
|
29
|
+
program
|
|
30
|
+
.command("init")
|
|
31
|
+
.description("Initialize Relentless in the current project")
|
|
32
|
+
.option("-d, --dir <path>", "Project directory", process.cwd())
|
|
33
|
+
.action(async (options) => {
|
|
34
|
+
await initProject(options.dir);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Run command
|
|
38
|
+
program
|
|
39
|
+
.command("run")
|
|
40
|
+
.description("Run the orchestration loop for a feature")
|
|
41
|
+
.requiredOption("-f, --feature <name>", "Feature name to run")
|
|
42
|
+
.option("-a, --agent <name>", "Agent to use (claude, amp, opencode, codex, droid, gemini, auto)", "claude")
|
|
43
|
+
.option("-m, --max-iterations <n>", "Maximum iterations", "20")
|
|
44
|
+
.option("--dry-run", "Show what would be executed without running", false)
|
|
45
|
+
.option("--tui", "Use beautiful terminal UI interface", false)
|
|
46
|
+
.option("-d, --dir <path>", "Working directory", process.cwd())
|
|
47
|
+
.action(async (options) => {
|
|
48
|
+
const agent = options.agent.toLowerCase();
|
|
49
|
+
if (agent !== "auto" && !isValidAgentName(agent)) {
|
|
50
|
+
console.error(chalk.red(`Invalid agent: ${agent}`));
|
|
51
|
+
console.log(`Valid agents: ${getAllAgentNames().join(", ")}, auto`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const relentlessDir = findRelentlessDir(options.dir);
|
|
56
|
+
if (!relentlessDir) {
|
|
57
|
+
console.error(chalk.red("Relentless not initialized. Run: relentless init"));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const featureDir = join(relentlessDir, "features", options.feature);
|
|
62
|
+
if (!existsSync(featureDir)) {
|
|
63
|
+
console.error(chalk.red(`Feature '${options.feature}' not found`));
|
|
64
|
+
console.log(chalk.dim(`Available features: ${listFeatures(options.dir).join(", ") || "none"}`));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const prdPath = join(featureDir, "prd.json");
|
|
69
|
+
const promptPath = join(relentlessDir, "prompt.md");
|
|
70
|
+
|
|
71
|
+
if (!existsSync(prdPath)) {
|
|
72
|
+
console.error(chalk.red(`PRD file not found: ${prdPath}`));
|
|
73
|
+
console.log(chalk.dim("Convert a PRD first: relentless convert <prd.md> --feature " + options.feature));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!existsSync(promptPath)) {
|
|
78
|
+
console.error(chalk.red(`Prompt file not found: ${promptPath}`));
|
|
79
|
+
console.log(chalk.dim("Run 'relentless init' to create default files"));
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const config = await loadConfig();
|
|
84
|
+
|
|
85
|
+
// Use TUI if requested
|
|
86
|
+
if (options.tui) {
|
|
87
|
+
const success = await runTUI({
|
|
88
|
+
agent: agent as AgentName | "auto",
|
|
89
|
+
maxIterations: parseInt(options.maxIterations, 10),
|
|
90
|
+
workingDirectory: options.dir,
|
|
91
|
+
prdPath,
|
|
92
|
+
promptPath,
|
|
93
|
+
feature: options.feature,
|
|
94
|
+
config,
|
|
95
|
+
dryRun: options.dryRun,
|
|
96
|
+
});
|
|
97
|
+
process.exit(success ? 0 : 1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Standard runner
|
|
101
|
+
const result = await run({
|
|
102
|
+
agent: agent as AgentName | "auto",
|
|
103
|
+
maxIterations: parseInt(options.maxIterations, 10),
|
|
104
|
+
workingDirectory: options.dir,
|
|
105
|
+
prdPath,
|
|
106
|
+
promptPath,
|
|
107
|
+
dryRun: options.dryRun,
|
|
108
|
+
config,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (result.success) {
|
|
112
|
+
console.log(chalk.green.bold(`\nā
Completed successfully!`));
|
|
113
|
+
} else {
|
|
114
|
+
console.log(chalk.yellow(`\nā ļø Stopped after ${result.iterations} iterations`));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(chalk.dim(`\nStats:`));
|
|
118
|
+
console.log(chalk.dim(` Iterations: ${result.iterations}`));
|
|
119
|
+
console.log(chalk.dim(` Stories completed: ${result.storiesCompleted}`));
|
|
120
|
+
console.log(chalk.dim(` Duration: ${(result.duration / 1000 / 60).toFixed(1)} minutes`));
|
|
121
|
+
|
|
122
|
+
process.exit(result.success ? 0 : 1);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Convert command
|
|
126
|
+
program
|
|
127
|
+
.command("convert <tasksMd>")
|
|
128
|
+
.description("Convert tasks.md to prd.json, optionally merging checklist.md criteria")
|
|
129
|
+
.requiredOption("-f, --feature <name>", "Feature name")
|
|
130
|
+
.option("-d, --dir <path>", "Project directory", process.cwd())
|
|
131
|
+
.option("--auto-number", "Auto-number the feature directory (e.g., 001-feature-name)", false)
|
|
132
|
+
.option("--with-checklist", "Merge checklist items into acceptance criteria", false)
|
|
133
|
+
.action(async (tasksMd, options) => {
|
|
134
|
+
if (!existsSync(tasksMd)) {
|
|
135
|
+
console.error(chalk.red(`File not found: ${tasksMd}`));
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const relentlessDir = findRelentlessDir(options.dir);
|
|
140
|
+
if (!relentlessDir) {
|
|
141
|
+
console.error(chalk.red("Relentless not initialized. Run: relentless init"));
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Determine final feature name (with auto-number if requested)
|
|
146
|
+
let finalFeatureName = options.feature;
|
|
147
|
+
if (options.autoNumber) {
|
|
148
|
+
const features = listFeatures(options.dir);
|
|
149
|
+
const numbers = features
|
|
150
|
+
.map((f) => {
|
|
151
|
+
const match = f.match(/^(\d{3})-/);
|
|
152
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
153
|
+
})
|
|
154
|
+
.filter((n) => n > 0);
|
|
155
|
+
const nextNumber = numbers.length > 0 ? Math.max(...numbers) + 1 : 1;
|
|
156
|
+
const numberPrefix = nextNumber.toString().padStart(3, "0");
|
|
157
|
+
finalFeatureName = `${numberPrefix}-${options.feature}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Create feature directory if it doesn't exist
|
|
161
|
+
const featureDir = join(relentlessDir, "features", finalFeatureName);
|
|
162
|
+
if (!existsSync(featureDir)) {
|
|
163
|
+
mkdirSync(featureDir, { recursive: true });
|
|
164
|
+
console.log(chalk.dim(`Created feature: ${finalFeatureName}`));
|
|
165
|
+
|
|
166
|
+
// Create progress.txt
|
|
167
|
+
const progressPath = join(featureDir, "progress.txt");
|
|
168
|
+
await Bun.write(progressPath, createProgressTemplate(finalFeatureName));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log(chalk.dim(`Converting ${tasksMd}...`));
|
|
172
|
+
|
|
173
|
+
// Read tasks.md (primary source)
|
|
174
|
+
const tasksContent = await Bun.file(tasksMd).text();
|
|
175
|
+
const parsed = parsePRDMarkdown(tasksContent);
|
|
176
|
+
|
|
177
|
+
// Optionally merge checklist items
|
|
178
|
+
if (options.withChecklist) {
|
|
179
|
+
const checklistPath = join(featureDir, "checklist.md");
|
|
180
|
+
if (existsSync(checklistPath)) {
|
|
181
|
+
console.log(chalk.dim(" Merging checklist.md criteria..."));
|
|
182
|
+
const checklistContent = await Bun.file(checklistPath).text();
|
|
183
|
+
// TODO: Parse checklist and merge into acceptance criteria
|
|
184
|
+
// For now, just note that it exists
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const prd = createPRD(parsed, finalFeatureName);
|
|
189
|
+
|
|
190
|
+
// Save prd.json to feature folder
|
|
191
|
+
const prdJsonPath = join(featureDir, "prd.json");
|
|
192
|
+
await savePRD(prd, prdJsonPath);
|
|
193
|
+
|
|
194
|
+
// Also copy the source files to the feature folder
|
|
195
|
+
const tasksMdPath = join(featureDir, "prd.md");
|
|
196
|
+
await Bun.write(tasksMdPath, tasksContent);
|
|
197
|
+
|
|
198
|
+
console.log(chalk.green(`ā
Created relentless/features/${finalFeatureName}/`));
|
|
199
|
+
console.log(chalk.dim(` prd.json - ${prd.userStories.length} stories`));
|
|
200
|
+
console.log(chalk.dim(` prd.md - from tasks.md`));
|
|
201
|
+
console.log(chalk.dim(` progress.txt - progress log`));
|
|
202
|
+
if (options.withChecklist && existsSync(join(featureDir, "checklist.md"))) {
|
|
203
|
+
console.log(chalk.dim(` ā Checklist criteria merged`));
|
|
204
|
+
}
|
|
205
|
+
console.log(chalk.dim(`\nProject: ${prd.project}`));
|
|
206
|
+
console.log(chalk.dim(`Branch: ${prd.branchName}`));
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Feature commands
|
|
210
|
+
const features = program.command("features").description("Manage features");
|
|
211
|
+
|
|
212
|
+
features
|
|
213
|
+
.command("list")
|
|
214
|
+
.description("List all features")
|
|
215
|
+
.option("-d, --dir <path>", "Project directory", process.cwd())
|
|
216
|
+
.action(async (options) => {
|
|
217
|
+
const featureList = listFeatures(options.dir);
|
|
218
|
+
|
|
219
|
+
if (featureList.length === 0) {
|
|
220
|
+
console.log(chalk.dim("\nNo features found."));
|
|
221
|
+
console.log(chalk.dim("Create one with: relentless convert <prd.md> --feature <name>\n"));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
console.log(chalk.bold("\nFeatures:\n"));
|
|
226
|
+
for (const feature of featureList) {
|
|
227
|
+
const relentlessDir = findRelentlessDir(options.dir);
|
|
228
|
+
const featureDir = join(relentlessDir!, "features", feature);
|
|
229
|
+
const prdPath = join(featureDir, "prd.json");
|
|
230
|
+
|
|
231
|
+
if (existsSync(prdPath)) {
|
|
232
|
+
try {
|
|
233
|
+
const prdFile = await Bun.file(prdPath).json();
|
|
234
|
+
const completed = prdFile.userStories?.filter((s: { passes?: boolean }) => s.passes).length ?? 0;
|
|
235
|
+
const total = prdFile.userStories?.length ?? 0;
|
|
236
|
+
console.log(` ${feature} - ${completed}/${total} stories complete`);
|
|
237
|
+
} catch {
|
|
238
|
+
console.log(` ${feature}`);
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
console.log(` ${feature} (no prd.json)`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
console.log("");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
features
|
|
248
|
+
.command("create <name>")
|
|
249
|
+
.description("Create a new feature folder")
|
|
250
|
+
.option("-d, --dir <path>", "Project directory", process.cwd())
|
|
251
|
+
.option("--with-plan", "Include plan.md template", false)
|
|
252
|
+
.option("--auto-number", "Auto-number the feature directory (e.g., 001-feature-name)", false)
|
|
253
|
+
.action(async (name, options) => {
|
|
254
|
+
try {
|
|
255
|
+
const featureDir = await createFeature(options.dir, name, {
|
|
256
|
+
withPlan: options.withPlan,
|
|
257
|
+
autoNumber: options.autoNumber,
|
|
258
|
+
});
|
|
259
|
+
const featureName = featureDir.split("/").pop() || name;
|
|
260
|
+
console.log(chalk.green(`ā
Created feature: ${featureName}`));
|
|
261
|
+
console.log(chalk.dim(` ${featureDir}/`));
|
|
262
|
+
console.log(chalk.dim("\nNext: Add prd.md and run: relentless convert prd.md --feature " + featureName));
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error(chalk.red((error as Error).message));
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Status command - show all stories with status
|
|
270
|
+
program
|
|
271
|
+
.command("status")
|
|
272
|
+
.description("Show status of all user stories in a feature")
|
|
273
|
+
.requiredOption("-f, --feature <name>", "Feature name")
|
|
274
|
+
.option("-d, --dir <path>", "Project directory", process.cwd())
|
|
275
|
+
.action(async (options) => {
|
|
276
|
+
const relentlessDir = findRelentlessDir(options.dir);
|
|
277
|
+
if (!relentlessDir) {
|
|
278
|
+
console.error(chalk.red("Relentless not initialized. Run: relentless init"));
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const featureDir = join(relentlessDir, "features", options.feature);
|
|
283
|
+
const prdPath = join(featureDir, "prd.json");
|
|
284
|
+
|
|
285
|
+
if (!existsSync(prdPath)) {
|
|
286
|
+
console.error(chalk.red(`Feature '${options.feature}' not found or has no prd.json`));
|
|
287
|
+
console.log(chalk.dim(`Available features: ${listFeatures(options.dir).join(", ") || "none"}`));
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const prd = await loadPRD(prdPath);
|
|
292
|
+
const completed = prd.userStories.filter((s) => s.passes).length;
|
|
293
|
+
const total = prd.userStories.length;
|
|
294
|
+
|
|
295
|
+
console.log(chalk.bold(`\nFeature: ${options.feature}`));
|
|
296
|
+
console.log(chalk.dim(`Progress: ${completed}/${total} stories complete\n`));
|
|
297
|
+
|
|
298
|
+
for (const story of prd.userStories) {
|
|
299
|
+
const status = story.passes
|
|
300
|
+
? chalk.green("ā")
|
|
301
|
+
: chalk.dim("ā");
|
|
302
|
+
const id = story.passes
|
|
303
|
+
? chalk.green(story.id.padEnd(8))
|
|
304
|
+
: chalk.dim(story.id.padEnd(8));
|
|
305
|
+
const title = story.passes
|
|
306
|
+
? chalk.strikethrough(chalk.dim(story.title))
|
|
307
|
+
: story.title;
|
|
308
|
+
console.log(` ${status} ${id} ${title}`);
|
|
309
|
+
}
|
|
310
|
+
console.log("");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Reset command - reset a story to incomplete
|
|
314
|
+
program
|
|
315
|
+
.command("reset <storyId>")
|
|
316
|
+
.description("Reset a user story to incomplete so it can be re-run")
|
|
317
|
+
.requiredOption("-f, --feature <name>", "Feature name")
|
|
318
|
+
.option("-d, --dir <path>", "Project directory", process.cwd())
|
|
319
|
+
.action(async (storyId, options) => {
|
|
320
|
+
const relentlessDir = findRelentlessDir(options.dir);
|
|
321
|
+
if (!relentlessDir) {
|
|
322
|
+
console.error(chalk.red("Relentless not initialized. Run: relentless init"));
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const featureDir = join(relentlessDir, "features", options.feature);
|
|
327
|
+
const prdPath = join(featureDir, "prd.json");
|
|
328
|
+
|
|
329
|
+
if (!existsSync(prdPath)) {
|
|
330
|
+
console.error(chalk.red(`Feature '${options.feature}' not found or has no prd.json`));
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const prd = await loadPRD(prdPath);
|
|
335
|
+
const storyIndex = prd.userStories.findIndex(
|
|
336
|
+
(s) => s.id.toLowerCase() === storyId.toLowerCase()
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
if (storyIndex === -1) {
|
|
340
|
+
console.error(chalk.red(`Story '${storyId}' not found`));
|
|
341
|
+
console.log(chalk.dim(`Available stories: ${prd.userStories.map((s) => s.id).join(", ")}`));
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const story = prd.userStories[storyIndex];
|
|
346
|
+
const wasComplete = story.passes;
|
|
347
|
+
const prevCompleted = prd.userStories.filter((s) => s.passes).length;
|
|
348
|
+
|
|
349
|
+
// Reset the story
|
|
350
|
+
prd.userStories[storyIndex].passes = false;
|
|
351
|
+
await savePRD(prd, prdPath);
|
|
352
|
+
|
|
353
|
+
const newCompleted = prd.userStories.filter((s) => s.passes).length;
|
|
354
|
+
|
|
355
|
+
if (wasComplete) {
|
|
356
|
+
console.log(chalk.green(`\nā Reset ${story.id} (${story.title}) to incomplete`));
|
|
357
|
+
console.log(chalk.dim(` Feature: ${options.feature}`));
|
|
358
|
+
console.log(chalk.dim(` Progress: ${newCompleted}/${prd.userStories.length} stories complete (was ${prevCompleted}/${prd.userStories.length})\n`));
|
|
359
|
+
} else {
|
|
360
|
+
console.log(chalk.yellow(`\nā ${story.id} (${story.title}) was already incomplete\n`));
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Agents command
|
|
365
|
+
const agents = program.command("agents").description("Manage AI agents");
|
|
366
|
+
|
|
367
|
+
agents
|
|
368
|
+
.command("list")
|
|
369
|
+
.description("List available agents")
|
|
370
|
+
.action(async () => {
|
|
371
|
+
const health = await checkAgentHealth();
|
|
372
|
+
|
|
373
|
+
console.log(chalk.bold("\nAvailable Agents:\n"));
|
|
374
|
+
|
|
375
|
+
for (const agent of health) {
|
|
376
|
+
const status = agent.installed
|
|
377
|
+
? chalk.green("ā installed")
|
|
378
|
+
: chalk.red("ā not found");
|
|
379
|
+
|
|
380
|
+
console.log(` ${agent.displayName.padEnd(15)} ${status}`);
|
|
381
|
+
|
|
382
|
+
if (agent.installed && agent.executablePath) {
|
|
383
|
+
console.log(chalk.dim(` ${agent.executablePath}`));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (agent.hasSkillSupport && agent.skillInstallCommand) {
|
|
387
|
+
console.log(chalk.dim(` Skills: ${agent.skillInstallCommand}`));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
console.log("");
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
agents
|
|
395
|
+
.command("doctor")
|
|
396
|
+
.description("Check agent health")
|
|
397
|
+
.action(async () => {
|
|
398
|
+
console.log(chalk.bold("\nš Agent Health Check\n"));
|
|
399
|
+
|
|
400
|
+
const health = await checkAgentHealth();
|
|
401
|
+
let allHealthy = true;
|
|
402
|
+
|
|
403
|
+
for (const agent of health) {
|
|
404
|
+
if (agent.installed) {
|
|
405
|
+
console.log(`${chalk.green("ā")} ${agent.displayName}`);
|
|
406
|
+
console.log(chalk.dim(` Path: ${agent.executablePath}`));
|
|
407
|
+
if (agent.hasSkillSupport) {
|
|
408
|
+
console.log(chalk.dim(` Skills: Supported`));
|
|
409
|
+
}
|
|
410
|
+
} else {
|
|
411
|
+
console.log(`${chalk.red("ā")} ${agent.displayName}`);
|
|
412
|
+
allHealthy = false;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
console.log("");
|
|
417
|
+
|
|
418
|
+
if (allHealthy) {
|
|
419
|
+
console.log(chalk.green("All agents healthy!"));
|
|
420
|
+
} else {
|
|
421
|
+
console.log(chalk.yellow("Some agents are not installed."));
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Analyze command
|
|
426
|
+
program
|
|
427
|
+
.command("analyze")
|
|
428
|
+
.description("Analyze cross-artifact consistency for a feature")
|
|
429
|
+
.requiredOption("-f, --feature <name>", "Feature name")
|
|
430
|
+
.option("-d, --dir <path>", "Project directory", process.cwd())
|
|
431
|
+
.action(async (options) => {
|
|
432
|
+
const relentlessDir = findRelentlessDir(options.dir);
|
|
433
|
+
if (!relentlessDir) {
|
|
434
|
+
console.error(chalk.red("Relentless not initialized. Run: relentless init"));
|
|
435
|
+
process.exit(1);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const featureDir = join(relentlessDir, "features", options.feature);
|
|
439
|
+
const prdPath = join(featureDir, "prd.json");
|
|
440
|
+
|
|
441
|
+
if (!existsSync(prdPath)) {
|
|
442
|
+
console.error(chalk.red(`Feature '${options.feature}' not found or has no prd.json`));
|
|
443
|
+
console.log(chalk.dim(`Available features: ${listFeatures(options.dir).join(", ") || "none"}`));
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const prd = await loadPRD(prdPath);
|
|
448
|
+
const report = await analyzeConsistency(prdPath, prd);
|
|
449
|
+
|
|
450
|
+
const formatted = formatReport(report);
|
|
451
|
+
console.log(formatted);
|
|
452
|
+
|
|
453
|
+
// Exit with error code if there are critical issues
|
|
454
|
+
if (report.summary.critical > 0) {
|
|
455
|
+
console.log(chalk.red("\nā Critical issues found. Please address them before proceeding.\n"));
|
|
456
|
+
process.exit(1);
|
|
457
|
+
} else if (report.summary.warnings > 0) {
|
|
458
|
+
console.log(chalk.yellow("\nā ļø Warnings found. Consider addressing them.\n"));
|
|
459
|
+
} else {
|
|
460
|
+
console.log(chalk.green("\nā
No critical issues or warnings found!\n"));
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// Issues command
|
|
465
|
+
program
|
|
466
|
+
.command("issues")
|
|
467
|
+
.description("Convert user stories to GitHub issues")
|
|
468
|
+
.requiredOption("-f, --feature <name>", "Feature name")
|
|
469
|
+
.option("-d, --dir <path>", "Project directory", process.cwd())
|
|
470
|
+
.option("--all", "Include completed stories (default: only incomplete)", false)
|
|
471
|
+
.option("--dry-run", "Show what would be created without actually creating issues", false)
|
|
472
|
+
.action(async (options) => {
|
|
473
|
+
const relentlessDir = findRelentlessDir(options.dir);
|
|
474
|
+
if (!relentlessDir) {
|
|
475
|
+
console.error(chalk.red("Relentless not initialized. Run: relentless init"));
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const featureDir = join(relentlessDir, "features", options.feature);
|
|
480
|
+
const prdPath = join(featureDir, "prd.json");
|
|
481
|
+
|
|
482
|
+
if (!existsSync(prdPath)) {
|
|
483
|
+
console.error(chalk.red(`Feature '${options.feature}' not found or has no prd.json`));
|
|
484
|
+
console.log(chalk.dim(`Available features: ${listFeatures(options.dir).join(", ") || "none"}`));
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const prd = await loadPRD(prdPath);
|
|
489
|
+
|
|
490
|
+
console.log(chalk.bold(`\nš GitHub Issues Generator\n`));
|
|
491
|
+
console.log(chalk.dim(`Feature: ${options.feature}`));
|
|
492
|
+
console.log(chalk.dim(`Project: ${prd.project}\n`));
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
const result = await generateGitHubIssues(prd, {
|
|
496
|
+
dryRun: options.dryRun,
|
|
497
|
+
onlyIncomplete: !options.all,
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
console.log("");
|
|
501
|
+
if (options.dryRun) {
|
|
502
|
+
console.log(chalk.yellow(`Would create ${result.created} issues`));
|
|
503
|
+
} else {
|
|
504
|
+
console.log(chalk.green(`ā
Created ${result.created} issues`));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (result.errors.length > 0) {
|
|
508
|
+
console.log(chalk.red(`\nā ${result.errors.length} errors occurred:`));
|
|
509
|
+
for (const error of result.errors) {
|
|
510
|
+
console.log(chalk.dim(` ${error}`));
|
|
511
|
+
}
|
|
512
|
+
process.exit(1);
|
|
513
|
+
}
|
|
514
|
+
} catch (error) {
|
|
515
|
+
console.error(chalk.red(`\nā ${(error as Error).message}\n`));
|
|
516
|
+
process.exit(1);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// PRD command (for agents without skill support)
|
|
521
|
+
program
|
|
522
|
+
.command("prd <description>")
|
|
523
|
+
.description("Generate a PRD using prompting (for agents without skill support)")
|
|
524
|
+
.option("-a, --agent <name>", "Agent to use", "claude")
|
|
525
|
+
.option("-f, --feature <name>", "Feature name")
|
|
526
|
+
.action(async (description, _options) => {
|
|
527
|
+
console.log(chalk.bold("\nš PRD Generation\n"));
|
|
528
|
+
console.log(chalk.dim("For agents with skill support, use:"));
|
|
529
|
+
console.log(chalk.cyan(' claude "Load the prd skill and create a PRD for ' + description + '"\n'));
|
|
530
|
+
|
|
531
|
+
console.log(chalk.yellow("Direct PRD generation coming soon..."));
|
|
532
|
+
// TODO: Implement direct PRD generation for agents without skill support
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// Parse arguments
|
|
536
|
+
program.parse();
|