@saga-ai/cli 0.3.0 → 0.5.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 +203 -44
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -3037,7 +3037,7 @@ var {
|
|
|
3037
3037
|
} = import_index.default;
|
|
3038
3038
|
|
|
3039
3039
|
// src/cli.ts
|
|
3040
|
-
var
|
|
3040
|
+
var import_node_path7 = require("node:path");
|
|
3041
3041
|
var import_node_fs6 = require("node:fs");
|
|
3042
3042
|
|
|
3043
3043
|
// src/commands/init.ts
|
|
@@ -4957,7 +4957,7 @@ function buildScopeSettings() {
|
|
|
4957
4957
|
hooks: {
|
|
4958
4958
|
PreToolUse: [
|
|
4959
4959
|
{
|
|
4960
|
-
matcher: "Read|Write|Edit",
|
|
4960
|
+
matcher: "Read|Write|Edit|Glob|Grep",
|
|
4961
4961
|
hooks: [hookCommand]
|
|
4962
4962
|
}
|
|
4963
4963
|
]
|
|
@@ -5022,7 +5022,114 @@ function spawnWorker(prompt, model, settings, workingDir) {
|
|
|
5022
5022
|
}
|
|
5023
5023
|
return result.stdout || "";
|
|
5024
5024
|
}
|
|
5025
|
-
function
|
|
5025
|
+
function formatStreamLine(line) {
|
|
5026
|
+
try {
|
|
5027
|
+
const data = JSON.parse(line);
|
|
5028
|
+
if (data.type === "assistant" && data.message?.content) {
|
|
5029
|
+
for (const block of data.message.content) {
|
|
5030
|
+
if (block.type === "text" && block.text) {
|
|
5031
|
+
return block.text;
|
|
5032
|
+
}
|
|
5033
|
+
if (block.type === "tool_use") {
|
|
5034
|
+
return `[Tool: ${block.name}]`;
|
|
5035
|
+
}
|
|
5036
|
+
}
|
|
5037
|
+
}
|
|
5038
|
+
if (data.type === "system" && data.subtype === "init") {
|
|
5039
|
+
return `[Session started: ${data.session_id}]`;
|
|
5040
|
+
}
|
|
5041
|
+
if (data.type === "result") {
|
|
5042
|
+
const status = data.subtype === "success" ? "completed" : "failed";
|
|
5043
|
+
return `
|
|
5044
|
+
[Worker ${status} in ${Math.round(data.duration_ms / 1e3)}s]`;
|
|
5045
|
+
}
|
|
5046
|
+
return null;
|
|
5047
|
+
} catch {
|
|
5048
|
+
return null;
|
|
5049
|
+
}
|
|
5050
|
+
}
|
|
5051
|
+
function parseStreamingResult(buffer) {
|
|
5052
|
+
const lines = buffer.split("\n").filter((line) => line.trim());
|
|
5053
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
5054
|
+
try {
|
|
5055
|
+
const data = JSON.parse(lines[i]);
|
|
5056
|
+
if (data.type === "result") {
|
|
5057
|
+
if (data.is_error) {
|
|
5058
|
+
throw new Error(`Worker failed: ${data.result || "Unknown error"}`);
|
|
5059
|
+
}
|
|
5060
|
+
if (!data.structured_output) {
|
|
5061
|
+
throw new Error("Worker result missing structured_output");
|
|
5062
|
+
}
|
|
5063
|
+
const output = data.structured_output;
|
|
5064
|
+
if (!VALID_STATUSES.has(output.status)) {
|
|
5065
|
+
throw new Error(`Invalid status: ${output.status}`);
|
|
5066
|
+
}
|
|
5067
|
+
return {
|
|
5068
|
+
status: output.status,
|
|
5069
|
+
summary: output.summary || "",
|
|
5070
|
+
blocker: output.blocker ?? null
|
|
5071
|
+
};
|
|
5072
|
+
}
|
|
5073
|
+
} catch (e) {
|
|
5074
|
+
if (e instanceof Error && e.message.startsWith("Worker")) {
|
|
5075
|
+
throw e;
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
}
|
|
5079
|
+
throw new Error("No result found in worker output");
|
|
5080
|
+
}
|
|
5081
|
+
function spawnWorkerAsync(prompt, model, settings, workingDir) {
|
|
5082
|
+
return new Promise((resolve2, reject) => {
|
|
5083
|
+
let buffer = "";
|
|
5084
|
+
const args = [
|
|
5085
|
+
"-p",
|
|
5086
|
+
prompt,
|
|
5087
|
+
"--model",
|
|
5088
|
+
model,
|
|
5089
|
+
"--output-format",
|
|
5090
|
+
"stream-json",
|
|
5091
|
+
"--verbose",
|
|
5092
|
+
"--json-schema",
|
|
5093
|
+
JSON.stringify(WORKER_OUTPUT_SCHEMA),
|
|
5094
|
+
"--settings",
|
|
5095
|
+
JSON.stringify(settings),
|
|
5096
|
+
"--dangerously-skip-permissions"
|
|
5097
|
+
];
|
|
5098
|
+
const child = (0, import_node_child_process.spawn)("claude", args, {
|
|
5099
|
+
cwd: workingDir,
|
|
5100
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
5101
|
+
});
|
|
5102
|
+
child.stdout.on("data", (chunk) => {
|
|
5103
|
+
const text = chunk.toString();
|
|
5104
|
+
buffer += text;
|
|
5105
|
+
const lines = text.split("\n");
|
|
5106
|
+
for (const line of lines) {
|
|
5107
|
+
if (line.trim()) {
|
|
5108
|
+
const formatted = formatStreamLine(line);
|
|
5109
|
+
if (formatted) {
|
|
5110
|
+
process.stdout.write(formatted);
|
|
5111
|
+
}
|
|
5112
|
+
}
|
|
5113
|
+
}
|
|
5114
|
+
});
|
|
5115
|
+
child.stderr.on("data", (chunk) => {
|
|
5116
|
+
process.stderr.write(chunk);
|
|
5117
|
+
});
|
|
5118
|
+
child.on("error", (err) => {
|
|
5119
|
+
reject(new Error(`Failed to spawn worker: ${err.message}`));
|
|
5120
|
+
});
|
|
5121
|
+
child.on("close", (code) => {
|
|
5122
|
+
process.stdout.write("\n");
|
|
5123
|
+
try {
|
|
5124
|
+
const result = parseStreamingResult(buffer);
|
|
5125
|
+
resolve2(result);
|
|
5126
|
+
} catch (e) {
|
|
5127
|
+
reject(e);
|
|
5128
|
+
}
|
|
5129
|
+
});
|
|
5130
|
+
});
|
|
5131
|
+
}
|
|
5132
|
+
async function runLoop(epicSlug, storySlug, maxCycles, maxTime, model, projectDir, pluginRoot, stream = false) {
|
|
5026
5133
|
const worktree = (0, import_node_path4.join)(projectDir, ".saga", "worktrees", epicSlug, storySlug);
|
|
5027
5134
|
const validation = validateStoryFiles(worktree, epicSlug, storySlug);
|
|
5028
5135
|
if (!validation.valid) {
|
|
@@ -5064,33 +5171,55 @@ function runLoop(epicSlug, storySlug, maxCycles, maxTime, model, projectDir, plu
|
|
|
5064
5171
|
break;
|
|
5065
5172
|
}
|
|
5066
5173
|
cycles += 1;
|
|
5067
|
-
let output;
|
|
5068
|
-
try {
|
|
5069
|
-
output = spawnWorker(workerPrompt, model, settings, worktree);
|
|
5070
|
-
} catch (e) {
|
|
5071
|
-
return {
|
|
5072
|
-
status: "ERROR",
|
|
5073
|
-
summary: e.message,
|
|
5074
|
-
cycles,
|
|
5075
|
-
elapsedMinutes: (Date.now() - startTime) / 6e4,
|
|
5076
|
-
blocker: null,
|
|
5077
|
-
epicSlug,
|
|
5078
|
-
storySlug
|
|
5079
|
-
};
|
|
5080
|
-
}
|
|
5081
5174
|
let parsed;
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5175
|
+
if (stream) {
|
|
5176
|
+
console.log(`
|
|
5177
|
+
--- Worker ${cycles} started ---
|
|
5178
|
+
`);
|
|
5179
|
+
try {
|
|
5180
|
+
parsed = await spawnWorkerAsync(workerPrompt, model, settings, worktree);
|
|
5181
|
+
} catch (e) {
|
|
5182
|
+
return {
|
|
5183
|
+
status: "ERROR",
|
|
5184
|
+
summary: e.message,
|
|
5185
|
+
cycles,
|
|
5186
|
+
elapsedMinutes: (Date.now() - startTime) / 6e4,
|
|
5187
|
+
blocker: null,
|
|
5188
|
+
epicSlug,
|
|
5189
|
+
storySlug
|
|
5190
|
+
};
|
|
5191
|
+
}
|
|
5192
|
+
console.log(`
|
|
5193
|
+
--- Worker ${cycles} result: ${parsed.status} ---
|
|
5194
|
+
`);
|
|
5195
|
+
} else {
|
|
5196
|
+
let output;
|
|
5197
|
+
try {
|
|
5198
|
+
output = spawnWorker(workerPrompt, model, settings, worktree);
|
|
5199
|
+
} catch (e) {
|
|
5200
|
+
return {
|
|
5201
|
+
status: "ERROR",
|
|
5202
|
+
summary: e.message,
|
|
5203
|
+
cycles,
|
|
5204
|
+
elapsedMinutes: (Date.now() - startTime) / 6e4,
|
|
5205
|
+
blocker: null,
|
|
5206
|
+
epicSlug,
|
|
5207
|
+
storySlug
|
|
5208
|
+
};
|
|
5209
|
+
}
|
|
5210
|
+
try {
|
|
5211
|
+
parsed = parseWorkerOutput(output);
|
|
5212
|
+
} catch (e) {
|
|
5213
|
+
return {
|
|
5214
|
+
status: "ERROR",
|
|
5215
|
+
summary: e.message,
|
|
5216
|
+
cycles,
|
|
5217
|
+
elapsedMinutes: (Date.now() - startTime) / 6e4,
|
|
5218
|
+
blocker: null,
|
|
5219
|
+
epicSlug,
|
|
5220
|
+
storySlug
|
|
5221
|
+
};
|
|
5222
|
+
}
|
|
5094
5223
|
}
|
|
5095
5224
|
summaries.push(parsed.summary);
|
|
5096
5225
|
if (parsed.status === "FINISH") {
|
|
@@ -5157,19 +5286,22 @@ Searched in: ${(0, import_node_path4.join)(projectPath, ".saga", "worktrees")}`)
|
|
|
5157
5286
|
const maxCycles = options.maxCycles ?? DEFAULT_MAX_CYCLES;
|
|
5158
5287
|
const maxTime = options.maxTime ?? DEFAULT_MAX_TIME;
|
|
5159
5288
|
const model = options.model ?? DEFAULT_MODEL;
|
|
5289
|
+
const stream = options.stream ?? false;
|
|
5160
5290
|
console.log("Starting story implementation...");
|
|
5161
5291
|
console.log(` Epic: ${storyInfo.epicSlug}`);
|
|
5162
5292
|
console.log(` Story: ${storyInfo.storySlug}`);
|
|
5163
5293
|
console.log(` Worktree: ${storyInfo.worktreePath}`);
|
|
5294
|
+
console.log(` Streaming: ${stream ? "enabled" : "disabled"}`);
|
|
5164
5295
|
console.log("");
|
|
5165
|
-
const result = runLoop(
|
|
5296
|
+
const result = await runLoop(
|
|
5166
5297
|
storyInfo.epicSlug,
|
|
5167
5298
|
storyInfo.storySlug,
|
|
5168
5299
|
maxCycles,
|
|
5169
5300
|
maxTime,
|
|
5170
5301
|
model,
|
|
5171
5302
|
projectPath,
|
|
5172
|
-
pluginRoot
|
|
5303
|
+
pluginRoot,
|
|
5304
|
+
stream
|
|
5173
5305
|
);
|
|
5174
5306
|
console.log(JSON.stringify(result, null, 2));
|
|
5175
5307
|
if (result.status === "ERROR") {
|
|
@@ -5196,10 +5328,12 @@ async function dashboardCommand(options) {
|
|
|
5196
5328
|
}
|
|
5197
5329
|
|
|
5198
5330
|
// src/commands/scope-validator.ts
|
|
5199
|
-
|
|
5331
|
+
var import_node_path5 = require("node:path");
|
|
5332
|
+
function getFilePathFromInput(hookInput) {
|
|
5200
5333
|
try {
|
|
5201
|
-
const data = JSON.parse(
|
|
5202
|
-
|
|
5334
|
+
const data = JSON.parse(hookInput);
|
|
5335
|
+
const toolInput = data.tool_input || {};
|
|
5336
|
+
return toolInput.file_path || toolInput.path || null;
|
|
5203
5337
|
} catch {
|
|
5204
5338
|
return null;
|
|
5205
5339
|
}
|
|
@@ -5213,6 +5347,15 @@ function normalizePath(path) {
|
|
|
5213
5347
|
function isArchiveAccess(path) {
|
|
5214
5348
|
return path.includes(".saga/archive");
|
|
5215
5349
|
}
|
|
5350
|
+
function isWithinWorktree(filePath, worktreePath) {
|
|
5351
|
+
const absoluteFilePath = (0, import_node_path5.resolve)(filePath);
|
|
5352
|
+
const absoluteWorktree = (0, import_node_path5.resolve)(worktreePath);
|
|
5353
|
+
const relativePath = (0, import_node_path5.relative)(absoluteWorktree, absoluteFilePath);
|
|
5354
|
+
if (relativePath.startsWith("..") || (0, import_node_path5.resolve)(relativePath) === relativePath) {
|
|
5355
|
+
return false;
|
|
5356
|
+
}
|
|
5357
|
+
return true;
|
|
5358
|
+
}
|
|
5216
5359
|
function checkStoryAccess(path, allowedEpic, allowedStory) {
|
|
5217
5360
|
if (!path.includes(".saga/epics/")) {
|
|
5218
5361
|
return true;
|
|
@@ -5233,24 +5376,27 @@ function checkStoryAccess(path, allowedEpic, allowedStory) {
|
|
|
5233
5376
|
return pathEpic === allowedEpic;
|
|
5234
5377
|
}
|
|
5235
5378
|
}
|
|
5236
|
-
function printScopeViolation(filePath, epicSlug, storySlug, reason) {
|
|
5379
|
+
function printScopeViolation(filePath, epicSlug, storySlug, worktreePath, reason) {
|
|
5237
5380
|
console.error(`SCOPE VIOLATION: ${reason}
|
|
5238
5381
|
|
|
5239
5382
|
Attempted path: ${filePath}
|
|
5240
5383
|
|
|
5241
5384
|
Your scope is limited to:
|
|
5385
|
+
Worktree: ${worktreePath}
|
|
5242
5386
|
Epic: ${epicSlug}
|
|
5243
5387
|
Story: ${storySlug}
|
|
5244
|
-
Allowed: .saga/epics/${epicSlug}/stories/${storySlug}/
|
|
5388
|
+
Allowed story files: .saga/epics/${epicSlug}/stories/${storySlug}/
|
|
5245
5389
|
|
|
5390
|
+
Workers cannot access files outside the worktree directory.
|
|
5246
5391
|
To access other stories, start a new /implement session for that story.`);
|
|
5247
5392
|
}
|
|
5248
5393
|
async function scopeValidatorCommand() {
|
|
5394
|
+
const worktreePath = process.env.SAGA_PROJECT_DIR || "";
|
|
5249
5395
|
const epicSlug = process.env.SAGA_EPIC_SLUG || "";
|
|
5250
5396
|
const storySlug = process.env.SAGA_STORY_SLUG || "";
|
|
5251
|
-
if (!epicSlug || !storySlug) {
|
|
5397
|
+
if (!worktreePath || !epicSlug || !storySlug) {
|
|
5252
5398
|
console.error(
|
|
5253
|
-
"ERROR: scope-validator requires SAGA_EPIC_SLUG and SAGA_STORY_SLUG environment variables"
|
|
5399
|
+
"ERROR: scope-validator requires SAGA_PROJECT_DIR, SAGA_EPIC_SLUG, and SAGA_STORY_SLUG environment variables"
|
|
5254
5400
|
);
|
|
5255
5401
|
process.exit(2);
|
|
5256
5402
|
}
|
|
@@ -5264,11 +5410,22 @@ async function scopeValidatorCommand() {
|
|
|
5264
5410
|
process.exit(0);
|
|
5265
5411
|
}
|
|
5266
5412
|
const normPath = normalizePath(filePath);
|
|
5413
|
+
if (!isWithinWorktree(normPath, worktreePath)) {
|
|
5414
|
+
printScopeViolation(
|
|
5415
|
+
filePath,
|
|
5416
|
+
epicSlug,
|
|
5417
|
+
storySlug,
|
|
5418
|
+
worktreePath,
|
|
5419
|
+
"Access outside worktree blocked\nReason: Workers can only access files within their assigned worktree directory."
|
|
5420
|
+
);
|
|
5421
|
+
process.exit(2);
|
|
5422
|
+
}
|
|
5267
5423
|
if (isArchiveAccess(normPath)) {
|
|
5268
5424
|
printScopeViolation(
|
|
5269
5425
|
filePath,
|
|
5270
5426
|
epicSlug,
|
|
5271
5427
|
storySlug,
|
|
5428
|
+
worktreePath,
|
|
5272
5429
|
"Access to archive folder blocked\nReason: The archive folder contains completed stories and is read-only during execution."
|
|
5273
5430
|
);
|
|
5274
5431
|
process.exit(2);
|
|
@@ -5278,6 +5435,7 @@ async function scopeValidatorCommand() {
|
|
|
5278
5435
|
filePath,
|
|
5279
5436
|
epicSlug,
|
|
5280
5437
|
storySlug,
|
|
5438
|
+
worktreePath,
|
|
5281
5439
|
"Access to other story blocked\nReason: Workers can only access their assigned story's files."
|
|
5282
5440
|
);
|
|
5283
5441
|
process.exit(2);
|
|
@@ -5308,7 +5466,7 @@ async function findCommand(query, options) {
|
|
|
5308
5466
|
}
|
|
5309
5467
|
|
|
5310
5468
|
// src/commands/worktree.ts
|
|
5311
|
-
var
|
|
5469
|
+
var import_node_path6 = require("node:path");
|
|
5312
5470
|
var import_node_fs5 = require("node:fs");
|
|
5313
5471
|
var import_node_child_process2 = require("node:child_process");
|
|
5314
5472
|
function runGitCommand(args, cwd) {
|
|
@@ -5340,7 +5498,7 @@ function getMainBranch(cwd) {
|
|
|
5340
5498
|
}
|
|
5341
5499
|
function createWorktree(projectPath, epicSlug, storySlug) {
|
|
5342
5500
|
const branchName = `story-${storySlug}-epic-${epicSlug}`;
|
|
5343
|
-
const worktreePath = (0,
|
|
5501
|
+
const worktreePath = (0, import_node_path6.join)(
|
|
5344
5502
|
projectPath,
|
|
5345
5503
|
".saga",
|
|
5346
5504
|
"worktrees",
|
|
@@ -5371,7 +5529,7 @@ function createWorktree(projectPath, epicSlug, storySlug) {
|
|
|
5371
5529
|
error: `Failed to create branch: ${createBranchResult.output}`
|
|
5372
5530
|
};
|
|
5373
5531
|
}
|
|
5374
|
-
const worktreeParent = (0,
|
|
5532
|
+
const worktreeParent = (0, import_node_path6.join)(projectPath, ".saga", "worktrees", epicSlug);
|
|
5375
5533
|
(0, import_node_fs5.mkdirSync)(worktreeParent, { recursive: true });
|
|
5376
5534
|
const createWorktreeResult = runGitCommand(
|
|
5377
5535
|
["worktree", "add", worktreePath, branchName],
|
|
@@ -5406,7 +5564,7 @@ async function worktreeCommand(epicSlug, storySlug, options) {
|
|
|
5406
5564
|
}
|
|
5407
5565
|
|
|
5408
5566
|
// src/cli.ts
|
|
5409
|
-
var packageJsonPath = (0,
|
|
5567
|
+
var packageJsonPath = (0, import_node_path7.join)(__dirname, "..", "package.json");
|
|
5410
5568
|
var packageJson = JSON.parse((0, import_node_fs6.readFileSync)(packageJsonPath, "utf-8"));
|
|
5411
5569
|
var program2 = new Command();
|
|
5412
5570
|
program2.name("saga").description("CLI for SAGA - Structured Autonomous Goal Achievement").version(packageJson.version).addHelpCommand("help [command]", "Display help for a command");
|
|
@@ -5415,14 +5573,15 @@ program2.command("init").description("Initialize .saga/ directory structure").op
|
|
|
5415
5573
|
const globalOpts = program2.opts();
|
|
5416
5574
|
await initCommand({ path: globalOpts.path, dryRun: options.dryRun });
|
|
5417
5575
|
});
|
|
5418
|
-
program2.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) => {
|
|
5576
|
+
program2.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) => {
|
|
5419
5577
|
const globalOpts = program2.opts();
|
|
5420
5578
|
await implementCommand(storySlug, {
|
|
5421
5579
|
path: globalOpts.path,
|
|
5422
5580
|
maxCycles: options.maxCycles,
|
|
5423
5581
|
maxTime: options.maxTime,
|
|
5424
5582
|
model: options.model,
|
|
5425
|
-
dryRun: options.dryRun
|
|
5583
|
+
dryRun: options.dryRun,
|
|
5584
|
+
stream: options.stream
|
|
5426
5585
|
});
|
|
5427
5586
|
});
|
|
5428
5587
|
program2.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) => {
|