@locusai/sdk 0.10.2 → 0.10.5

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.
@@ -768,10 +768,12 @@ class ClaudeRunner {
768
768
  currentToolName;
769
769
  activeTools = new Map;
770
770
  activeProcess = null;
771
- constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CLAUDE], log) {
771
+ timeoutMs;
772
+ constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CLAUDE], log, timeoutMs) {
772
773
  this.model = model;
773
774
  this.log = log;
774
775
  this.projectPath = import_node_path3.resolve(projectPath);
776
+ this.timeoutMs = timeoutMs ?? DEFAULT_TIMEOUT_MS;
775
777
  }
776
778
  setEventEmitter(emitter) {
777
779
  this.eventEmitter = emitter;
@@ -787,11 +789,14 @@ class ClaudeRunner {
787
789
  let lastError = null;
788
790
  for (let attempt = 1;attempt <= maxRetries; attempt++) {
789
791
  try {
790
- return await this.executeRun(prompt);
792
+ return await this.withTimeout(this.executeRun(prompt));
791
793
  } catch (error) {
792
794
  const err = error;
793
795
  lastError = err;
794
796
  const isLastAttempt = attempt === maxRetries;
797
+ if (err.message.includes("timed out")) {
798
+ throw err;
799
+ }
795
800
  if (!isLastAttempt) {
796
801
  const delay = Math.pow(2, attempt) * 1000;
797
802
  console.warn(`Claude CLI attempt ${attempt} failed: ${err.message}. Retrying in ${delay}ms...`);
@@ -801,6 +806,23 @@ class ClaudeRunner {
801
806
  }
802
807
  throw lastError || new Error("Claude CLI failed after multiple attempts");
803
808
  }
809
+ withTimeout(promise) {
810
+ if (this.timeoutMs <= 0)
811
+ return promise;
812
+ return new Promise((resolve2, reject) => {
813
+ const timer = setTimeout(() => {
814
+ this.abort();
815
+ reject(new Error(`Claude CLI execution timed out after ${Math.round(this.timeoutMs / 60000)} minutes`));
816
+ }, this.timeoutMs);
817
+ promise.then((value) => {
818
+ clearTimeout(timer);
819
+ resolve2(value);
820
+ }, (err) => {
821
+ clearTimeout(timer);
822
+ reject(err);
823
+ });
824
+ });
825
+ }
804
826
  async* runStream(prompt) {
805
827
  const args = [
806
828
  "--dangerously-skip-permissions",
@@ -1165,7 +1187,7 @@ ${c.primary("[Claude]")} ${c.bold(`Running ${content_block.name}...`)}
1165
1187
  return new Error(message);
1166
1188
  }
1167
1189
  }
1168
- var import_node_child_process, import_node_path3, SANDBOX_SETTINGS;
1190
+ var import_node_child_process, import_node_path3, SANDBOX_SETTINGS, DEFAULT_TIMEOUT_MS;
1169
1191
  var init_claude_runner = __esm(() => {
1170
1192
  init_config();
1171
1193
  init_colors();
@@ -1173,12 +1195,16 @@ var init_claude_runner = __esm(() => {
1173
1195
  import_node_child_process = require("node:child_process");
1174
1196
  import_node_path3 = require("node:path");
1175
1197
  SANDBOX_SETTINGS = JSON.stringify({
1198
+ permissions: {
1199
+ deny: ["Read(../**)", "Edit(../**)"]
1200
+ },
1176
1201
  sandbox: {
1177
1202
  enabled: true,
1178
1203
  autoAllow: true,
1179
1204
  allowUnsandboxedCommands: false
1180
1205
  }
1181
1206
  });
1207
+ DEFAULT_TIMEOUT_MS = 60 * 60 * 1000;
1182
1208
  });
1183
1209
 
1184
1210
  // src/ai/codex-runner.ts
@@ -1187,10 +1213,17 @@ class CodexRunner {
1187
1213
  model;
1188
1214
  log;
1189
1215
  activeProcess = null;
1190
- constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CODEX], log) {
1216
+ eventEmitter;
1217
+ currentToolName;
1218
+ timeoutMs;
1219
+ constructor(projectPath, model = DEFAULT_MODEL[PROVIDER.CODEX], log, timeoutMs) {
1191
1220
  this.projectPath = projectPath;
1192
1221
  this.model = model;
1193
1222
  this.log = log;
1223
+ this.timeoutMs = timeoutMs ?? DEFAULT_TIMEOUT_MS2;
1224
+ }
1225
+ setEventEmitter(emitter) {
1226
+ this.eventEmitter = emitter;
1194
1227
  }
1195
1228
  abort() {
1196
1229
  if (this.activeProcess && !this.activeProcess.killed) {
@@ -1203,9 +1236,12 @@ class CodexRunner {
1203
1236
  let lastError = null;
1204
1237
  for (let attempt = 1;attempt <= maxRetries; attempt++) {
1205
1238
  try {
1206
- return await this.executeRun(prompt);
1239
+ return await this.withTimeout(this.executeRun(prompt));
1207
1240
  } catch (error) {
1208
1241
  lastError = error;
1242
+ if (lastError.message.includes("timed out")) {
1243
+ throw lastError;
1244
+ }
1209
1245
  if (attempt < maxRetries) {
1210
1246
  const delay = Math.pow(2, attempt) * 1000;
1211
1247
  console.warn(`Codex CLI attempt ${attempt} failed: ${lastError.message}. Retrying in ${delay}ms...`);
@@ -1215,9 +1251,31 @@ class CodexRunner {
1215
1251
  }
1216
1252
  throw lastError || new Error("Codex CLI failed after multiple attempts");
1217
1253
  }
1254
+ withTimeout(promise) {
1255
+ if (this.timeoutMs <= 0)
1256
+ return promise;
1257
+ return new Promise((resolve2, reject) => {
1258
+ const timer = setTimeout(() => {
1259
+ this.abort();
1260
+ reject(new Error(`Codex CLI execution timed out after ${Math.round(this.timeoutMs / 60000)} minutes`));
1261
+ }, this.timeoutMs);
1262
+ promise.then((value) => {
1263
+ clearTimeout(timer);
1264
+ resolve2(value);
1265
+ }, (err) => {
1266
+ clearTimeout(timer);
1267
+ reject(err);
1268
+ });
1269
+ });
1270
+ }
1218
1271
  async* runStream(prompt) {
1219
1272
  const outputPath = import_node_path4.join(import_node_os2.tmpdir(), `locus-codex-${import_node_crypto.randomUUID()}.txt`);
1220
1273
  const args = this.buildArgs(outputPath);
1274
+ this.eventEmitter?.emitSessionStarted({
1275
+ model: this.model,
1276
+ provider: "codex"
1277
+ });
1278
+ this.eventEmitter?.emitPromptSubmitted(prompt, prompt.length > 500);
1221
1279
  const codex = import_node_child_process2.spawn("codex", args, {
1222
1280
  cwd: this.projectPath,
1223
1281
  stdio: ["pipe", "pipe", "pipe"],
@@ -1230,7 +1288,21 @@ class CodexRunner {
1230
1288
  let processEnded = false;
1231
1289
  let errorMessage = "";
1232
1290
  let finalOutput = "";
1291
+ let finalContent = "";
1292
+ let isThinking = false;
1233
1293
  const enqueueChunk = (chunk) => {
1294
+ this.emitEventForChunk(chunk, isThinking);
1295
+ if (chunk.type === "thinking") {
1296
+ isThinking = true;
1297
+ } else if (chunk.type === "text_delta" || chunk.type === "tool_use") {
1298
+ if (isThinking) {
1299
+ this.eventEmitter?.emitThinkingStoped();
1300
+ isThinking = false;
1301
+ }
1302
+ }
1303
+ if (chunk.type === "text_delta") {
1304
+ finalContent += chunk.content;
1305
+ }
1234
1306
  if (resolveChunk) {
1235
1307
  const resolve2 = resolveChunk;
1236
1308
  resolveChunk = null;
@@ -1271,16 +1343,21 @@ class CodexRunner {
1271
1343
  codex.stderr.on("data", processOutput);
1272
1344
  codex.on("error", (err) => {
1273
1345
  errorMessage = `Failed to start Codex CLI: ${err.message}. Ensure 'codex' is installed and available in PATH.`;
1346
+ this.eventEmitter?.emitErrorOccurred(errorMessage, "SPAWN_ERROR");
1274
1347
  signalEnd();
1275
1348
  });
1276
1349
  codex.on("close", (code) => {
1277
1350
  this.activeProcess = null;
1278
- this.cleanupTempFile(outputPath);
1279
1351
  if (code === 0) {
1280
1352
  const result = this.readOutput(outputPath, finalOutput);
1353
+ this.cleanupTempFile(outputPath);
1281
1354
  enqueueChunk({ type: "result", content: result });
1282
- } else if (!errorMessage) {
1283
- errorMessage = this.createErrorFromOutput(code, finalOutput).message;
1355
+ } else {
1356
+ this.cleanupTempFile(outputPath);
1357
+ if (!errorMessage) {
1358
+ errorMessage = this.createErrorFromOutput(code, finalOutput).message;
1359
+ this.eventEmitter?.emitErrorOccurred(errorMessage, `EXIT_${code}`);
1360
+ }
1284
1361
  }
1285
1362
  signalEnd();
1286
1363
  });
@@ -1294,6 +1371,12 @@ class CodexRunner {
1294
1371
  } else if (processEnded) {
1295
1372
  if (errorMessage) {
1296
1373
  yield { type: "error", error: errorMessage };
1374
+ this.eventEmitter?.emitSessionEnded(false);
1375
+ } else {
1376
+ if (finalContent) {
1377
+ this.eventEmitter?.emitResponseCompleted(finalContent);
1378
+ }
1379
+ this.eventEmitter?.emitSessionEnded(true);
1297
1380
  }
1298
1381
  break;
1299
1382
  } else {
@@ -1303,6 +1386,12 @@ class CodexRunner {
1303
1386
  if (chunk === null) {
1304
1387
  if (errorMessage) {
1305
1388
  yield { type: "error", error: errorMessage };
1389
+ this.eventEmitter?.emitSessionEnded(false);
1390
+ } else {
1391
+ if (finalContent) {
1392
+ this.eventEmitter?.emitResponseCompleted(finalContent);
1393
+ }
1394
+ this.eventEmitter?.emitSessionEnded(true);
1306
1395
  }
1307
1396
  break;
1308
1397
  }
@@ -1310,6 +1399,36 @@ class CodexRunner {
1310
1399
  }
1311
1400
  }
1312
1401
  }
1402
+ emitEventForChunk(chunk, isThinking) {
1403
+ if (!this.eventEmitter)
1404
+ return;
1405
+ switch (chunk.type) {
1406
+ case "text_delta":
1407
+ this.eventEmitter.emitTextDelta(chunk.content);
1408
+ break;
1409
+ case "tool_use":
1410
+ if (this.currentToolName) {
1411
+ this.eventEmitter.emitToolCompleted(this.currentToolName);
1412
+ }
1413
+ this.currentToolName = chunk.tool;
1414
+ this.eventEmitter.emitToolStarted(chunk.tool);
1415
+ break;
1416
+ case "thinking":
1417
+ if (!isThinking) {
1418
+ this.eventEmitter.emitThinkingStarted(chunk.content);
1419
+ }
1420
+ break;
1421
+ case "result":
1422
+ if (this.currentToolName) {
1423
+ this.eventEmitter.emitToolCompleted(this.currentToolName);
1424
+ this.currentToolName = undefined;
1425
+ }
1426
+ break;
1427
+ case "error":
1428
+ this.eventEmitter.emitErrorOccurred(chunk.error);
1429
+ break;
1430
+ }
1431
+ }
1313
1432
  executeRun(prompt) {
1314
1433
  return new Promise((resolve2, reject) => {
1315
1434
  const outputPath = import_node_path4.join(import_node_os2.tmpdir(), `locus-codex-${import_node_crypto.randomUUID()}.txt`);
@@ -1339,10 +1458,12 @@ class CodexRunner {
1339
1458
  });
1340
1459
  codex.on("close", (code) => {
1341
1460
  this.activeProcess = null;
1342
- this.cleanupTempFile(outputPath);
1343
1461
  if (code === 0) {
1344
- resolve2(this.readOutput(outputPath, output));
1462
+ const result = this.readOutput(outputPath, output);
1463
+ this.cleanupTempFile(outputPath);
1464
+ resolve2(result);
1345
1465
  } else {
1466
+ this.cleanupTempFile(outputPath);
1346
1467
  reject(this.createErrorFromOutput(code, errorOutput));
1347
1468
  }
1348
1469
  });
@@ -1352,12 +1473,18 @@ class CodexRunner {
1352
1473
  }
1353
1474
  buildArgs(outputPath) {
1354
1475
  const args = [
1476
+ "--ask-for-approval",
1477
+ "never",
1355
1478
  "exec",
1356
1479
  "--sandbox",
1357
1480
  "workspace-write",
1358
1481
  "--skip-git-repo-check",
1359
1482
  "--output-last-message",
1360
- outputPath
1483
+ outputPath,
1484
+ "-c",
1485
+ "sandbox_workspace_write.network_access=true",
1486
+ "-c",
1487
+ 'sandbox.excludedCommands=["git", "gh"]'
1361
1488
  ];
1362
1489
  if (this.model) {
1363
1490
  args.push("--model", this.model);
@@ -1408,7 +1535,7 @@ class CodexRunner {
1408
1535
  return new Promise((resolve2) => setTimeout(resolve2, ms));
1409
1536
  }
1410
1537
  }
1411
- var import_node_child_process2, import_node_crypto, import_node_fs2, import_node_os2, import_node_path4;
1538
+ var import_node_child_process2, import_node_crypto, import_node_fs2, import_node_os2, import_node_path4, DEFAULT_TIMEOUT_MS2;
1412
1539
  var init_codex_runner = __esm(() => {
1413
1540
  init_config();
1414
1541
  init_resolve_bin();
@@ -1417,6 +1544,7 @@ var init_codex_runner = __esm(() => {
1417
1544
  import_node_fs2 = require("node:fs");
1418
1545
  import_node_os2 = require("node:os");
1419
1546
  import_node_path4 = require("node:path");
1547
+ DEFAULT_TIMEOUT_MS2 = 60 * 60 * 1000;
1420
1548
  });
1421
1549
 
1422
1550
  // src/ai/factory.ts
@@ -1425,9 +1553,9 @@ function createAiRunner(provider, config) {
1425
1553
  const model = config.model ?? DEFAULT_MODEL[resolvedProvider];
1426
1554
  switch (resolvedProvider) {
1427
1555
  case PROVIDER.CODEX:
1428
- return new CodexRunner(config.projectPath, model, config.log);
1556
+ return new CodexRunner(config.projectPath, model, config.log, config.timeoutMs);
1429
1557
  default:
1430
- return new ClaudeRunner(config.projectPath, model, config.log);
1558
+ return new ClaudeRunner(config.projectPath, model, config.log, config.timeoutMs);
1431
1559
  }
1432
1560
  }
1433
1561
  var init_factory = __esm(() => {
@@ -1952,8 +2080,9 @@ class WorktreeManager {
1952
2080
  this.ensureDirectory(this.rootPath, "Worktree root");
1953
2081
  addWorktree();
1954
2082
  }
1955
- this.log(`Worktree created at ${worktreePath}`, "success");
1956
- return { worktreePath, branch, baseBranch };
2083
+ const baseCommitHash = this.git("rev-parse HEAD", worktreePath).trim();
2084
+ this.log(`Worktree created at ${worktreePath} (base: ${baseCommitHash.slice(0, 8)})`, "success");
2085
+ return { worktreePath, branch, baseBranch, baseCommitHash };
1957
2086
  }
1958
2087
  list() {
1959
2088
  const output = this.git("worktree list --porcelain", this.projectPath);
@@ -2058,27 +2187,54 @@ class WorktreeManager {
2058
2187
  try {
2059
2188
  const count = this.git(`rev-list --count "${baseBranch}..HEAD"`, worktreePath).trim();
2060
2189
  return Number.parseInt(count, 10) > 0;
2190
+ } catch (err) {
2191
+ this.log(`Could not compare HEAD against base branch "${baseBranch}": ${err instanceof Error ? err.message : String(err)}`, "warn");
2192
+ return false;
2193
+ }
2194
+ }
2195
+ hasCommitsAheadOfHash(worktreePath, baseHash) {
2196
+ try {
2197
+ const headHash = this.git("rev-parse HEAD", worktreePath).trim();
2198
+ return headHash !== baseHash;
2061
2199
  } catch {
2062
2200
  return false;
2063
2201
  }
2064
2202
  }
2065
- commitChanges(worktreePath, message, baseBranch) {
2203
+ commitChanges(worktreePath, message, baseBranch, baseCommitHash) {
2066
2204
  const hasUncommittedChanges = this.hasChanges(worktreePath);
2205
+ if (hasUncommittedChanges) {
2206
+ const statusOutput = this.git("status --porcelain", worktreePath).trim();
2207
+ this.log(`Detected uncommitted changes:
2208
+ ${statusOutput.split(`
2209
+ `).slice(0, 10).join(`
2210
+ `)}${statusOutput.split(`
2211
+ `).length > 10 ? `
2212
+ ... and ${statusOutput.split(`
2213
+ `).length - 10} more` : ""}`, "info");
2214
+ }
2067
2215
  if (!hasUncommittedChanges) {
2068
2216
  if (baseBranch && this.hasCommitsAhead(worktreePath, baseBranch)) {
2069
2217
  const hash2 = this.git("rev-parse HEAD", worktreePath).trim();
2070
2218
  this.log(`Agent already committed changes (${hash2.slice(0, 8)}); skipping additional commit`, "info");
2071
2219
  return hash2;
2072
2220
  }
2073
- this.log("No changes to commit", "info");
2221
+ if (baseCommitHash && this.hasCommitsAheadOfHash(worktreePath, baseCommitHash)) {
2222
+ const hash2 = this.git("rev-parse HEAD", worktreePath).trim();
2223
+ this.log(`Agent already committed changes (${hash2.slice(0, 8)}, detected via base commit hash); skipping additional commit`, "info");
2224
+ return hash2;
2225
+ }
2226
+ const branch = this.getBranch(worktreePath);
2227
+ this.log(`No changes detected in worktree (branch: ${branch}, baseBranch: ${baseBranch ?? "none"}, baseCommitHash: ${baseCommitHash?.slice(0, 8) ?? "none"})`, "warn");
2074
2228
  return null;
2075
2229
  }
2076
2230
  this.git("add -A", worktreePath);
2077
2231
  const staged = this.git("diff --cached --name-only", worktreePath).trim();
2078
2232
  if (!staged) {
2079
- this.log("No changes to commit", "info");
2233
+ this.log("All changes were ignored by .gitignore — nothing to commit", "warn");
2080
2234
  return null;
2081
2235
  }
2236
+ this.log(`Staging ${staged.split(`
2237
+ `).length} file(s) for commit`, "info");
2082
2238
  this.gitExec(["commit", "-m", message], worktreePath);
2083
2239
  const hash = this.git("rev-parse HEAD", worktreePath).trim();
2084
2240
  this.log(`Committed: ${hash.slice(0, 8)}`, "success");
@@ -2500,15 +2656,27 @@ class TaskExecutor {
2500
2656
  const basePrompt = await this.promptBuilder.build(task);
2501
2657
  try {
2502
2658
  this.deps.log("Starting Execution...", "info");
2503
- await this.deps.aiRunner.run(basePrompt);
2504
- return {
2505
- success: true,
2506
- summary: "Task completed by the agent"
2507
- };
2659
+ const output = await this.deps.aiRunner.run(basePrompt);
2660
+ const summary = this.extractSummary(output);
2661
+ return { success: true, summary };
2508
2662
  } catch (error) {
2509
2663
  return { success: false, summary: `Error: ${error}` };
2510
2664
  }
2511
2665
  }
2666
+ extractSummary(output) {
2667
+ if (!output || !output.trim()) {
2668
+ return "Task completed by the agent";
2669
+ }
2670
+ const paragraphs = output.split(/\n\n+/).map((p) => p.trim()).filter((p) => p.length > 0);
2671
+ if (paragraphs.length === 0) {
2672
+ return "Task completed by the agent";
2673
+ }
2674
+ const last = paragraphs[paragraphs.length - 1];
2675
+ if (last.length > 500) {
2676
+ return `${last.slice(0, 497)}...`;
2677
+ }
2678
+ return last;
2679
+ }
2512
2680
  }
2513
2681
  var init_task_executor = __esm(() => {
2514
2682
  init_prompt_builder();
@@ -2526,7 +2694,7 @@ class GitWorkflow {
2526
2694
  this.log = log;
2527
2695
  this.ghUsername = ghUsername;
2528
2696
  const projectPath = config.projectPath || process.cwd();
2529
- this.worktreeManager = config.useWorktrees ? new WorktreeManager(projectPath, { cleanupPolicy: "auto" }) : null;
2697
+ this.worktreeManager = config.useWorktrees ? new WorktreeManager(projectPath, { cleanupPolicy: "auto" }, log) : null;
2530
2698
  this.prService = config.autoPush ? new PrService(projectPath, log) : null;
2531
2699
  }
2532
2700
  createTaskWorktree(task, defaultExecutor) {
@@ -2534,6 +2702,7 @@ class GitWorkflow {
2534
2702
  return {
2535
2703
  worktreePath: null,
2536
2704
  baseBranch: null,
2705
+ baseCommitHash: null,
2537
2706
  executor: defaultExecutor
2538
2707
  };
2539
2708
  }
@@ -2559,10 +2728,11 @@ class GitWorkflow {
2559
2728
  return {
2560
2729
  worktreePath: result.worktreePath,
2561
2730
  baseBranch: result.baseBranch,
2731
+ baseCommitHash: result.baseCommitHash,
2562
2732
  executor: taskExecutor
2563
2733
  };
2564
2734
  }
2565
- commitAndPush(worktreePath, task, baseBranch) {
2735
+ commitAndPush(worktreePath, task, baseBranch, baseCommitHash) {
2566
2736
  if (!this.worktreeManager) {
2567
2737
  return { branch: null, pushed: false, pushFailed: false };
2568
2738
  }
@@ -2579,7 +2749,7 @@ class GitWorkflow {
2579
2749
 
2580
2750
  ${trailers.join(`
2581
2751
  `)}`;
2582
- const hash = this.worktreeManager.commitChanges(worktreePath, commitMessage, baseBranch);
2752
+ const hash = this.worktreeManager.commitChanges(worktreePath, commitMessage, baseBranch, baseCommitHash);
2583
2753
  if (!hash) {
2584
2754
  this.log("No changes to commit for this task", "info");
2585
2755
  return {
@@ -2619,7 +2789,12 @@ ${trailers.join(`
2619
2789
  } catch (err) {
2620
2790
  const errorMessage = err instanceof Error ? err.message : String(err);
2621
2791
  this.log(`Git commit failed: ${errorMessage}`, "error");
2622
- return { branch: null, pushed: false, pushFailed: false };
2792
+ return {
2793
+ branch: null,
2794
+ pushed: false,
2795
+ pushFailed: true,
2796
+ pushError: `Git commit/push failed: ${errorMessage}`
2797
+ };
2623
2798
  }
2624
2799
  }
2625
2800
  createPullRequest(task, branch, summary, baseBranch) {
@@ -2851,7 +3026,7 @@ class AgentWorker {
2851
3026
  }
2852
3027
  async executeTask(task) {
2853
3028
  const fullTask = await this.client.tasks.getById(task.id, this.config.workspaceId);
2854
- const { worktreePath, baseBranch, executor } = this.gitWorkflow.createTaskWorktree(fullTask, this.taskExecutor);
3029
+ const { worktreePath, baseBranch, baseCommitHash, executor } = this.gitWorkflow.createTaskWorktree(fullTask, this.taskExecutor);
2855
3030
  this.currentWorktreePath = worktreePath;
2856
3031
  let branchPushed = false;
2857
3032
  let keepBranch = false;
@@ -2863,7 +3038,7 @@ class AgentWorker {
2863
3038
  let prError = null;
2864
3039
  let noChanges = false;
2865
3040
  if (result.success && worktreePath) {
2866
- const commitResult = this.gitWorkflow.commitAndPush(worktreePath, fullTask, baseBranch ?? undefined);
3041
+ const commitResult = this.gitWorkflow.commitAndPush(worktreePath, fullTask, baseBranch ?? undefined, baseCommitHash ?? undefined);
2867
3042
  taskBranch = commitResult.branch;
2868
3043
  branchPushed = commitResult.pushed;
2869
3044
  keepBranch = taskBranch !== null;
@@ -4986,6 +5161,9 @@ class TierMergeService {
4986
5161
  }
4987
5162
  }
4988
5163
  findTierTaskBranches(tier) {
5164
+ const tierTaskIds = this.tierTaskIds.get(tier);
5165
+ if (!tierTaskIds || tierTaskIds.length === 0)
5166
+ return [];
4989
5167
  try {
4990
5168
  const output = import_node_child_process8.execSync('git branch -r --list "origin/agent/*" --format="%(refname:short)"', { cwd: this.projectPath, encoding: "utf-8" }).trim();
4991
5169
  if (!output)
@@ -4993,13 +5171,13 @@ class TierMergeService {
4993
5171
  const remoteBranches = output.split(`
4994
5172
  `).map((b) => b.replace("origin/", ""));
4995
5173
  return remoteBranches.filter((branch) => {
4996
- const match = branch.match(/^agent\/([^-]+)/);
4997
- if (!match)
5174
+ const branchSuffix = branch.replace(/^agent\//, "");
5175
+ if (!branchSuffix)
4998
5176
  return false;
4999
- const taskIdPrefix = match[1];
5000
- return this.tierTaskIds.get(tier)?.some((id) => id.startsWith(taskIdPrefix) || taskIdPrefix.startsWith(id.slice(0, 8))) ?? false;
5177
+ return tierTaskIds.some((id) => branchSuffix.startsWith(`${id}-`) || branchSuffix === id || branchSuffix.startsWith(id));
5001
5178
  });
5002
- } catch {
5179
+ } catch (err) {
5180
+ console.log(c.dim(` Could not list remote branches for tier ${tier}: ${err instanceof Error ? err.message : String(err)}`));
5003
5181
  return [];
5004
5182
  }
5005
5183
  }
@@ -5550,6 +5728,7 @@ Review and refine the Tech Lead's breakdown:
5550
5728
  3. **Task Merging** — If two tasks are trivially small and related, merge them.
5551
5729
  4. **Complexity Scoring** — Rate each task 1-5 (1=trivial, 5=very complex).
5552
5730
  5. **Missing Tasks** — Add any tasks the Tech Lead missed (database migrations, configuration, testing, etc.).
5731
+ 6. **Description Quality** — Review and improve each task description to be a clear, actionable implementation guide. Each description must tell the executing agent exactly what to do, where to do it (specific files/modules), how to do it (patterns, utilities, data flow), and what is NOT in scope. Vague descriptions like "Add authentication" must be rewritten with specific file paths, implementation approach, and boundaries.
5553
5732
 
5554
5733
  ## CRITICAL: Task Isolation & Overlap Detection
5555
5734
 
@@ -5575,7 +5754,7 @@ Your entire response must be a single JSON object — no text before it, no text
5575
5754
  "tasks": [
5576
5755
  {
5577
5756
  "title": "string",
5578
- "description": "string (2-4 sentences)",
5757
+ "description": "string (detailed implementation guide: what to do, where to do it, how to do it, and boundaries)",
5579
5758
  "assigneeRole": "BACKEND | FRONTEND | QA | PM | DESIGN",
5580
5759
  "priority": "HIGH | MEDIUM | LOW | CRITICAL",
5581
5760
  "labels": ["string"],
@@ -5668,6 +5847,15 @@ Verify tier assignments are correct:
5668
5847
  ### 5. Merge Conflict Risk Zones
5669
5848
  Identify the highest-risk files (files that multiple same-tier tasks might touch) and ensure only ONE task per tier modifies each.
5670
5849
 
5850
+ ### 6. Description Quality Validation
5851
+ For each task, verify the description is a clear, actionable implementation guide. Each description must specify:
5852
+ - **What to do** — the specific goal and expected behavior/outcome
5853
+ - **Where to do it** — specific files, modules, or directories to modify or create
5854
+ - **How to do it** — implementation approach, patterns to follow, existing utilities to use
5855
+ - **Boundaries** — what is NOT in scope for this task
5856
+
5857
+ If any description is vague (e.g., "Add authentication", "Update the API", "Fix the frontend"), rewrite it with concrete implementation details. The executing agent receives ONLY the task title, description, and acceptance criteria as its instructions.
5858
+
5671
5859
  ## Output Format
5672
5860
 
5673
5861
  Your entire response must be a single JSON object — no text before it, no text after it, no markdown code blocks, no explanation. Start your response with the "{" character:
@@ -5676,7 +5864,7 @@ Your entire response must be a single JSON object — no text before it, no text
5676
5864
  "hasIssues": true | false,
5677
5865
  "issues": [
5678
5866
  {
5679
- "type": "file_overlap" | "duplicated_work" | "not_self_contained" | "merge_conflict_risk" | "wrong_tier",
5867
+ "type": "file_overlap" | "duplicated_work" | "not_self_contained" | "merge_conflict_risk" | "wrong_tier" | "vague_description",
5680
5868
  "description": "string describing the specific issue",
5681
5869
  "affectedTasks": ["Task Title 1", "Task Title 2"],
5682
5870
  "resolution": "string describing how to fix it (merge, move to different tier, consolidate)"
@@ -5689,7 +5877,7 @@ Your entire response must be a single JSON object — no text before it, no text
5689
5877
  "tasks": [
5690
5878
  {
5691
5879
  "title": "string",
5692
- "description": "string",
5880
+ "description": "string (detailed implementation guide: what to do, where to do it, how to do it, and boundaries)",
5693
5881
  "assigneeRole": "BACKEND | FRONTEND | QA | PM | DESIGN",
5694
5882
  "priority": "CRITICAL | HIGH | MEDIUM | LOW",
5695
5883
  "labels": ["string"],
@@ -5713,6 +5901,7 @@ IMPORTANT:
5713
5901
  - If hasIssues is false, the revisedPlan should be identical to the input plan (no changes needed)
5714
5902
  - The revisedPlan is ALWAYS required — it becomes the final plan
5715
5903
  - When merging tasks, combine their acceptance criteria and update descriptions to cover all consolidated work
5904
+ - Ensure every task description is a detailed implementation guide (what, where, how, boundaries) — rewrite vague descriptions
5716
5905
  - Prefer fewer, larger, self-contained tasks over many small conflicting ones
5717
5906
  - Every task MUST have a "tier" field (integer >= 0)
5718
5907
  - tier 0 = foundational (runs first), tier 1 = depends on tier 0, tier 2 = depends on tier 1, etc.
@@ -5751,6 +5940,7 @@ Produce the final sprint plan:
5751
5940
  4. **Tier Assignment** — Assign each task an execution tier (integer, starting at 0). Tasks within the same tier run IN PARALLEL on separate git branches. Tasks in tier N+1 only start AFTER all tier N tasks are complete and merged. Tier 0 = foundational tasks (config, schemas, shared code). Higher tiers build on lower tier outputs.
5752
5941
  5. **Duration Estimate** — How many days this sprint will take with 2-3 agents working in parallel
5753
5942
  6. **Final Task List** — Each task with all fields filled in, ordered by execution priority
5943
+ 7. **Description Quality Check** — Ensure every task description is a clear, actionable implementation guide. Each description must specify: what to do, where to do it (specific files/modules/directories), how to do it (implementation approach, patterns to follow, existing utilities to use), and what is NOT in scope. If any description is vague or generic, rewrite it with specifics. Remember: an independent agent will receive ONLY the task title, description, and acceptance criteria — the description is its primary instruction.
5754
5944
 
5755
5945
  Guidelines:
5756
5946
  - The order of tasks in the array determines execution order. Tasks are dispatched sequentially from first to last.
@@ -5760,6 +5950,7 @@ Guidelines:
5760
5950
  - Group related independent tasks in the same tier for maximum parallelism
5761
5951
  - Ensure acceptance criteria are specific and testable
5762
5952
  - Keep the sprint focused — if it's too large (>12 tasks), consider reducing scope
5953
+ - Ensure every task description reads as a standalone implementation brief — not a summary
5763
5954
 
5764
5955
  ## CRITICAL: Task Isolation Validation
5765
5956
 
@@ -5781,7 +5972,7 @@ Your entire response must be a single JSON object — no text before it, no text
5781
5972
  "tasks": [
5782
5973
  {
5783
5974
  "title": "string",
5784
- "description": "string",
5975
+ "description": "string (detailed implementation guide: what to do, where to do it, how to do it, and boundaries)",
5785
5976
  "assigneeRole": "BACKEND | FRONTEND | QA | PM | DESIGN",
5786
5977
  "priority": "CRITICAL | HIGH | MEDIUM | LOW",
5787
5978
  "labels": ["string"],
@@ -5837,7 +6028,7 @@ ${input.codebaseIndex || "No codebase index available."}
5837
6028
  Analyze the CEO's directive and produce a detailed task breakdown. For each task:
5838
6029
 
5839
6030
  1. **Title** — Clear, action-oriented (e.g., "Implement user registration API endpoint")
5840
- 2. **Description** — What needs to be done technically, which files/modules are involved
6031
+ 2. **Description** — A detailed, actionable implementation guide (see description requirements below)
5841
6032
  3. **Assignee Role** — Who should work on this: BACKEND, FRONTEND, QA, PM, or DESIGN
5842
6033
  4. **Priority** — HIGH, MEDIUM, or LOW based on business impact
5843
6034
  5. **Labels** — Relevant tags (e.g., "api", "database", "ui", "auth")
@@ -5849,6 +6040,19 @@ Think about:
5849
6040
  - What the right granularity is (not too big, not too small)
5850
6041
  - What risks or unknowns exist
5851
6042
 
6043
+ ## CRITICAL: Task Description Requirements
6044
+
6045
+ Each task description will be handed to an INDEPENDENT agent as its primary instruction. The agent will have access to the codebase but NO context about the planning meeting. Descriptions must be clear enough for the agent to execute the task without ambiguity.
6046
+
6047
+ Each description MUST include:
6048
+ 1. **What to do** — Clearly state the goal and what needs to be implemented, changed, or created. Be specific about the expected behavior or outcome.
6049
+ 2. **Where to do it** — List the specific files, modules, or directories that need to be modified or created. Reference existing code paths when extending functionality.
6050
+ 3. **How to do it** — Provide key implementation details: which patterns to follow, which existing utilities or services to use, what the data flow looks like.
6051
+ 4. **Boundaries** — Clarify what is NOT in scope for this task to prevent overlap with other tasks.
6052
+
6053
+ Bad example: "Add authentication to the API."
6054
+ Good example: "Implement JWT-based authentication middleware in src/middleware/auth.ts. Create a verifyToken middleware that extracts the Bearer token from the Authorization header, validates it using the existing JWT_SECRET from env config, and attaches the decoded user payload to req.user. Apply this middleware to all routes in src/routes/protected/. Add a POST /auth/login endpoint in src/routes/auth.ts that accepts {email, password}, validates credentials against the users table, and returns a signed JWT. This task does NOT include user registration or password reset — those are handled separately."
6055
+
5852
6056
  ## CRITICAL: Task Isolation Rules
5853
6057
 
5854
6058
  Tasks will be executed by INDEPENDENT agents on SEPARATE git branches that get merged together. Each agent has NO knowledge of what other agents are doing. Therefore:
@@ -5867,7 +6071,7 @@ Your entire response must be a single JSON object — no text before it, no text
5867
6071
  "tasks": [
5868
6072
  {
5869
6073
  "title": "string",
5870
- "description": "string (2-4 sentences)",
6074
+ "description": "string (detailed implementation guide: what to do, where to do it, how to do it, and boundaries — see description requirements above)",
5871
6075
  "assigneeRole": "BACKEND | FRONTEND | QA | PM | DESIGN",
5872
6076
  "priority": "HIGH | MEDIUM | LOW",
5873
6077
  "labels": ["string"],
@@ -1 +1 @@
1
- {"version":3,"file":"tier-merge.d.ts","sourceRoot":"","sources":["../../src/orchestrator/tier-merge.ts"],"names":[],"mappings":"AAMA;;;;;;;;;GASG;AACH,qBAAa,gBAAgB;IAKzB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,QAAQ;IALlB,yDAAyD;IACzD,OAAO,CAAC,WAAW,CAAoC;gBAG7C,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,GAAG,IAAI;IAGjC;;OAEG;IACH,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,GAAG,IAAI;IAY3E;;;OAGG;IACH,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAKpC;;OAEG;IACH,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAiB3C;;;;;;;;OAQG;IACH,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IA0ElE;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAkC5B,OAAO,CAAC,OAAO;CAOhB"}
1
+ {"version":3,"file":"tier-merge.d.ts","sourceRoot":"","sources":["../../src/orchestrator/tier-merge.ts"],"names":[],"mappings":"AAMA;;;;;;;;;GASG;AACH,qBAAa,gBAAgB;IAKzB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,QAAQ;IALlB,yDAAyD;IACzD,OAAO,CAAC,WAAW,CAAoC;gBAG7C,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,GAAG,IAAI;IAGjC;;OAEG;IACH,iBAAiB,CAAC,KAAK,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,GAAG,IAAI;IAY3E;;;OAGG;IACH,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAKpC;;OAEG;IACH,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAiB3C;;;;;;;;OAQG;IACH,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IA0ElE;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IA0C5B,OAAO,CAAC,OAAO;CAOhB"}
@@ -1 +1 @@
1
- {"version":3,"file":"architect.d.ts","sourceRoot":"","sources":["../../../src/planning/agents/architect.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CA8ElE"}
1
+ {"version":3,"file":"architect.d.ts","sourceRoot":"","sources":["../../../src/planning/agents/architect.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CA+ElE"}
@@ -1 +1 @@
1
- {"version":3,"file":"cross-task-reviewer.d.ts","sourceRoot":"","sources":["../../../src/planning/agents/cross-task-reviewer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,sBAAsB,GAC5B,MAAM,CA6HR"}
1
+ {"version":3,"file":"cross-task-reviewer.d.ts","sourceRoot":"","sources":["../../../src/planning/agents/cross-task-reviewer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,sBAAsB,GAC5B,MAAM,CAuIR"}
@@ -1 +1 @@
1
- {"version":3,"file":"sprint-organizer.d.ts","sourceRoot":"","sources":["../../../src/planning/agents/sprint-organizer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,oBAAoB,GAC1B,MAAM,CAwFR"}
1
+ {"version":3,"file":"sprint-organizer.d.ts","sourceRoot":"","sources":["../../../src/planning/agents/sprint-organizer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,oBAAoB,GAC1B,MAAM,CA0FR"}
@@ -1 +1 @@
1
- {"version":3,"file":"tech-lead.d.ts","sourceRoot":"","sources":["../../../src/planning/agents/tech-lead.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAuEhE"}
1
+ {"version":3,"file":"tech-lead.d.ts","sourceRoot":"","sources":["../../../src/planning/agents/tech-lead.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAoFhE"}
@@ -52,6 +52,8 @@ export interface CreateWorktreeResult {
52
52
  branch: string;
53
53
  /** Base branch used to create the task branch */
54
54
  baseBranch: string;
55
+ /** Commit hash of the base branch at worktree creation time */
56
+ baseCommitHash: string;
55
57
  }
56
58
  /** Default worktree configuration */
57
59
  export declare const DEFAULT_WORKTREE_CONFIG: WorktreeConfig;