@saga-ai/cli 0.6.0 → 0.7.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/dist/cli.cjs +301 -47
- package/package.json +14 -3
package/dist/cli.cjs
CHANGED
|
@@ -25,8 +25,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
25
25
|
|
|
26
26
|
// src/cli.ts
|
|
27
27
|
var import_commander = require("commander");
|
|
28
|
-
var
|
|
29
|
-
var
|
|
28
|
+
var import_node_path8 = require("node:path");
|
|
29
|
+
var import_node_fs7 = require("node:fs");
|
|
30
30
|
|
|
31
31
|
// src/commands/init.ts
|
|
32
32
|
var import_node_path2 = require("node:path");
|
|
@@ -193,9 +193,9 @@ async function initCommand(options) {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
// src/commands/implement.ts
|
|
196
|
-
var
|
|
197
|
-
var
|
|
198
|
-
var
|
|
196
|
+
var import_node_child_process2 = require("node:child_process");
|
|
197
|
+
var import_node_path5 = require("node:path");
|
|
198
|
+
var import_node_fs5 = require("node:fs");
|
|
199
199
|
|
|
200
200
|
// src/utils/finder.ts
|
|
201
201
|
var import_node_fs3 = require("node:fs");
|
|
@@ -503,7 +503,7 @@ function findEpic(projectPath, query) {
|
|
|
503
503
|
matches: results.map((r) => r.item)
|
|
504
504
|
};
|
|
505
505
|
}
|
|
506
|
-
async function findStory(projectPath, query) {
|
|
506
|
+
async function findStory(projectPath, query, options = {}) {
|
|
507
507
|
if (!worktreesDirectoryExists(projectPath) && !epicsDirectoryExists(projectPath)) {
|
|
508
508
|
return {
|
|
509
509
|
found: false,
|
|
@@ -517,7 +517,16 @@ async function findStory(projectPath, query) {
|
|
|
517
517
|
error: `No story found matching '${query}'`
|
|
518
518
|
};
|
|
519
519
|
}
|
|
520
|
-
|
|
520
|
+
let allStories = scannedStories.map(toStoryInfo);
|
|
521
|
+
if (options.status) {
|
|
522
|
+
allStories = allStories.filter((story) => story.status === options.status);
|
|
523
|
+
if (allStories.length === 0) {
|
|
524
|
+
return {
|
|
525
|
+
found: false,
|
|
526
|
+
error: `No story found matching '${query}' with status '${options.status}'`
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
}
|
|
521
530
|
const queryNormalized = normalize(query);
|
|
522
531
|
for (const story of allStories) {
|
|
523
532
|
if (normalize(story.slug) === queryNormalized) {
|
|
@@ -571,6 +580,165 @@ async function findStory(projectPath, query) {
|
|
|
571
580
|
};
|
|
572
581
|
}
|
|
573
582
|
|
|
583
|
+
// src/lib/sessions.ts
|
|
584
|
+
var import_node_child_process = require("node:child_process");
|
|
585
|
+
var import_node_fs4 = require("node:fs");
|
|
586
|
+
var import_node_path4 = require("node:path");
|
|
587
|
+
var OUTPUT_DIR = "/tmp/saga-sessions";
|
|
588
|
+
function shellEscape(str) {
|
|
589
|
+
return "'" + str.replace(/'/g, "'\\''") + "'";
|
|
590
|
+
}
|
|
591
|
+
function shellEscapeArgs(args) {
|
|
592
|
+
return args.map(shellEscape).join(" ");
|
|
593
|
+
}
|
|
594
|
+
function validateSlug(slug) {
|
|
595
|
+
if (!slug || slug.length === 0) {
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
if (!/^[a-z0-9-]+$/.test(slug)) {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
if (slug.startsWith("-") || slug.endsWith("-")) {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
function checkTmuxAvailable() {
|
|
607
|
+
const result = (0, import_node_child_process.spawnSync)("which", ["tmux"], { encoding: "utf-8" });
|
|
608
|
+
if (result.status !== 0) {
|
|
609
|
+
throw new Error("tmux is not installed or not found in PATH");
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
async function createSession(epicSlug, storySlug, command) {
|
|
613
|
+
if (!validateSlug(epicSlug)) {
|
|
614
|
+
throw new Error(`Invalid epic slug: '${epicSlug}'. Must contain only [a-z0-9-] and not start/end with hyphen.`);
|
|
615
|
+
}
|
|
616
|
+
if (!validateSlug(storySlug)) {
|
|
617
|
+
throw new Error(`Invalid story slug: '${storySlug}'. Must contain only [a-z0-9-] and not start/end with hyphen.`);
|
|
618
|
+
}
|
|
619
|
+
checkTmuxAvailable();
|
|
620
|
+
if (!(0, import_node_fs4.existsSync)(OUTPUT_DIR)) {
|
|
621
|
+
(0, import_node_fs4.mkdirSync)(OUTPUT_DIR, { recursive: true });
|
|
622
|
+
}
|
|
623
|
+
const timestamp = Date.now();
|
|
624
|
+
const sessionName = `saga-${epicSlug}-${storySlug}-${timestamp}`;
|
|
625
|
+
const outputFile = (0, import_node_path4.join)(OUTPUT_DIR, `${sessionName}.out`);
|
|
626
|
+
const commandFilePath = (0, import_node_path4.join)(OUTPUT_DIR, `${sessionName}.cmd`);
|
|
627
|
+
const wrapperScriptPath = (0, import_node_path4.join)(OUTPUT_DIR, `${sessionName}.sh`);
|
|
628
|
+
(0, import_node_fs4.writeFileSync)(commandFilePath, command, { mode: 384 });
|
|
629
|
+
const wrapperScriptContent = `#!/bin/bash
|
|
630
|
+
# Auto-generated wrapper script for SAGA session
|
|
631
|
+
set -e
|
|
632
|
+
|
|
633
|
+
COMMAND_FILE="${commandFilePath}"
|
|
634
|
+
OUTPUT_FILE="${outputFile}"
|
|
635
|
+
SCRIPT_FILE="${wrapperScriptPath}"
|
|
636
|
+
|
|
637
|
+
# Read the command from file
|
|
638
|
+
COMMAND="$(cat "$COMMAND_FILE")"
|
|
639
|
+
|
|
640
|
+
# Cleanup temporary files on exit
|
|
641
|
+
cleanup() {
|
|
642
|
+
rm -f "$COMMAND_FILE" "$SCRIPT_FILE"
|
|
643
|
+
}
|
|
644
|
+
trap cleanup EXIT
|
|
645
|
+
|
|
646
|
+
# Execute the command with output capture
|
|
647
|
+
# script syntax differs between macOS and Linux:
|
|
648
|
+
# macOS (Darwin): script -q <file> <shell> -c <command>
|
|
649
|
+
# Linux: script -q -c <command> <file>
|
|
650
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
651
|
+
# -F: flush output after each write (ensures immediate visibility)
|
|
652
|
+
exec script -qF "$OUTPUT_FILE" /bin/bash -c "$COMMAND"
|
|
653
|
+
else
|
|
654
|
+
exec script -q -c "$COMMAND" "$OUTPUT_FILE"
|
|
655
|
+
fi
|
|
656
|
+
`;
|
|
657
|
+
(0, import_node_fs4.writeFileSync)(wrapperScriptPath, wrapperScriptContent, { mode: 448 });
|
|
658
|
+
const createResult = (0, import_node_child_process.spawnSync)("tmux", [
|
|
659
|
+
"new-session",
|
|
660
|
+
"-d",
|
|
661
|
+
// detached
|
|
662
|
+
"-s",
|
|
663
|
+
sessionName,
|
|
664
|
+
// session name
|
|
665
|
+
wrapperScriptPath
|
|
666
|
+
// run the wrapper script
|
|
667
|
+
], { encoding: "utf-8" });
|
|
668
|
+
if (createResult.status !== 0) {
|
|
669
|
+
throw new Error(`Failed to create tmux session: ${createResult.stderr || "unknown error"}`);
|
|
670
|
+
}
|
|
671
|
+
return {
|
|
672
|
+
sessionName,
|
|
673
|
+
outputFile
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
async function listSessions() {
|
|
677
|
+
const result = (0, import_node_child_process.spawnSync)("tmux", ["ls"], { encoding: "utf-8" });
|
|
678
|
+
if (result.status !== 0) {
|
|
679
|
+
return [];
|
|
680
|
+
}
|
|
681
|
+
const sessions = [];
|
|
682
|
+
const lines = result.stdout.trim().split("\n");
|
|
683
|
+
for (const line of lines) {
|
|
684
|
+
const match = line.match(/^(saga-[a-z0-9]+(?:-[a-z0-9]+)*-\d+):/);
|
|
685
|
+
if (match) {
|
|
686
|
+
const name = match[1];
|
|
687
|
+
sessions.push({
|
|
688
|
+
name,
|
|
689
|
+
status: "running",
|
|
690
|
+
// If it shows up in tmux ls, it's running
|
|
691
|
+
outputFile: (0, import_node_path4.join)(OUTPUT_DIR, `${name}.out`)
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return sessions;
|
|
696
|
+
}
|
|
697
|
+
async function getSessionStatus(sessionName) {
|
|
698
|
+
const result = (0, import_node_child_process.spawnSync)("tmux", ["has-session", "-t", sessionName], {
|
|
699
|
+
encoding: "utf-8"
|
|
700
|
+
});
|
|
701
|
+
return {
|
|
702
|
+
running: result.status === 0
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
async function streamLogs(sessionName) {
|
|
706
|
+
const outputFile = (0, import_node_path4.join)(OUTPUT_DIR, `${sessionName}.out`);
|
|
707
|
+
if (!(0, import_node_fs4.existsSync)(outputFile)) {
|
|
708
|
+
throw new Error(`Output file not found: ${outputFile}`);
|
|
709
|
+
}
|
|
710
|
+
return new Promise((resolve2, reject) => {
|
|
711
|
+
const child = (0, import_node_child_process.spawn)("tail", ["-f", outputFile], {
|
|
712
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
713
|
+
});
|
|
714
|
+
child.stdout?.on("data", (chunk) => {
|
|
715
|
+
process.stdout.write(chunk);
|
|
716
|
+
});
|
|
717
|
+
child.stderr?.on("data", (chunk) => {
|
|
718
|
+
process.stderr.write(chunk);
|
|
719
|
+
});
|
|
720
|
+
child.on("error", (err) => {
|
|
721
|
+
reject(new Error(`Failed to stream logs: ${err.message}`));
|
|
722
|
+
});
|
|
723
|
+
child.on("close", (code) => {
|
|
724
|
+
resolve2();
|
|
725
|
+
});
|
|
726
|
+
const sigintHandler = () => {
|
|
727
|
+
child.kill("SIGTERM");
|
|
728
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
729
|
+
};
|
|
730
|
+
process.on("SIGINT", sigintHandler);
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
async function killSession(sessionName) {
|
|
734
|
+
const result = (0, import_node_child_process.spawnSync)("tmux", ["kill-session", "-t", sessionName], {
|
|
735
|
+
encoding: "utf-8"
|
|
736
|
+
});
|
|
737
|
+
return {
|
|
738
|
+
killed: result.status === 0
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
574
742
|
// src/commands/implement.ts
|
|
575
743
|
var DEFAULT_MAX_CYCLES = 10;
|
|
576
744
|
var DEFAULT_MAX_TIME = 60;
|
|
@@ -608,10 +776,10 @@ async function findStory2(projectPath, storySlug) {
|
|
|
608
776
|
};
|
|
609
777
|
}
|
|
610
778
|
function computeStoryPath(worktree, epicSlug, storySlug) {
|
|
611
|
-
return (0,
|
|
779
|
+
return (0, import_node_path5.join)(worktree, ".saga", "epics", epicSlug, "stories", storySlug, "story.md");
|
|
612
780
|
}
|
|
613
781
|
function validateStoryFiles(worktree, epicSlug, storySlug) {
|
|
614
|
-
if (!(0,
|
|
782
|
+
if (!(0, import_node_fs5.existsSync)(worktree)) {
|
|
615
783
|
return {
|
|
616
784
|
valid: false,
|
|
617
785
|
error: `Worktree not found at ${worktree}
|
|
@@ -624,7 +792,7 @@ To create the worktree, use: /task-resume ${storySlug}`
|
|
|
624
792
|
};
|
|
625
793
|
}
|
|
626
794
|
const storyPath = computeStoryPath(worktree, epicSlug, storySlug);
|
|
627
|
-
if (!(0,
|
|
795
|
+
if (!(0, import_node_fs5.existsSync)(storyPath)) {
|
|
628
796
|
return {
|
|
629
797
|
valid: false,
|
|
630
798
|
error: `story.md not found in worktree.
|
|
@@ -638,11 +806,11 @@ This may indicate an incomplete story setup.`
|
|
|
638
806
|
return { valid: true };
|
|
639
807
|
}
|
|
640
808
|
function getSkillRoot(pluginRoot) {
|
|
641
|
-
return (0,
|
|
809
|
+
return (0, import_node_path5.join)(pluginRoot, "skills", "execute-story");
|
|
642
810
|
}
|
|
643
811
|
function checkCommandExists(command) {
|
|
644
812
|
try {
|
|
645
|
-
const result = (0,
|
|
813
|
+
const result = (0, import_node_child_process2.spawnSync)("which", [command], { encoding: "utf-8" });
|
|
646
814
|
if (result.status === 0 && result.stdout.trim()) {
|
|
647
815
|
return { exists: true, path: result.stdout.trim() };
|
|
648
816
|
}
|
|
@@ -678,8 +846,8 @@ function runDryRun(storyInfo, projectPath, pluginRoot) {
|
|
|
678
846
|
if (!claudeCheck.exists) allPassed = false;
|
|
679
847
|
if (pluginRoot) {
|
|
680
848
|
const skillRoot = getSkillRoot(pluginRoot);
|
|
681
|
-
const workerPromptPath = (0,
|
|
682
|
-
if ((0,
|
|
849
|
+
const workerPromptPath = (0, import_node_path5.join)(skillRoot, WORKER_PROMPT_RELATIVE);
|
|
850
|
+
if ((0, import_node_fs5.existsSync)(workerPromptPath)) {
|
|
683
851
|
checks.push({
|
|
684
852
|
name: "Worker prompt",
|
|
685
853
|
path: workerPromptPath,
|
|
@@ -700,7 +868,7 @@ function runDryRun(storyInfo, projectPath, pluginRoot) {
|
|
|
700
868
|
path: `${storyInfo.storySlug} (epic: ${storyInfo.epicSlug})`,
|
|
701
869
|
passed: true
|
|
702
870
|
});
|
|
703
|
-
if ((0,
|
|
871
|
+
if ((0, import_node_fs5.existsSync)(storyInfo.worktreePath)) {
|
|
704
872
|
checks.push({
|
|
705
873
|
name: "Worktree exists",
|
|
706
874
|
path: storyInfo.worktreePath,
|
|
@@ -715,13 +883,13 @@ function runDryRun(storyInfo, projectPath, pluginRoot) {
|
|
|
715
883
|
});
|
|
716
884
|
allPassed = false;
|
|
717
885
|
}
|
|
718
|
-
if ((0,
|
|
886
|
+
if ((0, import_node_fs5.existsSync)(storyInfo.worktreePath)) {
|
|
719
887
|
const storyMdPath = computeStoryPath(
|
|
720
888
|
storyInfo.worktreePath,
|
|
721
889
|
storyInfo.epicSlug,
|
|
722
890
|
storyInfo.storySlug
|
|
723
891
|
);
|
|
724
|
-
if ((0,
|
|
892
|
+
if ((0, import_node_fs5.existsSync)(storyMdPath)) {
|
|
725
893
|
checks.push({
|
|
726
894
|
name: "story.md in worktree",
|
|
727
895
|
path: storyMdPath,
|
|
@@ -771,11 +939,11 @@ function printDryRunResults(result) {
|
|
|
771
939
|
}
|
|
772
940
|
function loadWorkerPrompt(pluginRoot) {
|
|
773
941
|
const skillRoot = getSkillRoot(pluginRoot);
|
|
774
|
-
const promptPath = (0,
|
|
775
|
-
if (!(0,
|
|
942
|
+
const promptPath = (0, import_node_path5.join)(skillRoot, WORKER_PROMPT_RELATIVE);
|
|
943
|
+
if (!(0, import_node_fs5.existsSync)(promptPath)) {
|
|
776
944
|
throw new Error(`Worker prompt not found at ${promptPath}`);
|
|
777
945
|
}
|
|
778
|
-
return (0,
|
|
946
|
+
return (0, import_node_fs5.readFileSync)(promptPath, "utf-8");
|
|
779
947
|
}
|
|
780
948
|
function buildScopeSettings() {
|
|
781
949
|
const hookCommand = "npx @saga-ai/cli scope-validator";
|
|
@@ -837,7 +1005,7 @@ function spawnWorker(prompt, model, settings, workingDir) {
|
|
|
837
1005
|
JSON.stringify(settings),
|
|
838
1006
|
"--dangerously-skip-permissions"
|
|
839
1007
|
];
|
|
840
|
-
const result = (0,
|
|
1008
|
+
const result = (0, import_node_child_process2.spawnSync)("claude", args, {
|
|
841
1009
|
cwd: workingDir,
|
|
842
1010
|
encoding: "utf-8",
|
|
843
1011
|
maxBuffer: 50 * 1024 * 1024
|
|
@@ -921,7 +1089,7 @@ function spawnWorkerAsync(prompt, model, settings, workingDir) {
|
|
|
921
1089
|
JSON.stringify(settings),
|
|
922
1090
|
"--dangerously-skip-permissions"
|
|
923
1091
|
];
|
|
924
|
-
const child = (0,
|
|
1092
|
+
const child = (0, import_node_child_process2.spawn)("claude", args, {
|
|
925
1093
|
cwd: workingDir,
|
|
926
1094
|
stdio: ["ignore", "pipe", "pipe"]
|
|
927
1095
|
});
|
|
@@ -956,7 +1124,7 @@ function spawnWorkerAsync(prompt, model, settings, workingDir) {
|
|
|
956
1124
|
});
|
|
957
1125
|
}
|
|
958
1126
|
async function runLoop(epicSlug, storySlug, maxCycles, maxTime, model, projectDir, pluginRoot, stream = false) {
|
|
959
|
-
const worktree = (0,
|
|
1127
|
+
const worktree = (0, import_node_path5.join)(projectDir, ".saga", "worktrees", epicSlug, storySlug);
|
|
960
1128
|
const validation = validateStoryFiles(worktree, epicSlug, storySlug);
|
|
961
1129
|
if (!validation.valid) {
|
|
962
1130
|
return {
|
|
@@ -1072,6 +1240,23 @@ async function runLoop(epicSlug, storySlug, maxCycles, maxTime, model, projectDi
|
|
|
1072
1240
|
storySlug
|
|
1073
1241
|
};
|
|
1074
1242
|
}
|
|
1243
|
+
function buildDetachedCommand(storySlug, projectPath, options) {
|
|
1244
|
+
const parts = ["saga", "implement", storySlug, "--attached"];
|
|
1245
|
+
parts.push("--path", projectPath);
|
|
1246
|
+
if (options.maxCycles !== void 0) {
|
|
1247
|
+
parts.push("--max-cycles", String(options.maxCycles));
|
|
1248
|
+
}
|
|
1249
|
+
if (options.maxTime !== void 0) {
|
|
1250
|
+
parts.push("--max-time", String(options.maxTime));
|
|
1251
|
+
}
|
|
1252
|
+
if (options.model !== void 0) {
|
|
1253
|
+
parts.push("--model", options.model);
|
|
1254
|
+
}
|
|
1255
|
+
if (options.stream) {
|
|
1256
|
+
parts.push("--stream");
|
|
1257
|
+
}
|
|
1258
|
+
return shellEscapeArgs(parts);
|
|
1259
|
+
}
|
|
1075
1260
|
async function implementCommand(storySlug, options) {
|
|
1076
1261
|
let projectPath;
|
|
1077
1262
|
try {
|
|
@@ -1084,7 +1269,7 @@ async function implementCommand(storySlug, options) {
|
|
|
1084
1269
|
if (!storyInfo) {
|
|
1085
1270
|
console.error(`Error: Story '${storySlug}' not found in SAGA project.`);
|
|
1086
1271
|
console.error(`
|
|
1087
|
-
Searched in: ${(0,
|
|
1272
|
+
Searched in: ${(0, import_node_path5.join)(projectPath, ".saga", "worktrees")}`);
|
|
1088
1273
|
console.error("\nMake sure the story worktree exists and has a story.md file.");
|
|
1089
1274
|
console.error("Run /generate-stories to create story worktrees.");
|
|
1090
1275
|
process.exit(1);
|
|
@@ -1095,7 +1280,7 @@ Searched in: ${(0, import_node_path4.join)(projectPath, ".saga", "worktrees")}`)
|
|
|
1095
1280
|
printDryRunResults(dryRunResult);
|
|
1096
1281
|
process.exit(dryRunResult.success ? 0 : 1);
|
|
1097
1282
|
}
|
|
1098
|
-
if (!(0,
|
|
1283
|
+
if (!(0, import_node_fs5.existsSync)(storyInfo.worktreePath)) {
|
|
1099
1284
|
console.error(`Error: Worktree not found at ${storyInfo.worktreePath}`);
|
|
1100
1285
|
console.error("\nThe story worktree has not been created yet.");
|
|
1101
1286
|
console.error("Make sure the story was properly generated with /generate-stories.");
|
|
@@ -1113,6 +1298,38 @@ Searched in: ${(0, import_node_path4.join)(projectPath, ".saga", "worktrees")}`)
|
|
|
1113
1298
|
const maxTime = options.maxTime ?? DEFAULT_MAX_TIME;
|
|
1114
1299
|
const model = options.model ?? DEFAULT_MODEL;
|
|
1115
1300
|
const stream = options.stream ?? false;
|
|
1301
|
+
const attached = options.attached ?? false;
|
|
1302
|
+
if (!attached) {
|
|
1303
|
+
if (stream) {
|
|
1304
|
+
console.error("Warning: --stream is ignored in detached mode. Use --attached --stream for streaming output.");
|
|
1305
|
+
}
|
|
1306
|
+
const detachedCommand = buildDetachedCommand(storySlug, projectPath, {
|
|
1307
|
+
maxCycles: options.maxCycles,
|
|
1308
|
+
maxTime: options.maxTime,
|
|
1309
|
+
model: options.model,
|
|
1310
|
+
stream: true
|
|
1311
|
+
// Always use streaming in detached mode so output is captured
|
|
1312
|
+
});
|
|
1313
|
+
try {
|
|
1314
|
+
const sessionResult = await createSession(
|
|
1315
|
+
storyInfo.epicSlug,
|
|
1316
|
+
storyInfo.storySlug,
|
|
1317
|
+
detachedCommand
|
|
1318
|
+
);
|
|
1319
|
+
console.log(JSON.stringify({
|
|
1320
|
+
mode: "detached",
|
|
1321
|
+
sessionName: sessionResult.sessionName,
|
|
1322
|
+
outputFile: sessionResult.outputFile,
|
|
1323
|
+
epicSlug: storyInfo.epicSlug,
|
|
1324
|
+
storySlug: storyInfo.storySlug,
|
|
1325
|
+
worktreePath: storyInfo.worktreePath
|
|
1326
|
+
}, null, 2));
|
|
1327
|
+
return;
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
console.error(`Error: Failed to create detached session: ${error.message}`);
|
|
1330
|
+
process.exit(1);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1116
1333
|
console.log("Starting story implementation...");
|
|
1117
1334
|
console.log(` Epic: ${storyInfo.epicSlug}`);
|
|
1118
1335
|
console.log(` Story: ${storyInfo.storySlug}`);
|
|
@@ -1194,7 +1411,7 @@ function validateTaskStatus(status) {
|
|
|
1194
1411
|
return "pending";
|
|
1195
1412
|
}
|
|
1196
1413
|
async function parseStory(storyPath, epicSlug) {
|
|
1197
|
-
const { join:
|
|
1414
|
+
const { join: join12 } = await import("path");
|
|
1198
1415
|
const { stat: stat2 } = await import("fs/promises");
|
|
1199
1416
|
let content;
|
|
1200
1417
|
try {
|
|
@@ -1214,7 +1431,7 @@ async function parseStory(storyPath, epicSlug) {
|
|
|
1214
1431
|
const title = frontmatter.title || dirName;
|
|
1215
1432
|
const status = validateStatus(frontmatter.status);
|
|
1216
1433
|
const tasks = parseTasks(frontmatter.tasks);
|
|
1217
|
-
const journalPath =
|
|
1434
|
+
const journalPath = join12(storyDir, "journal.md");
|
|
1218
1435
|
let hasJournal = false;
|
|
1219
1436
|
try {
|
|
1220
1437
|
await stat2(journalPath);
|
|
@@ -1804,7 +2021,7 @@ async function dashboardCommand(options) {
|
|
|
1804
2021
|
}
|
|
1805
2022
|
|
|
1806
2023
|
// src/commands/scope-validator.ts
|
|
1807
|
-
var
|
|
2024
|
+
var import_node_path6 = require("node:path");
|
|
1808
2025
|
function getFilePathFromInput(hookInput) {
|
|
1809
2026
|
try {
|
|
1810
2027
|
const data = JSON.parse(hookInput);
|
|
@@ -1824,10 +2041,10 @@ function isArchiveAccess(path) {
|
|
|
1824
2041
|
return path.includes(".saga/archive");
|
|
1825
2042
|
}
|
|
1826
2043
|
function isWithinWorktree(filePath, worktreePath) {
|
|
1827
|
-
const absoluteFilePath = (0,
|
|
1828
|
-
const absoluteWorktree = (0,
|
|
1829
|
-
const relativePath = (0,
|
|
1830
|
-
if (relativePath.startsWith("..") || (0,
|
|
2044
|
+
const absoluteFilePath = (0, import_node_path6.resolve)(filePath);
|
|
2045
|
+
const absoluteWorktree = (0, import_node_path6.resolve)(worktreePath);
|
|
2046
|
+
const relativePath = (0, import_node_path6.relative)(absoluteWorktree, absoluteFilePath);
|
|
2047
|
+
if (relativePath.startsWith("..") || (0, import_node_path6.resolve)(relativePath) === relativePath) {
|
|
1831
2048
|
return false;
|
|
1832
2049
|
}
|
|
1833
2050
|
return true;
|
|
@@ -1933,7 +2150,7 @@ async function findCommand(query, options) {
|
|
|
1933
2150
|
if (type === "epic") {
|
|
1934
2151
|
result = findEpic(projectPath, query);
|
|
1935
2152
|
} else {
|
|
1936
|
-
result = await findStory(projectPath, query);
|
|
2153
|
+
result = await findStory(projectPath, query, { status: options.status });
|
|
1937
2154
|
}
|
|
1938
2155
|
console.log(JSON.stringify(result, null, 2));
|
|
1939
2156
|
if (!result.found) {
|
|
@@ -1942,12 +2159,12 @@ async function findCommand(query, options) {
|
|
|
1942
2159
|
}
|
|
1943
2160
|
|
|
1944
2161
|
// src/commands/worktree.ts
|
|
1945
|
-
var
|
|
1946
|
-
var
|
|
1947
|
-
var
|
|
2162
|
+
var import_node_path7 = require("node:path");
|
|
2163
|
+
var import_node_fs6 = require("node:fs");
|
|
2164
|
+
var import_node_child_process3 = require("node:child_process");
|
|
1948
2165
|
function runGitCommand(args, cwd) {
|
|
1949
2166
|
try {
|
|
1950
|
-
const output = (0,
|
|
2167
|
+
const output = (0, import_node_child_process3.execSync)(`git ${args.join(" ")}`, {
|
|
1951
2168
|
cwd,
|
|
1952
2169
|
encoding: "utf-8",
|
|
1953
2170
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1974,7 +2191,7 @@ function getMainBranch(cwd) {
|
|
|
1974
2191
|
}
|
|
1975
2192
|
function createWorktree(projectPath, epicSlug, storySlug) {
|
|
1976
2193
|
const branchName = `story-${storySlug}-epic-${epicSlug}`;
|
|
1977
|
-
const worktreePath = (0,
|
|
2194
|
+
const worktreePath = (0, import_node_path7.join)(
|
|
1978
2195
|
projectPath,
|
|
1979
2196
|
".saga",
|
|
1980
2197
|
"worktrees",
|
|
@@ -1987,7 +2204,7 @@ function createWorktree(projectPath, epicSlug, storySlug) {
|
|
|
1987
2204
|
error: `Branch already exists: ${branchName}`
|
|
1988
2205
|
};
|
|
1989
2206
|
}
|
|
1990
|
-
if ((0,
|
|
2207
|
+
if ((0, import_node_fs6.existsSync)(worktreePath)) {
|
|
1991
2208
|
return {
|
|
1992
2209
|
success: false,
|
|
1993
2210
|
error: `Worktree directory already exists: ${worktreePath}`
|
|
@@ -2005,8 +2222,8 @@ function createWorktree(projectPath, epicSlug, storySlug) {
|
|
|
2005
2222
|
error: `Failed to create branch: ${createBranchResult.output}`
|
|
2006
2223
|
};
|
|
2007
2224
|
}
|
|
2008
|
-
const worktreeParent = (0,
|
|
2009
|
-
(0,
|
|
2225
|
+
const worktreeParent = (0, import_node_path7.join)(projectPath, ".saga", "worktrees", epicSlug);
|
|
2226
|
+
(0, import_node_fs6.mkdirSync)(worktreeParent, { recursive: true });
|
|
2010
2227
|
const createWorktreeResult = runGitCommand(
|
|
2011
2228
|
["worktree", "add", worktreePath, branchName],
|
|
2012
2229
|
projectPath
|
|
@@ -2039,9 +2256,31 @@ async function worktreeCommand(epicSlug, storySlug, options) {
|
|
|
2039
2256
|
}
|
|
2040
2257
|
}
|
|
2041
2258
|
|
|
2259
|
+
// src/commands/sessions/index.ts
|
|
2260
|
+
async function sessionsListCommand() {
|
|
2261
|
+
const sessions = await listSessions();
|
|
2262
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
2263
|
+
}
|
|
2264
|
+
async function sessionsStatusCommand(sessionName) {
|
|
2265
|
+
const status = await getSessionStatus(sessionName);
|
|
2266
|
+
console.log(JSON.stringify(status, null, 2));
|
|
2267
|
+
}
|
|
2268
|
+
async function sessionsLogsCommand(sessionName) {
|
|
2269
|
+
try {
|
|
2270
|
+
await streamLogs(sessionName);
|
|
2271
|
+
} catch (error) {
|
|
2272
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
2273
|
+
process.exit(1);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
async function sessionsKillCommand(sessionName) {
|
|
2277
|
+
const result = await killSession(sessionName);
|
|
2278
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2042
2281
|
// src/cli.ts
|
|
2043
|
-
var packageJsonPath = (0,
|
|
2044
|
-
var packageJson = JSON.parse((0,
|
|
2282
|
+
var packageJsonPath = (0, import_node_path8.join)(__dirname, "..", "package.json");
|
|
2283
|
+
var packageJson = JSON.parse((0, import_node_fs7.readFileSync)(packageJsonPath, "utf-8"));
|
|
2045
2284
|
var program = new import_commander.Command();
|
|
2046
2285
|
program.name("saga").description("CLI for SAGA - Structured Autonomous Goal Achievement").version(packageJson.version).addHelpCommand("help [command]", "Display help for a command");
|
|
2047
2286
|
program.option("-p, --path <dir>", "Path to SAGA project directory (overrides auto-discovery)");
|
|
@@ -2049,7 +2288,7 @@ program.command("init").description("Initialize .saga/ directory structure").opt
|
|
|
2049
2288
|
const globalOpts = program.opts();
|
|
2050
2289
|
await initCommand({ path: globalOpts.path, dryRun: options.dryRun });
|
|
2051
2290
|
});
|
|
2052
|
-
program.command("implement <story-slug>").description("Run story implementation").option("--max-cycles <n>", "Maximum number of implementation cycles", parseInt).option("--max-time <n>", "Maximum time in minutes", parseInt).option("--model <name>", "Model to use for implementation").option("--dry-run", "Validate dependencies without running implementation").option("--stream", "Stream worker output in real-time").action(async (storySlug, options) => {
|
|
2291
|
+
program.command("implement <story-slug>").description("Run story implementation").option("--max-cycles <n>", "Maximum number of implementation cycles", parseInt).option("--max-time <n>", "Maximum time in minutes", parseInt).option("--model <name>", "Model to use for implementation").option("--dry-run", "Validate dependencies without running implementation").option("--stream", "Stream worker output in real-time").option("--attached", "Run in attached mode (synchronous, tied to terminal)").action(async (storySlug, options) => {
|
|
2053
2292
|
const globalOpts = program.opts();
|
|
2054
2293
|
await implementCommand(storySlug, {
|
|
2055
2294
|
path: globalOpts.path,
|
|
@@ -2057,14 +2296,16 @@ program.command("implement <story-slug>").description("Run story implementation"
|
|
|
2057
2296
|
maxTime: options.maxTime,
|
|
2058
2297
|
model: options.model,
|
|
2059
2298
|
dryRun: options.dryRun,
|
|
2060
|
-
stream: options.stream
|
|
2299
|
+
stream: options.stream,
|
|
2300
|
+
attached: options.attached
|
|
2061
2301
|
});
|
|
2062
2302
|
});
|
|
2063
|
-
program.command("find <query>").description("Find an epic or story by slug/title").option("--type <type>", "Type to search for: epic or story (default: story)").action(async (query, options) => {
|
|
2303
|
+
program.command("find <query>").description("Find an epic or story by slug/title").option("--type <type>", "Type to search for: epic or story (default: story)").option("--status <status>", "Filter stories by status (e.g., ready, in-progress, completed)").action(async (query, options) => {
|
|
2064
2304
|
const globalOpts = program.opts();
|
|
2065
2305
|
await findCommand(query, {
|
|
2066
2306
|
path: globalOpts.path,
|
|
2067
|
-
type: options.type
|
|
2307
|
+
type: options.type,
|
|
2308
|
+
status: options.status
|
|
2068
2309
|
});
|
|
2069
2310
|
});
|
|
2070
2311
|
program.command("worktree <epic-slug> <story-slug>").description("Create git worktree for a story").action(async (epicSlug, storySlug) => {
|
|
@@ -2081,6 +2322,19 @@ program.command("dashboard").description("Start the dashboard server").option("-
|
|
|
2081
2322
|
program.command("scope-validator").description("Validate file operations against story scope (internal)").action(async () => {
|
|
2082
2323
|
await scopeValidatorCommand();
|
|
2083
2324
|
});
|
|
2325
|
+
var sessionsCommand = program.command("sessions").description("Manage SAGA tmux sessions");
|
|
2326
|
+
sessionsCommand.command("list").description("List all SAGA sessions").action(async () => {
|
|
2327
|
+
await sessionsListCommand();
|
|
2328
|
+
});
|
|
2329
|
+
sessionsCommand.command("status <name>").description("Show session status").action(async (name) => {
|
|
2330
|
+
await sessionsStatusCommand(name);
|
|
2331
|
+
});
|
|
2332
|
+
sessionsCommand.command("logs <name>").description("Stream session output").action(async (name) => {
|
|
2333
|
+
await sessionsLogsCommand(name);
|
|
2334
|
+
});
|
|
2335
|
+
sessionsCommand.command("kill <name>").description("Terminate a session").action(async (name) => {
|
|
2336
|
+
await sessionsKillCommand(name);
|
|
2337
|
+
});
|
|
2084
2338
|
program.on("command:*", (operands) => {
|
|
2085
2339
|
console.error(`error: unknown command '${operands[0]}'`);
|
|
2086
2340
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saga-ai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "CLI for SAGA - Structured Autonomous Goal Achievement",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -60,7 +60,16 @@
|
|
|
60
60
|
"typescript": "^5.7.0",
|
|
61
61
|
"vite": "^6.0.5",
|
|
62
62
|
"vitest": "^3.2.4",
|
|
63
|
-
"xstate": "^5.26.0"
|
|
63
|
+
"xstate": "^5.26.0",
|
|
64
|
+
"storybook": "^10.2.1",
|
|
65
|
+
"@storybook/react-vite": "^10.2.1",
|
|
66
|
+
"@chromatic-com/storybook": "^5.0.0",
|
|
67
|
+
"@storybook/addon-vitest": "^10.2.1",
|
|
68
|
+
"@storybook/addon-a11y": "^10.2.1",
|
|
69
|
+
"@storybook/addon-docs": "^10.2.1",
|
|
70
|
+
"@storybook/addon-onboarding": "^10.2.1",
|
|
71
|
+
"playwright": "^1.58.0",
|
|
72
|
+
"@vitest/browser": "^3.2.4"
|
|
64
73
|
},
|
|
65
74
|
"engines": {
|
|
66
75
|
"node": ">=18.0.0"
|
|
@@ -77,6 +86,8 @@
|
|
|
77
86
|
"dev:client": "vite --config src/client/vite.config.ts",
|
|
78
87
|
"test": "vitest run",
|
|
79
88
|
"test:watch": "vitest",
|
|
80
|
-
"publish:npm": "pnpm build && pnpm test && pnpm publish --access public"
|
|
89
|
+
"publish:npm": "pnpm build && pnpm test && pnpm publish --access public",
|
|
90
|
+
"storybook": "storybook dev -p 6006 -c src/client/.storybook",
|
|
91
|
+
"build-storybook": "storybook build -c src/client/.storybook -o storybook-static"
|
|
81
92
|
}
|
|
82
93
|
}
|