@arvorco/relentless 0.1.27 → 0.2.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/package.json +1 -1
- package/src/agents/amp.ts +26 -13
- package/src/agents/codex.ts +35 -1
- package/src/agents/droid.ts +35 -1
- package/src/agents/opencode.ts +35 -1
- package/src/init/scaffolder.ts +343 -117
package/package.json
CHANGED
package/src/agents/amp.ts
CHANGED
|
@@ -94,22 +94,35 @@ export const ampAdapter: AgentAdapter = {
|
|
|
94
94
|
|
|
95
95
|
async installSkills(projectPath: string): Promise<void> {
|
|
96
96
|
// Amp can install skills globally via amp skill add
|
|
97
|
-
// For project-local, we copy to the project
|
|
97
|
+
// For project-local, we copy to the project's .amp/skills/
|
|
98
98
|
const skillsDir = `${projectPath}/.amp/skills`;
|
|
99
99
|
await Bun.spawn(["mkdir", "-p", skillsDir]).exited;
|
|
100
100
|
|
|
101
101
|
const relentlessRoot = import.meta.dir.replace("/src/agents", "");
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
"
|
|
110
|
-
"
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
102
|
+
const sourceSkillsDir = `${relentlessRoot}/.claude/skills`;
|
|
103
|
+
|
|
104
|
+
// Copy all skills
|
|
105
|
+
const skills = [
|
|
106
|
+
"prd",
|
|
107
|
+
"relentless",
|
|
108
|
+
"constitution",
|
|
109
|
+
"specify",
|
|
110
|
+
"plan",
|
|
111
|
+
"tasks",
|
|
112
|
+
"checklist",
|
|
113
|
+
"clarify",
|
|
114
|
+
"analyze",
|
|
115
|
+
"implement",
|
|
116
|
+
"taskstoissues",
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
for (const skill of skills) {
|
|
120
|
+
await Bun.spawn([
|
|
121
|
+
"cp",
|
|
122
|
+
"-r",
|
|
123
|
+
`${sourceSkillsDir}/${skill}`,
|
|
124
|
+
`${skillsDir}/`,
|
|
125
|
+
]).exited;
|
|
126
|
+
}
|
|
114
127
|
},
|
|
115
128
|
};
|
package/src/agents/codex.ts
CHANGED
|
@@ -10,7 +10,8 @@ import type { AgentAdapter, AgentResult, InvokeOptions, RateLimitInfo } from "./
|
|
|
10
10
|
export const codexAdapter: AgentAdapter = {
|
|
11
11
|
name: "codex",
|
|
12
12
|
displayName: "OpenAI Codex",
|
|
13
|
-
hasSkillSupport:
|
|
13
|
+
hasSkillSupport: true,
|
|
14
|
+
skillInstallCommand: "codex skill add <skill-name>",
|
|
14
15
|
|
|
15
16
|
async isInstalled(): Promise<boolean> {
|
|
16
17
|
try {
|
|
@@ -86,4 +87,37 @@ export const codexAdapter: AgentAdapter = {
|
|
|
86
87
|
|
|
87
88
|
return { limited: false };
|
|
88
89
|
},
|
|
90
|
+
|
|
91
|
+
async installSkills(projectPath: string): Promise<void> {
|
|
92
|
+
// Codex uses .codex/skills/ for project-level skills
|
|
93
|
+
const skillsDir = `${projectPath}/.codex/skills`;
|
|
94
|
+
await Bun.spawn(["mkdir", "-p", skillsDir]).exited;
|
|
95
|
+
|
|
96
|
+
const relentlessRoot = import.meta.dir.replace("/src/agents", "");
|
|
97
|
+
const sourceSkillsDir = `${relentlessRoot}/.claude/skills`;
|
|
98
|
+
|
|
99
|
+
// Copy all skills
|
|
100
|
+
const skills = [
|
|
101
|
+
"prd",
|
|
102
|
+
"relentless",
|
|
103
|
+
"constitution",
|
|
104
|
+
"specify",
|
|
105
|
+
"plan",
|
|
106
|
+
"tasks",
|
|
107
|
+
"checklist",
|
|
108
|
+
"clarify",
|
|
109
|
+
"analyze",
|
|
110
|
+
"implement",
|
|
111
|
+
"taskstoissues",
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
for (const skill of skills) {
|
|
115
|
+
await Bun.spawn([
|
|
116
|
+
"cp",
|
|
117
|
+
"-r",
|
|
118
|
+
`${sourceSkillsDir}/${skill}`,
|
|
119
|
+
`${skillsDir}/`,
|
|
120
|
+
]).exited;
|
|
121
|
+
}
|
|
122
|
+
},
|
|
89
123
|
};
|
package/src/agents/droid.ts
CHANGED
|
@@ -10,7 +10,8 @@ import type { AgentAdapter, AgentResult, InvokeOptions, RateLimitInfo } from "./
|
|
|
10
10
|
export const droidAdapter: AgentAdapter = {
|
|
11
11
|
name: "droid",
|
|
12
12
|
displayName: "Factory Droid",
|
|
13
|
-
hasSkillSupport:
|
|
13
|
+
hasSkillSupport: true,
|
|
14
|
+
skillInstallCommand: "droid skill install <skill-name>",
|
|
14
15
|
|
|
15
16
|
async isInstalled(): Promise<boolean> {
|
|
16
17
|
try {
|
|
@@ -87,4 +88,37 @@ export const droidAdapter: AgentAdapter = {
|
|
|
87
88
|
|
|
88
89
|
return { limited: false };
|
|
89
90
|
},
|
|
91
|
+
|
|
92
|
+
async installSkills(projectPath: string): Promise<void> {
|
|
93
|
+
// Factory uses .factory/skills/ for skills (PLURAL)
|
|
94
|
+
const skillsDir = `${projectPath}/.factory/skills`;
|
|
95
|
+
await Bun.spawn(["mkdir", "-p", skillsDir]).exited;
|
|
96
|
+
|
|
97
|
+
const relentlessRoot = import.meta.dir.replace("/src/agents", "");
|
|
98
|
+
const sourceSkillsDir = `${relentlessRoot}/.claude/skills`;
|
|
99
|
+
|
|
100
|
+
// Copy all skills
|
|
101
|
+
const skills = [
|
|
102
|
+
"prd",
|
|
103
|
+
"relentless",
|
|
104
|
+
"constitution",
|
|
105
|
+
"specify",
|
|
106
|
+
"plan",
|
|
107
|
+
"tasks",
|
|
108
|
+
"checklist",
|
|
109
|
+
"clarify",
|
|
110
|
+
"analyze",
|
|
111
|
+
"implement",
|
|
112
|
+
"taskstoissues",
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
for (const skill of skills) {
|
|
116
|
+
await Bun.spawn([
|
|
117
|
+
"cp",
|
|
118
|
+
"-r",
|
|
119
|
+
`${sourceSkillsDir}/${skill}`,
|
|
120
|
+
`${skillsDir}/`,
|
|
121
|
+
]).exited;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
90
124
|
};
|
package/src/agents/opencode.ts
CHANGED
|
@@ -10,7 +10,8 @@ import type { AgentAdapter, AgentResult, InvokeOptions, RateLimitInfo } from "./
|
|
|
10
10
|
export const opencodeAdapter: AgentAdapter = {
|
|
11
11
|
name: "opencode",
|
|
12
12
|
displayName: "OpenCode",
|
|
13
|
-
hasSkillSupport:
|
|
13
|
+
hasSkillSupport: true,
|
|
14
|
+
skillInstallCommand: "opencode skill add <skill-name>",
|
|
14
15
|
|
|
15
16
|
async isInstalled(): Promise<boolean> {
|
|
16
17
|
try {
|
|
@@ -85,4 +86,37 @@ export const opencodeAdapter: AgentAdapter = {
|
|
|
85
86
|
|
|
86
87
|
return { limited: false };
|
|
87
88
|
},
|
|
89
|
+
|
|
90
|
+
async installSkills(projectPath: string): Promise<void> {
|
|
91
|
+
// OpenCode uses .opencode/skill/ (SINGULAR!) for skills
|
|
92
|
+
const skillsDir = `${projectPath}/.opencode/skill`;
|
|
93
|
+
await Bun.spawn(["mkdir", "-p", skillsDir]).exited;
|
|
94
|
+
|
|
95
|
+
const relentlessRoot = import.meta.dir.replace("/src/agents", "");
|
|
96
|
+
const sourceSkillsDir = `${relentlessRoot}/.claude/skills`;
|
|
97
|
+
|
|
98
|
+
// Copy all skills
|
|
99
|
+
const skills = [
|
|
100
|
+
"prd",
|
|
101
|
+
"relentless",
|
|
102
|
+
"constitution",
|
|
103
|
+
"specify",
|
|
104
|
+
"plan",
|
|
105
|
+
"tasks",
|
|
106
|
+
"checklist",
|
|
107
|
+
"clarify",
|
|
108
|
+
"analyze",
|
|
109
|
+
"implement",
|
|
110
|
+
"taskstoissues",
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
for (const skill of skills) {
|
|
114
|
+
await Bun.spawn([
|
|
115
|
+
"cp",
|
|
116
|
+
"-r",
|
|
117
|
+
`${sourceSkillsDir}/${skill}`,
|
|
118
|
+
`${skillsDir}/`,
|
|
119
|
+
]).exited;
|
|
120
|
+
}
|
|
121
|
+
},
|
|
88
122
|
};
|
package/src/init/scaffolder.ts
CHANGED
|
@@ -55,76 +55,6 @@ const RELENTLESS_FILES: Record<string, () => string> = {
|
|
|
55
55
|
"config.json": () => JSON.stringify(DEFAULT_CONFIG, null, 2),
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
-
/**
|
|
59
|
-
* Files that should NEVER be overwritten, even with -f flag
|
|
60
|
-
* These are personalized files that users customize for their project
|
|
61
|
-
*/
|
|
62
|
-
const PROTECTED_FILES: Record<string, () => string> = {
|
|
63
|
-
"prompt.md": () => PROMPT_TEMPLATE,
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const PROMPT_TEMPLATE = `# Relentless Agent Instructions
|
|
67
|
-
|
|
68
|
-
You are an autonomous coding agent. Follow these instructions exactly.
|
|
69
|
-
|
|
70
|
-
**⚠️ This is a generic template. Personalize it for your project using:**
|
|
71
|
-
\`\`\`bash
|
|
72
|
-
/relentless.prompt
|
|
73
|
-
\`\`\`
|
|
74
|
-
|
|
75
|
-
---
|
|
76
|
-
|
|
77
|
-
## Your Task (Per Iteration)
|
|
78
|
-
|
|
79
|
-
1. Read \`relentless/features/<feature>/prd.json\`
|
|
80
|
-
2. Read \`relentless/features/<feature>/progress.txt\`
|
|
81
|
-
3. Check you're on the correct branch from PRD \`branchName\`
|
|
82
|
-
4. Pick the **highest priority** story where \`passes: false\`
|
|
83
|
-
5. Review existing code to understand patterns
|
|
84
|
-
6. Implement the story
|
|
85
|
-
7. Run quality checks (typecheck, lint, test)
|
|
86
|
-
8. If ALL checks pass, commit: \`feat: [Story ID] - [Story Title]\`
|
|
87
|
-
9. Update PRD: set \`passes: true\`
|
|
88
|
-
10. Append progress to \`progress.txt\`
|
|
89
|
-
|
|
90
|
-
---
|
|
91
|
-
|
|
92
|
-
## Quality Requirements
|
|
93
|
-
|
|
94
|
-
Before marking a story complete:
|
|
95
|
-
- [ ] All quality checks pass (typecheck, lint, test)
|
|
96
|
-
- [ ] Zero errors and zero warnings
|
|
97
|
-
- [ ] No debug code (console.log, debugger)
|
|
98
|
-
- [ ] No unused imports or variables
|
|
99
|
-
- [ ] Follows existing patterns
|
|
100
|
-
|
|
101
|
-
---
|
|
102
|
-
|
|
103
|
-
## Progress Report Format
|
|
104
|
-
|
|
105
|
-
APPEND to progress.txt:
|
|
106
|
-
\`\`\`
|
|
107
|
-
## [Date/Time] - [Story ID]
|
|
108
|
-
- What was implemented
|
|
109
|
-
- Files changed
|
|
110
|
-
- Learnings for future iterations
|
|
111
|
-
---
|
|
112
|
-
\`\`\`
|
|
113
|
-
|
|
114
|
-
---
|
|
115
|
-
|
|
116
|
-
## Stop Condition
|
|
117
|
-
|
|
118
|
-
After completing a story, check if ALL stories have \`passes: true\`.
|
|
119
|
-
|
|
120
|
-
If ALL complete:
|
|
121
|
-
\`\`\`
|
|
122
|
-
<promise>COMPLETE</promise>
|
|
123
|
-
\`\`\`
|
|
124
|
-
|
|
125
|
-
Otherwise, end normally (next iteration continues).
|
|
126
|
-
`;
|
|
127
|
-
|
|
128
58
|
/**
|
|
129
59
|
* Default progress.txt content for a new feature with YAML frontmatter
|
|
130
60
|
*/
|
|
@@ -195,21 +125,9 @@ export async function initProject(projectDir: string = process.cwd(), force: boo
|
|
|
195
125
|
console.log(` ${chalk.green("✓")} relentless/${filename} ${force ? `(${action})` : ""}`);
|
|
196
126
|
}
|
|
197
127
|
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (existsSync(path)) {
|
|
203
|
-
console.log(` ${chalk.yellow("⚠")} relentless/${filename} already exists (protected, not overwriting)`);
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
await Bun.write(path, contentFn());
|
|
208
|
-
console.log(` ${chalk.green("✓")} relentless/${filename} (created)`);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Note: constitution.md is NOT copied - it should be created by /relentless.constitution command
|
|
212
|
-
// This ensures each project gets a personalized constitution
|
|
128
|
+
// Note: constitution.md and prompt.md are NOT created here
|
|
129
|
+
// They should be generated by /relentless.constitution command
|
|
130
|
+
// This ensures each project gets personalized governance and agent instructions
|
|
213
131
|
|
|
214
132
|
// Create features directory with .gitkeep
|
|
215
133
|
const gitkeepPath = join(featuresDir, ".gitkeep");
|
|
@@ -227,6 +145,13 @@ export async function initProject(projectDir: string = process.cwd(), force: boo
|
|
|
227
145
|
|
|
228
146
|
const sourceSkillsDir = join(relentlessRoot, ".claude", "skills");
|
|
229
147
|
|
|
148
|
+
// Check if we're running in the relentless project itself (source == destination)
|
|
149
|
+
// This prevents accidentally deleting our own source files with -f flag
|
|
150
|
+
const isRelentlessProject = skillsDir === sourceSkillsDir;
|
|
151
|
+
if (isRelentlessProject) {
|
|
152
|
+
console.log(chalk.yellow(" ⚠ Running in Relentless project itself - skipping skill copy to avoid self-destruction"));
|
|
153
|
+
}
|
|
154
|
+
|
|
230
155
|
if (!existsSync(sourceSkillsDir)) {
|
|
231
156
|
console.error(chalk.red(`\n❌ Error: Skills directory not found at ${sourceSkillsDir}`));
|
|
232
157
|
console.error(chalk.red(` Relentless root: ${relentlessRoot}`));
|
|
@@ -238,21 +163,22 @@ export async function initProject(projectDir: string = process.cwd(), force: boo
|
|
|
238
163
|
process.exit(1);
|
|
239
164
|
}
|
|
240
165
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
166
|
+
// List of skills to install (used for both Claude Code and Amp)
|
|
167
|
+
const skills = [
|
|
168
|
+
"prd",
|
|
169
|
+
"relentless",
|
|
170
|
+
"constitution",
|
|
171
|
+
"specify",
|
|
172
|
+
"plan",
|
|
173
|
+
"tasks",
|
|
174
|
+
"checklist",
|
|
175
|
+
"clarify",
|
|
176
|
+
"analyze",
|
|
177
|
+
"implement",
|
|
178
|
+
"taskstoissues",
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
if (existsSync(sourceSkillsDir) && !isRelentlessProject) {
|
|
256
182
|
for (const skill of skills) {
|
|
257
183
|
const sourcePath = join(sourceSkillsDir, skill);
|
|
258
184
|
const destPath = join(skillsDir, skill);
|
|
@@ -283,6 +209,158 @@ export async function initProject(projectDir: string = process.cwd(), force: boo
|
|
|
283
209
|
}
|
|
284
210
|
}
|
|
285
211
|
|
|
212
|
+
// Copy skills to .amp/skills/ if Amp is installed
|
|
213
|
+
const ampInstalled = installed.some((a) => a.name === "amp");
|
|
214
|
+
if (ampInstalled) {
|
|
215
|
+
console.log(chalk.dim("\nInstalling skills for Amp..."));
|
|
216
|
+
const ampSkillsDir = join(projectDir, ".amp", "skills");
|
|
217
|
+
if (!existsSync(ampSkillsDir)) {
|
|
218
|
+
mkdirSync(ampSkillsDir, { recursive: true });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (const skill of skills) {
|
|
222
|
+
const sourcePath = join(sourceSkillsDir, skill);
|
|
223
|
+
const destPath = join(ampSkillsDir, skill);
|
|
224
|
+
|
|
225
|
+
if (!existsSync(sourcePath)) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (existsSync(destPath) && !force) {
|
|
230
|
+
console.log(` ${chalk.yellow("⚠")} .amp/skills/${skill} already exists, skipping`);
|
|
231
|
+
} else {
|
|
232
|
+
try {
|
|
233
|
+
if (existsSync(destPath) && force) {
|
|
234
|
+
await Bun.spawn(["rm", "-rf", destPath]).exited;
|
|
235
|
+
}
|
|
236
|
+
const result = await Bun.spawn(["cp", "-r", sourcePath, destPath]).exited;
|
|
237
|
+
if (result !== 0) {
|
|
238
|
+
console.log(` ${chalk.red("✗")} .amp/skills/${skill} - copy failed`);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const action = force ? "updated" : "created";
|
|
242
|
+
console.log(` ${chalk.green("✓")} .amp/skills/${skill} (${action})`);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.log(` ${chalk.red("✗")} .amp/skills/${skill} - error: ${error}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Copy skills to .opencode/skill/ if OpenCode is installed (SINGULAR!)
|
|
251
|
+
const opencodeInstalled = installed.some((a) => a.name === "opencode");
|
|
252
|
+
if (opencodeInstalled) {
|
|
253
|
+
console.log(chalk.dim("\nInstalling skills for OpenCode..."));
|
|
254
|
+
const opencodeSkillsDir = join(projectDir, ".opencode", "skill");
|
|
255
|
+
if (!existsSync(opencodeSkillsDir)) {
|
|
256
|
+
mkdirSync(opencodeSkillsDir, { recursive: true });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (const skill of skills) {
|
|
260
|
+
const sourcePath = join(sourceSkillsDir, skill);
|
|
261
|
+
const destPath = join(opencodeSkillsDir, skill);
|
|
262
|
+
|
|
263
|
+
if (!existsSync(sourcePath)) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (existsSync(destPath) && !force) {
|
|
268
|
+
console.log(` ${chalk.yellow("⚠")} .opencode/skill/${skill} already exists, skipping`);
|
|
269
|
+
} else {
|
|
270
|
+
try {
|
|
271
|
+
if (existsSync(destPath) && force) {
|
|
272
|
+
await Bun.spawn(["rm", "-rf", destPath]).exited;
|
|
273
|
+
}
|
|
274
|
+
const result = await Bun.spawn(["cp", "-r", sourcePath, destPath]).exited;
|
|
275
|
+
if (result !== 0) {
|
|
276
|
+
console.log(` ${chalk.red("✗")} .opencode/skill/${skill} - copy failed`);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const action = force ? "updated" : "created";
|
|
280
|
+
console.log(` ${chalk.green("✓")} .opencode/skill/${skill} (${action})`);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
console.log(` ${chalk.red("✗")} .opencode/skill/${skill} - error: ${error}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Copy skills to .codex/skills/ if Codex is installed
|
|
289
|
+
const codexInstalled = installed.some((a) => a.name === "codex");
|
|
290
|
+
if (codexInstalled) {
|
|
291
|
+
console.log(chalk.dim("\nInstalling skills for Codex..."));
|
|
292
|
+
const codexSkillsDir = join(projectDir, ".codex", "skills");
|
|
293
|
+
if (!existsSync(codexSkillsDir)) {
|
|
294
|
+
mkdirSync(codexSkillsDir, { recursive: true });
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
for (const skill of skills) {
|
|
298
|
+
const sourcePath = join(sourceSkillsDir, skill);
|
|
299
|
+
const destPath = join(codexSkillsDir, skill);
|
|
300
|
+
|
|
301
|
+
if (!existsSync(sourcePath)) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (existsSync(destPath) && !force) {
|
|
306
|
+
console.log(` ${chalk.yellow("⚠")} .codex/skills/${skill} already exists, skipping`);
|
|
307
|
+
} else {
|
|
308
|
+
try {
|
|
309
|
+
if (existsSync(destPath) && force) {
|
|
310
|
+
await Bun.spawn(["rm", "-rf", destPath]).exited;
|
|
311
|
+
}
|
|
312
|
+
const result = await Bun.spawn(["cp", "-r", sourcePath, destPath]).exited;
|
|
313
|
+
if (result !== 0) {
|
|
314
|
+
console.log(` ${chalk.red("✗")} .codex/skills/${skill} - copy failed`);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const action = force ? "updated" : "created";
|
|
318
|
+
console.log(` ${chalk.green("✓")} .codex/skills/${skill} (${action})`);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
console.log(` ${chalk.red("✗")} .codex/skills/${skill} - error: ${error}`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Copy skills to .factory/skills/ if Droid (Factory) is installed
|
|
327
|
+
const droidInstalled = installed.some((a) => a.name === "droid");
|
|
328
|
+
if (droidInstalled) {
|
|
329
|
+
console.log(chalk.dim("\nInstalling skills for Droid (Factory)..."));
|
|
330
|
+
const factorySkillsDir = join(projectDir, ".factory", "skills");
|
|
331
|
+
if (!existsSync(factorySkillsDir)) {
|
|
332
|
+
mkdirSync(factorySkillsDir, { recursive: true });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
for (const skill of skills) {
|
|
336
|
+
const sourcePath = join(sourceSkillsDir, skill);
|
|
337
|
+
const destPath = join(factorySkillsDir, skill);
|
|
338
|
+
|
|
339
|
+
if (!existsSync(sourcePath)) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (existsSync(destPath) && !force) {
|
|
344
|
+
console.log(` ${chalk.yellow("⚠")} .factory/skills/${skill} already exists, skipping`);
|
|
345
|
+
} else {
|
|
346
|
+
try {
|
|
347
|
+
if (existsSync(destPath) && force) {
|
|
348
|
+
await Bun.spawn(["rm", "-rf", destPath]).exited;
|
|
349
|
+
}
|
|
350
|
+
const result = await Bun.spawn(["cp", "-r", sourcePath, destPath]).exited;
|
|
351
|
+
if (result !== 0) {
|
|
352
|
+
console.log(` ${chalk.red("✗")} .factory/skills/${skill} - copy failed`);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const action = force ? "updated" : "created";
|
|
356
|
+
console.log(` ${chalk.green("✓")} .factory/skills/${skill} (${action})`);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
console.log(` ${chalk.red("✗")} .factory/skills/${skill} - error: ${error}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
286
364
|
// Copy commands to .claude/commands/ (for Claude Code)
|
|
287
365
|
console.log(chalk.dim("\nInstalling commands..."));
|
|
288
366
|
const commandsDir = join(projectDir, ".claude", "commands");
|
|
@@ -292,19 +370,26 @@ export async function initProject(projectDir: string = process.cwd(), force: boo
|
|
|
292
370
|
|
|
293
371
|
const sourceCommandsDir = join(relentlessRoot, ".claude", "commands");
|
|
294
372
|
|
|
295
|
-
if (
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
"relentless.constitution.md",
|
|
301
|
-
"relentless.implement.md",
|
|
302
|
-
"relentless.plan.md",
|
|
303
|
-
"relentless.specify.md",
|
|
304
|
-
"relentless.tasks.md",
|
|
305
|
-
"relentless.taskstoissues.md",
|
|
306
|
-
];
|
|
373
|
+
// Check if we're running in the relentless project itself (source == destination)
|
|
374
|
+
const isRelentlessProjectCommands = commandsDir === sourceCommandsDir;
|
|
375
|
+
if (isRelentlessProjectCommands) {
|
|
376
|
+
console.log(chalk.yellow(" ⚠ Running in Relentless project itself - skipping command copy to avoid self-destruction"));
|
|
377
|
+
}
|
|
307
378
|
|
|
379
|
+
// List of commands to install (used for Claude, OpenCode, Factory, and Codex)
|
|
380
|
+
const commands = [
|
|
381
|
+
"relentless.analyze.md",
|
|
382
|
+
"relentless.checklist.md",
|
|
383
|
+
"relentless.clarify.md",
|
|
384
|
+
"relentless.constitution.md",
|
|
385
|
+
"relentless.implement.md",
|
|
386
|
+
"relentless.plan.md",
|
|
387
|
+
"relentless.specify.md",
|
|
388
|
+
"relentless.tasks.md",
|
|
389
|
+
"relentless.taskstoissues.md",
|
|
390
|
+
];
|
|
391
|
+
|
|
392
|
+
if (existsSync(sourceCommandsDir) && !isRelentlessProjectCommands) {
|
|
308
393
|
for (const command of commands) {
|
|
309
394
|
const sourcePath = join(sourceCommandsDir, command);
|
|
310
395
|
const destPath = join(commandsDir, command);
|
|
@@ -322,21 +407,162 @@ export async function initProject(projectDir: string = process.cwd(), force: boo
|
|
|
322
407
|
}
|
|
323
408
|
}
|
|
324
409
|
|
|
410
|
+
// Copy commands to .opencode/command/ if OpenCode is installed (SINGULAR!)
|
|
411
|
+
if (opencodeInstalled && existsSync(sourceCommandsDir)) {
|
|
412
|
+
console.log(chalk.dim("\nInstalling commands for OpenCode..."));
|
|
413
|
+
const opencodeCommandsDir = join(projectDir, ".opencode", "command");
|
|
414
|
+
if (!existsSync(opencodeCommandsDir)) {
|
|
415
|
+
mkdirSync(opencodeCommandsDir, { recursive: true });
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
for (const command of commands) {
|
|
419
|
+
const sourcePath = join(sourceCommandsDir, command);
|
|
420
|
+
const destPath = join(opencodeCommandsDir, command);
|
|
421
|
+
|
|
422
|
+
if (existsSync(sourcePath)) {
|
|
423
|
+
if (existsSync(destPath) && !force) {
|
|
424
|
+
console.log(` ${chalk.yellow("⚠")} .opencode/command/${command} already exists, skipping`);
|
|
425
|
+
} else {
|
|
426
|
+
const content = await Bun.file(sourcePath).text();
|
|
427
|
+
await Bun.write(destPath, content);
|
|
428
|
+
const action = existsSync(destPath) && force ? "updated" : "created";
|
|
429
|
+
console.log(` ${chalk.green("✓")} .opencode/command/${command} (${action})`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Copy commands to .factory/commands/ if Droid (Factory) is installed
|
|
436
|
+
if (droidInstalled && existsSync(sourceCommandsDir)) {
|
|
437
|
+
console.log(chalk.dim("\nInstalling commands for Droid (Factory)..."));
|
|
438
|
+
const factoryCommandsDir = join(projectDir, ".factory", "commands");
|
|
439
|
+
if (!existsSync(factoryCommandsDir)) {
|
|
440
|
+
mkdirSync(factoryCommandsDir, { recursive: true });
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
for (const command of commands) {
|
|
444
|
+
const sourcePath = join(sourceCommandsDir, command);
|
|
445
|
+
const destPath = join(factoryCommandsDir, command);
|
|
446
|
+
|
|
447
|
+
if (existsSync(sourcePath)) {
|
|
448
|
+
if (existsSync(destPath) && !force) {
|
|
449
|
+
console.log(` ${chalk.yellow("⚠")} .factory/commands/${command} already exists, skipping`);
|
|
450
|
+
} else {
|
|
451
|
+
const content = await Bun.file(sourcePath).text();
|
|
452
|
+
await Bun.write(destPath, content);
|
|
453
|
+
const action = existsSync(destPath) && force ? "updated" : "created";
|
|
454
|
+
console.log(` ${chalk.green("✓")} .factory/commands/${command} (${action})`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Copy prompts to ~/.codex/prompts/ if Codex is installed (user-level only)
|
|
461
|
+
if (codexInstalled && existsSync(sourceCommandsDir)) {
|
|
462
|
+
console.log(chalk.dim("\nInstalling prompts for Codex (user-level)..."));
|
|
463
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
464
|
+
const codexPromptsDir = join(homeDir, ".codex", "prompts");
|
|
465
|
+
if (!existsSync(codexPromptsDir)) {
|
|
466
|
+
mkdirSync(codexPromptsDir, { recursive: true });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
for (const command of commands) {
|
|
470
|
+
const sourcePath = join(sourceCommandsDir, command);
|
|
471
|
+
// Codex prompts are invoked as /prompts:name, so we keep the same filename
|
|
472
|
+
const destPath = join(codexPromptsDir, command);
|
|
473
|
+
|
|
474
|
+
if (existsSync(sourcePath)) {
|
|
475
|
+
if (existsSync(destPath) && !force) {
|
|
476
|
+
console.log(` ${chalk.yellow("⚠")} ~/.codex/prompts/${command} already exists, skipping`);
|
|
477
|
+
} else {
|
|
478
|
+
const content = await Bun.file(sourcePath).text();
|
|
479
|
+
await Bun.write(destPath, content);
|
|
480
|
+
const action = existsSync(destPath) && force ? "updated" : "created";
|
|
481
|
+
console.log(` ${chalk.green("✓")} ~/.codex/prompts/${command} (${action})`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Create .gemini/GEMINI.md context file if Gemini is installed
|
|
488
|
+
const geminiInstalled = installed.some((a) => a.name === "gemini");
|
|
489
|
+
if (geminiInstalled) {
|
|
490
|
+
console.log(chalk.dim("\nInstalling context for Gemini..."));
|
|
491
|
+
const geminiDir = join(projectDir, ".gemini");
|
|
492
|
+
if (!existsSync(geminiDir)) {
|
|
493
|
+
mkdirSync(geminiDir, { recursive: true });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const geminiContextPath = join(geminiDir, "GEMINI.md");
|
|
497
|
+
const geminiContextContent = `# Relentless - Universal AI Agent Orchestrator
|
|
498
|
+
|
|
499
|
+
This project uses Relentless for feature-driven development with AI agents.
|
|
500
|
+
|
|
501
|
+
## Available Skills
|
|
502
|
+
|
|
503
|
+
The following skills are available in \`.claude/skills/\`:
|
|
504
|
+
|
|
505
|
+
- **prd** - Generate Product Requirements Documents
|
|
506
|
+
- **constitution** - Create project governance and coding principles
|
|
507
|
+
- **specify** - Create feature specifications
|
|
508
|
+
- **plan** - Generate technical implementation plans
|
|
509
|
+
- **tasks** - Generate user stories and tasks
|
|
510
|
+
- **checklist** - Generate quality validation checklists
|
|
511
|
+
- **clarify** - Resolve ambiguities in specifications
|
|
512
|
+
- **analyze** - Analyze consistency across artifacts
|
|
513
|
+
- **implement** - Execute implementation workflows
|
|
514
|
+
- **taskstoissues** - Convert user stories to GitHub issues
|
|
515
|
+
|
|
516
|
+
## Workflow
|
|
517
|
+
|
|
518
|
+
1. Run \`/relentless.constitution\` to create project governance
|
|
519
|
+
2. Run \`/relentless.specify "feature description"\` to create a feature spec
|
|
520
|
+
3. Run \`/relentless.plan\` to generate technical plan
|
|
521
|
+
4. Run \`/relentless.tasks\` to generate user stories
|
|
522
|
+
5. Run \`/relentless.checklist\` to generate quality checklist
|
|
523
|
+
|
|
524
|
+
## Feature Directory Structure
|
|
525
|
+
|
|
526
|
+
\`\`\`
|
|
527
|
+
relentless/features/<feature-name>/
|
|
528
|
+
├── spec.md # Feature specification
|
|
529
|
+
├── plan.md # Technical plan
|
|
530
|
+
├── tasks.md # User stories
|
|
531
|
+
├── checklist.md # Quality checklist
|
|
532
|
+
├── prd.json # PRD JSON (for orchestrator)
|
|
533
|
+
└── progress.txt # Progress log
|
|
534
|
+
\`\`\`
|
|
535
|
+
|
|
536
|
+
For full documentation, see: https://github.com/ArvorCo/Relentless
|
|
537
|
+
`;
|
|
538
|
+
|
|
539
|
+
if (existsSync(geminiContextPath) && !force) {
|
|
540
|
+
console.log(` ${chalk.yellow("⚠")} .gemini/GEMINI.md already exists, skipping`);
|
|
541
|
+
} else {
|
|
542
|
+
await Bun.write(geminiContextPath, geminiContextContent);
|
|
543
|
+
const action = existsSync(geminiContextPath) && force ? "updated" : "created";
|
|
544
|
+
console.log(` ${chalk.green("✓")} .gemini/GEMINI.md (${action})`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
325
548
|
// Print next steps
|
|
326
549
|
console.log(chalk.bold.green("\n✅ Relentless initialized!\n"));
|
|
327
550
|
console.log(chalk.dim("Structure:"));
|
|
328
551
|
console.log(chalk.dim(" relentless/"));
|
|
329
552
|
console.log(chalk.dim(" ├── config.json # Configuration"));
|
|
330
|
-
console.log(chalk.dim(" ├── constitution.md # Project governance"));
|
|
331
|
-
console.log(chalk.dim(" ├── prompt.md #
|
|
553
|
+
console.log(chalk.dim(" ├── constitution.md # Project governance (run /relentless.constitution)"));
|
|
554
|
+
console.log(chalk.dim(" ├── prompt.md # Agent instructions (run /relentless.constitution)"));
|
|
332
555
|
console.log(chalk.dim(" └── features/ # Feature folders"));
|
|
333
556
|
console.log(chalk.dim(" └── <feature>/ # Each feature has:"));
|
|
334
|
-
console.log(chalk.dim(" ├──
|
|
335
|
-
console.log(chalk.dim(" ├──
|
|
557
|
+
console.log(chalk.dim(" ├── spec.md # Feature specification"));
|
|
558
|
+
console.log(chalk.dim(" ├── plan.md # Technical plan"));
|
|
559
|
+
console.log(chalk.dim(" ├── tasks.md # User stories"));
|
|
560
|
+
console.log(chalk.dim(" ├── checklist.md # Quality checklist"));
|
|
561
|
+
console.log(chalk.dim(" ├── prd.json # PRD JSON (for orchestrator)"));
|
|
336
562
|
console.log(chalk.dim(" └── progress.txt # Progress log\n"));
|
|
337
563
|
|
|
338
564
|
console.log("Next steps:");
|
|
339
|
-
console.log(chalk.dim("1. Create project constitution (
|
|
565
|
+
console.log(chalk.dim("1. Create project constitution and prompt (required):"));
|
|
340
566
|
console.log(` ${chalk.cyan("/relentless.constitution")}`);
|
|
341
567
|
console.log(chalk.dim("\n2. Create a feature specification:"));
|
|
342
568
|
console.log(` ${chalk.cyan("/relentless.specify Add user authentication")}`);
|