ralphctl 0.1.2 → 0.1.4
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/dist/{add-HGJCLWED.mjs → add-7LBVENXM.mjs} +6 -4
- package/dist/{add-MRGCS3US.mjs → add-DVEYDCTR.mjs} +6 -4
- package/dist/{chunk-NTWO2LXB.mjs → chunk-7LZ6GOGN.mjs} +13 -12
- package/dist/{chunk-JON4GCLR.mjs → chunk-DZ6HHTM5.mjs} +1 -1
- package/dist/chunk-EDJX7TT6.mjs +148 -0
- package/dist/{chunk-MNMQC36F.mjs → chunk-F2MMCTB5.mjs} +71 -77
- package/dist/{chunk-EKMZZRWI.mjs → chunk-LFDW6MWF.mjs} +65 -70
- package/dist/{chunk-LOR7QBXX.mjs → chunk-M7JV6MKD.mjs} +270 -349
- package/dist/chunk-OEUJDSHY.mjs +27 -0
- package/dist/{chunk-WGHJI3OI.mjs → chunk-PDI6HBZ7.mjs} +32 -37
- package/dist/{chunk-6PYTKGB5.mjs → chunk-W3TY22IS.mjs} +45 -39
- package/dist/{chunk-MRKOFVTM.mjs → chunk-YIB7QYU4.mjs} +102 -100
- package/dist/cli.mjs +761 -739
- package/dist/create-MQ4OHZAX.mjs +12 -0
- package/dist/{handle-UG5M2OON.mjs → handle-K2AZLTKU.mjs} +1 -1
- package/dist/{project-NT3L4FTB.mjs → project-Q4LKML42.mjs} +6 -4
- package/dist/{resolver-WSFWKACM.mjs → resolver-NH34HTB6.mjs} +27 -17
- package/dist/{sprint-4VHDLGFN.mjs → sprint-UHYXSEBJ.mjs} +8 -5
- package/dist/{wizard-LRELAN2J.mjs → wizard-MCDDXLGE.mjs} +45 -48
- package/package.json +2 -1
- package/dist/create-MG7E7PLQ.mjs +0 -10
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
escapableSelect
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-7LZ6GOGN.mjs";
|
|
5
5
|
import {
|
|
6
|
-
IssueFetchError,
|
|
7
6
|
allRequirementsApproved,
|
|
8
7
|
fetchIssueFromUrl,
|
|
9
8
|
formatIssueContext,
|
|
@@ -12,7 +11,7 @@ import {
|
|
|
12
11
|
getPendingRequirements,
|
|
13
12
|
groupTicketsByProject,
|
|
14
13
|
listTickets
|
|
15
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-F2MMCTB5.mjs";
|
|
16
15
|
import {
|
|
17
16
|
EXIT_ALL_BLOCKED,
|
|
18
17
|
EXIT_ERROR,
|
|
@@ -22,13 +21,10 @@ import {
|
|
|
22
21
|
exitWithCode
|
|
23
22
|
} from "./chunk-7TG3EAQ2.mjs";
|
|
24
23
|
import {
|
|
25
|
-
ProjectNotFoundError,
|
|
26
24
|
getProject,
|
|
27
25
|
listProjects
|
|
28
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-PDI6HBZ7.mjs";
|
|
29
27
|
import {
|
|
30
|
-
SprintNotFoundError,
|
|
31
|
-
SprintStatusError,
|
|
32
28
|
activateSprint,
|
|
33
29
|
assertSprintStatus,
|
|
34
30
|
closeSprint,
|
|
@@ -43,7 +39,12 @@ import {
|
|
|
43
39
|
setAiProvider,
|
|
44
40
|
summarizeProgressForContext,
|
|
45
41
|
withFileLock
|
|
46
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-LFDW6MWF.mjs";
|
|
43
|
+
import {
|
|
44
|
+
ensureError,
|
|
45
|
+
unwrapOrThrow,
|
|
46
|
+
wrapAsync
|
|
47
|
+
} from "./chunk-OEUJDSHY.mjs";
|
|
47
48
|
import {
|
|
48
49
|
ImportTasksSchema,
|
|
49
50
|
RefinedRequirementsSchema,
|
|
@@ -59,7 +60,17 @@ import {
|
|
|
59
60
|
getTasksFilePath,
|
|
60
61
|
readValidatedJson,
|
|
61
62
|
writeValidatedJson
|
|
62
|
-
} from "./chunk-
|
|
63
|
+
} from "./chunk-W3TY22IS.mjs";
|
|
64
|
+
import {
|
|
65
|
+
DependencyCycleError,
|
|
66
|
+
IOError,
|
|
67
|
+
IssueFetchError,
|
|
68
|
+
ProjectNotFoundError,
|
|
69
|
+
SpawnError,
|
|
70
|
+
SprintNotFoundError,
|
|
71
|
+
SprintStatusError,
|
|
72
|
+
TaskNotFoundError
|
|
73
|
+
} from "./chunk-EDJX7TT6.mjs";
|
|
63
74
|
import {
|
|
64
75
|
colors,
|
|
65
76
|
createSpinner,
|
|
@@ -95,6 +106,7 @@ import {
|
|
|
95
106
|
import { mkdir, readFile } from "fs/promises";
|
|
96
107
|
import { join as join4 } from "path";
|
|
97
108
|
import { confirm } from "@inquirer/prompts";
|
|
109
|
+
import { Result as Result3 } from "typescript-result";
|
|
98
110
|
|
|
99
111
|
// src/ai/prompts/index.ts
|
|
100
112
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -211,6 +223,7 @@ function providerDisplayName(provider) {
|
|
|
211
223
|
// src/commands/ticket/refine-utils.ts
|
|
212
224
|
import { writeFile as writeFile2 } from "fs/promises";
|
|
213
225
|
import { join as join3 } from "path";
|
|
226
|
+
import { Result as Result2 } from "typescript-result";
|
|
214
227
|
|
|
215
228
|
// src/ai/session.ts
|
|
216
229
|
import { spawn, spawnSync } from "child_process";
|
|
@@ -357,8 +370,7 @@ var ProcessManager = class _ProcessManager {
|
|
|
357
370
|
try {
|
|
358
371
|
callback();
|
|
359
372
|
} catch (err) {
|
|
360
|
-
|
|
361
|
-
console.error("Error in cleanup callback:", error2.message);
|
|
373
|
+
console.error("Error in cleanup callback:", err instanceof Error ? err.message : String(err));
|
|
362
374
|
}
|
|
363
375
|
}
|
|
364
376
|
this.cleanupCallbacks.clear();
|
|
@@ -399,6 +411,7 @@ var ProcessManager = class _ProcessManager {
|
|
|
399
411
|
};
|
|
400
412
|
|
|
401
413
|
// src/providers/claude.ts
|
|
414
|
+
import { Result } from "typescript-result";
|
|
402
415
|
var claudeAdapter = {
|
|
403
416
|
name: "claude",
|
|
404
417
|
displayName: "Claude",
|
|
@@ -412,15 +425,15 @@ var claudeAdapter = {
|
|
|
412
425
|
return ["-p", "--output-format", "json", ...this.baseArgs, ...extraArgs];
|
|
413
426
|
},
|
|
414
427
|
parseJsonOutput(stdout) {
|
|
415
|
-
try
|
|
416
|
-
|
|
417
|
-
return {
|
|
418
|
-
result: parsed.result ?? stdout,
|
|
419
|
-
sessionId: parsed.session_id ?? null
|
|
420
|
-
};
|
|
421
|
-
} catch {
|
|
428
|
+
const jsonResult = Result.try(() => JSON.parse(stdout));
|
|
429
|
+
if (!jsonResult.ok) {
|
|
422
430
|
return { result: stdout, sessionId: null };
|
|
423
431
|
}
|
|
432
|
+
const parsed = jsonResult.value;
|
|
433
|
+
return {
|
|
434
|
+
result: parsed.result ?? stdout,
|
|
435
|
+
sessionId: parsed.session_id ?? null
|
|
436
|
+
};
|
|
424
437
|
},
|
|
425
438
|
detectRateLimit(stderr) {
|
|
426
439
|
const patterns = [/rate.?limit/i, /\b429\b/, /too many requests/i, /overloaded/i, /\b529\b/];
|
|
@@ -456,22 +469,23 @@ var copilotAdapter = {
|
|
|
456
469
|
return { result: stdout.trim(), sessionId: null };
|
|
457
470
|
},
|
|
458
471
|
async extractSessionId(cwd) {
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
472
|
+
const filesResult = await wrapAsync(
|
|
473
|
+
() => readdir(cwd),
|
|
474
|
+
(err) => new IOError(`Failed to read directory: ${cwd}`, err instanceof Error ? err : void 0)
|
|
475
|
+
);
|
|
476
|
+
if (!filesResult.ok) return null;
|
|
477
|
+
const files = filesResult.value;
|
|
478
|
+
const shareFile = files.find((f) => /^copilot-session-[a-zA-Z0-9_][a-zA-Z0-9_-]*\.md$/.test(f));
|
|
479
|
+
if (!shareFile) return null;
|
|
480
|
+
const match = /^copilot-session-([a-zA-Z0-9_][a-zA-Z0-9_-]{0,127})\.md$/.exec(shareFile);
|
|
481
|
+
if (!match?.[1]) return null;
|
|
482
|
+
const filePath = join2(cwd, shareFile);
|
|
483
|
+
const stat = await lstat(filePath).catch(() => null);
|
|
484
|
+
if (stat?.isFile()) {
|
|
485
|
+
await unlink(filePath).catch(() => {
|
|
486
|
+
});
|
|
474
487
|
}
|
|
488
|
+
return match[1];
|
|
475
489
|
},
|
|
476
490
|
detectRateLimit(stderr) {
|
|
477
491
|
const patterns = [/rate.?limit/i, /\b429\b/, /too many requests/i, /overloaded/i, /\b529\b/];
|
|
@@ -507,34 +521,6 @@ async function getActiveProvider() {
|
|
|
507
521
|
}
|
|
508
522
|
|
|
509
523
|
// src/ai/session.ts
|
|
510
|
-
var SpawnError = class extends Error {
|
|
511
|
-
stderr;
|
|
512
|
-
exitCode;
|
|
513
|
-
rateLimited;
|
|
514
|
-
retryAfterMs;
|
|
515
|
-
/** Session ID if available (for resume after rate limit) */
|
|
516
|
-
sessionId;
|
|
517
|
-
constructor(message, stderr, exitCode, sessionId, provider) {
|
|
518
|
-
super(message);
|
|
519
|
-
this.name = "SpawnError";
|
|
520
|
-
this.stderr = stderr;
|
|
521
|
-
this.exitCode = exitCode;
|
|
522
|
-
this.sessionId = sessionId ?? null;
|
|
523
|
-
const rl = provider ? provider.detectRateLimit(stderr) : detectRateLimitFallback(stderr);
|
|
524
|
-
this.rateLimited = rl.rateLimited;
|
|
525
|
-
this.retryAfterMs = rl.retryAfterMs;
|
|
526
|
-
}
|
|
527
|
-
};
|
|
528
|
-
function detectRateLimitFallback(stderr) {
|
|
529
|
-
const patterns = [/rate.?limit/i, /\b429\b/, /too many requests/i, /overloaded/i, /\b529\b/];
|
|
530
|
-
const isRateLimited = patterns.some((p) => p.test(stderr));
|
|
531
|
-
if (!isRateLimited) {
|
|
532
|
-
return { rateLimited: false, retryAfterMs: null };
|
|
533
|
-
}
|
|
534
|
-
const retryMatch = /retry.?after:?\s*(\d+)/i.exec(stderr);
|
|
535
|
-
const retryAfterMs = retryMatch?.[1] ? parseInt(retryMatch[1], 10) * 1e3 : null;
|
|
536
|
-
return { rateLimited: true, retryAfterMs };
|
|
537
|
-
}
|
|
538
524
|
function spawnInteractive(prompt, options, provider) {
|
|
539
525
|
assertSafeCwd(options.cwd);
|
|
540
526
|
const p = provider ?? {
|
|
@@ -570,7 +556,7 @@ async function spawnHeadlessRaw(options, provider) {
|
|
|
570
556
|
const allArgs = p.buildHeadlessArgs(options.args ?? []);
|
|
571
557
|
if (options.resumeSessionId) {
|
|
572
558
|
if (!/^[a-zA-Z0-9_][a-zA-Z0-9_-]{0,127}$/.test(options.resumeSessionId)) {
|
|
573
|
-
reject(new SpawnError("Invalid session ID format", "", 1
|
|
559
|
+
reject(new SpawnError("Invalid session ID format", "", 1));
|
|
574
560
|
return;
|
|
575
561
|
}
|
|
576
562
|
allArgs.push("--resume", options.resumeSessionId);
|
|
@@ -584,13 +570,13 @@ async function spawnHeadlessRaw(options, provider) {
|
|
|
584
570
|
try {
|
|
585
571
|
manager.registerChild(child);
|
|
586
572
|
} catch {
|
|
587
|
-
reject(new SpawnError("Cannot spawn during shutdown", "", 1
|
|
573
|
+
reject(new SpawnError("Cannot spawn during shutdown", "", 1));
|
|
588
574
|
return;
|
|
589
575
|
}
|
|
590
576
|
const MAX_PROMPT_SIZE = 1e6;
|
|
591
577
|
if (options.prompt) {
|
|
592
578
|
if (options.prompt.length > MAX_PROMPT_SIZE) {
|
|
593
|
-
reject(new SpawnError("Prompt exceeds maximum size (1MB)", "", 1
|
|
579
|
+
reject(new SpawnError("Prompt exceeds maximum size (1MB)", "", 1));
|
|
594
580
|
return;
|
|
595
581
|
}
|
|
596
582
|
child.stdin.write(options.prompt);
|
|
@@ -615,19 +601,18 @@ async function spawnHeadlessRaw(options, provider) {
|
|
|
615
601
|
`${p.displayName} CLI exited with code ${String(exitCode)}: ${stderr}`,
|
|
616
602
|
stderr,
|
|
617
603
|
exitCode,
|
|
618
|
-
sessionId
|
|
619
|
-
p
|
|
604
|
+
sessionId
|
|
620
605
|
)
|
|
621
606
|
);
|
|
622
607
|
} else {
|
|
623
608
|
resolve({ stdout: result, stderr, exitCode: 0, sessionId });
|
|
624
609
|
}
|
|
625
610
|
})().catch((err) => {
|
|
626
|
-
reject(new SpawnError(`Unexpected error in close handler: ${String(err)}`, "", 1
|
|
611
|
+
reject(new SpawnError(`Unexpected error in close handler: ${String(err)}`, "", 1));
|
|
627
612
|
});
|
|
628
613
|
});
|
|
629
614
|
child.on("error", (err) => {
|
|
630
|
-
reject(new SpawnError(`Failed to spawn ${p.binary} CLI: ${err.message}`, "", 1
|
|
615
|
+
reject(new SpawnError(`Failed to spawn ${p.binary} CLI: ${err.message}`, "", 1));
|
|
631
616
|
});
|
|
632
617
|
});
|
|
633
618
|
}
|
|
@@ -650,24 +635,23 @@ async function spawnWithRetry(options, retryOptions, provider) {
|
|
|
650
635
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
651
636
|
const elapsed = Date.now() - startTime;
|
|
652
637
|
if (attempt > 0 && elapsed >= totalTimeoutMs) {
|
|
653
|
-
throw new SpawnError(`Total retry timeout exceeded (${String(totalTimeoutMs)}ms)`, "", 1, resumeSessionId
|
|
638
|
+
throw new SpawnError(`Total retry timeout exceeded (${String(totalTimeoutMs)}ms)`, "", 1, resumeSessionId);
|
|
654
639
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
if (attempt >= maxRetries) {
|
|
665
|
-
throw err;
|
|
666
|
-
}
|
|
667
|
-
const delay = Math.min(err.retryAfterMs ?? BASE_DELAY_MS * Math.pow(2, attempt), MAX_DELAY_MS) + jitter();
|
|
668
|
-
retryOptions?.onRetry?.(attempt + 1, delay, err);
|
|
669
|
-
await sleep(delay);
|
|
640
|
+
const r = await wrapAsync(async () => spawnHeadlessRaw({ ...options, resumeSessionId }, p), ensureError);
|
|
641
|
+
if (r.ok) return r.value;
|
|
642
|
+
const err = r.error;
|
|
643
|
+
if (!(err instanceof SpawnError) || !err.rateLimited) {
|
|
644
|
+
throw err;
|
|
645
|
+
}
|
|
646
|
+
if (err.sessionId) {
|
|
647
|
+
resumeSessionId = err.sessionId;
|
|
670
648
|
}
|
|
649
|
+
if (attempt >= maxRetries) {
|
|
650
|
+
throw err;
|
|
651
|
+
}
|
|
652
|
+
const delay = Math.min(err.retryAfterMs ?? BASE_DELAY_MS * Math.pow(2, attempt), MAX_DELAY_MS) + jitter();
|
|
653
|
+
retryOptions?.onRetry?.(attempt + 1, delay, err);
|
|
654
|
+
await sleep(delay);
|
|
671
655
|
}
|
|
672
656
|
throw new Error("Max retries exceeded");
|
|
673
657
|
}
|
|
@@ -732,12 +716,11 @@ function formatTicketForPrompt(ticket) {
|
|
|
732
716
|
}
|
|
733
717
|
function parseRequirementsFile(content) {
|
|
734
718
|
const jsonStr = extractJsonArray(content);
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
} catch (err) {
|
|
739
|
-
throw new Error(`Invalid JSON: ${err instanceof Error ? err.message : "parse error"}`, { cause: err });
|
|
719
|
+
const parseR = Result2.try(() => JSON.parse(jsonStr));
|
|
720
|
+
if (!parseR.ok) {
|
|
721
|
+
throw new Error(`Invalid JSON: ${parseR.error.message}`, { cause: parseR.error });
|
|
740
722
|
}
|
|
723
|
+
const parsed = parseR.value;
|
|
741
724
|
if (!Array.isArray(parsed)) {
|
|
742
725
|
throw new Error("Expected JSON array");
|
|
743
726
|
}
|
|
@@ -787,23 +770,20 @@ function parseArgs(args) {
|
|
|
787
770
|
}
|
|
788
771
|
async function sprintRefineCommand(args) {
|
|
789
772
|
const { sprintId, options } = parseArgs(args);
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
id = await resolveSprintId(sprintId);
|
|
793
|
-
} catch {
|
|
773
|
+
const idR = await wrapAsync(() => resolveSprintId(sprintId), ensureError);
|
|
774
|
+
if (!idR.ok) {
|
|
794
775
|
showWarning("No sprint specified and no current sprint set.");
|
|
795
776
|
showTip("Specify a sprint ID or create one first.");
|
|
796
777
|
log.newline();
|
|
797
778
|
return;
|
|
798
779
|
}
|
|
780
|
+
const id = idR.value;
|
|
799
781
|
const sprint = await getSprint(id);
|
|
800
782
|
try {
|
|
801
783
|
assertSprintStatus(sprint, ["draft"], "refine");
|
|
802
784
|
} catch (err) {
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
log.newline();
|
|
806
|
-
}
|
|
785
|
+
showError(err instanceof Error ? err.message : String(err));
|
|
786
|
+
log.newline();
|
|
807
787
|
return;
|
|
808
788
|
}
|
|
809
789
|
if (sprint.tickets.length === 0) {
|
|
@@ -861,9 +841,8 @@ async function sprintRefineCommand(args) {
|
|
|
861
841
|
console.log(fieldMultiline("Description", ticket.description, 14));
|
|
862
842
|
}
|
|
863
843
|
log.newline();
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
} catch {
|
|
844
|
+
const projectR = await wrapAsync(() => getProject(ticket.projectName), ensureError);
|
|
845
|
+
if (!projectR.ok) {
|
|
867
846
|
showWarning(`Project '${ticket.projectName}' not found.`);
|
|
868
847
|
log.dim("Skipping this ticket.");
|
|
869
848
|
log.newline();
|
|
@@ -884,21 +863,20 @@ async function sprintRefineCommand(args) {
|
|
|
884
863
|
if (ticket.link) {
|
|
885
864
|
const fetchSpinner = createSpinner("Fetching issue data...");
|
|
886
865
|
fetchSpinner.start();
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
fetchSpinner.stop();
|
|
894
|
-
}
|
|
895
|
-
} catch (err) {
|
|
866
|
+
const link = ticket.link;
|
|
867
|
+
const issueR = Result3.try(() => fetchIssueFromUrl(link));
|
|
868
|
+
if (issueR.ok && issueR.value) {
|
|
869
|
+
issueContext = formatIssueContext(issueR.value);
|
|
870
|
+
fetchSpinner.succeed(`Issue data fetched (${String(issueR.value.comments.length)} comment(s))`);
|
|
871
|
+
} else if (!issueR.ok) {
|
|
896
872
|
fetchSpinner.fail("Could not fetch issue data");
|
|
897
|
-
if (
|
|
898
|
-
showWarning(`${
|
|
899
|
-
} else
|
|
900
|
-
showWarning(`${
|
|
873
|
+
if (issueR.error instanceof IssueFetchError) {
|
|
874
|
+
showWarning(`${issueR.error.message} \u2014 continuing without issue context`);
|
|
875
|
+
} else {
|
|
876
|
+
showWarning(`${issueR.error.message} \u2014 continuing without issue context`);
|
|
901
877
|
}
|
|
878
|
+
} else {
|
|
879
|
+
fetchSpinner.stop();
|
|
902
880
|
}
|
|
903
881
|
}
|
|
904
882
|
const refineDir = getRefinementDir(id, ticket.id);
|
|
@@ -911,40 +889,32 @@ async function sprintRefineCommand(args) {
|
|
|
911
889
|
log.newline();
|
|
912
890
|
const spinner = createSpinner(`Starting ${providerName} session...`);
|
|
913
891
|
spinner.start();
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
spinner.succeed(`${providerName} session completed`);
|
|
917
|
-
} catch (err) {
|
|
892
|
+
const sessionR = await wrapAsync(() => runAiSession(refineDir, prompt, ticket.title), ensureError);
|
|
893
|
+
if (!sessionR.ok) {
|
|
918
894
|
spinner.fail(`${providerName} session failed`);
|
|
919
|
-
|
|
920
|
-
showError(err.message);
|
|
921
|
-
}
|
|
895
|
+
showError(sessionR.error.message);
|
|
922
896
|
log.newline();
|
|
923
897
|
skipped++;
|
|
924
898
|
continue;
|
|
925
899
|
}
|
|
900
|
+
spinner.succeed(`${providerName} session completed`);
|
|
926
901
|
log.newline();
|
|
927
902
|
if (await fileExists(outputFile)) {
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
content = await readFile(outputFile, "utf-8");
|
|
931
|
-
} catch {
|
|
903
|
+
const contentR = await wrapAsync(() => readFile(outputFile, "utf-8"), ensureError);
|
|
904
|
+
if (!contentR.ok) {
|
|
932
905
|
showError(`Failed to read requirements file: ${outputFile}`);
|
|
933
906
|
log.newline();
|
|
934
907
|
skipped++;
|
|
935
908
|
continue;
|
|
936
909
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
} catch (err) {
|
|
941
|
-
if (err instanceof Error) {
|
|
942
|
-
showError(`Failed to parse requirements file: ${err.message}`);
|
|
943
|
-
}
|
|
910
|
+
const parseR = Result3.try(() => parseRequirementsFile(contentR.value));
|
|
911
|
+
if (!parseR.ok) {
|
|
912
|
+
showError(`Failed to parse requirements file: ${parseR.error.message}`);
|
|
944
913
|
log.newline();
|
|
945
914
|
skipped++;
|
|
946
915
|
continue;
|
|
947
916
|
}
|
|
917
|
+
const refinedRequirements = parseR.value;
|
|
948
918
|
if (refinedRequirements.length === 0) {
|
|
949
919
|
showWarning("No requirements found in output file.");
|
|
950
920
|
log.newline();
|
|
@@ -1012,15 +982,11 @@ ${text}`;
|
|
|
1012
982
|
showSuccess("All requirements approved!");
|
|
1013
983
|
const sprintDir = getSprintDir(id);
|
|
1014
984
|
const outputPath = join4(sprintDir, "requirements.md");
|
|
1015
|
-
|
|
1016
|
-
|
|
985
|
+
const exportR = await wrapAsync(() => exportRequirementsToMarkdown(updatedSprint, outputPath), ensureError);
|
|
986
|
+
if (exportR.ok) {
|
|
1017
987
|
log.dim(`Requirements saved to: ${outputPath}`);
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
showError(`Failed to write requirements: ${err.message}`);
|
|
1021
|
-
} else {
|
|
1022
|
-
showError("Failed to write requirements: Unknown error");
|
|
1023
|
-
}
|
|
988
|
+
} else {
|
|
989
|
+
showError(`Failed to write requirements: ${exportR.error.message}`);
|
|
1024
990
|
}
|
|
1025
991
|
showTip('Run "ralphctl sprint plan" to generate tasks.');
|
|
1026
992
|
} else {
|
|
@@ -1034,31 +1000,19 @@ ${text}`;
|
|
|
1034
1000
|
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
1035
1001
|
import { join as join5 } from "path";
|
|
1036
1002
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
1003
|
+
import { Result as Result4 } from "typescript-result";
|
|
1037
1004
|
|
|
1038
1005
|
// src/store/task.ts
|
|
1039
|
-
var TaskNotFoundError = class extends Error {
|
|
1040
|
-
taskId;
|
|
1041
|
-
constructor(taskId) {
|
|
1042
|
-
super(`Task not found: ${taskId}`);
|
|
1043
|
-
this.name = "TaskNotFoundError";
|
|
1044
|
-
this.taskId = taskId;
|
|
1045
|
-
}
|
|
1046
|
-
};
|
|
1047
|
-
var DependencyCycleError = class extends Error {
|
|
1048
|
-
cycle;
|
|
1049
|
-
constructor(cycle) {
|
|
1050
|
-
super(`Dependency cycle detected: ${cycle.join(" \u2192 ")}`);
|
|
1051
|
-
this.name = "DependencyCycleError";
|
|
1052
|
-
this.cycle = cycle;
|
|
1053
|
-
}
|
|
1054
|
-
};
|
|
1055
1006
|
async function getTasks(sprintId) {
|
|
1056
1007
|
const id = await resolveSprintId(sprintId);
|
|
1057
|
-
|
|
1008
|
+
const result = await readValidatedJson(getTasksFilePath(id), TasksSchema);
|
|
1009
|
+
if (!result.ok) throw result.error;
|
|
1010
|
+
return result.value;
|
|
1058
1011
|
}
|
|
1059
1012
|
async function saveTasks(tasks, sprintId) {
|
|
1060
1013
|
const id = await resolveSprintId(sprintId);
|
|
1061
|
-
await writeValidatedJson(getTasksFilePath(id), tasks, TasksSchema);
|
|
1014
|
+
const result = await writeValidatedJson(getTasksFilePath(id), tasks, TasksSchema);
|
|
1015
|
+
if (!result.ok) throw result.error;
|
|
1062
1016
|
}
|
|
1063
1017
|
async function getTask(taskId, sprintId) {
|
|
1064
1018
|
const tasks = await getTasks(sprintId);
|
|
@@ -1073,7 +1027,7 @@ async function addTask(input3, sprintId) {
|
|
|
1073
1027
|
const sprint = await getSprint(id);
|
|
1074
1028
|
assertSprintStatus(sprint, ["draft"], "add tasks");
|
|
1075
1029
|
const tasksFilePath = getTasksFilePath(id);
|
|
1076
|
-
|
|
1030
|
+
const lockResult = await withFileLock(tasksFilePath, async () => {
|
|
1077
1031
|
const tasks = await getTasks(id);
|
|
1078
1032
|
const maxOrder = tasks.reduce((max, t) => Math.max(max, t.order), 0);
|
|
1079
1033
|
const task = {
|
|
@@ -1092,13 +1046,15 @@ async function addTask(input3, sprintId) {
|
|
|
1092
1046
|
await saveTasks(tasks, id);
|
|
1093
1047
|
return task;
|
|
1094
1048
|
});
|
|
1049
|
+
if (!lockResult.ok) throw lockResult.error;
|
|
1050
|
+
return lockResult.value;
|
|
1095
1051
|
}
|
|
1096
1052
|
async function removeTask(taskId, sprintId) {
|
|
1097
1053
|
const id = await resolveSprintId(sprintId);
|
|
1098
1054
|
const sprint = await getSprint(id);
|
|
1099
1055
|
assertSprintStatus(sprint, ["draft"], "remove tasks");
|
|
1100
1056
|
const tasksFilePath = getTasksFilePath(id);
|
|
1101
|
-
await withFileLock(tasksFilePath, async () => {
|
|
1057
|
+
const lockResult = await withFileLock(tasksFilePath, async () => {
|
|
1102
1058
|
const tasks = await getTasks(id);
|
|
1103
1059
|
const index = tasks.findIndex((t) => t.id === taskId);
|
|
1104
1060
|
if (index === -1) {
|
|
@@ -1107,13 +1063,14 @@ async function removeTask(taskId, sprintId) {
|
|
|
1107
1063
|
tasks.splice(index, 1);
|
|
1108
1064
|
await saveTasks(tasks, id);
|
|
1109
1065
|
});
|
|
1066
|
+
if (!lockResult.ok) throw lockResult.error;
|
|
1110
1067
|
}
|
|
1111
1068
|
async function updateTaskStatus(taskId, status, sprintId) {
|
|
1112
1069
|
const id = await resolveSprintId(sprintId);
|
|
1113
1070
|
const sprint = await getSprint(id);
|
|
1114
1071
|
assertSprintStatus(sprint, ["active"], "update task status");
|
|
1115
1072
|
const tasksFilePath = getTasksFilePath(id);
|
|
1116
|
-
|
|
1073
|
+
const lockResult = await withFileLock(tasksFilePath, async () => {
|
|
1117
1074
|
const tasks = await getTasks(id);
|
|
1118
1075
|
const task = tasks.find((t) => t.id === taskId);
|
|
1119
1076
|
if (!task) {
|
|
@@ -1123,13 +1080,15 @@ async function updateTaskStatus(taskId, status, sprintId) {
|
|
|
1123
1080
|
await saveTasks(tasks, id);
|
|
1124
1081
|
return task;
|
|
1125
1082
|
});
|
|
1083
|
+
if (!lockResult.ok) throw lockResult.error;
|
|
1084
|
+
return lockResult.value;
|
|
1126
1085
|
}
|
|
1127
1086
|
async function updateTask(taskId, updates, sprintId) {
|
|
1128
1087
|
const id = await resolveSprintId(sprintId);
|
|
1129
1088
|
const sprint = await getSprint(id);
|
|
1130
1089
|
assertSprintStatus(sprint, ["active"], "update task");
|
|
1131
1090
|
const tasksFilePath = getTasksFilePath(id);
|
|
1132
|
-
|
|
1091
|
+
const lockResult = await withFileLock(tasksFilePath, async () => {
|
|
1133
1092
|
const tasks = await getTasks(id);
|
|
1134
1093
|
const task = tasks.find((t) => t.id === taskId);
|
|
1135
1094
|
if (!task) {
|
|
@@ -1144,6 +1103,8 @@ async function updateTask(taskId, updates, sprintId) {
|
|
|
1144
1103
|
await saveTasks(tasks, id);
|
|
1145
1104
|
return task;
|
|
1146
1105
|
});
|
|
1106
|
+
if (!lockResult.ok) throw lockResult.error;
|
|
1107
|
+
return lockResult.value;
|
|
1147
1108
|
}
|
|
1148
1109
|
async function isTaskBlocked(taskId, sprintId) {
|
|
1149
1110
|
const tasks = await getTasks(sprintId);
|
|
@@ -1175,7 +1136,7 @@ async function reorderTask(taskId, newOrder, sprintId) {
|
|
|
1175
1136
|
const sprint = await getSprint(id);
|
|
1176
1137
|
assertSprintStatus(sprint, ["draft"], "reorder tasks");
|
|
1177
1138
|
const tasksFilePath = getTasksFilePath(id);
|
|
1178
|
-
|
|
1139
|
+
const lockResult = await withFileLock(tasksFilePath, async () => {
|
|
1179
1140
|
const tasks = await getTasks(id);
|
|
1180
1141
|
const task = tasks.find((t) => t.id === taskId);
|
|
1181
1142
|
if (!task) {
|
|
@@ -1198,6 +1159,8 @@ async function reorderTask(taskId, newOrder, sprintId) {
|
|
|
1198
1159
|
await saveTasks(tasks, id);
|
|
1199
1160
|
return task;
|
|
1200
1161
|
});
|
|
1162
|
+
if (!lockResult.ok) throw lockResult.error;
|
|
1163
|
+
return lockResult.value;
|
|
1201
1164
|
}
|
|
1202
1165
|
async function listTasks(sprintId) {
|
|
1203
1166
|
const tasks = await getTasks(sprintId);
|
|
@@ -1240,7 +1203,7 @@ function topologicalSort(tasks) {
|
|
|
1240
1203
|
async function reorderByDependencies(sprintId) {
|
|
1241
1204
|
const id = await resolveSprintId(sprintId);
|
|
1242
1205
|
const tasksFilePath = getTasksFilePath(id);
|
|
1243
|
-
await withFileLock(tasksFilePath, async () => {
|
|
1206
|
+
const lockResult = await withFileLock(tasksFilePath, async () => {
|
|
1244
1207
|
const tasks = await getTasks(id);
|
|
1245
1208
|
if (tasks.length === 0) return;
|
|
1246
1209
|
const sorted = topologicalSort(tasks);
|
|
@@ -1249,6 +1212,7 @@ async function reorderByDependencies(sprintId) {
|
|
|
1249
1212
|
});
|
|
1250
1213
|
await saveTasks(sorted, id);
|
|
1251
1214
|
});
|
|
1215
|
+
if (!lockResult.ok) throw lockResult.error;
|
|
1252
1216
|
}
|
|
1253
1217
|
function validateImportTasks(importTasks2, existingTasks, ticketIds) {
|
|
1254
1218
|
const errors = [];
|
|
@@ -1331,7 +1295,7 @@ async function selectProject(message = "Select project:") {
|
|
|
1331
1295
|
default: true
|
|
1332
1296
|
});
|
|
1333
1297
|
if (create) {
|
|
1334
|
-
const { projectAddCommand } = await import("./add-
|
|
1298
|
+
const { projectAddCommand } = await import("./add-DVEYDCTR.mjs");
|
|
1335
1299
|
await projectAddCommand({ interactive: true });
|
|
1336
1300
|
const updated = await listProjects();
|
|
1337
1301
|
if (updated.length === 0) return null;
|
|
@@ -1404,7 +1368,7 @@ async function selectSprint(message = "Select sprint:", filter) {
|
|
|
1404
1368
|
default: true
|
|
1405
1369
|
});
|
|
1406
1370
|
if (create) {
|
|
1407
|
-
const { sprintCreateCommand } = await import("./create-
|
|
1371
|
+
const { sprintCreateCommand } = await import("./create-MQ4OHZAX.mjs");
|
|
1408
1372
|
await sprintCreateCommand({ interactive: true });
|
|
1409
1373
|
const updated = await listSprints();
|
|
1410
1374
|
const refiltered = filter ? updated.filter((s) => filter.includes(s.status)) : updated;
|
|
@@ -1439,7 +1403,7 @@ async function selectTicket(message = "Select ticket:", filter) {
|
|
|
1439
1403
|
default: true
|
|
1440
1404
|
});
|
|
1441
1405
|
if (create) {
|
|
1442
|
-
const { ticketAddCommand } = await import("./add-
|
|
1406
|
+
const { ticketAddCommand } = await import("./add-7LBVENXM.mjs");
|
|
1443
1407
|
await ticketAddCommand({ interactive: true });
|
|
1444
1408
|
const updated = await listTickets();
|
|
1445
1409
|
const refiltered = filter ? updated.filter(filter) : updated;
|
|
@@ -1569,7 +1533,7 @@ async function importTasksAppend(tasks, sprintId) {
|
|
|
1569
1533
|
const localToRealId = /* @__PURE__ */ new Map();
|
|
1570
1534
|
const createdTasks = [];
|
|
1571
1535
|
for (const taskInput of tasks) {
|
|
1572
|
-
|
|
1536
|
+
const addR = await wrapAsync(async () => {
|
|
1573
1537
|
const projectPath = taskInput.projectPath;
|
|
1574
1538
|
const task = await addTask(
|
|
1575
1539
|
{
|
|
@@ -1583,32 +1547,36 @@ async function importTasksAppend(tasks, sprintId) {
|
|
|
1583
1547
|
},
|
|
1584
1548
|
sprintId
|
|
1585
1549
|
);
|
|
1550
|
+
return task;
|
|
1551
|
+
}, ensureError);
|
|
1552
|
+
if (addR.ok) {
|
|
1553
|
+
const task = addR.value;
|
|
1586
1554
|
if (taskInput.id) {
|
|
1587
1555
|
localToRealId.set(taskInput.id, task.id);
|
|
1588
1556
|
}
|
|
1589
1557
|
createdTasks.push({ task: taskInput, realId: task.id });
|
|
1590
1558
|
log.itemSuccess(`${task.id}: ${task.name}`);
|
|
1591
|
-
}
|
|
1559
|
+
} else {
|
|
1592
1560
|
log.itemError(`Failed to add: ${taskInput.name}`);
|
|
1593
|
-
|
|
1594
|
-
console.log(muted(` ${err.message}`));
|
|
1595
|
-
}
|
|
1561
|
+
console.log(muted(` ${addR.error.message}`));
|
|
1596
1562
|
}
|
|
1597
1563
|
}
|
|
1598
1564
|
const tasksFilePath = getTasksFilePath(sprintId);
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
const
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
taskToUpdate
|
|
1565
|
+
unwrapOrThrow(
|
|
1566
|
+
await withFileLock(tasksFilePath, async () => {
|
|
1567
|
+
const allTasks = await getTasks(sprintId);
|
|
1568
|
+
for (const { task: taskInput, realId } of createdTasks) {
|
|
1569
|
+
const blockedBy = (taskInput.blockedBy ?? []).map((localId) => localToRealId.get(localId) ?? "").filter((id) => id !== "");
|
|
1570
|
+
if (blockedBy.length > 0) {
|
|
1571
|
+
const taskToUpdate = allTasks.find((t) => t.id === realId);
|
|
1572
|
+
if (taskToUpdate) {
|
|
1573
|
+
taskToUpdate.blockedBy = blockedBy;
|
|
1574
|
+
}
|
|
1607
1575
|
}
|
|
1608
1576
|
}
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1577
|
+
await saveTasks(allTasks, sprintId);
|
|
1578
|
+
})
|
|
1579
|
+
);
|
|
1612
1580
|
return createdTasks.length;
|
|
1613
1581
|
}
|
|
1614
1582
|
async function importTasksReplace(tasks, sprintId) {
|
|
@@ -1669,17 +1637,17 @@ async function getSprintContext(sprintName, ticketsByProject, existingTasks) {
|
|
|
1669
1637
|
for (const [projectName, tickets] of ticketsByProject) {
|
|
1670
1638
|
lines.push("");
|
|
1671
1639
|
lines.push(`## Project: ${projectName}`);
|
|
1672
|
-
|
|
1673
|
-
|
|
1640
|
+
const projectR = await wrapAsync(() => getProject(projectName), ensureError);
|
|
1641
|
+
if (projectR.ok) {
|
|
1674
1642
|
lines.push("");
|
|
1675
1643
|
lines.push("### Repositories");
|
|
1676
|
-
for (const repo of
|
|
1644
|
+
for (const repo of projectR.value.repositories) {
|
|
1677
1645
|
lines.push(`- **${repo.name}**: ${repo.path}`);
|
|
1678
1646
|
if (repo.checkScript) {
|
|
1679
1647
|
lines.push(` - Check: \`${repo.checkScript}\``);
|
|
1680
1648
|
}
|
|
1681
1649
|
}
|
|
1682
|
-
}
|
|
1650
|
+
} else {
|
|
1683
1651
|
lines.push("Repositories: (project not found)");
|
|
1684
1652
|
}
|
|
1685
1653
|
lines.push("");
|
|
@@ -1758,23 +1726,20 @@ async function invokeAiAuto(prompt, repoPaths, planDir) {
|
|
|
1758
1726
|
}
|
|
1759
1727
|
async function sprintPlanCommand(args) {
|
|
1760
1728
|
const { sprintId, options } = parseArgs2(args);
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
id = await resolveSprintId(sprintId);
|
|
1764
|
-
} catch {
|
|
1729
|
+
const idR = await wrapAsync(() => resolveSprintId(sprintId), ensureError);
|
|
1730
|
+
if (!idR.ok) {
|
|
1765
1731
|
showWarning("No sprint specified and no current sprint set.");
|
|
1766
1732
|
showNextStep("ralphctl sprint create", "create a new sprint");
|
|
1767
1733
|
log.newline();
|
|
1768
1734
|
return;
|
|
1769
1735
|
}
|
|
1736
|
+
const id = idR.value;
|
|
1770
1737
|
const sprint = await getSprint(id);
|
|
1771
1738
|
try {
|
|
1772
1739
|
assertSprintStatus(sprint, ["draft"], "plan");
|
|
1773
1740
|
} catch (err) {
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
log.newline();
|
|
1777
|
-
}
|
|
1741
|
+
showError(err instanceof Error ? err.message : String(err));
|
|
1742
|
+
log.newline();
|
|
1778
1743
|
return;
|
|
1779
1744
|
}
|
|
1780
1745
|
if (sprint.tickets.length === 0) {
|
|
@@ -1831,11 +1796,10 @@ async function sprintPlanCommand(args) {
|
|
|
1831
1796
|
const defaultPaths = [];
|
|
1832
1797
|
for (const ticket of ticketsToProcess) {
|
|
1833
1798
|
if (reposByProject.has(ticket.projectName)) continue;
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
reposByProject.set(ticket.projectName,
|
|
1837
|
-
if (
|
|
1838
|
-
} catch {
|
|
1799
|
+
const projectR = await wrapAsync(() => getProject(ticket.projectName), ensureError);
|
|
1800
|
+
if (projectR.ok) {
|
|
1801
|
+
reposByProject.set(ticket.projectName, projectR.value.repositories);
|
|
1802
|
+
if (projectR.value.repositories[0]) defaultPaths.push(projectR.value.repositories[0].path);
|
|
1839
1803
|
}
|
|
1840
1804
|
}
|
|
1841
1805
|
const savedPaths = /* @__PURE__ */ new Set();
|
|
@@ -1900,19 +1864,16 @@ async function sprintPlanCommand(args) {
|
|
|
1900
1864
|
const prompt = buildAutoPrompt(context, schema);
|
|
1901
1865
|
const spinner = createSpinner(`${providerName} is planning tasks...`);
|
|
1902
1866
|
spinner.start();
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
output = await invokeAiAuto(prompt, selectedPaths, planDir);
|
|
1906
|
-
spinner.succeed(`${providerName} finished planning`);
|
|
1907
|
-
} catch (err) {
|
|
1867
|
+
const outputR = await wrapAsync(() => invokeAiAuto(prompt, selectedPaths, planDir), ensureError);
|
|
1868
|
+
if (!outputR.ok) {
|
|
1908
1869
|
spinner.fail(`${providerName} planning failed`);
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
log.newline();
|
|
1913
|
-
}
|
|
1870
|
+
showError(`Failed to invoke ${providerName}: ${outputR.error.message}`);
|
|
1871
|
+
showTip(`Make sure the ${providerName.toLowerCase()} CLI is installed and configured.`);
|
|
1872
|
+
log.newline();
|
|
1914
1873
|
return;
|
|
1915
1874
|
}
|
|
1875
|
+
spinner.succeed(`${providerName} finished planning`);
|
|
1876
|
+
const output = outputR.value;
|
|
1916
1877
|
const blockedReason = parsePlanningBlocked(output);
|
|
1917
1878
|
if (blockedReason) {
|
|
1918
1879
|
showWarning(`Planning blocked: ${blockedReason}`);
|
|
@@ -1920,18 +1881,15 @@ async function sprintPlanCommand(args) {
|
|
|
1920
1881
|
return;
|
|
1921
1882
|
}
|
|
1922
1883
|
console.log(muted("Parsing response..."));
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
log.dim("Raw output:");
|
|
1930
|
-
console.log(output);
|
|
1931
|
-
log.newline();
|
|
1932
|
-
}
|
|
1884
|
+
const parsedR = Result4.try(() => parseTasksJson(output));
|
|
1885
|
+
if (!parsedR.ok) {
|
|
1886
|
+
showError(`Failed to parse ${providerName} output: ${parsedR.error.message}`);
|
|
1887
|
+
log.dim("Raw output:");
|
|
1888
|
+
console.log(output);
|
|
1889
|
+
log.newline();
|
|
1933
1890
|
return;
|
|
1934
1891
|
}
|
|
1892
|
+
const parsedTasks = parsedR.value;
|
|
1935
1893
|
if (parsedTasks.length === 0) {
|
|
1936
1894
|
showWarning("No tasks generated.");
|
|
1937
1895
|
log.newline();
|
|
@@ -1972,37 +1930,29 @@ async function sprintPlanCommand(args) {
|
|
|
1972
1930
|
${providerName} will read planning-context.md and explore the repos.`));
|
|
1973
1931
|
console.log(muted(` When done, ask ${providerName} to write tasks to: ${outputFile}
|
|
1974
1932
|
`));
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
showTip(`Make sure the ${providerName.toLowerCase()} CLI is installed and configured.`);
|
|
1981
|
-
log.newline();
|
|
1982
|
-
}
|
|
1933
|
+
const interactiveR = await wrapAsync(() => invokeAiInteractive(prompt, selectedPaths, planDir), ensureError);
|
|
1934
|
+
if (!interactiveR.ok) {
|
|
1935
|
+
showError(`Failed to invoke ${providerName}: ${interactiveR.error.message}`);
|
|
1936
|
+
showTip(`Make sure the ${providerName.toLowerCase()} CLI is installed and configured.`);
|
|
1937
|
+
log.newline();
|
|
1983
1938
|
return;
|
|
1984
1939
|
}
|
|
1985
1940
|
console.log("");
|
|
1986
1941
|
if (await fileExists(outputFile)) {
|
|
1987
1942
|
showInfo("Task file found. Processing...");
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
content = await readFile3(outputFile, "utf-8");
|
|
1991
|
-
} catch {
|
|
1943
|
+
const contentR = await wrapAsync(() => readFile3(outputFile, "utf-8"), ensureError);
|
|
1944
|
+
if (!contentR.ok) {
|
|
1992
1945
|
showError(`Failed to read task file: ${outputFile}`);
|
|
1993
1946
|
log.newline();
|
|
1994
1947
|
return;
|
|
1995
1948
|
}
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
if (err instanceof Error) {
|
|
2001
|
-
showError(`Failed to parse task file: ${err.message}`);
|
|
2002
|
-
log.newline();
|
|
2003
|
-
}
|
|
1949
|
+
const parsedR = Result4.try(() => parseTasksJson(contentR.value));
|
|
1950
|
+
if (!parsedR.ok) {
|
|
1951
|
+
showError(`Failed to parse task file: ${parsedR.error.message}`);
|
|
1952
|
+
log.newline();
|
|
2004
1953
|
return;
|
|
2005
1954
|
}
|
|
1955
|
+
const parsedTasks = parsedR.value;
|
|
2006
1956
|
if (parsedTasks.length === 0) {
|
|
2007
1957
|
showWarning("No tasks in file.");
|
|
2008
1958
|
log.newline();
|
|
@@ -2038,8 +1988,12 @@ async function sprintPlanCommand(args) {
|
|
|
2038
1988
|
}
|
|
2039
1989
|
}
|
|
2040
1990
|
|
|
1991
|
+
// src/commands/sprint/start.ts
|
|
1992
|
+
import { Result as Result8 } from "typescript-result";
|
|
1993
|
+
|
|
2041
1994
|
// src/ai/runner.ts
|
|
2042
1995
|
import { confirm as confirm5, input as input2, select as select2 } from "@inquirer/prompts";
|
|
1996
|
+
import { Result as Result7 } from "typescript-result";
|
|
2043
1997
|
|
|
2044
1998
|
// src/ai/executor.ts
|
|
2045
1999
|
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
@@ -2141,11 +2095,13 @@ var RateLimitCoordinator = class {
|
|
|
2141
2095
|
import { execSync } from "child_process";
|
|
2142
2096
|
import { writeFile as writeFile4 } from "fs/promises";
|
|
2143
2097
|
import { join as join7 } from "path";
|
|
2098
|
+
import { Result as Result6 } from "typescript-result";
|
|
2144
2099
|
|
|
2145
2100
|
// src/ai/permissions.ts
|
|
2146
2101
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
2147
2102
|
import { join as join6 } from "path";
|
|
2148
2103
|
import { homedir } from "os";
|
|
2104
|
+
import { Result as Result5 } from "typescript-result";
|
|
2149
2105
|
function getProviderPermissions(projectPath, provider) {
|
|
2150
2106
|
const permissions = {
|
|
2151
2107
|
allow: [],
|
|
@@ -2156,30 +2112,34 @@ function getProviderPermissions(projectPath, provider) {
|
|
|
2156
2112
|
}
|
|
2157
2113
|
const projectSettingsPath = join6(projectPath, ".claude", "settings.local.json");
|
|
2158
2114
|
if (existsSync2(projectSettingsPath)) {
|
|
2159
|
-
try {
|
|
2115
|
+
const projectResult = Result5.try(() => {
|
|
2160
2116
|
const content = readFileSync2(projectSettingsPath, "utf-8");
|
|
2161
|
-
|
|
2117
|
+
return JSON.parse(content);
|
|
2118
|
+
});
|
|
2119
|
+
if (projectResult.ok) {
|
|
2120
|
+
const settings = projectResult.value;
|
|
2162
2121
|
if (settings.permissions?.allow) {
|
|
2163
2122
|
permissions.allow.push(...settings.permissions.allow);
|
|
2164
2123
|
}
|
|
2165
2124
|
if (settings.permissions?.deny) {
|
|
2166
2125
|
permissions.deny.push(...settings.permissions.deny);
|
|
2167
2126
|
}
|
|
2168
|
-
} catch {
|
|
2169
2127
|
}
|
|
2170
2128
|
}
|
|
2171
2129
|
const userSettingsPath = join6(homedir(), ".claude", "settings.json");
|
|
2172
2130
|
if (existsSync2(userSettingsPath)) {
|
|
2173
|
-
try {
|
|
2131
|
+
const userResult = Result5.try(() => {
|
|
2174
2132
|
const content = readFileSync2(userSettingsPath, "utf-8");
|
|
2175
|
-
|
|
2133
|
+
return JSON.parse(content);
|
|
2134
|
+
});
|
|
2135
|
+
if (userResult.ok) {
|
|
2136
|
+
const settings = userResult.value;
|
|
2176
2137
|
if (settings.permissions?.allow) {
|
|
2177
2138
|
permissions.allow.push(...settings.permissions.allow);
|
|
2178
2139
|
}
|
|
2179
2140
|
if (settings.permissions?.deny) {
|
|
2180
2141
|
permissions.deny.push(...settings.permissions.deny);
|
|
2181
2142
|
}
|
|
2182
|
-
} catch {
|
|
2183
2143
|
}
|
|
2184
2144
|
}
|
|
2185
2145
|
return permissions;
|
|
@@ -2251,7 +2211,7 @@ function checkTaskPermissions(projectPath, options) {
|
|
|
2251
2211
|
|
|
2252
2212
|
// src/ai/task-context.ts
|
|
2253
2213
|
function getRecentGitHistory(projectPath, count = 20) {
|
|
2254
|
-
try {
|
|
2214
|
+
const r = Result6.try(() => {
|
|
2255
2215
|
assertSafeCwd(projectPath);
|
|
2256
2216
|
const result = execSync(`git log -${String(count)} --oneline --no-decorate`, {
|
|
2257
2217
|
cwd: projectPath,
|
|
@@ -2259,9 +2219,8 @@ function getRecentGitHistory(projectPath, count = 20) {
|
|
|
2259
2219
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2260
2220
|
});
|
|
2261
2221
|
return result.trim();
|
|
2262
|
-
}
|
|
2263
|
-
|
|
2264
|
-
}
|
|
2222
|
+
});
|
|
2223
|
+
return r.ok ? r.value : "(Unable to retrieve git history)";
|
|
2265
2224
|
}
|
|
2266
2225
|
function getEffectiveCheckScript(project, projectPath) {
|
|
2267
2226
|
if (project) {
|
|
@@ -2390,14 +2349,10 @@ async function getProjectForTask(task, sprint) {
|
|
|
2390
2349
|
if (!task.ticketId) return void 0;
|
|
2391
2350
|
const ticket = sprint.tickets.find((t) => t.id === task.ticketId);
|
|
2392
2351
|
if (!ticket) return void 0;
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
return void 0;
|
|
2398
|
-
}
|
|
2399
|
-
throw err;
|
|
2400
|
-
}
|
|
2352
|
+
const r = await wrapAsync(async () => getProject(ticket.projectName), ensureError);
|
|
2353
|
+
if (r.ok) return r.value;
|
|
2354
|
+
if (r.error instanceof ProjectNotFoundError) return void 0;
|
|
2355
|
+
throw r.error;
|
|
2401
2356
|
}
|
|
2402
2357
|
function runPermissionCheck(ctx, noCommit, provider) {
|
|
2403
2358
|
const checkScript = getEffectiveCheckScript(ctx.project, ctx.task.projectPath);
|
|
@@ -2479,10 +2434,7 @@ async function executeTask(ctx, options, sprintId, resumeSessionId, provider, ch
|
|
|
2479
2434
|
sessionId: null
|
|
2480
2435
|
};
|
|
2481
2436
|
} finally {
|
|
2482
|
-
|
|
2483
|
-
await unlink2(contextFile);
|
|
2484
|
-
} catch {
|
|
2485
|
-
}
|
|
2437
|
+
await unlink2(contextFile).catch(() => void 0);
|
|
2486
2438
|
}
|
|
2487
2439
|
}
|
|
2488
2440
|
let spawnResult;
|
|
@@ -2552,10 +2504,7 @@ async function executeTask(ctx, options, sprintId, resumeSessionId, provider, ch
|
|
|
2552
2504
|
throw err;
|
|
2553
2505
|
} finally {
|
|
2554
2506
|
deregister();
|
|
2555
|
-
|
|
2556
|
-
await unlink2(contextFile);
|
|
2557
|
-
} catch {
|
|
2558
|
-
}
|
|
2507
|
+
await unlink2(contextFile).catch(() => void 0);
|
|
2559
2508
|
}
|
|
2560
2509
|
}
|
|
2561
2510
|
const parsed = parseExecutionResult(spawnResult.stdout);
|
|
@@ -2923,43 +2872,28 @@ Resuming ${String(inProgressTasks.length)} in-progress task(s):`));
|
|
|
2923
2872
|
}
|
|
2924
2873
|
inFlightPaths.add(task.projectPath);
|
|
2925
2874
|
const taskPromise = (async () => {
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
checkResults?.get(task.projectPath)
|
|
2935
|
-
);
|
|
2936
|
-
if (result.sessionId) {
|
|
2937
|
-
taskSessionIds.set(task.id, result.sessionId);
|
|
2938
|
-
}
|
|
2939
|
-
return { task, result, error: null, isRateLimited: false };
|
|
2940
|
-
} catch (err) {
|
|
2875
|
+
const ctx = { sprint, task, project };
|
|
2876
|
+
const resultR = await wrapAsync(
|
|
2877
|
+
() => executeTask(ctx, options, sprintId, resumeId, provider, checkResults?.get(task.projectPath)),
|
|
2878
|
+
ensureError
|
|
2879
|
+
);
|
|
2880
|
+
inFlightPaths.delete(task.projectPath);
|
|
2881
|
+
if (!resultR.ok) {
|
|
2882
|
+
const err = resultR.error;
|
|
2941
2883
|
if (err instanceof SpawnError && err.rateLimited) {
|
|
2942
2884
|
if (err.sessionId) {
|
|
2943
2885
|
taskSessionIds.set(task.id, err.sessionId);
|
|
2944
2886
|
}
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
return {
|
|
2948
|
-
task,
|
|
2949
|
-
result: null,
|
|
2950
|
-
error: err,
|
|
2951
|
-
isRateLimited: true
|
|
2952
|
-
};
|
|
2887
|
+
coordinator.pause(err.retryAfterMs ?? 6e4);
|
|
2888
|
+
return { task, result: null, error: err, isRateLimited: true };
|
|
2953
2889
|
}
|
|
2954
|
-
return {
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
};
|
|
2960
|
-
} finally {
|
|
2961
|
-
inFlightPaths.delete(task.projectPath);
|
|
2890
|
+
return { task, result: null, error: err, isRateLimited: false };
|
|
2891
|
+
}
|
|
2892
|
+
const result = resultR.value;
|
|
2893
|
+
if (result.sessionId) {
|
|
2894
|
+
taskSessionIds.set(task.id, result.sessionId);
|
|
2962
2895
|
}
|
|
2896
|
+
return { task, result, error: null, isRateLimited: false };
|
|
2963
2897
|
})();
|
|
2964
2898
|
running.set(task.id, taskPromise);
|
|
2965
2899
|
}
|
|
@@ -3286,22 +3220,19 @@ async function ensureSprintBranches(sprintId, sprint, branchName) {
|
|
|
3286
3220
|
const uniquePaths = [...new Set(remainingTasks.map((t) => t.projectPath))];
|
|
3287
3221
|
if (uniquePaths.length === 0) return;
|
|
3288
3222
|
for (const projectPath of uniquePaths) {
|
|
3289
|
-
try
|
|
3290
|
-
|
|
3291
|
-
throw new Error(
|
|
3292
|
-
`Repository at ${projectPath} has uncommitted changes. Commit or stash them before starting the sprint.`
|
|
3293
|
-
);
|
|
3294
|
-
}
|
|
3295
|
-
} catch (err) {
|
|
3296
|
-
if (err instanceof Error && err.message.includes("uncommitted changes")) {
|
|
3297
|
-
throw err;
|
|
3298
|
-
}
|
|
3223
|
+
const uncommittedR = Result7.try(() => hasUncommittedChanges(projectPath));
|
|
3224
|
+
if (!uncommittedR.ok) {
|
|
3299
3225
|
log.dim(` Skipping ${projectPath} \u2014 not a git repository`);
|
|
3300
3226
|
continue;
|
|
3301
3227
|
}
|
|
3228
|
+
if (uncommittedR.value) {
|
|
3229
|
+
throw new Error(
|
|
3230
|
+
`Repository at ${projectPath} has uncommitted changes. Commit or stash them before starting the sprint.`
|
|
3231
|
+
);
|
|
3232
|
+
}
|
|
3302
3233
|
}
|
|
3303
3234
|
for (const projectPath of uniquePaths) {
|
|
3304
|
-
try {
|
|
3235
|
+
const branchR = Result7.try(() => {
|
|
3305
3236
|
const currentBranch = getCurrentBranch(projectPath);
|
|
3306
3237
|
if (currentBranch === branchName) {
|
|
3307
3238
|
log.dim(` Already on branch '${branchName}' in ${projectPath}`);
|
|
@@ -3309,11 +3240,11 @@ async function ensureSprintBranches(sprintId, sprint, branchName) {
|
|
|
3309
3240
|
createAndCheckoutBranch(projectPath, branchName);
|
|
3310
3241
|
log.success(` Branch '${branchName}' ready in ${projectPath}`);
|
|
3311
3242
|
}
|
|
3312
|
-
}
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
);
|
|
3243
|
+
});
|
|
3244
|
+
if (!branchR.ok) {
|
|
3245
|
+
throw new Error(`Failed to create branch '${branchName}' in ${projectPath}: ${branchR.error.message}`, {
|
|
3246
|
+
cause: branchR.error
|
|
3247
|
+
});
|
|
3317
3248
|
}
|
|
3318
3249
|
}
|
|
3319
3250
|
if (sprint.branch !== branchName) {
|
|
@@ -3322,16 +3253,13 @@ async function ensureSprintBranches(sprintId, sprint, branchName) {
|
|
|
3322
3253
|
}
|
|
3323
3254
|
}
|
|
3324
3255
|
function verifySprintBranch(projectPath, expectedBranch) {
|
|
3325
|
-
try {
|
|
3326
|
-
if (verifyCurrentBranch(projectPath, expectedBranch))
|
|
3327
|
-
return true;
|
|
3328
|
-
}
|
|
3256
|
+
const r = Result7.try(() => {
|
|
3257
|
+
if (verifyCurrentBranch(projectPath, expectedBranch)) return true;
|
|
3329
3258
|
log.dim(` Branch mismatch in ${projectPath} \u2014 checking out '${expectedBranch}'`);
|
|
3330
3259
|
createAndCheckoutBranch(projectPath, expectedBranch);
|
|
3331
3260
|
return verifyCurrentBranch(projectPath, expectedBranch);
|
|
3332
|
-
}
|
|
3333
|
-
|
|
3334
|
-
}
|
|
3261
|
+
});
|
|
3262
|
+
return r.ok ? r.value : false;
|
|
3335
3263
|
}
|
|
3336
3264
|
async function runCheckScripts(sprintId, sprint, refreshCheck = false) {
|
|
3337
3265
|
const results = /* @__PURE__ */ new Map();
|
|
@@ -3461,28 +3389,26 @@ async function runSprint(sprintId, options) {
|
|
|
3461
3389
|
log.info(`Branch: ${branchName}`);
|
|
3462
3390
|
}
|
|
3463
3391
|
if (branchName) {
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
} catch (err) {
|
|
3392
|
+
const ensureR = await wrapAsync(() => ensureSprintBranches(id, sprint, branchName), ensureError);
|
|
3393
|
+
if (!ensureR.ok) {
|
|
3467
3394
|
log.newline();
|
|
3468
|
-
showError(
|
|
3395
|
+
showError(ensureR.error.message);
|
|
3469
3396
|
log.newline();
|
|
3470
3397
|
return void 0;
|
|
3471
3398
|
}
|
|
3472
3399
|
}
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
} catch (err) {
|
|
3477
|
-
if (err instanceof DependencyCycleError) {
|
|
3400
|
+
const reorderR = await wrapAsync(() => reorderByDependencies(id), ensureError);
|
|
3401
|
+
if (!reorderR.ok) {
|
|
3402
|
+
if (reorderR.error instanceof DependencyCycleError) {
|
|
3478
3403
|
log.newline();
|
|
3479
|
-
showWarning(
|
|
3404
|
+
showWarning(reorderR.error.message);
|
|
3480
3405
|
log.dim("Fix the dependency cycle before starting.");
|
|
3481
3406
|
log.newline();
|
|
3482
3407
|
return void 0;
|
|
3483
3408
|
}
|
|
3484
|
-
throw
|
|
3409
|
+
throw reorderR.error;
|
|
3485
3410
|
}
|
|
3411
|
+
log.dim("Tasks reordered by dependencies");
|
|
3486
3412
|
const checkResult = await runCheckScripts(id, sprint, options.refreshCheck);
|
|
3487
3413
|
if (!checkResult.success) {
|
|
3488
3414
|
log.newline();
|
|
@@ -3595,25 +3521,16 @@ function parseArgs3(args) {
|
|
|
3595
3521
|
return { sprintId, options };
|
|
3596
3522
|
}
|
|
3597
3523
|
async function sprintStartCommand(args) {
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
sprintId = parsed.sprintId;
|
|
3603
|
-
options = parsed.options;
|
|
3604
|
-
} catch (err) {
|
|
3605
|
-
if (err instanceof Error) {
|
|
3606
|
-
showError(err.message);
|
|
3607
|
-
log.newline();
|
|
3608
|
-
}
|
|
3524
|
+
const parseR = Result8.try(() => parseArgs3(args));
|
|
3525
|
+
if (!parseR.ok) {
|
|
3526
|
+
showError(parseR.error.message);
|
|
3527
|
+
log.newline();
|
|
3609
3528
|
exitWithCode(EXIT_ERROR);
|
|
3610
3529
|
}
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
}
|
|
3616
|
-
} catch (err) {
|
|
3530
|
+
const { sprintId, options } = parseR.value;
|
|
3531
|
+
const runR = await wrapAsync(() => runSprint(sprintId, options), ensureError);
|
|
3532
|
+
if (!runR.ok) {
|
|
3533
|
+
const err = runR.error;
|
|
3617
3534
|
if (err instanceof SprintNotFoundError) {
|
|
3618
3535
|
showError(`Sprint not found: ${sprintId ?? "unknown"}`);
|
|
3619
3536
|
log.newline();
|
|
@@ -3622,7 +3539,7 @@ async function sprintStartCommand(args) {
|
|
|
3622
3539
|
showError(err.message);
|
|
3623
3540
|
log.newline();
|
|
3624
3541
|
exitWithCode(EXIT_ERROR);
|
|
3625
|
-
} else if (err
|
|
3542
|
+
} else if (err.message.includes("No sprint specified")) {
|
|
3626
3543
|
showWarning("No sprint specified and no active sprint set.");
|
|
3627
3544
|
showNextStep("ralphctl sprint start <id>", "specify a sprint ID");
|
|
3628
3545
|
log.newline();
|
|
@@ -3630,11 +3547,15 @@ async function sprintStartCommand(args) {
|
|
|
3630
3547
|
} else {
|
|
3631
3548
|
throw err;
|
|
3632
3549
|
}
|
|
3550
|
+
return;
|
|
3551
|
+
}
|
|
3552
|
+
const summary = runR.value;
|
|
3553
|
+
if (summary) {
|
|
3554
|
+
exitWithCode(summary.exitCode);
|
|
3633
3555
|
}
|
|
3634
3556
|
}
|
|
3635
3557
|
|
|
3636
3558
|
export {
|
|
3637
|
-
TaskNotFoundError,
|
|
3638
3559
|
getTasks,
|
|
3639
3560
|
saveTasks,
|
|
3640
3561
|
getTask,
|