@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arvorco/relentless",
3
- "version": "0.1.27",
3
+ "version": "0.2.0",
4
4
  "description": "Universal AI agent orchestrator - works with Claude Code, Amp, OpenCode, Codex, Droid, and Gemini",
5
5
  "type": "module",
6
6
  "publishConfig": {
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
- await Bun.spawn([
103
- "cp",
104
- "-r",
105
- `${relentlessRoot}/skills/prd`,
106
- `${skillsDir}/`,
107
- ]).exited;
108
- await Bun.spawn([
109
- "cp",
110
- "-r",
111
- `${relentlessRoot}/skills/relentless`,
112
- `${skillsDir}/`,
113
- ]).exited;
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
  };
@@ -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: false, // Uses SKILL.md but requires manual setup
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
  };
@@ -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: false, // No skill system, uses prompting
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
  };
@@ -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: false, // Uses agent system, different from skills
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
  };
@@ -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
- // Create protected files (NEVER overwritten, even with -f)
199
- for (const [filename, contentFn] of Object.entries(PROTECTED_FILES)) {
200
- const path = join(relentlessDir, filename);
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
- if (existsSync(sourceSkillsDir)) {
242
- const skills = [
243
- "prd",
244
- "relentless",
245
- "constitution",
246
- "specify",
247
- "plan",
248
- "tasks",
249
- "checklist",
250
- "clarify",
251
- "analyze",
252
- "implement",
253
- "taskstoissues",
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 (existsSync(sourceCommandsDir)) {
296
- const commands = [
297
- "relentless.analyze.md",
298
- "relentless.checklist.md",
299
- "relentless.clarify.md",
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 # Base prompt template"));
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(" ├── prd.md # PRD markdown"));
335
- console.log(chalk.dim(" ├── prd.json # PRD JSON"));
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 (recommended):"));
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")}`);