@arvorco/relentless 0.5.3 → 0.6.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/CHANGELOG.md CHANGED
@@ -7,7 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.0](https://github.com/ArvorCo/Relentless/releases/tag/v0.6.0) - 2026-01-23
11
+
12
+ ### Major Features
13
+
14
+ #### Claude Code Tasks Integration - Cross-Session Coordination (#8)
15
+ - **TaskList-Per-Feature**: Each feature gets its own TaskList ID (e.g., `relentless-auth`)
16
+ - **Bidirectional Sync**: Convert PRD user stories ↔ Claude Tasks
17
+ - **Cross-session sharing**: Multiple Claude instances coordinate via `CLAUDE_CODE_TASK_LIST_ID` env var
18
+ - **New `--tasks` flag**: Enable Tasks integration with `relentless run --feature auth --tasks`
19
+
20
+ #### New Tasks Module (`src/tasks/`)
21
+ - `types.ts` - Task types and Zod schemas (`ClaudeTask`, `TaskList`, `SyncResult`, `ImportResult`)
22
+ - `tasklist.ts` - TaskList CRUD operations (create, load, save, delete, addTask, updateTask, etc.)
23
+ - `sync.ts` - Bidirectional sync: `syncPrdToTasks()`, `syncTasksToPrd()`, `importTasksToPrd()`, `bidirectionalSync()`
24
+ - `index.ts` - Module exports
25
+
26
+ #### New CLI Commands
27
+ ```bash
28
+ # Run with Tasks integration
29
+ relentless run --feature auth --tasks
30
+
31
+ # Task management
32
+ relentless tasks list -f <feature> # List tasks for a feature
33
+ relentless tasks sync -f <feature> # Sync PRD ↔ Claude Tasks
34
+ relentless tasks import <id> -f <feature> # Import Claude Tasks to PRD
35
+ relentless tasks clear -f <feature> # Clear TaskList
36
+ ```
37
+
38
+ ### Added
39
+ - `taskListId` option in `InvokeOptions` for agent adapters
40
+ - `env` option in `RunCommandOptions` for passing environment variables
41
+ - 38 new tests for Tasks module (types + tasklist CRUD)
42
+
10
43
  ### Fixed
44
+ - **Rate Limit Detection**: Fixed false positive when Claude mentions "model" and "not_found_error" in conversation
45
+ - Previous pattern was too broad and matched normal Claude output
46
+ - Now uses specific pattern for actual API error responses
11
47
  - **Commands**: Use abstract `[skills_path]` pattern for cross-agent compatibility (#7)
12
48
  - Commands now reference `[skills_path]/skillname/SKILL.md` instead of hardcoded `.claude/skills/`
13
49
  - Each agent can resolve the path to their own skills directory
@@ -474,7 +510,9 @@ Relentless evolved from the [Ralph Wiggum Pattern](https://ghuntley.com/ralph/)
474
510
  - **License**: MIT
475
511
  - **Inspiration**: [Ralph Wiggum Pattern](https://ghuntley.com/ralph/) by Geoffrey Huntley
476
512
 
477
- [Unreleased]: https://github.com/ArvorCo/Relentless/compare/v0.5.2...HEAD
513
+ [Unreleased]: https://github.com/ArvorCo/Relentless/compare/v0.6.0...HEAD
514
+ [0.6.0]: https://github.com/ArvorCo/Relentless/compare/v0.5.3...v0.6.0
515
+ [0.5.3]: https://github.com/ArvorCo/Relentless/compare/v0.5.2...v0.5.3
478
516
  [0.5.2]: https://github.com/ArvorCo/Relentless/compare/v0.5.1...v0.5.2
479
517
  [0.5.1]: https://github.com/ArvorCo/Relentless/compare/v0.5.0...v0.5.1
480
518
  [0.5.0]: https://github.com/ArvorCo/Relentless/compare/v0.4.5...v0.5.0
package/bin/relentless.ts CHANGED
@@ -64,6 +64,7 @@ program
64
64
  .option("--review-mode <mode>", `Review quality mode (${VALID_REVIEW_MODES.join(", ")})`)
65
65
  .option("--dry-run", "Show what would be executed without running", false)
66
66
  .option("--tui", "Use beautiful terminal UI interface", false)
67
+ .option("--tasks", "Enable Claude Code Tasks integration for cross-session coordination", false)
67
68
  .option("-d, --dir <path>", "Working directory", process.cwd())
68
69
  .action(async (options) => {
69
70
  // Load config first to get defaultAgent
@@ -200,6 +201,8 @@ program
200
201
  fallbackOrder,
201
202
  skipReview,
202
203
  reviewMode,
204
+ enableTasks: options.tasks,
205
+ featureName: options.feature,
203
206
  });
204
207
 
205
208
  if (result.success) {
@@ -747,6 +750,167 @@ queue
747
750
  }
748
751
  });
749
752
 
753
+ // Tasks commands - Claude Code Tasks integration
754
+ const tasks = program.command("tasks").description("Manage Claude Code Tasks for cross-session coordination");
755
+
756
+ tasks
757
+ .command("list")
758
+ .description("List tasks for a feature")
759
+ .requiredOption("-f, --feature <name>", "Feature name")
760
+ .option("-d, --dir <path>", "Project directory", process.cwd())
761
+ .action(async (options) => {
762
+ const { generateTaskListId, loadTaskList, getTaskListSummary } = await import("../src/tasks");
763
+
764
+ const taskListId = generateTaskListId(options.feature);
765
+ const taskList = await loadTaskList(taskListId);
766
+
767
+ if (!taskList) {
768
+ console.log(chalk.dim(`\nNo task list found for feature: ${options.feature}`));
769
+ console.log(chalk.dim(`Run 'relentless run --feature ${options.feature} --tasks' to create one.\n`));
770
+ return;
771
+ }
772
+
773
+ const summary = await getTaskListSummary(taskListId);
774
+ if (!summary) {
775
+ console.log(chalk.dim(`\nNo task list found for feature: ${options.feature}\n`));
776
+ return;
777
+ }
778
+
779
+ console.log(chalk.bold(`\n📋 Tasks: ${options.feature}\n`));
780
+ console.log(chalk.dim(`TaskList ID: ${taskListId}`));
781
+ console.log(chalk.dim(`Progress: ${summary.completedTasks}/${summary.totalTasks} complete\n`));
782
+
783
+ for (const task of taskList.tasks) {
784
+ const status = task.status === "completed"
785
+ ? chalk.green("✓")
786
+ : task.status === "in_progress"
787
+ ? chalk.yellow("◐")
788
+ : chalk.dim("○");
789
+ const content = task.status === "completed"
790
+ ? chalk.strikethrough(chalk.dim(task.content))
791
+ : task.content;
792
+ console.log(` ${status} ${content}`);
793
+ }
794
+ console.log("");
795
+ });
796
+
797
+ tasks
798
+ .command("sync")
799
+ .description("Sync PRD stories with Claude Tasks")
800
+ .requiredOption("-f, --feature <name>", "Feature name")
801
+ .option("-d, --dir <path>", "Project directory", process.cwd())
802
+ .action(async (options) => {
803
+ const { bidirectionalSync } = await import("../src/tasks");
804
+
805
+ const relentlessDir = findRelentlessDir(options.dir);
806
+ if (!relentlessDir) {
807
+ console.error(chalk.red("Relentless not initialized. Run: relentless init"));
808
+ process.exit(1);
809
+ }
810
+
811
+ const featureDir = join(relentlessDir, "features", options.feature);
812
+ const prdPath = join(featureDir, "prd.json");
813
+
814
+ if (!existsSync(prdPath)) {
815
+ console.error(chalk.red(`Feature '${options.feature}' not found or has no prd.json`));
816
+ console.log(chalk.dim(`Available features: ${listFeatures(options.dir).join(", ") || "none"}`));
817
+ process.exit(1);
818
+ }
819
+
820
+ console.log(chalk.dim(`Syncing tasks for ${options.feature}...`));
821
+
822
+ const result = await bidirectionalSync(prdPath, options.feature);
823
+
824
+ if (result.success) {
825
+ console.log(chalk.green(`\n✓ Sync complete`));
826
+ if (result.added > 0) {
827
+ console.log(chalk.dim(` Added: ${result.added} tasks`));
828
+ }
829
+ if (result.updated > 0) {
830
+ console.log(chalk.dim(` Updated: ${result.updated} tasks`));
831
+ }
832
+ if (result.removed > 0) {
833
+ console.log(chalk.dim(` Removed: ${result.removed} tasks`));
834
+ }
835
+ } else {
836
+ console.error(chalk.red(`\n❌ Sync failed`));
837
+ for (const error of result.errors) {
838
+ console.error(chalk.red(` ${error}`));
839
+ }
840
+ process.exit(1);
841
+ }
842
+
843
+ for (const warning of result.warnings) {
844
+ console.log(chalk.yellow(` ⚠️ ${warning}`));
845
+ }
846
+ console.log("");
847
+ });
848
+
849
+ tasks
850
+ .command("import <taskListId>")
851
+ .description("Import Claude Tasks to create a new PRD")
852
+ .requiredOption("-f, --feature <name>", "Feature name for the new PRD")
853
+ .option("-d, --dir <path>", "Project directory", process.cwd())
854
+ .action(async (taskListId, options) => {
855
+ const { importTasksToPrd } = await import("../src/tasks");
856
+
857
+ const relentlessDir = findRelentlessDir(options.dir);
858
+ if (!relentlessDir) {
859
+ console.error(chalk.red("Relentless not initialized. Run: relentless init"));
860
+ process.exit(1);
861
+ }
862
+
863
+ // Create feature directory if it doesn't exist
864
+ const featureDir = join(relentlessDir, "features", options.feature);
865
+ if (!existsSync(featureDir)) {
866
+ mkdirSync(featureDir, { recursive: true });
867
+ }
868
+
869
+ console.log(chalk.dim(`Importing tasks from ${taskListId}...`));
870
+
871
+ const result = await importTasksToPrd(taskListId, options.feature, featureDir);
872
+
873
+ if (result.success) {
874
+ console.log(chalk.green(`\n✓ Import complete`));
875
+ console.log(chalk.dim(` Created ${result.storiesCreated} stories`));
876
+ console.log(chalk.dim(` PRD saved to: ${result.prdPath}`));
877
+ console.log(chalk.dim(`\nNext step:`));
878
+ console.log(chalk.cyan(` relentless run --feature ${options.feature} --tasks`));
879
+ } else {
880
+ console.error(chalk.red(`\n❌ Import failed`));
881
+ for (const error of result.errors) {
882
+ console.error(chalk.red(` ${error}`));
883
+ }
884
+ process.exit(1);
885
+ }
886
+ console.log("");
887
+ });
888
+
889
+ tasks
890
+ .command("clear")
891
+ .description("Clear all tasks for a feature")
892
+ .requiredOption("-f, --feature <name>", "Feature name")
893
+ .option("-d, --dir <path>", "Project directory", process.cwd())
894
+ .action(async (options) => {
895
+ const { generateTaskListId, clearTaskList, taskListExists } = await import("../src/tasks");
896
+
897
+ const taskListId = generateTaskListId(options.feature);
898
+
899
+ if (!await taskListExists(taskListId)) {
900
+ console.log(chalk.dim(`\nNo task list found for feature: ${options.feature}\n`));
901
+ return;
902
+ }
903
+
904
+ const cleared = await clearTaskList(taskListId);
905
+
906
+ if (cleared) {
907
+ console.log(chalk.green(`\n✓ Cleared all tasks for ${options.feature}\n`));
908
+ } else {
909
+ console.error(chalk.red(`\n❌ Failed to clear tasks\n`));
910
+ process.exit(1);
911
+ }
912
+ });
913
+
750
914
  // Analyze command
751
915
  program
752
916
  .command("analyze")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arvorco/relentless",
3
- "version": "0.5.3",
3
+ "version": "0.6.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": {
@@ -79,10 +79,17 @@ export const claudeAdapter: AgentAdapter = {
79
79
  }
80
80
  }
81
81
 
82
+ // Build environment variables
83
+ const env: Record<string, string> = {};
84
+ if (options?.taskListId) {
85
+ env.CLAUDE_CODE_TASK_LIST_ID = options.taskListId;
86
+ }
87
+
82
88
  const result = await runCommand(["claude", ...args], {
83
89
  cwd: options?.workingDirectory,
84
90
  stdin: new Blob([prompt]),
85
91
  timeoutMs: options?.timeout,
92
+ env: Object.keys(env).length > 0 ? env : undefined,
86
93
  });
87
94
 
88
95
  const timeoutNote =
@@ -120,11 +127,20 @@ export const claudeAdapter: AgentAdapter = {
120
127
  }
121
128
  }
122
129
 
130
+ // Build environment variables for streaming
131
+ const streamEnv: Record<string, string> = {};
132
+ if (options?.taskListId) {
133
+ streamEnv.CLAUDE_CODE_TASK_LIST_ID = options.taskListId;
134
+ }
135
+
123
136
  const proc = Bun.spawn(["claude", ...args], {
124
137
  cwd: options?.workingDirectory,
125
138
  stdin: new Blob([prompt]),
126
139
  stdout: "pipe",
127
140
  stderr: "pipe",
141
+ env: Object.keys(streamEnv).length > 0
142
+ ? { ...process.env, ...streamEnv }
143
+ : undefined,
128
144
  });
129
145
 
130
146
  const decoder = new TextDecoder();
@@ -178,7 +194,9 @@ export const claudeAdapter: AgentAdapter = {
178
194
  };
179
195
  }
180
196
 
181
- if (/not_found_error/i.test(output) && /model/i.test(output)) {
197
+ // More specific pattern for actual API model not found errors
198
+ // Avoid matching if Claude just mentions "model" and "not_found_error" in conversation
199
+ if (/error.*model.*not[_\s]?found|model.*not[_\s]?found.*error|"type":\s*"not_found_error"/i.test(output)) {
182
200
  return {
183
201
  limited: true,
184
202
  message: "Claude model not found",
@@ -6,6 +6,8 @@ export interface RunCommandOptions {
6
6
  cwd?: string;
7
7
  stdin?: Blob;
8
8
  timeoutMs?: number;
9
+ /** Environment variables to pass to the command */
10
+ env?: Record<string, string>;
9
11
  }
10
12
 
11
13
  export interface RunCommandResult {
@@ -48,11 +50,18 @@ export async function runCommand(
48
50
  options: RunCommandOptions = {}
49
51
  ): Promise<RunCommandResult> {
50
52
  const startTime = Date.now();
53
+
54
+ // Merge custom env vars with current process env
55
+ const env = options.env
56
+ ? { ...process.env, ...options.env }
57
+ : undefined;
58
+
51
59
  const proc = Bun.spawn(command, {
52
60
  cwd: options.cwd,
53
61
  stdin: options.stdin,
54
62
  stdout: "pipe",
55
63
  stderr: "pipe",
64
+ env,
56
65
  });
57
66
 
58
67
  let lastOutput = Date.now();
@@ -16,6 +16,8 @@ export interface InvokeOptions {
16
16
  model?: string;
17
17
  /** Skip all permission prompts */
18
18
  dangerouslyAllowAll?: boolean;
19
+ /** Claude Code TaskList ID for cross-session coordination */
20
+ taskListId?: string;
19
21
  }
20
22
 
21
23
  /**
@@ -42,6 +42,7 @@ import {
42
42
  logSkipRejectedToProgress,
43
43
  formatSkipMessage,
44
44
  } from "./commands";
45
+ import { generateTaskListId, syncPrdToTasks, syncTasksToPrd } from "../tasks";
45
46
 
46
47
  export interface RunOptions {
47
48
  /** Agent to use (or "auto" for smart routing) */
@@ -66,6 +67,10 @@ export interface RunOptions {
66
67
  skipReview?: boolean;
67
68
  /** Review quality mode (can differ from execution mode) */
68
69
  reviewMode?: "free" | "cheap" | "good" | "genius";
70
+ /** Enable Claude Code Tasks integration for cross-session coordination */
71
+ enableTasks?: boolean;
72
+ /** Feature name (for TaskList ID generation) */
73
+ featureName?: string;
69
74
  }
70
75
 
71
76
  export interface RunResult {
@@ -479,6 +484,23 @@ export async function run(options: RunOptions): Promise<RunResult> {
479
484
  console.log(chalk.yellow("\n⚠️ Dry run mode - not executing\n"));
480
485
  }
481
486
 
487
+ // Generate TaskList ID if tasks integration is enabled
488
+ let taskListId: string | undefined;
489
+ if (options.enableTasks && options.featureName) {
490
+ taskListId = generateTaskListId(options.featureName);
491
+ console.log(`Tasks: ${chalk.green("enabled")} (${taskListId})`);
492
+
493
+ // Sync PRD to tasks at start
494
+ try {
495
+ const syncResult = await syncPrdToTasks(prd, options.featureName);
496
+ if (syncResult.added > 0 || syncResult.updated > 0) {
497
+ console.log(chalk.dim(` Synced ${syncResult.added} new, ${syncResult.updated} updated tasks`));
498
+ }
499
+ } catch (syncError) {
500
+ console.warn(chalk.yellow(` ⚠️ Failed to sync PRD to tasks: ${syncError}`));
501
+ }
502
+ }
503
+
482
504
  // Main loop
483
505
  for (let i = 1; i <= options.maxIterations; i++) {
484
506
  iterations = i;
@@ -737,6 +759,7 @@ export async function run(options: RunOptions): Promise<RunResult> {
737
759
  dangerouslyAllowAll: options.config.agents[agent.name]?.dangerouslyAllowAll ?? true,
738
760
  model: autoModeEnabled ? autoRoutingModel : options.config.agents[agent.name]?.model,
739
761
  timeout: options.config.execution.timeout,
762
+ taskListId,
740
763
  });
741
764
 
742
765
  // Check for rate limit during research phase
@@ -798,6 +821,7 @@ export async function run(options: RunOptions): Promise<RunResult> {
798
821
  dangerouslyAllowAll: options.config.agents[agent.name]?.dangerouslyAllowAll ?? true,
799
822
  model: autoModeEnabled ? autoRoutingModel : options.config.agents[agent.name]?.model,
800
823
  timeout: options.config.execution.timeout,
824
+ taskListId,
801
825
  });
802
826
 
803
827
  // Check for rate limit
@@ -942,6 +966,18 @@ export async function run(options: RunOptions): Promise<RunResult> {
942
966
  const success = isComplete(finalPRD);
943
967
  const duration = Date.now() - startTime;
944
968
 
969
+ // Final sync tasks back to PRD
970
+ if (options.enableTasks && options.featureName) {
971
+ try {
972
+ const syncResult = await syncTasksToPrd(options.prdPath, options.featureName);
973
+ if (syncResult.updated > 0) {
974
+ console.log(chalk.dim(` Synced ${syncResult.updated} task updates back to PRD`));
975
+ }
976
+ } catch (syncError) {
977
+ console.warn(chalk.yellow(` ⚠️ Failed to sync tasks to PRD: ${syncError}`));
978
+ }
979
+ }
980
+
945
981
  if (!success) {
946
982
  console.log(chalk.yellow(`\n⚠️ Reached max iterations (${options.maxIterations}) without completing all stories.`));
947
983
  console.log(chalk.dim(`Check progress.txt for status.`));
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Claude Code Tasks Module
3
+ *
4
+ * Integration with Claude Code's native Tasks feature for cross-session
5
+ * coordination and parallel story execution.
6
+ *
7
+ * ## Usage
8
+ *
9
+ * ### Enable TaskList for a feature
10
+ * ```bash
11
+ * relentless run --feature auth --tasks
12
+ * # Sets CLAUDE_CODE_TASK_LIST_ID=relentless-auth
13
+ * ```
14
+ *
15
+ * ### Sync PRD to Tasks
16
+ * ```bash
17
+ * relentless tasks sync -f auth
18
+ * ```
19
+ *
20
+ * ### Import Claude Tasks to PRD
21
+ * ```bash
22
+ * relentless tasks import my-task-list --feature my-feature
23
+ * ```
24
+ *
25
+ * ## Key Concepts
26
+ *
27
+ * - **TaskList**: A collection of tasks stored in ~/.claude/tasks/{id}.json
28
+ * - **Task**: Individual work item with status, dependencies, and story link
29
+ * - **Sync**: Bidirectional sync between PRD stories and Claude Tasks
30
+ *
31
+ * @see https://docs.anthropic.com/claude-code
32
+ */
33
+
34
+ // Types
35
+ export {
36
+ type TaskStatus,
37
+ type ClaudeTask,
38
+ type TaskList,
39
+ type TaskListSummary,
40
+ type SyncResult,
41
+ type ImportResult,
42
+ ClaudeTaskSchema,
43
+ TaskListSchema,
44
+ generateTaskListId,
45
+ getTasksDirectory,
46
+ getTaskListPath,
47
+ } from "./types";
48
+
49
+ // TaskList CRUD operations
50
+ export {
51
+ ensureTasksDirectory,
52
+ taskListExists,
53
+ loadTaskList,
54
+ saveTaskList,
55
+ createTaskList,
56
+ deleteTaskList,
57
+ listTaskLists,
58
+ getTaskListSummary,
59
+ addTask,
60
+ updateTask,
61
+ removeTask,
62
+ markTaskComplete,
63
+ markTaskInProgress,
64
+ getNextTask,
65
+ getParallelTasks,
66
+ isTaskListComplete,
67
+ clearTaskList,
68
+ getOrCreateTaskList,
69
+ } from "./tasklist";
70
+
71
+ // PRD Sync operations
72
+ export {
73
+ storyToTask,
74
+ taskToStory,
75
+ syncPrdToTasks,
76
+ syncTasksToPrd,
77
+ importTasksToPrd,
78
+ bidirectionalSync,
79
+ } from "./sync";