@saga-ai/cli 0.2.0 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +18 -0
  2. package/dist/cli.cjs +140 -12
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -14,6 +14,7 @@ Run commands using npx (no global installation required):
14
14
  ```bash
15
15
  npx @saga-ai/cli init
16
16
  npx @saga-ai/cli find <query>
17
+ npx @saga-ai/cli worktree <epic-slug> <story-slug>
17
18
  npx @saga-ai/cli implement <story-slug>
18
19
  npx @saga-ai/cli dashboard
19
20
  ```
@@ -56,6 +57,23 @@ Returns JSON with:
56
57
 
57
58
  Supports typo tolerance (e.g., "implment" matches "implement").
58
59
 
60
+ ### `saga worktree <epic-slug> <story-slug>`
61
+
62
+ Create a git worktree and branch for story isolation.
63
+
64
+ ```bash
65
+ saga worktree my-epic my-story
66
+ saga worktree auth-system login-form --path /path/to/project
67
+ ```
68
+
69
+ Creates:
70
+ - Branch: `story-<story-slug>-epic-<epic-slug>`
71
+ - Worktree: `.saga/worktrees/<epic-slug>/<story-slug>/`
72
+
73
+ Returns JSON with:
74
+ - `success: true` + `worktreePath`, `branch` - Worktree created
75
+ - `success: false` + `error` - Creation failed (branch exists, directory exists, etc.)
76
+
59
77
  ### `saga implement <story-slug>`
60
78
 
61
79
  Run autonomous story implementation using Claude workers.
package/dist/cli.cjs CHANGED
@@ -3037,8 +3037,8 @@ var {
3037
3037
  } = import_index.default;
3038
3038
 
3039
3039
  // src/cli.ts
3040
- var import_node_path5 = require("node:path");
3041
- var import_node_fs5 = require("node:fs");
3040
+ var import_node_path7 = require("node:path");
3041
+ var import_node_fs6 = require("node:fs");
3042
3042
 
3043
3043
  // src/commands/init.ts
3044
3044
  var import_node_path2 = require("node:path");
@@ -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
  ]
@@ -5196,10 +5196,12 @@ async function dashboardCommand(options) {
5196
5196
  }
5197
5197
 
5198
5198
  // src/commands/scope-validator.ts
5199
- function getFilePathFromInput(toolInput) {
5199
+ var import_node_path5 = require("node:path");
5200
+ function getFilePathFromInput(hookInput) {
5200
5201
  try {
5201
- const data = JSON.parse(toolInput);
5202
- return data.file_path || data.path || null;
5202
+ const data = JSON.parse(hookInput);
5203
+ const toolInput = data.tool_input || {};
5204
+ return toolInput.file_path || toolInput.path || null;
5203
5205
  } catch {
5204
5206
  return null;
5205
5207
  }
@@ -5213,6 +5215,15 @@ function normalizePath(path) {
5213
5215
  function isArchiveAccess(path) {
5214
5216
  return path.includes(".saga/archive");
5215
5217
  }
5218
+ function isWithinWorktree(filePath, worktreePath) {
5219
+ const absoluteFilePath = (0, import_node_path5.resolve)(filePath);
5220
+ const absoluteWorktree = (0, import_node_path5.resolve)(worktreePath);
5221
+ const relativePath = (0, import_node_path5.relative)(absoluteWorktree, absoluteFilePath);
5222
+ if (relativePath.startsWith("..") || (0, import_node_path5.resolve)(relativePath) === relativePath) {
5223
+ return false;
5224
+ }
5225
+ return true;
5226
+ }
5216
5227
  function checkStoryAccess(path, allowedEpic, allowedStory) {
5217
5228
  if (!path.includes(".saga/epics/")) {
5218
5229
  return true;
@@ -5233,24 +5244,27 @@ function checkStoryAccess(path, allowedEpic, allowedStory) {
5233
5244
  return pathEpic === allowedEpic;
5234
5245
  }
5235
5246
  }
5236
- function printScopeViolation(filePath, epicSlug, storySlug, reason) {
5247
+ function printScopeViolation(filePath, epicSlug, storySlug, worktreePath, reason) {
5237
5248
  console.error(`SCOPE VIOLATION: ${reason}
5238
5249
 
5239
5250
  Attempted path: ${filePath}
5240
5251
 
5241
5252
  Your scope is limited to:
5253
+ Worktree: ${worktreePath}
5242
5254
  Epic: ${epicSlug}
5243
5255
  Story: ${storySlug}
5244
- Allowed: .saga/epics/${epicSlug}/stories/${storySlug}/
5256
+ Allowed story files: .saga/epics/${epicSlug}/stories/${storySlug}/
5245
5257
 
5258
+ Workers cannot access files outside the worktree directory.
5246
5259
  To access other stories, start a new /implement session for that story.`);
5247
5260
  }
5248
5261
  async function scopeValidatorCommand() {
5262
+ const worktreePath = process.env.SAGA_PROJECT_DIR || "";
5249
5263
  const epicSlug = process.env.SAGA_EPIC_SLUG || "";
5250
5264
  const storySlug = process.env.SAGA_STORY_SLUG || "";
5251
- if (!epicSlug || !storySlug) {
5265
+ if (!worktreePath || !epicSlug || !storySlug) {
5252
5266
  console.error(
5253
- "ERROR: scope-validator requires SAGA_EPIC_SLUG and SAGA_STORY_SLUG environment variables"
5267
+ "ERROR: scope-validator requires SAGA_PROJECT_DIR, SAGA_EPIC_SLUG, and SAGA_STORY_SLUG environment variables"
5254
5268
  );
5255
5269
  process.exit(2);
5256
5270
  }
@@ -5264,11 +5278,22 @@ async function scopeValidatorCommand() {
5264
5278
  process.exit(0);
5265
5279
  }
5266
5280
  const normPath = normalizePath(filePath);
5281
+ if (!isWithinWorktree(normPath, worktreePath)) {
5282
+ printScopeViolation(
5283
+ filePath,
5284
+ epicSlug,
5285
+ storySlug,
5286
+ worktreePath,
5287
+ "Access outside worktree blocked\nReason: Workers can only access files within their assigned worktree directory."
5288
+ );
5289
+ process.exit(2);
5290
+ }
5267
5291
  if (isArchiveAccess(normPath)) {
5268
5292
  printScopeViolation(
5269
5293
  filePath,
5270
5294
  epicSlug,
5271
5295
  storySlug,
5296
+ worktreePath,
5272
5297
  "Access to archive folder blocked\nReason: The archive folder contains completed stories and is read-only during execution."
5273
5298
  );
5274
5299
  process.exit(2);
@@ -5278,6 +5303,7 @@ async function scopeValidatorCommand() {
5278
5303
  filePath,
5279
5304
  epicSlug,
5280
5305
  storySlug,
5306
+ worktreePath,
5281
5307
  "Access to other story blocked\nReason: Workers can only access their assigned story's files."
5282
5308
  );
5283
5309
  process.exit(2);
@@ -5307,9 +5333,107 @@ async function findCommand(query, options) {
5307
5333
  }
5308
5334
  }
5309
5335
 
5336
+ // src/commands/worktree.ts
5337
+ var import_node_path6 = require("node:path");
5338
+ var import_node_fs5 = require("node:fs");
5339
+ var import_node_child_process2 = require("node:child_process");
5340
+ function runGitCommand(args, cwd) {
5341
+ try {
5342
+ const output = (0, import_node_child_process2.execSync)(`git ${args.join(" ")}`, {
5343
+ cwd,
5344
+ encoding: "utf-8",
5345
+ stdio: ["pipe", "pipe", "pipe"]
5346
+ });
5347
+ return { success: true, output: output.trim() };
5348
+ } catch (error) {
5349
+ const stderr = error.stderr?.toString().trim() || error.message;
5350
+ return { success: false, output: stderr };
5351
+ }
5352
+ }
5353
+ function branchExists(branchName, cwd) {
5354
+ const result = runGitCommand(["rev-parse", "--verify", branchName], cwd);
5355
+ return result.success;
5356
+ }
5357
+ function getMainBranch(cwd) {
5358
+ const result = runGitCommand(
5359
+ ["symbolic-ref", "refs/remotes/origin/HEAD"],
5360
+ cwd
5361
+ );
5362
+ if (result.success) {
5363
+ return result.output.replace("refs/remotes/origin/", "");
5364
+ }
5365
+ return "main";
5366
+ }
5367
+ function createWorktree(projectPath, epicSlug, storySlug) {
5368
+ const branchName = `story-${storySlug}-epic-${epicSlug}`;
5369
+ const worktreePath = (0, import_node_path6.join)(
5370
+ projectPath,
5371
+ ".saga",
5372
+ "worktrees",
5373
+ epicSlug,
5374
+ storySlug
5375
+ );
5376
+ if (branchExists(branchName, projectPath)) {
5377
+ return {
5378
+ success: false,
5379
+ error: `Branch already exists: ${branchName}`
5380
+ };
5381
+ }
5382
+ if ((0, import_node_fs5.existsSync)(worktreePath)) {
5383
+ return {
5384
+ success: false,
5385
+ error: `Worktree directory already exists: ${worktreePath}`
5386
+ };
5387
+ }
5388
+ const mainBranch = getMainBranch(projectPath);
5389
+ runGitCommand(["fetch", "origin", mainBranch], projectPath);
5390
+ const createBranchResult = runGitCommand(
5391
+ ["branch", branchName, `origin/${mainBranch}`],
5392
+ projectPath
5393
+ );
5394
+ if (!createBranchResult.success) {
5395
+ return {
5396
+ success: false,
5397
+ error: `Failed to create branch: ${createBranchResult.output}`
5398
+ };
5399
+ }
5400
+ const worktreeParent = (0, import_node_path6.join)(projectPath, ".saga", "worktrees", epicSlug);
5401
+ (0, import_node_fs5.mkdirSync)(worktreeParent, { recursive: true });
5402
+ const createWorktreeResult = runGitCommand(
5403
+ ["worktree", "add", worktreePath, branchName],
5404
+ projectPath
5405
+ );
5406
+ if (!createWorktreeResult.success) {
5407
+ return {
5408
+ success: false,
5409
+ error: `Failed to create worktree: ${createWorktreeResult.output}`
5410
+ };
5411
+ }
5412
+ return {
5413
+ success: true,
5414
+ worktreePath,
5415
+ branch: branchName
5416
+ };
5417
+ }
5418
+ async function worktreeCommand(epicSlug, storySlug, options) {
5419
+ let projectPath;
5420
+ try {
5421
+ projectPath = resolveProjectPath(options.path);
5422
+ } catch (error) {
5423
+ const result2 = { success: false, error: error.message };
5424
+ console.log(JSON.stringify(result2));
5425
+ process.exit(1);
5426
+ }
5427
+ const result = createWorktree(projectPath, epicSlug, storySlug);
5428
+ console.log(JSON.stringify(result));
5429
+ if (!result.success) {
5430
+ process.exit(1);
5431
+ }
5432
+ }
5433
+
5310
5434
  // src/cli.ts
5311
- var packageJsonPath = (0, import_node_path5.join)(__dirname, "..", "package.json");
5312
- var packageJson = JSON.parse((0, import_node_fs5.readFileSync)(packageJsonPath, "utf-8"));
5435
+ var packageJsonPath = (0, import_node_path7.join)(__dirname, "..", "package.json");
5436
+ var packageJson = JSON.parse((0, import_node_fs6.readFileSync)(packageJsonPath, "utf-8"));
5313
5437
  var program2 = new Command();
5314
5438
  program2.name("saga").description("CLI for SAGA - Structured Autonomous Goal Achievement").version(packageJson.version).addHelpCommand("help [command]", "Display help for a command");
5315
5439
  program2.option("-p, --path <dir>", "Path to SAGA project directory (overrides auto-discovery)");
@@ -5334,6 +5458,10 @@ program2.command("find <query>").description("Find an epic or story by slug/titl
5334
5458
  type: options.type
5335
5459
  });
5336
5460
  });
5461
+ program2.command("worktree <epic-slug> <story-slug>").description("Create git worktree for a story").action(async (epicSlug, storySlug) => {
5462
+ const globalOpts = program2.opts();
5463
+ await worktreeCommand(epicSlug, storySlug, { path: globalOpts.path });
5464
+ });
5337
5465
  program2.command("dashboard").description("Start the dashboard server").option("--port <n>", "Port to run the server on (default: 3847)", parseInt).action(async (options) => {
5338
5466
  const globalOpts = program2.opts();
5339
5467
  await dashboardCommand({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saga-ai/cli",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "CLI for SAGA - Structured Autonomous Goal Achievement",
5
5
  "type": "module",
6
6
  "bin": {