ralphctl 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +58 -24
  2. package/dist/add-HGJCLWED.mjs +14 -0
  3. package/dist/add-MRGCS3US.mjs +14 -0
  4. package/dist/chunk-6PYTKGB5.mjs +316 -0
  5. package/dist/chunk-7TG3EAQ2.mjs +20 -0
  6. package/dist/chunk-EKMZZRWI.mjs +521 -0
  7. package/dist/chunk-JON4GCLR.mjs +59 -0
  8. package/dist/chunk-LOR7QBXX.mjs +3683 -0
  9. package/dist/chunk-MNMQC36F.mjs +556 -0
  10. package/dist/chunk-MRKOFVTM.mjs +537 -0
  11. package/dist/chunk-NTWO2LXB.mjs +52 -0
  12. package/dist/chunk-QBXHAXHI.mjs +562 -0
  13. package/dist/chunk-WGHJI3OI.mjs +214 -0
  14. package/dist/cli.mjs +4245 -0
  15. package/dist/create-MG7E7PLQ.mjs +10 -0
  16. package/dist/handle-UG5M2OON.mjs +22 -0
  17. package/dist/multiline-OHSNFCRG.mjs +40 -0
  18. package/dist/project-NT3L4FTB.mjs +28 -0
  19. package/dist/resolver-WSFWKACM.mjs +153 -0
  20. package/dist/sprint-4VHDLGFN.mjs +37 -0
  21. package/dist/wizard-LRELAN2J.mjs +196 -0
  22. package/package.json +19 -28
  23. package/CHANGELOG.md +0 -94
  24. package/bin/ralphctl +0 -13
  25. package/src/ai/executor.ts +0 -973
  26. package/src/ai/lifecycle.ts +0 -45
  27. package/src/ai/parser.ts +0 -40
  28. package/src/ai/permissions.ts +0 -207
  29. package/src/ai/process-manager.ts +0 -248
  30. package/src/ai/prompts/index.ts +0 -89
  31. package/src/ai/rate-limiter.ts +0 -89
  32. package/src/ai/runner.ts +0 -478
  33. package/src/ai/session.ts +0 -319
  34. package/src/ai/task-context.ts +0 -270
  35. package/src/cli-metadata.ts +0 -7
  36. package/src/cli.ts +0 -65
  37. package/src/commands/completion/index.ts +0 -33
  38. package/src/commands/config/config.ts +0 -58
  39. package/src/commands/config/index.ts +0 -33
  40. package/src/commands/dashboard/dashboard.ts +0 -5
  41. package/src/commands/dashboard/index.ts +0 -6
  42. package/src/commands/doctor/doctor.ts +0 -271
  43. package/src/commands/doctor/index.ts +0 -25
  44. package/src/commands/progress/index.ts +0 -25
  45. package/src/commands/progress/log.ts +0 -64
  46. package/src/commands/progress/show.ts +0 -14
  47. package/src/commands/project/add.ts +0 -336
  48. package/src/commands/project/index.ts +0 -104
  49. package/src/commands/project/list.ts +0 -31
  50. package/src/commands/project/remove.ts +0 -43
  51. package/src/commands/project/repo.ts +0 -118
  52. package/src/commands/project/show.ts +0 -49
  53. package/src/commands/sprint/close.ts +0 -180
  54. package/src/commands/sprint/context.ts +0 -109
  55. package/src/commands/sprint/create.ts +0 -60
  56. package/src/commands/sprint/current.ts +0 -75
  57. package/src/commands/sprint/delete.ts +0 -72
  58. package/src/commands/sprint/health.ts +0 -229
  59. package/src/commands/sprint/ideate.ts +0 -496
  60. package/src/commands/sprint/index.ts +0 -226
  61. package/src/commands/sprint/list.ts +0 -86
  62. package/src/commands/sprint/plan-utils.ts +0 -207
  63. package/src/commands/sprint/plan.ts +0 -549
  64. package/src/commands/sprint/refine.ts +0 -359
  65. package/src/commands/sprint/requirements.ts +0 -58
  66. package/src/commands/sprint/show.ts +0 -140
  67. package/src/commands/sprint/start.ts +0 -119
  68. package/src/commands/sprint/switch.ts +0 -20
  69. package/src/commands/task/add.ts +0 -316
  70. package/src/commands/task/import.ts +0 -150
  71. package/src/commands/task/index.ts +0 -123
  72. package/src/commands/task/list.ts +0 -145
  73. package/src/commands/task/next.ts +0 -45
  74. package/src/commands/task/remove.ts +0 -47
  75. package/src/commands/task/reorder.ts +0 -45
  76. package/src/commands/task/show.ts +0 -111
  77. package/src/commands/task/status.ts +0 -99
  78. package/src/commands/ticket/add.ts +0 -265
  79. package/src/commands/ticket/edit.ts +0 -166
  80. package/src/commands/ticket/index.ts +0 -114
  81. package/src/commands/ticket/list.ts +0 -128
  82. package/src/commands/ticket/refine-utils.ts +0 -89
  83. package/src/commands/ticket/refine.ts +0 -268
  84. package/src/commands/ticket/remove.ts +0 -48
  85. package/src/commands/ticket/show.ts +0 -74
  86. package/src/completion/handle.ts +0 -30
  87. package/src/completion/resolver.ts +0 -241
  88. package/src/interactive/dashboard.ts +0 -268
  89. package/src/interactive/escapable.ts +0 -81
  90. package/src/interactive/file-browser.ts +0 -153
  91. package/src/interactive/index.ts +0 -429
  92. package/src/interactive/menu.ts +0 -403
  93. package/src/interactive/selectors.ts +0 -273
  94. package/src/interactive/wizard.ts +0 -221
  95. package/src/providers/claude.ts +0 -53
  96. package/src/providers/copilot.ts +0 -86
  97. package/src/providers/index.ts +0 -43
  98. package/src/providers/types.ts +0 -85
  99. package/src/schemas/index.ts +0 -130
  100. package/src/store/config.ts +0 -74
  101. package/src/store/progress.ts +0 -230
  102. package/src/store/project.ts +0 -276
  103. package/src/store/sprint.ts +0 -229
  104. package/src/store/task.ts +0 -443
  105. package/src/store/ticket.ts +0 -178
  106. package/src/theme/index.ts +0 -215
  107. package/src/theme/ui.ts +0 -872
  108. package/src/utils/detect-scripts.ts +0 -247
  109. package/src/utils/editor-input.ts +0 -41
  110. package/src/utils/editor.ts +0 -37
  111. package/src/utils/exit-codes.ts +0 -27
  112. package/src/utils/file-lock.ts +0 -135
  113. package/src/utils/git.ts +0 -185
  114. package/src/utils/ids.ts +0 -37
  115. package/src/utils/issue-fetch.ts +0 -244
  116. package/src/utils/json-extract.ts +0 -62
  117. package/src/utils/multiline.ts +0 -61
  118. package/src/utils/path-selector.ts +0 -236
  119. package/src/utils/paths.ts +0 -108
  120. package/src/utils/provider.ts +0 -34
  121. package/src/utils/requirements-export.ts +0 -63
  122. package/src/utils/storage.ts +0 -107
  123. package/tsconfig.json +0 -25
  124. /package/{src/ai → dist}/prompts/ideate-auto.md +0 -0
  125. /package/{src/ai → dist}/prompts/ideate.md +0 -0
  126. /package/{src/ai → dist}/prompts/plan-auto.md +0 -0
  127. /package/{src/ai → dist}/prompts/plan-common.md +0 -0
  128. /package/{src/ai → dist}/prompts/plan-interactive.md +0 -0
  129. /package/{src/ai → dist}/prompts/task-execution.md +0 -0
  130. /package/{src/ai → dist}/prompts/ticket-refine.md +0 -0
@@ -1,226 +0,0 @@
1
- import type { Command } from 'commander';
2
- import { sprintCreateCommand } from '@src/commands/sprint/create.ts';
3
- import { sprintListCommand } from '@src/commands/sprint/list.ts';
4
- import { sprintShowCommand } from '@src/commands/sprint/show.ts';
5
- import { sprintContextCommand } from '@src/commands/sprint/context.ts';
6
- import { sprintCloseCommand } from '@src/commands/sprint/close.ts';
7
- import { sprintStartCommand } from '@src/commands/sprint/start.ts';
8
- import { sprintPlanCommand } from '@src/commands/sprint/plan.ts';
9
- import { sprintCurrentCommand } from '@src/commands/sprint/current.ts';
10
- import { sprintSwitchCommand } from '@src/commands/sprint/switch.ts';
11
- import { sprintRefineCommand } from '@src/commands/sprint/refine.ts';
12
- import { sprintIdeateCommand } from '@src/commands/sprint/ideate.ts';
13
- import { sprintRequirementsCommand } from '@src/commands/sprint/requirements.ts';
14
- import { sprintHealthCommand } from '@src/commands/sprint/health.ts';
15
- import { sprintDeleteCommand } from '@src/commands/sprint/delete.ts';
16
-
17
- export function registerSprintCommands(program: Command): void {
18
- const sprint = program.command('sprint').description('Manage sprints');
19
-
20
- sprint.addHelpText(
21
- 'after',
22
- `
23
- Examples:
24
- $ ralphctl sprint create --name "Sprint 1"
25
- $ ralphctl sprint refine # Refine ticket requirements with AI
26
- $ ralphctl sprint plan --auto # Generate tasks automatically
27
- $ ralphctl sprint start -s # Start with interactive session
28
- `
29
- );
30
-
31
- sprint
32
- .command('create')
33
- .description('Create a new sprint')
34
- .option('--name <name>', 'Sprint name')
35
- .option('-n, --no-interactive', 'Non-interactive mode (error on missing params)')
36
- .action(async (opts: { name?: string; interactive?: boolean }) => {
37
- await sprintCreateCommand({
38
- name: opts.name,
39
- // --no-interactive sets interactive=false, otherwise true (prompt for missing)
40
- interactive: opts.interactive !== false,
41
- });
42
- });
43
-
44
- sprint
45
- .command('list')
46
- .description('List all sprints')
47
- .option('--status <status>', 'Filter by status (draft, active, closed)')
48
- .action(async (opts: { status?: string }) => {
49
- const args: string[] = [];
50
- if (opts.status) args.push('--status', opts.status);
51
- await sprintListCommand(args);
52
- });
53
-
54
- sprint
55
- .command('show [id]')
56
- .description('Show sprint details')
57
- .action(async (id?: string) => {
58
- await sprintShowCommand(id ? [id] : []);
59
- });
60
-
61
- sprint
62
- .command('context [id]')
63
- .description('Output full context for planning')
64
- .action(async (id?: string) => {
65
- await sprintContextCommand(id ? [id] : []);
66
- });
67
-
68
- sprint
69
- .command('current [id]')
70
- .description('Show/set current sprint (use "-" to open selector)')
71
- .action(async (id?: string) => {
72
- await sprintCurrentCommand(id ? [id] : []);
73
- });
74
-
75
- sprint
76
- .command('switch')
77
- .description('Quick sprint switcher (opens selector)')
78
- .action(async () => {
79
- await sprintSwitchCommand();
80
- });
81
-
82
- sprint
83
- .command('refine [id]')
84
- .description('Refine ticket specifications')
85
- .option('--project <name>', 'Only refine tickets for specific project')
86
- .action(async (id?: string, opts?: { project?: string }) => {
87
- const args: string[] = [];
88
- if (id) args.push(id);
89
- if (opts?.project) args.push('--project', opts.project);
90
- await sprintRefineCommand(args);
91
- });
92
-
93
- sprint
94
- .command('ideate [id]')
95
- .description('Quick idea to tasks (refine + plan in one session)')
96
- .option('--auto', 'Run without user interaction (AI decides autonomously)')
97
- .option('--all-paths', 'Explore all project repositories instead of prompting for selection')
98
- .option('--project <name>', 'Pre-select project (skip interactive selection)')
99
- .action(async (id?: string, opts?: { auto?: boolean; allPaths?: boolean; project?: string }) => {
100
- const args: string[] = [];
101
- if (id) args.push(id);
102
- if (opts?.auto) args.push('--auto');
103
- if (opts?.allPaths) args.push('--all-paths');
104
- if (opts?.project) args.push('--project', opts.project);
105
- await sprintIdeateCommand(args);
106
- });
107
-
108
- sprint
109
- .command('plan [id]')
110
- .description('Generate tasks using AI CLI')
111
- .option('--auto', 'Run without user interaction (AI decides autonomously)')
112
- .option('--all-paths', 'Explore all project repositories instead of prompting for selection')
113
- .action(async (id?: string, opts?: { auto?: boolean; allPaths?: boolean }) => {
114
- const args: string[] = [];
115
- if (id) args.push(id);
116
- if (opts?.auto) args.push('--auto');
117
- if (opts?.allPaths) args.push('--all-paths');
118
- await sprintPlanCommand(args);
119
- });
120
-
121
- sprint
122
- .command('close [id]')
123
- .description('Close an active sprint')
124
- .option('--create-pr', 'Create pull requests for sprint branches')
125
- .action(async (id?: string, opts?: { createPr?: boolean }) => {
126
- const args: string[] = [];
127
- if (id) args.push(id);
128
- if (opts?.createPr) args.push('--create-pr');
129
- await sprintCloseCommand(args);
130
- });
131
-
132
- sprint
133
- .command('delete [id]')
134
- .description('Delete a sprint permanently')
135
- .option('-y, --yes', 'Skip confirmation')
136
- .action(async (id?: string, opts?: { yes?: boolean }) => {
137
- const args: string[] = [];
138
- if (id) args.push(id);
139
- if (opts?.yes) args.push('-y');
140
- await sprintDeleteCommand(args);
141
- });
142
-
143
- sprint
144
- .command('requirements [id]')
145
- .description('Export refined requirements to file')
146
- .action(async (id?: string) => {
147
- await sprintRequirementsCommand(id ? [id] : []);
148
- });
149
-
150
- sprint
151
- .command('health')
152
- .description('Check sprint health')
153
- .action(async () => {
154
- await sprintHealthCommand();
155
- });
156
-
157
- sprint
158
- .command('start [id]')
159
- .description('Run automated implementation loop')
160
- .option('-s, --session', 'Interactive AI session (collaborate with your AI provider)')
161
- .option('-t, --step', 'Step through tasks with approval between each')
162
- .option('-c, --count <n>', 'Limit to N tasks')
163
- .option('--no-commit', 'Skip automatic git commit after each task completes')
164
- .option('--concurrency <n>', 'Max parallel tasks (default: auto based on unique repos)')
165
- .option('--max-retries <n>', 'Max rate-limit retries per task (default: 5)')
166
- .option('--fail-fast', 'Stop launching new tasks on first failure')
167
- .option('-f, --force', 'Skip precondition checks (e.g., unplanned tickets)')
168
- .option('--refresh-check', 'Force re-run check scripts even if they already ran this sprint')
169
- .option('-b, --branch', 'Create sprint branch (ralphctl/<sprint-id>) in all repos')
170
- .option('--branch-name <name>', 'Use a custom branch name for sprint execution')
171
- .addHelpText(
172
- 'after',
173
- `
174
- Exit Codes:
175
- 0 - Success (all requested operations completed)
176
- 1 - Error (validation, missing params, execution failed)
177
- 2 - No tasks available
178
- 3 - All remaining tasks blocked by dependencies
179
-
180
- Parallel Execution:
181
- Tasks targeting different repos run concurrently by default.
182
- At most one task per repository runs at a time to avoid git conflicts.
183
- Use --concurrency 1 to force sequential execution.
184
- Session (--session) and step (--step) modes always run sequentially.
185
-
186
- Branch Management:
187
- Use -b/--branch to auto-create a sprint branch in all repos.
188
- Use --branch-name <name> to specify a custom branch name.
189
- On first run, an interactive prompt offers branch strategy selection.
190
- The chosen branch is persisted and reused on subsequent runs.
191
- `
192
- )
193
- .action(
194
- async (
195
- id?: string,
196
- opts?: {
197
- session?: boolean;
198
- step?: boolean;
199
- count?: string;
200
- commit?: boolean;
201
- concurrency?: string;
202
- maxRetries?: string;
203
- failFast?: boolean;
204
- force?: boolean;
205
- refreshCheck?: boolean;
206
- branch?: boolean;
207
- branchName?: string;
208
- }
209
- ) => {
210
- const args: string[] = [];
211
- if (id) args.push(id);
212
- if (opts?.session) args.push('--session');
213
- if (opts?.step) args.push('--step');
214
- if (opts?.count) args.push('--count', opts.count);
215
- if (opts?.commit === false) args.push('--no-commit');
216
- if (opts?.concurrency) args.push('--concurrency', opts.concurrency);
217
- if (opts?.maxRetries) args.push('--max-retries', opts.maxRetries);
218
- if (opts?.failFast) args.push('--fail-fast');
219
- if (opts?.force) args.push('--force');
220
- if (opts?.refreshCheck) args.push('--refresh-check');
221
- if (opts?.branch) args.push('--branch');
222
- if (opts?.branchName) args.push('--branch-name', opts.branchName);
223
- await sprintStartCommand(args);
224
- }
225
- );
226
- }
@@ -1,86 +0,0 @@
1
- import { listSprints } from '@src/store/sprint.ts';
2
- import { getCurrentSprint } from '@src/store/config.ts';
3
- import { SprintStatusSchema } from '@src/schemas/index.ts';
4
- import {
5
- badge,
6
- formatSprintStatus,
7
- icons,
8
- log,
9
- printHeader,
10
- renderTable,
11
- showEmpty,
12
- showError,
13
- showNextStep,
14
- } from '@src/theme/ui.ts';
15
-
16
- export async function sprintListCommand(args: string[] = []): Promise<void> {
17
- // Parse status filter
18
- let statusFilter: string | undefined;
19
- for (let i = 0; i < args.length; i++) {
20
- if (args[i] === '--status' && args[i + 1]) {
21
- statusFilter = args[i + 1];
22
- i++;
23
- }
24
- }
25
-
26
- // Validate status filter
27
- if (statusFilter) {
28
- const result = SprintStatusSchema.safeParse(statusFilter);
29
- if (!result.success) {
30
- showError(`Invalid status: "${statusFilter}". Valid values: draft, active, closed`);
31
- return;
32
- }
33
- }
34
-
35
- const sprints = await listSprints();
36
-
37
- if (sprints.length === 0) {
38
- showEmpty('sprints', 'Create one with: ralphctl sprint create');
39
- return;
40
- }
41
-
42
- const filtered = statusFilter ? sprints.filter((s) => s.status === statusFilter) : sprints;
43
- const isFiltered = filtered.length !== sprints.length;
44
- const filterStr = statusFilter ? ` (filtered: status=${statusFilter})` : '';
45
-
46
- if (filtered.length === 0) {
47
- showEmpty('matching sprints', 'Try adjusting your filters');
48
- return;
49
- }
50
-
51
- printHeader('Sprints', icons.sprint);
52
-
53
- const currentSprintId = await getCurrentSprint();
54
-
55
- const rows: string[][] = filtered.map((sprint) => {
56
- const isCurrent = sprint.id === currentSprintId;
57
- const marker = isCurrent ? badge('current', 'success') : '';
58
- return [marker, sprint.id, formatSprintStatus(sprint.status), sprint.name, String(sprint.tickets.length)];
59
- });
60
-
61
- console.log(
62
- renderTable(
63
- [
64
- { header: '', minWidth: 0 },
65
- { header: 'ID' },
66
- { header: 'Status' },
67
- { header: 'Name' },
68
- { header: 'Tickets', align: 'right' },
69
- ],
70
- rows
71
- )
72
- );
73
-
74
- log.newline();
75
- const showingLabel = isFiltered
76
- ? `Showing ${String(filtered.length)} of ${String(sprints.length)} sprint(s)${filterStr}`
77
- : `Showing ${String(sprints.length)} sprint(s)`;
78
- log.dim(showingLabel);
79
-
80
- const hasActive = sprints.some((s) => s.status === 'active');
81
- if (!hasActive) {
82
- log.newline();
83
- showNextStep('ralphctl sprint start', 'start a sprint');
84
- }
85
- log.newline();
86
- }
@@ -1,207 +0,0 @@
1
- import { readFile } from 'node:fs/promises';
2
- import { muted } from '@src/theme/index.ts';
3
- import { log, renderTable } from '@src/theme/ui.ts';
4
- import { addTask, getTasks, saveTasks } from '@src/store/task.ts';
5
- import { getSchemaPath, getTasksFilePath } from '@src/utils/paths.ts';
6
- import { withFileLock } from '@src/utils/file-lock.ts';
7
- import { type ImportTask, ImportTasksSchema, type Task } from '@src/schemas/index.ts';
8
- import { extractJsonArray } from '@src/utils/json-extract.ts';
9
- import { generateUuid8 } from '@src/utils/ids.ts';
10
-
11
- /**
12
- * Load the task import JSON schema from file.
13
- */
14
- export async function getTaskImportSchema(): Promise<string> {
15
- const schemaPath = getSchemaPath('task-import.schema.json');
16
- return readFile(schemaPath, 'utf-8');
17
- }
18
-
19
- /**
20
- * Check if AI output contains a planning-blocked signal.
21
- * Returns the reason if blocked, null otherwise.
22
- */
23
- export function parsePlanningBlocked(output: string): string | null {
24
- const match = /<planning-blocked>([\s\S]*?)<\/planning-blocked>/.exec(output);
25
- return match?.[1]?.trim() ?? null;
26
- }
27
-
28
- /**
29
- * Parse AI output to extract and validate task JSON array.
30
- */
31
- export function parseTasksJson(output: string): ImportTask[] {
32
- // Try to extract a balanced JSON array from the output (handles nested arrays like steps)
33
- const jsonStr = extractJsonArray(output);
34
-
35
- let parsed: unknown;
36
- try {
37
- parsed = JSON.parse(jsonStr);
38
- } catch (err) {
39
- throw new Error(`Invalid JSON: ${err instanceof Error ? err.message : 'parse error'}`, { cause: err });
40
- }
41
-
42
- if (!Array.isArray(parsed)) {
43
- throw new Error('Expected JSON array');
44
- }
45
-
46
- // Validate against schema
47
- const result = ImportTasksSchema.safeParse(parsed);
48
- if (!result.success) {
49
- const issues = result.error.issues
50
- .map((issue) => {
51
- const path = issue.path.length > 0 ? `[${issue.path.join('.')}]` : '';
52
- return ` ${path}: ${issue.message}`;
53
- })
54
- .join('\n');
55
- throw new Error(`Invalid task format:\n${issues}`);
56
- }
57
-
58
- return result.data;
59
- }
60
-
61
- /**
62
- * Render parsed tasks as a formatted table.
63
- */
64
- export function renderParsedTasksTable(parsedTasks: ImportTask[]): string {
65
- const rows = parsedTasks.map((task, i) => {
66
- const deps = task.blockedBy?.length ? task.blockedBy.join(', ') : '';
67
- return [String(i + 1), task.name, task.projectPath, deps];
68
- });
69
- return renderTable(
70
- [{ header: '#', align: 'right' as const }, { header: 'Name' }, { header: 'Path' }, { header: 'Blocked By' }],
71
- rows
72
- );
73
- }
74
-
75
- /**
76
- * Import tasks with two-pass ID resolution.
77
- * When `replace: true`, builds the complete task list in memory and writes atomically
78
- * (interruption-safe: original tasks.json untouched until final write).
79
- * When `replace: false` (default), appends via addTask() one-by-one.
80
- * Returns the number of successfully imported tasks.
81
- */
82
- export async function importTasks(
83
- tasks: ImportTask[],
84
- sprintId: string,
85
- options?: { replace?: boolean }
86
- ): Promise<number> {
87
- if (options?.replace) {
88
- return importTasksReplace(tasks, sprintId);
89
- }
90
-
91
- return importTasksAppend(tasks, sprintId);
92
- }
93
-
94
- /**
95
- * Append tasks one-by-one via addTask() (first plan — no existing tasks).
96
- */
97
- async function importTasksAppend(tasks: ImportTask[], sprintId: string): Promise<number> {
98
- // Build mapping from local IDs to real IDs
99
- const localToRealId = new Map<string, string>();
100
-
101
- // First pass: create all tasks and build ID mapping
102
- const createdTasks: { task: ImportTask; realId: string }[] = [];
103
-
104
- for (const taskInput of tasks) {
105
- try {
106
- const projectPath = taskInput.projectPath;
107
-
108
- // Create task without blockedBy first
109
- const task = await addTask(
110
- {
111
- name: taskInput.name,
112
- description: taskInput.description,
113
- steps: taskInput.steps ?? [],
114
- ticketId: taskInput.ticketId,
115
- blockedBy: [], // Set later
116
- projectPath,
117
- },
118
- sprintId
119
- );
120
-
121
- // Map local ID to real ID
122
- if (taskInput.id) {
123
- localToRealId.set(taskInput.id, task.id);
124
- }
125
-
126
- createdTasks.push({ task: taskInput, realId: task.id });
127
- log.itemSuccess(`${task.id}: ${task.name}`);
128
- } catch (err) {
129
- log.itemError(`Failed to add: ${taskInput.name}`);
130
- if (err instanceof Error) {
131
- console.log(muted(` ${err.message}`));
132
- }
133
- }
134
- }
135
-
136
- // Second pass: update blockedBy with resolved real IDs (under file lock)
137
- const tasksFilePath = getTasksFilePath(sprintId);
138
- await withFileLock(tasksFilePath, async () => {
139
- const allTasks = await getTasks(sprintId);
140
- for (const { task: taskInput, realId } of createdTasks) {
141
- const blockedBy = (taskInput.blockedBy ?? [])
142
- .map((localId) => localToRealId.get(localId) ?? '')
143
- .filter((id) => id !== '');
144
-
145
- if (blockedBy.length > 0) {
146
- const taskToUpdate = allTasks.find((t) => t.id === realId);
147
- if (taskToUpdate) {
148
- taskToUpdate.blockedBy = blockedBy;
149
- }
150
- }
151
- }
152
- await saveTasks(allTasks, sprintId);
153
- });
154
-
155
- return createdTasks.length;
156
- }
157
-
158
- /**
159
- * Build the complete task list in memory and write atomically via saveTasks().
160
- * Original tasks.json is untouched until the final write — interruption-safe.
161
- */
162
- async function importTasksReplace(tasks: ImportTask[], sprintId: string): Promise<number> {
163
- // Build mapping from local IDs to real IDs
164
- const localToRealId = new Map<string, string>();
165
- const newTasks: Task[] = [];
166
-
167
- // First pass: generate real IDs and build mapping
168
- for (const taskInput of tasks) {
169
- const realId = generateUuid8();
170
- if (taskInput.id) {
171
- localToRealId.set(taskInput.id, realId);
172
- }
173
-
174
- newTasks.push({
175
- id: realId,
176
- name: taskInput.name,
177
- description: taskInput.description,
178
- steps: taskInput.steps ?? [],
179
- status: 'todo',
180
- order: newTasks.length + 1,
181
- ticketId: taskInput.ticketId,
182
- blockedBy: [], // Set in second pass
183
- projectPath: taskInput.projectPath,
184
- verified: false,
185
- });
186
-
187
- log.itemSuccess(`${realId}: ${taskInput.name}`);
188
- }
189
-
190
- // Second pass: resolve blockedBy references
191
- for (let i = 0; i < tasks.length; i++) {
192
- const taskInput = tasks[i];
193
- const newTask = newTasks[i];
194
- if (!taskInput || !newTask) continue;
195
-
196
- const blockedBy = (taskInput.blockedBy ?? [])
197
- .map((localId) => localToRealId.get(localId) ?? '')
198
- .filter((id) => id !== '');
199
-
200
- newTask.blockedBy = blockedBy;
201
- }
202
-
203
- // Atomic write — replaces all existing tasks in one operation
204
- await saveTasks(newTasks, sprintId);
205
-
206
- return newTasks.length;
207
- }