@askexenow/exe-os 0.9.10 → 0.9.11

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/bin/cli.js CHANGED
@@ -5261,6 +5261,12 @@ async function cloudSync(config) {
5261
5261
  pulled = pullResult.records.length;
5262
5262
  }
5263
5263
  }
5264
+ if (pulled > 0) {
5265
+ try {
5266
+ await pushToPostgres(pullResult.records);
5267
+ } catch {
5268
+ }
5269
+ }
5264
5270
  if (pullResult.maxVersion > lastPullVersion) {
5265
5271
  await client.execute({
5266
5272
  sql: "INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('last_cloud_pull_version', ?)",
@@ -10393,8 +10399,35 @@ var init_tasks_crud = __esm({
10393
10399
  });
10394
10400
 
10395
10401
  // src/lib/tasks-review.ts
10402
+ var tasks_review_exports = {};
10403
+ __export(tasks_review_exports, {
10404
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
10405
+ cleanupReviewFile: () => cleanupReviewFile,
10406
+ countNewPendingReviewsSince: () => countNewPendingReviewsSince,
10407
+ countPendingReviews: () => countPendingReviews,
10408
+ createReviewForCompletedTask: () => createReviewForCompletedTask,
10409
+ formatAge: () => formatAge,
10410
+ getReviewChecklist: () => getReviewChecklist,
10411
+ isStale: () => isStale,
10412
+ listPendingReviews: () => listPendingReviews
10413
+ });
10396
10414
  import path25 from "path";
10397
10415
  import { existsSync as existsSync22, readdirSync as readdirSync5, unlinkSync as unlinkSync6 } from "fs";
10416
+ function formatAge(isoTimestamp) {
10417
+ if (!isoTimestamp) return "";
10418
+ const ms = Date.now() - new Date(isoTimestamp).getTime();
10419
+ if (ms < 0) return "just now";
10420
+ const minutes = Math.floor(ms / 6e4);
10421
+ if (minutes < 60) return `${minutes}m ago`;
10422
+ const hours = Math.floor(minutes / 60);
10423
+ if (hours < 24) return `${hours}h ago`;
10424
+ const days = Math.floor(hours / 24);
10425
+ return `${days}d ago`;
10426
+ }
10427
+ function isStale(isoTimestamp) {
10428
+ if (!isoTimestamp) return false;
10429
+ return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
10430
+ }
10398
10431
  async function countPendingReviews(sessionScope) {
10399
10432
  const client = getClient();
10400
10433
  const scope = strictSessionScopeFilter(
@@ -10515,6 +10548,95 @@ function getReviewChecklist(role, agent, taskSlug) {
10515
10548
  ]
10516
10549
  };
10517
10550
  }
10551
+ async function createReviewForCompletedTask(row, result, _baseDir, now) {
10552
+ const taskFile = String(row.task_file);
10553
+ const employees = await loadEmployees();
10554
+ const coordinatorName = getCoordinatorName(employees);
10555
+ if (isCoordinatorName(String(row.assigned_to), employees)) return;
10556
+ if (String(row.title).startsWith("Review:")) return;
10557
+ const fileName = taskFile.split("/").pop() ?? "";
10558
+ if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
10559
+ if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
10560
+ const client = getClient();
10561
+ const agent = String(row.assigned_to);
10562
+ const rawReviewer = row.reviewer || row.assigned_by;
10563
+ const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
10564
+ const currentStatus = String(row.status ?? "");
10565
+ if (currentStatus === "done") return;
10566
+ const existingResult = String(row.result ?? "");
10567
+ if (existingResult.includes("## Review notes")) return;
10568
+ let reviewerRole = "unknown";
10569
+ try {
10570
+ const emp = getEmployee(employees, reviewer);
10571
+ if (emp) reviewerRole = emp.role;
10572
+ } catch {
10573
+ }
10574
+ const taskTitle = String(row.title);
10575
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
10576
+ const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
10577
+ process.stderr.write(
10578
+ `[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
10579
+ `
10580
+ );
10581
+ const reviewNotes = [
10582
+ `
10583
+ ---
10584
+ ## Review notes`,
10585
+ `Review lens: ${lens}`,
10586
+ `Reviewer: **${reviewer}** (${reviewerRole})`,
10587
+ "",
10588
+ "### Checklist",
10589
+ ...checklist,
10590
+ "",
10591
+ "### Verdict",
10592
+ "- **Approved:** mark this task as done",
10593
+ "- **Needs work:** re-open with notes"
10594
+ ].join("\n");
10595
+ const originalTaskId = String(row.id);
10596
+ const updatedResult = (result ?? "No result summary provided") + reviewNotes;
10597
+ await client.execute({
10598
+ sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
10599
+ WHERE id = ?`,
10600
+ args: [updatedResult, now, originalTaskId]
10601
+ });
10602
+ orgBus.emit({
10603
+ type: "review_created",
10604
+ reviewId: originalTaskId,
10605
+ employee: agent,
10606
+ reviewer,
10607
+ timestamp: now
10608
+ });
10609
+ await writeNotification({
10610
+ agentId: agent,
10611
+ agentRole: String(row.assigned_to),
10612
+ event: "task_complete",
10613
+ project: String(row.project_name),
10614
+ summary: `completed "${taskTitle}" \u2014 ready for review`,
10615
+ taskFile
10616
+ });
10617
+ const originalPriority = String(row.priority).toLowerCase();
10618
+ const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
10619
+ if (!autoApprove) {
10620
+ try {
10621
+ const key = getSessionKey();
10622
+ const exeSession = getParentExe(key);
10623
+ if (exeSession) {
10624
+ sendIntercom(exeSession);
10625
+ }
10626
+ } catch {
10627
+ }
10628
+ }
10629
+ if (autoApprove) {
10630
+ process.stderr.write(
10631
+ `[review] Auto-approving "${taskTitle}" (P2 + tests pass)
10632
+ `
10633
+ );
10634
+ await client.execute({
10635
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
10636
+ args: [now, originalTaskId]
10637
+ });
10638
+ }
10639
+ }
10518
10640
  async function cleanupReviewFile(row, taskFile, _baseDir) {
10519
10641
  if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
10520
10642
  try {
@@ -11878,15 +12000,17 @@ function sendIntercom(targetSession) {
11878
12000
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
11879
12001
  return "queued";
11880
12002
  }
11881
- try {
11882
- const rawAgent = targetSession.split("-")[0] ?? targetSession;
11883
- const agent = baseAgentName(rawAgent);
11884
- const markerPath = path28.join(SESSION_CACHE, `current-task-${agent}.json`);
11885
- if (existsSync23(markerPath)) {
11886
- logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
11887
- return "debounced";
12003
+ if (sessionState !== "idle") {
12004
+ try {
12005
+ const rawAgent = targetSession.split("-")[0] ?? targetSession;
12006
+ const agent = baseAgentName(rawAgent);
12007
+ const markerPath = path28.join(SESSION_CACHE, `current-task-${agent}.json`);
12008
+ if (existsSync23(markerPath)) {
12009
+ logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
12010
+ return "debounced";
12011
+ }
12012
+ } catch {
11888
12013
  }
11889
- } catch {
11890
12014
  }
11891
12015
  try {
11892
12016
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
@@ -11957,6 +12081,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
11957
12081
  try {
11958
12082
  const sessions = transport.listSessions();
11959
12083
  if (!sessions.includes(coordinatorSession)) return false;
12084
+ try {
12085
+ const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
12086
+ const pending = countPendingReviews2(coordinatorSession);
12087
+ if (pending instanceof Promise) {
12088
+ pending.then((count) => {
12089
+ if (count > 0) {
12090
+ execSync8(
12091
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
12092
+ { timeout: 3e3 }
12093
+ );
12094
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
12095
+ }
12096
+ }).catch(() => {
12097
+ });
12098
+ return true;
12099
+ }
12100
+ } catch {
12101
+ }
11960
12102
  execSync8(
11961
12103
  `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
11962
12104
  { timeout: 3e3 }
@@ -5477,8 +5477,35 @@ var init_tasks_crud = __esm({
5477
5477
  });
5478
5478
 
5479
5479
  // src/lib/tasks-review.ts
5480
+ var tasks_review_exports = {};
5481
+ __export(tasks_review_exports, {
5482
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
5483
+ cleanupReviewFile: () => cleanupReviewFile,
5484
+ countNewPendingReviewsSince: () => countNewPendingReviewsSince,
5485
+ countPendingReviews: () => countPendingReviews,
5486
+ createReviewForCompletedTask: () => createReviewForCompletedTask,
5487
+ formatAge: () => formatAge,
5488
+ getReviewChecklist: () => getReviewChecklist,
5489
+ isStale: () => isStale,
5490
+ listPendingReviews: () => listPendingReviews
5491
+ });
5480
5492
  import path16 from "path";
5481
5493
  import { existsSync as existsSync15, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
5494
+ function formatAge(isoTimestamp) {
5495
+ if (!isoTimestamp) return "";
5496
+ const ms = Date.now() - new Date(isoTimestamp).getTime();
5497
+ if (ms < 0) return "just now";
5498
+ const minutes = Math.floor(ms / 6e4);
5499
+ if (minutes < 60) return `${minutes}m ago`;
5500
+ const hours = Math.floor(minutes / 60);
5501
+ if (hours < 24) return `${hours}h ago`;
5502
+ const days = Math.floor(hours / 24);
5503
+ return `${days}d ago`;
5504
+ }
5505
+ function isStale(isoTimestamp) {
5506
+ if (!isoTimestamp) return false;
5507
+ return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
5508
+ }
5482
5509
  async function countPendingReviews(sessionScope) {
5483
5510
  const client = getClient();
5484
5511
  const scope = strictSessionScopeFilter(
@@ -5599,6 +5626,95 @@ function getReviewChecklist(role, agent, taskSlug) {
5599
5626
  ]
5600
5627
  };
5601
5628
  }
5629
+ async function createReviewForCompletedTask(row, result, _baseDir, now) {
5630
+ const taskFile = String(row.task_file);
5631
+ const employees = await loadEmployees();
5632
+ const coordinatorName = getCoordinatorName(employees);
5633
+ if (isCoordinatorName(String(row.assigned_to), employees)) return;
5634
+ if (String(row.title).startsWith("Review:")) return;
5635
+ const fileName = taskFile.split("/").pop() ?? "";
5636
+ if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
5637
+ if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
5638
+ const client = getClient();
5639
+ const agent = String(row.assigned_to);
5640
+ const rawReviewer = row.reviewer || row.assigned_by;
5641
+ const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
5642
+ const currentStatus = String(row.status ?? "");
5643
+ if (currentStatus === "done") return;
5644
+ const existingResult = String(row.result ?? "");
5645
+ if (existingResult.includes("## Review notes")) return;
5646
+ let reviewerRole = "unknown";
5647
+ try {
5648
+ const emp = getEmployee(employees, reviewer);
5649
+ if (emp) reviewerRole = emp.role;
5650
+ } catch {
5651
+ }
5652
+ const taskTitle = String(row.title);
5653
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
5654
+ const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
5655
+ process.stderr.write(
5656
+ `[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
5657
+ `
5658
+ );
5659
+ const reviewNotes = [
5660
+ `
5661
+ ---
5662
+ ## Review notes`,
5663
+ `Review lens: ${lens}`,
5664
+ `Reviewer: **${reviewer}** (${reviewerRole})`,
5665
+ "",
5666
+ "### Checklist",
5667
+ ...checklist,
5668
+ "",
5669
+ "### Verdict",
5670
+ "- **Approved:** mark this task as done",
5671
+ "- **Needs work:** re-open with notes"
5672
+ ].join("\n");
5673
+ const originalTaskId = String(row.id);
5674
+ const updatedResult = (result ?? "No result summary provided") + reviewNotes;
5675
+ await client.execute({
5676
+ sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
5677
+ WHERE id = ?`,
5678
+ args: [updatedResult, now, originalTaskId]
5679
+ });
5680
+ orgBus.emit({
5681
+ type: "review_created",
5682
+ reviewId: originalTaskId,
5683
+ employee: agent,
5684
+ reviewer,
5685
+ timestamp: now
5686
+ });
5687
+ await writeNotification({
5688
+ agentId: agent,
5689
+ agentRole: String(row.assigned_to),
5690
+ event: "task_complete",
5691
+ project: String(row.project_name),
5692
+ summary: `completed "${taskTitle}" \u2014 ready for review`,
5693
+ taskFile
5694
+ });
5695
+ const originalPriority = String(row.priority).toLowerCase();
5696
+ const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
5697
+ if (!autoApprove) {
5698
+ try {
5699
+ const key = getSessionKey();
5700
+ const exeSession = getParentExe(key);
5701
+ if (exeSession) {
5702
+ sendIntercom(exeSession);
5703
+ }
5704
+ } catch {
5705
+ }
5706
+ }
5707
+ if (autoApprove) {
5708
+ process.stderr.write(
5709
+ `[review] Auto-approving "${taskTitle}" (P2 + tests pass)
5710
+ `
5711
+ );
5712
+ await client.execute({
5713
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
5714
+ args: [now, originalTaskId]
5715
+ });
5716
+ }
5717
+ }
5602
5718
  async function cleanupReviewFile(row, taskFile, _baseDir) {
5603
5719
  if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
5604
5720
  try {
@@ -6962,15 +7078,17 @@ function sendIntercom(targetSession) {
6962
7078
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
6963
7079
  return "queued";
6964
7080
  }
6965
- try {
6966
- const rawAgent = targetSession.split("-")[0] ?? targetSession;
6967
- const agent = baseAgentName(rawAgent);
6968
- const markerPath = path19.join(SESSION_CACHE, `current-task-${agent}.json`);
6969
- if (existsSync16(markerPath)) {
6970
- logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
6971
- return "debounced";
7081
+ if (sessionState !== "idle") {
7082
+ try {
7083
+ const rawAgent = targetSession.split("-")[0] ?? targetSession;
7084
+ const agent = baseAgentName(rawAgent);
7085
+ const markerPath = path19.join(SESSION_CACHE, `current-task-${agent}.json`);
7086
+ if (existsSync16(markerPath)) {
7087
+ logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
7088
+ return "debounced";
7089
+ }
7090
+ } catch {
6972
7091
  }
6973
- } catch {
6974
7092
  }
6975
7093
  try {
6976
7094
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
@@ -7041,6 +7159,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
7041
7159
  try {
7042
7160
  const sessions = transport.listSessions();
7043
7161
  if (!sessions.includes(coordinatorSession)) return false;
7162
+ try {
7163
+ const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
7164
+ const pending = countPendingReviews2(coordinatorSession);
7165
+ if (pending instanceof Promise) {
7166
+ pending.then((count) => {
7167
+ if (count > 0) {
7168
+ execSync7(
7169
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
7170
+ { timeout: 3e3 }
7171
+ );
7172
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
7173
+ }
7174
+ }).catch(() => {
7175
+ });
7176
+ return true;
7177
+ }
7178
+ } catch {
7179
+ }
7044
7180
  execSync7(
7045
7181
  `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
7046
7182
  { timeout: 3e3 }
@@ -8230,6 +8366,12 @@ async function cloudSync(config) {
8230
8366
  pulled = pullResult.records.length;
8231
8367
  }
8232
8368
  }
8369
+ if (pulled > 0) {
8370
+ try {
8371
+ await pushToPostgres(pullResult.records);
8372
+ } catch {
8373
+ }
8374
+ }
8233
8375
  if (pullResult.maxVersion > lastPullVersion) {
8234
8376
  await client.execute({
8235
8377
  sql: "INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('last_cloud_pull_version', ?)",
@@ -783,6 +783,17 @@ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
783
783
  if (!agentName) return false;
784
784
  return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
785
785
  }
786
+ async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
787
+ if (!existsSync6(employeesPath)) {
788
+ return [];
789
+ }
790
+ const raw = await readFile2(employeesPath, "utf-8");
791
+ try {
792
+ return JSON.parse(raw);
793
+ } catch {
794
+ return [];
795
+ }
796
+ }
786
797
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
787
798
  if (!existsSync6(employeesPath)) return [];
788
799
  try {
@@ -3436,8 +3447,35 @@ var init_tasks_crud = __esm({
3436
3447
  });
3437
3448
 
3438
3449
  // src/lib/tasks-review.ts
3450
+ var tasks_review_exports = {};
3451
+ __export(tasks_review_exports, {
3452
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
3453
+ cleanupReviewFile: () => cleanupReviewFile,
3454
+ countNewPendingReviewsSince: () => countNewPendingReviewsSince,
3455
+ countPendingReviews: () => countPendingReviews,
3456
+ createReviewForCompletedTask: () => createReviewForCompletedTask,
3457
+ formatAge: () => formatAge,
3458
+ getReviewChecklist: () => getReviewChecklist,
3459
+ isStale: () => isStale,
3460
+ listPendingReviews: () => listPendingReviews
3461
+ });
3439
3462
  import path12 from "path";
3440
3463
  import { existsSync as existsSync11, readdirSync as readdirSync2, unlinkSync as unlinkSync3 } from "fs";
3464
+ function formatAge(isoTimestamp) {
3465
+ if (!isoTimestamp) return "";
3466
+ const ms = Date.now() - new Date(isoTimestamp).getTime();
3467
+ if (ms < 0) return "just now";
3468
+ const minutes = Math.floor(ms / 6e4);
3469
+ if (minutes < 60) return `${minutes}m ago`;
3470
+ const hours = Math.floor(minutes / 60);
3471
+ if (hours < 24) return `${hours}h ago`;
3472
+ const days = Math.floor(hours / 24);
3473
+ return `${days}d ago`;
3474
+ }
3475
+ function isStale(isoTimestamp) {
3476
+ if (!isoTimestamp) return false;
3477
+ return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
3478
+ }
3441
3479
  async function countPendingReviews(sessionScope) {
3442
3480
  const client = getClient();
3443
3481
  const scope = strictSessionScopeFilter(
@@ -3558,6 +3596,95 @@ function getReviewChecklist(role, agent, taskSlug) {
3558
3596
  ]
3559
3597
  };
3560
3598
  }
3599
+ async function createReviewForCompletedTask(row, result2, _baseDir, now) {
3600
+ const taskFile = String(row.task_file);
3601
+ const employees = await loadEmployees();
3602
+ const coordinatorName = getCoordinatorName(employees);
3603
+ if (isCoordinatorName(String(row.assigned_to), employees)) return;
3604
+ if (String(row.title).startsWith("Review:")) return;
3605
+ const fileName = taskFile.split("/").pop() ?? "";
3606
+ if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
3607
+ if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
3608
+ const client = getClient();
3609
+ const agent = String(row.assigned_to);
3610
+ const rawReviewer = row.reviewer || row.assigned_by;
3611
+ const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
3612
+ const currentStatus = String(row.status ?? "");
3613
+ if (currentStatus === "done") return;
3614
+ const existingResult = String(row.result ?? "");
3615
+ if (existingResult.includes("## Review notes")) return;
3616
+ let reviewerRole = "unknown";
3617
+ try {
3618
+ const emp = getEmployee(employees, reviewer);
3619
+ if (emp) reviewerRole = emp.role;
3620
+ } catch {
3621
+ }
3622
+ const taskTitle = String(row.title);
3623
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
3624
+ const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
3625
+ process.stderr.write(
3626
+ `[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
3627
+ `
3628
+ );
3629
+ const reviewNotes = [
3630
+ `
3631
+ ---
3632
+ ## Review notes`,
3633
+ `Review lens: ${lens}`,
3634
+ `Reviewer: **${reviewer}** (${reviewerRole})`,
3635
+ "",
3636
+ "### Checklist",
3637
+ ...checklist,
3638
+ "",
3639
+ "### Verdict",
3640
+ "- **Approved:** mark this task as done",
3641
+ "- **Needs work:** re-open with notes"
3642
+ ].join("\n");
3643
+ const originalTaskId = String(row.id);
3644
+ const updatedResult = (result2 ?? "No result summary provided") + reviewNotes;
3645
+ await client.execute({
3646
+ sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
3647
+ WHERE id = ?`,
3648
+ args: [updatedResult, now, originalTaskId]
3649
+ });
3650
+ orgBus.emit({
3651
+ type: "review_created",
3652
+ reviewId: originalTaskId,
3653
+ employee: agent,
3654
+ reviewer,
3655
+ timestamp: now
3656
+ });
3657
+ await writeNotification({
3658
+ agentId: agent,
3659
+ agentRole: String(row.assigned_to),
3660
+ event: "task_complete",
3661
+ project: String(row.project_name),
3662
+ summary: `completed "${taskTitle}" \u2014 ready for review`,
3663
+ taskFile
3664
+ });
3665
+ const originalPriority = String(row.priority).toLowerCase();
3666
+ const autoApprove = originalPriority === "p2" && result2?.toLowerCase().includes("tests pass");
3667
+ if (!autoApprove) {
3668
+ try {
3669
+ const key = getSessionKey();
3670
+ const exeSession2 = getParentExe(key);
3671
+ if (exeSession2) {
3672
+ sendIntercom(exeSession2);
3673
+ }
3674
+ } catch {
3675
+ }
3676
+ }
3677
+ if (autoApprove) {
3678
+ process.stderr.write(
3679
+ `[review] Auto-approving "${taskTitle}" (P2 + tests pass)
3680
+ `
3681
+ );
3682
+ await client.execute({
3683
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
3684
+ args: [now, originalTaskId]
3685
+ });
3686
+ }
3687
+ }
3561
3688
  async function cleanupReviewFile(row, taskFile, _baseDir) {
3562
3689
  if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
3563
3690
  try {
@@ -4921,15 +5048,17 @@ function sendIntercom(targetSession) {
4921
5048
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
4922
5049
  return "queued";
4923
5050
  }
4924
- try {
4925
- const rawAgent = targetSession.split("-")[0] ?? targetSession;
4926
- const agent = baseAgentName(rawAgent);
4927
- const markerPath = path15.join(SESSION_CACHE, `current-task-${agent}.json`);
4928
- if (existsSync12(markerPath)) {
4929
- logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
4930
- return "debounced";
5051
+ if (sessionState !== "idle") {
5052
+ try {
5053
+ const rawAgent = targetSession.split("-")[0] ?? targetSession;
5054
+ const agent = baseAgentName(rawAgent);
5055
+ const markerPath = path15.join(SESSION_CACHE, `current-task-${agent}.json`);
5056
+ if (existsSync12(markerPath)) {
5057
+ logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
5058
+ return "debounced";
5059
+ }
5060
+ } catch {
4931
5061
  }
4932
- } catch {
4933
5062
  }
4934
5063
  try {
4935
5064
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
@@ -5000,6 +5129,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
5000
5129
  try {
5001
5130
  const sessions = transport.listSessions();
5002
5131
  if (!sessions.includes(coordinatorSession)) return false;
5132
+ try {
5133
+ const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
5134
+ const pending = countPendingReviews2(coordinatorSession);
5135
+ if (pending instanceof Promise) {
5136
+ pending.then((count) => {
5137
+ if (count > 0) {
5138
+ execSync6(
5139
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
5140
+ { timeout: 3e3 }
5141
+ );
5142
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
5143
+ }
5144
+ }).catch(() => {
5145
+ });
5146
+ return true;
5147
+ }
5148
+ } catch {
5149
+ }
5003
5150
  execSync6(
5004
5151
  `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
5005
5152
  { timeout: 3e3 }