@saga-ai/cli 0.7.1 → 0.7.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.
- package/README.md +0 -3
- package/dist/cli.cjs +26 -124
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,15 +85,12 @@ saga implement my-story
|
|
|
85
85
|
saga implement my-story --max-cycles 5
|
|
86
86
|
saga implement my-story --max-time 30
|
|
87
87
|
saga implement my-story --model sonnet
|
|
88
|
-
saga implement my-story --attached --stream
|
|
89
88
|
```
|
|
90
89
|
|
|
91
90
|
Options:
|
|
92
91
|
- `--max-cycles <n>` - Maximum number of worker cycles (default: 10)
|
|
93
92
|
- `--max-time <n>` - Maximum time in minutes (default: 60)
|
|
94
93
|
- `--model <name>` - Model to use (default: opus)
|
|
95
|
-
- `--attached` - Run in attached mode (synchronous). Default is detached (runs in tmux session)
|
|
96
|
-
- `--stream` - Stream worker output to stdout (only works with `--attached`)
|
|
97
94
|
|
|
98
95
|
By default, `implement` runs in **detached mode** using a tmux session. This allows the worker to continue running even if your terminal disconnects. Use `saga sessions` to monitor detached sessions.
|
|
99
96
|
|
package/dist/cli.cjs
CHANGED
|
@@ -630,6 +630,9 @@ async function createSession(epicSlug, storySlug, command) {
|
|
|
630
630
|
# Auto-generated wrapper script for SAGA session
|
|
631
631
|
set -e
|
|
632
632
|
|
|
633
|
+
# Mark this as an internal SAGA session (used by CLI to detect it's running inside tmux)
|
|
634
|
+
export SAGA_INTERNAL_SESSION=1
|
|
635
|
+
|
|
633
636
|
COMMAND_FILE="${commandFilePath}"
|
|
634
637
|
OUTPUT_FILE="${outputFile}"
|
|
635
638
|
SCRIPT_FILE="${wrapperScriptPath}"
|
|
@@ -958,64 +961,6 @@ function buildScopeSettings() {
|
|
|
958
961
|
}
|
|
959
962
|
};
|
|
960
963
|
}
|
|
961
|
-
function parseWorkerOutput(output) {
|
|
962
|
-
if (!output || !output.trim()) {
|
|
963
|
-
throw new Error("Worker output is empty");
|
|
964
|
-
}
|
|
965
|
-
let cliResponse;
|
|
966
|
-
try {
|
|
967
|
-
cliResponse = JSON.parse(output.trim());
|
|
968
|
-
} catch (e) {
|
|
969
|
-
throw new Error(`Invalid JSON in worker output: ${e}`);
|
|
970
|
-
}
|
|
971
|
-
if (!("structured_output" in cliResponse)) {
|
|
972
|
-
if (cliResponse.is_error) {
|
|
973
|
-
const errorMsg = cliResponse.result || "Unknown error";
|
|
974
|
-
throw new Error(`Worker failed: ${errorMsg}`);
|
|
975
|
-
}
|
|
976
|
-
throw new Error(`Worker output missing structured_output field. Got keys: ${Object.keys(cliResponse).join(", ")}`);
|
|
977
|
-
}
|
|
978
|
-
const parsed = cliResponse.structured_output;
|
|
979
|
-
if (!("status" in parsed)) {
|
|
980
|
-
throw new Error("Worker output missing required field: status");
|
|
981
|
-
}
|
|
982
|
-
if (!("summary" in parsed)) {
|
|
983
|
-
throw new Error("Worker output missing required field: summary");
|
|
984
|
-
}
|
|
985
|
-
if (!VALID_STATUSES.has(parsed.status)) {
|
|
986
|
-
throw new Error(`Invalid status value: ${parsed.status}. Must be one of: ${[...VALID_STATUSES].join(", ")}`);
|
|
987
|
-
}
|
|
988
|
-
return {
|
|
989
|
-
status: parsed.status,
|
|
990
|
-
summary: parsed.summary,
|
|
991
|
-
blocker: parsed.blocker ?? null
|
|
992
|
-
};
|
|
993
|
-
}
|
|
994
|
-
function spawnWorker(prompt, model, settings, workingDir) {
|
|
995
|
-
const args = [
|
|
996
|
-
"-p",
|
|
997
|
-
prompt,
|
|
998
|
-
"--model",
|
|
999
|
-
model,
|
|
1000
|
-
"--output-format",
|
|
1001
|
-
"json",
|
|
1002
|
-
"--json-schema",
|
|
1003
|
-
JSON.stringify(WORKER_OUTPUT_SCHEMA),
|
|
1004
|
-
"--settings",
|
|
1005
|
-
JSON.stringify(settings),
|
|
1006
|
-
"--dangerously-skip-permissions"
|
|
1007
|
-
];
|
|
1008
|
-
const result = (0, import_node_child_process2.spawnSync)("claude", args, {
|
|
1009
|
-
cwd: workingDir,
|
|
1010
|
-
encoding: "utf-8",
|
|
1011
|
-
maxBuffer: 50 * 1024 * 1024
|
|
1012
|
-
// 50MB buffer for large outputs
|
|
1013
|
-
});
|
|
1014
|
-
if (result.error) {
|
|
1015
|
-
throw new Error(`Failed to spawn worker: ${result.error.message}`);
|
|
1016
|
-
}
|
|
1017
|
-
return result.stdout || "";
|
|
1018
|
-
}
|
|
1019
964
|
function formatStreamLine(line) {
|
|
1020
965
|
try {
|
|
1021
966
|
const data = JSON.parse(line);
|
|
@@ -1142,7 +1087,7 @@ function spawnWorkerAsync(prompt, model, settings, workingDir) {
|
|
|
1142
1087
|
});
|
|
1143
1088
|
});
|
|
1144
1089
|
}
|
|
1145
|
-
async function runLoop(epicSlug, storySlug, maxCycles, maxTime, model, projectDir, pluginRoot
|
|
1090
|
+
async function runLoop(epicSlug, storySlug, maxCycles, maxTime, model, projectDir, pluginRoot) {
|
|
1146
1091
|
const worktree = (0, import_node_path5.join)(projectDir, ".saga", "worktrees", epicSlug, storySlug);
|
|
1147
1092
|
const validation = validateStoryFiles(worktree, epicSlug, storySlug);
|
|
1148
1093
|
if (!validation.valid) {
|
|
@@ -1185,55 +1130,25 @@ async function runLoop(epicSlug, storySlug, maxCycles, maxTime, model, projectDi
|
|
|
1185
1130
|
}
|
|
1186
1131
|
cycles += 1;
|
|
1187
1132
|
let parsed;
|
|
1188
|
-
|
|
1189
|
-
console.log(`
|
|
1133
|
+
console.log(`
|
|
1190
1134
|
--- Worker ${cycles} started ---
|
|
1191
1135
|
`);
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1136
|
+
try {
|
|
1137
|
+
parsed = await spawnWorkerAsync(workerPrompt, model, settings, worktree);
|
|
1138
|
+
} catch (e) {
|
|
1139
|
+
return {
|
|
1140
|
+
status: "ERROR",
|
|
1141
|
+
summary: e.message,
|
|
1142
|
+
cycles,
|
|
1143
|
+
elapsedMinutes: (Date.now() - startTime) / 6e4,
|
|
1144
|
+
blocker: null,
|
|
1145
|
+
epicSlug,
|
|
1146
|
+
storySlug
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
console.log(`
|
|
1206
1150
|
--- Worker ${cycles} result: ${parsed.status} ---
|
|
1207
1151
|
`);
|
|
1208
|
-
} else {
|
|
1209
|
-
let output;
|
|
1210
|
-
try {
|
|
1211
|
-
output = spawnWorker(workerPrompt, model, settings, worktree);
|
|
1212
|
-
} catch (e) {
|
|
1213
|
-
return {
|
|
1214
|
-
status: "ERROR",
|
|
1215
|
-
summary: e.message,
|
|
1216
|
-
cycles,
|
|
1217
|
-
elapsedMinutes: (Date.now() - startTime) / 6e4,
|
|
1218
|
-
blocker: null,
|
|
1219
|
-
epicSlug,
|
|
1220
|
-
storySlug
|
|
1221
|
-
};
|
|
1222
|
-
}
|
|
1223
|
-
try {
|
|
1224
|
-
parsed = parseWorkerOutput(output);
|
|
1225
|
-
} catch (e) {
|
|
1226
|
-
return {
|
|
1227
|
-
status: "ERROR",
|
|
1228
|
-
summary: e.message,
|
|
1229
|
-
cycles,
|
|
1230
|
-
elapsedMinutes: (Date.now() - startTime) / 6e4,
|
|
1231
|
-
blocker: null,
|
|
1232
|
-
epicSlug,
|
|
1233
|
-
storySlug
|
|
1234
|
-
};
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
1152
|
summaries.push(parsed.summary);
|
|
1238
1153
|
if (parsed.status === "FINISH") {
|
|
1239
1154
|
finalStatus = "FINISH";
|
|
@@ -1260,7 +1175,7 @@ async function runLoop(epicSlug, storySlug, maxCycles, maxTime, model, projectDi
|
|
|
1260
1175
|
};
|
|
1261
1176
|
}
|
|
1262
1177
|
function buildDetachedCommand(storySlug, projectPath, options) {
|
|
1263
|
-
const parts = ["saga", "implement", storySlug
|
|
1178
|
+
const parts = ["saga", "implement", storySlug];
|
|
1264
1179
|
parts.push("--path", projectPath);
|
|
1265
1180
|
if (options.maxCycles !== void 0) {
|
|
1266
1181
|
parts.push("--max-cycles", String(options.maxCycles));
|
|
@@ -1271,9 +1186,6 @@ function buildDetachedCommand(storySlug, projectPath, options) {
|
|
|
1271
1186
|
if (options.model !== void 0) {
|
|
1272
1187
|
parts.push("--model", options.model);
|
|
1273
1188
|
}
|
|
1274
|
-
if (options.stream) {
|
|
1275
|
-
parts.push("--stream");
|
|
1276
|
-
}
|
|
1277
1189
|
return shellEscapeArgs(parts);
|
|
1278
1190
|
}
|
|
1279
1191
|
async function implementCommand(storySlug, options) {
|
|
@@ -1316,18 +1228,12 @@ Searched in: ${(0, import_node_path5.join)(projectPath, ".saga", "worktrees")}`)
|
|
|
1316
1228
|
const maxCycles = options.maxCycles ?? DEFAULT_MAX_CYCLES;
|
|
1317
1229
|
const maxTime = options.maxTime ?? DEFAULT_MAX_TIME;
|
|
1318
1230
|
const model = options.model ?? DEFAULT_MODEL;
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
if (!attached) {
|
|
1322
|
-
if (stream) {
|
|
1323
|
-
console.error("Warning: --stream is ignored in detached mode. Use --attached --stream for streaming output.");
|
|
1324
|
-
}
|
|
1231
|
+
const isInternalSession = process.env.SAGA_INTERNAL_SESSION === "1";
|
|
1232
|
+
if (!isInternalSession) {
|
|
1325
1233
|
const detachedCommand = buildDetachedCommand(storySlug, projectPath, {
|
|
1326
1234
|
maxCycles: options.maxCycles,
|
|
1327
1235
|
maxTime: options.maxTime,
|
|
1328
|
-
model: options.model
|
|
1329
|
-
stream: true
|
|
1330
|
-
// Always use streaming in detached mode so output is captured
|
|
1236
|
+
model: options.model
|
|
1331
1237
|
});
|
|
1332
1238
|
try {
|
|
1333
1239
|
const sessionResult = await createSession(
|
|
@@ -1353,7 +1259,6 @@ Searched in: ${(0, import_node_path5.join)(projectPath, ".saga", "worktrees")}`)
|
|
|
1353
1259
|
console.log(` Epic: ${storyInfo.epicSlug}`);
|
|
1354
1260
|
console.log(` Story: ${storyInfo.storySlug}`);
|
|
1355
1261
|
console.log(` Worktree: ${storyInfo.worktreePath}`);
|
|
1356
|
-
console.log(` Streaming: ${stream ? "enabled" : "disabled"}`);
|
|
1357
1262
|
console.log("");
|
|
1358
1263
|
const result = await runLoop(
|
|
1359
1264
|
storyInfo.epicSlug,
|
|
@@ -1362,8 +1267,7 @@ Searched in: ${(0, import_node_path5.join)(projectPath, ".saga", "worktrees")}`)
|
|
|
1362
1267
|
maxTime,
|
|
1363
1268
|
model,
|
|
1364
1269
|
projectPath,
|
|
1365
|
-
pluginRoot
|
|
1366
|
-
stream
|
|
1270
|
+
pluginRoot
|
|
1367
1271
|
);
|
|
1368
1272
|
console.log(JSON.stringify(result, null, 2));
|
|
1369
1273
|
if (result.status === "ERROR") {
|
|
@@ -2307,16 +2211,14 @@ program.command("init").description("Initialize .saga/ directory structure").opt
|
|
|
2307
2211
|
const globalOpts = program.opts();
|
|
2308
2212
|
await initCommand({ path: globalOpts.path, dryRun: options.dryRun });
|
|
2309
2213
|
});
|
|
2310
|
-
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").
|
|
2214
|
+
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").action(async (storySlug, options) => {
|
|
2311
2215
|
const globalOpts = program.opts();
|
|
2312
2216
|
await implementCommand(storySlug, {
|
|
2313
2217
|
path: globalOpts.path,
|
|
2314
2218
|
maxCycles: options.maxCycles,
|
|
2315
2219
|
maxTime: options.maxTime,
|
|
2316
2220
|
model: options.model,
|
|
2317
|
-
dryRun: options.dryRun
|
|
2318
|
-
stream: options.stream,
|
|
2319
|
-
attached: options.attached
|
|
2221
|
+
dryRun: options.dryRun
|
|
2320
2222
|
});
|
|
2321
2223
|
});
|
|
2322
2224
|
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) => {
|