@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.
@@ -2569,15 +2569,17 @@ function sendIntercom(targetSession) {
2569
2569
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
2570
2570
  return "queued";
2571
2571
  }
2572
- try {
2573
- const rawAgent = targetSession.split("-")[0] ?? targetSession;
2574
- const agent = baseAgentName(rawAgent);
2575
- const markerPath = path9.join(SESSION_CACHE, `current-task-${agent}.json`);
2576
- if (existsSync8(markerPath)) {
2577
- logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
2578
- return "debounced";
2572
+ if (sessionState !== "idle") {
2573
+ try {
2574
+ const rawAgent = targetSession.split("-")[0] ?? targetSession;
2575
+ const agent = baseAgentName(rawAgent);
2576
+ const markerPath = path9.join(SESSION_CACHE, `current-task-${agent}.json`);
2577
+ if (existsSync8(markerPath)) {
2578
+ logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
2579
+ return "debounced";
2580
+ }
2581
+ } catch {
2579
2582
  }
2580
- } catch {
2581
2583
  }
2582
2584
  try {
2583
2585
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
@@ -795,6 +795,17 @@ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
795
795
  if (!agentName) return false;
796
796
  return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
797
797
  }
798
+ async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
799
+ if (!existsSync6(employeesPath)) {
800
+ return [];
801
+ }
802
+ const raw = await readFile2(employeesPath, "utf-8");
803
+ try {
804
+ return JSON.parse(raw);
805
+ } catch {
806
+ return [];
807
+ }
808
+ }
798
809
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
799
810
  if (!existsSync6(employeesPath)) return [];
800
811
  try {
@@ -3978,8 +3989,35 @@ var init_tasks_crud = __esm({
3978
3989
  });
3979
3990
 
3980
3991
  // src/lib/tasks-review.ts
3992
+ var tasks_review_exports = {};
3993
+ __export(tasks_review_exports, {
3994
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
3995
+ cleanupReviewFile: () => cleanupReviewFile,
3996
+ countNewPendingReviewsSince: () => countNewPendingReviewsSince,
3997
+ countPendingReviews: () => countPendingReviews,
3998
+ createReviewForCompletedTask: () => createReviewForCompletedTask,
3999
+ formatAge: () => formatAge,
4000
+ getReviewChecklist: () => getReviewChecklist,
4001
+ isStale: () => isStale,
4002
+ listPendingReviews: () => listPendingReviews
4003
+ });
3981
4004
  import path14 from "path";
3982
4005
  import { existsSync as existsSync13, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
4006
+ function formatAge(isoTimestamp) {
4007
+ if (!isoTimestamp) return "";
4008
+ const ms = Date.now() - new Date(isoTimestamp).getTime();
4009
+ if (ms < 0) return "just now";
4010
+ const minutes = Math.floor(ms / 6e4);
4011
+ if (minutes < 60) return `${minutes}m ago`;
4012
+ const hours = Math.floor(minutes / 60);
4013
+ if (hours < 24) return `${hours}h ago`;
4014
+ const days = Math.floor(hours / 24);
4015
+ return `${days}d ago`;
4016
+ }
4017
+ function isStale(isoTimestamp) {
4018
+ if (!isoTimestamp) return false;
4019
+ return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
4020
+ }
3983
4021
  async function countPendingReviews(sessionScope) {
3984
4022
  const client = getClient();
3985
4023
  const scope = strictSessionScopeFilter(
@@ -4100,6 +4138,95 @@ function getReviewChecklist(role, agent, taskSlug) {
4100
4138
  ]
4101
4139
  };
4102
4140
  }
4141
+ async function createReviewForCompletedTask(row, result, _baseDir, now) {
4142
+ const taskFile = String(row.task_file);
4143
+ const employees = await loadEmployees();
4144
+ const coordinatorName = getCoordinatorName(employees);
4145
+ if (isCoordinatorName(String(row.assigned_to), employees)) return;
4146
+ if (String(row.title).startsWith("Review:")) return;
4147
+ const fileName = taskFile.split("/").pop() ?? "";
4148
+ if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
4149
+ if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
4150
+ const client = getClient();
4151
+ const agent = String(row.assigned_to);
4152
+ const rawReviewer = row.reviewer || row.assigned_by;
4153
+ const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
4154
+ const currentStatus = String(row.status ?? "");
4155
+ if (currentStatus === "done") return;
4156
+ const existingResult = String(row.result ?? "");
4157
+ if (existingResult.includes("## Review notes")) return;
4158
+ let reviewerRole = "unknown";
4159
+ try {
4160
+ const emp = getEmployee(employees, reviewer);
4161
+ if (emp) reviewerRole = emp.role;
4162
+ } catch {
4163
+ }
4164
+ const taskTitle = String(row.title);
4165
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
4166
+ const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
4167
+ process.stderr.write(
4168
+ `[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
4169
+ `
4170
+ );
4171
+ const reviewNotes = [
4172
+ `
4173
+ ---
4174
+ ## Review notes`,
4175
+ `Review lens: ${lens}`,
4176
+ `Reviewer: **${reviewer}** (${reviewerRole})`,
4177
+ "",
4178
+ "### Checklist",
4179
+ ...checklist,
4180
+ "",
4181
+ "### Verdict",
4182
+ "- **Approved:** mark this task as done",
4183
+ "- **Needs work:** re-open with notes"
4184
+ ].join("\n");
4185
+ const originalTaskId = String(row.id);
4186
+ const updatedResult = (result ?? "No result summary provided") + reviewNotes;
4187
+ await client.execute({
4188
+ sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
4189
+ WHERE id = ?`,
4190
+ args: [updatedResult, now, originalTaskId]
4191
+ });
4192
+ orgBus.emit({
4193
+ type: "review_created",
4194
+ reviewId: originalTaskId,
4195
+ employee: agent,
4196
+ reviewer,
4197
+ timestamp: now
4198
+ });
4199
+ await writeNotification({
4200
+ agentId: agent,
4201
+ agentRole: String(row.assigned_to),
4202
+ event: "task_complete",
4203
+ project: String(row.project_name),
4204
+ summary: `completed "${taskTitle}" \u2014 ready for review`,
4205
+ taskFile
4206
+ });
4207
+ const originalPriority = String(row.priority).toLowerCase();
4208
+ const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
4209
+ if (!autoApprove) {
4210
+ try {
4211
+ const key = getSessionKey();
4212
+ const exeSession = getParentExe(key);
4213
+ if (exeSession) {
4214
+ sendIntercom(exeSession);
4215
+ }
4216
+ } catch {
4217
+ }
4218
+ }
4219
+ if (autoApprove) {
4220
+ process.stderr.write(
4221
+ `[review] Auto-approving "${taskTitle}" (P2 + tests pass)
4222
+ `
4223
+ );
4224
+ await client.execute({
4225
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
4226
+ args: [now, originalTaskId]
4227
+ });
4228
+ }
4229
+ }
4103
4230
  async function cleanupReviewFile(row, taskFile, _baseDir) {
4104
4231
  if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
4105
4232
  try {
@@ -5463,15 +5590,17 @@ function sendIntercom(targetSession) {
5463
5590
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
5464
5591
  return "queued";
5465
5592
  }
5466
- try {
5467
- const rawAgent = targetSession.split("-")[0] ?? targetSession;
5468
- const agent = baseAgentName(rawAgent);
5469
- const markerPath = path17.join(SESSION_CACHE, `current-task-${agent}.json`);
5470
- if (existsSync14(markerPath)) {
5471
- logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
5472
- return "debounced";
5593
+ if (sessionState !== "idle") {
5594
+ try {
5595
+ const rawAgent = targetSession.split("-")[0] ?? targetSession;
5596
+ const agent = baseAgentName(rawAgent);
5597
+ const markerPath = path17.join(SESSION_CACHE, `current-task-${agent}.json`);
5598
+ if (existsSync14(markerPath)) {
5599
+ logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
5600
+ return "debounced";
5601
+ }
5602
+ } catch {
5473
5603
  }
5474
- } catch {
5475
5604
  }
5476
5605
  try {
5477
5606
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
@@ -5542,6 +5671,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
5542
5671
  try {
5543
5672
  const sessions = transport.listSessions();
5544
5673
  if (!sessions.includes(coordinatorSession)) return false;
5674
+ try {
5675
+ const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
5676
+ const pending = countPendingReviews2(coordinatorSession);
5677
+ if (pending instanceof Promise) {
5678
+ pending.then((count) => {
5679
+ if (count > 0) {
5680
+ execSync6(
5681
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
5682
+ { timeout: 3e3 }
5683
+ );
5684
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
5685
+ }
5686
+ }).catch(() => {
5687
+ });
5688
+ return true;
5689
+ }
5690
+ } catch {
5691
+ }
5545
5692
  execSync6(
5546
5693
  `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
5547
5694
  { timeout: 3e3 }
@@ -467,6 +467,17 @@ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
467
467
  function canCoordinate(agentName, agentRole, employees = loadEmployeesSync()) {
468
468
  return agentName === "default" || isCoordinatorRole(agentRole) || isCoordinatorName(agentName, employees);
469
469
  }
470
+ async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
471
+ if (!existsSync3(employeesPath)) {
472
+ return [];
473
+ }
474
+ const raw = await readFile2(employeesPath, "utf-8");
475
+ try {
476
+ return JSON.parse(raw);
477
+ } catch {
478
+ return [];
479
+ }
480
+ }
470
481
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
471
482
  if (!existsSync3(employeesPath)) return [];
472
483
  try {
@@ -4944,8 +4955,35 @@ var init_tasks_crud = __esm({
4944
4955
  });
4945
4956
 
4946
4957
  // src/lib/tasks-review.ts
4958
+ var tasks_review_exports = {};
4959
+ __export(tasks_review_exports, {
4960
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
4961
+ cleanupReviewFile: () => cleanupReviewFile,
4962
+ countNewPendingReviewsSince: () => countNewPendingReviewsSince,
4963
+ countPendingReviews: () => countPendingReviews,
4964
+ createReviewForCompletedTask: () => createReviewForCompletedTask,
4965
+ formatAge: () => formatAge,
4966
+ getReviewChecklist: () => getReviewChecklist,
4967
+ isStale: () => isStale,
4968
+ listPendingReviews: () => listPendingReviews
4969
+ });
4947
4970
  import path15 from "path";
4948
4971
  import { existsSync as existsSync14, readdirSync as readdirSync2, unlinkSync as unlinkSync3 } from "fs";
4972
+ function formatAge(isoTimestamp) {
4973
+ if (!isoTimestamp) return "";
4974
+ const ms = Date.now() - new Date(isoTimestamp).getTime();
4975
+ if (ms < 0) return "just now";
4976
+ const minutes = Math.floor(ms / 6e4);
4977
+ if (minutes < 60) return `${minutes}m ago`;
4978
+ const hours = Math.floor(minutes / 60);
4979
+ if (hours < 24) return `${hours}h ago`;
4980
+ const days = Math.floor(hours / 24);
4981
+ return `${days}d ago`;
4982
+ }
4983
+ function isStale(isoTimestamp) {
4984
+ if (!isoTimestamp) return false;
4985
+ return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
4986
+ }
4949
4987
  async function countPendingReviews(sessionScope) {
4950
4988
  const client = getClient();
4951
4989
  const scope = strictSessionScopeFilter(
@@ -5066,6 +5104,95 @@ function getReviewChecklist(role, agent, taskSlug) {
5066
5104
  ]
5067
5105
  };
5068
5106
  }
5107
+ async function createReviewForCompletedTask(row, result, _baseDir, now) {
5108
+ const taskFile = String(row.task_file);
5109
+ const employees = await loadEmployees();
5110
+ const coordinatorName = getCoordinatorName(employees);
5111
+ if (isCoordinatorName(String(row.assigned_to), employees)) return;
5112
+ if (String(row.title).startsWith("Review:")) return;
5113
+ const fileName = taskFile.split("/").pop() ?? "";
5114
+ if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
5115
+ if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
5116
+ const client = getClient();
5117
+ const agent = String(row.assigned_to);
5118
+ const rawReviewer = row.reviewer || row.assigned_by;
5119
+ const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
5120
+ const currentStatus = String(row.status ?? "");
5121
+ if (currentStatus === "done") return;
5122
+ const existingResult = String(row.result ?? "");
5123
+ if (existingResult.includes("## Review notes")) return;
5124
+ let reviewerRole = "unknown";
5125
+ try {
5126
+ const emp = getEmployee(employees, reviewer);
5127
+ if (emp) reviewerRole = emp.role;
5128
+ } catch {
5129
+ }
5130
+ const taskTitle = String(row.title);
5131
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
5132
+ const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
5133
+ process.stderr.write(
5134
+ `[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
5135
+ `
5136
+ );
5137
+ const reviewNotes = [
5138
+ `
5139
+ ---
5140
+ ## Review notes`,
5141
+ `Review lens: ${lens}`,
5142
+ `Reviewer: **${reviewer}** (${reviewerRole})`,
5143
+ "",
5144
+ "### Checklist",
5145
+ ...checklist,
5146
+ "",
5147
+ "### Verdict",
5148
+ "- **Approved:** mark this task as done",
5149
+ "- **Needs work:** re-open with notes"
5150
+ ].join("\n");
5151
+ const originalTaskId = String(row.id);
5152
+ const updatedResult = (result ?? "No result summary provided") + reviewNotes;
5153
+ await client.execute({
5154
+ sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
5155
+ WHERE id = ?`,
5156
+ args: [updatedResult, now, originalTaskId]
5157
+ });
5158
+ orgBus.emit({
5159
+ type: "review_created",
5160
+ reviewId: originalTaskId,
5161
+ employee: agent,
5162
+ reviewer,
5163
+ timestamp: now
5164
+ });
5165
+ await writeNotification({
5166
+ agentId: agent,
5167
+ agentRole: String(row.assigned_to),
5168
+ event: "task_complete",
5169
+ project: String(row.project_name),
5170
+ summary: `completed "${taskTitle}" \u2014 ready for review`,
5171
+ taskFile
5172
+ });
5173
+ const originalPriority = String(row.priority).toLowerCase();
5174
+ const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
5175
+ if (!autoApprove) {
5176
+ try {
5177
+ const key = getSessionKey();
5178
+ const exeSession = getParentExe(key);
5179
+ if (exeSession) {
5180
+ sendIntercom(exeSession);
5181
+ }
5182
+ } catch {
5183
+ }
5184
+ }
5185
+ if (autoApprove) {
5186
+ process.stderr.write(
5187
+ `[review] Auto-approving "${taskTitle}" (P2 + tests pass)
5188
+ `
5189
+ );
5190
+ await client.execute({
5191
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
5192
+ args: [now, originalTaskId]
5193
+ });
5194
+ }
5195
+ }
5069
5196
  async function cleanupReviewFile(row, taskFile, _baseDir) {
5070
5197
  if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
5071
5198
  try {
@@ -6429,15 +6556,17 @@ function sendIntercom(targetSession) {
6429
6556
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
6430
6557
  return "queued";
6431
6558
  }
6432
- try {
6433
- const rawAgent = targetSession.split("-")[0] ?? targetSession;
6434
- const agent = baseAgentName(rawAgent);
6435
- const markerPath = path18.join(SESSION_CACHE, `current-task-${agent}.json`);
6436
- if (existsSync15(markerPath)) {
6437
- logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
6438
- return "debounced";
6559
+ if (sessionState !== "idle") {
6560
+ try {
6561
+ const rawAgent = targetSession.split("-")[0] ?? targetSession;
6562
+ const agent = baseAgentName(rawAgent);
6563
+ const markerPath = path18.join(SESSION_CACHE, `current-task-${agent}.json`);
6564
+ if (existsSync15(markerPath)) {
6565
+ logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
6566
+ return "debounced";
6567
+ }
6568
+ } catch {
6439
6569
  }
6440
- } catch {
6441
6570
  }
6442
6571
  try {
6443
6572
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
@@ -6508,6 +6637,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
6508
6637
  try {
6509
6638
  const sessions = transport.listSessions();
6510
6639
  if (!sessions.includes(coordinatorSession)) return false;
6640
+ try {
6641
+ const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
6642
+ const pending = countPendingReviews2(coordinatorSession);
6643
+ if (pending instanceof Promise) {
6644
+ pending.then((count) => {
6645
+ if (count > 0) {
6646
+ execSync6(
6647
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
6648
+ { timeout: 3e3 }
6649
+ );
6650
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
6651
+ }
6652
+ }).catch(() => {
6653
+ });
6654
+ return true;
6655
+ }
6656
+ } catch {
6657
+ }
6511
6658
  execSync6(
6512
6659
  `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
6513
6660
  { timeout: 3e3 }
@@ -338,6 +338,17 @@ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
338
338
  if (!agentName) return false;
339
339
  return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
340
340
  }
341
+ async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
342
+ if (!existsSync3(employeesPath)) {
343
+ return [];
344
+ }
345
+ const raw = await readFile2(employeesPath, "utf-8");
346
+ try {
347
+ return JSON.parse(raw);
348
+ } catch {
349
+ return [];
350
+ }
351
+ }
341
352
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
342
353
  if (!existsSync3(employeesPath)) return [];
343
354
  try {
@@ -3962,8 +3973,35 @@ var init_tasks_crud = __esm({
3962
3973
  });
3963
3974
 
3964
3975
  // src/lib/tasks-review.ts
3976
+ var tasks_review_exports = {};
3977
+ __export(tasks_review_exports, {
3978
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
3979
+ cleanupReviewFile: () => cleanupReviewFile,
3980
+ countNewPendingReviewsSince: () => countNewPendingReviewsSince,
3981
+ countPendingReviews: () => countPendingReviews,
3982
+ createReviewForCompletedTask: () => createReviewForCompletedTask,
3983
+ formatAge: () => formatAge,
3984
+ getReviewChecklist: () => getReviewChecklist,
3985
+ isStale: () => isStale,
3986
+ listPendingReviews: () => listPendingReviews
3987
+ });
3965
3988
  import path15 from "path";
3966
3989
  import { existsSync as existsSync13, readdirSync as readdirSync3, unlinkSync as unlinkSync5 } from "fs";
3990
+ function formatAge(isoTimestamp) {
3991
+ if (!isoTimestamp) return "";
3992
+ const ms = Date.now() - new Date(isoTimestamp).getTime();
3993
+ if (ms < 0) return "just now";
3994
+ const minutes = Math.floor(ms / 6e4);
3995
+ if (minutes < 60) return `${minutes}m ago`;
3996
+ const hours = Math.floor(minutes / 60);
3997
+ if (hours < 24) return `${hours}h ago`;
3998
+ const days = Math.floor(hours / 24);
3999
+ return `${days}d ago`;
4000
+ }
4001
+ function isStale(isoTimestamp) {
4002
+ if (!isoTimestamp) return false;
4003
+ return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
4004
+ }
3967
4005
  async function countPendingReviews(sessionScope) {
3968
4006
  const client = getClient();
3969
4007
  const scope = strictSessionScopeFilter(
@@ -4084,6 +4122,95 @@ function getReviewChecklist(role, agent, taskSlug) {
4084
4122
  ]
4085
4123
  };
4086
4124
  }
4125
+ async function createReviewForCompletedTask(row, result, _baseDir, now) {
4126
+ const taskFile = String(row.task_file);
4127
+ const employees = await loadEmployees();
4128
+ const coordinatorName = getCoordinatorName(employees);
4129
+ if (isCoordinatorName(String(row.assigned_to), employees)) return;
4130
+ if (String(row.title).startsWith("Review:")) return;
4131
+ const fileName = taskFile.split("/").pop() ?? "";
4132
+ if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
4133
+ if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
4134
+ const client = getClient();
4135
+ const agent = String(row.assigned_to);
4136
+ const rawReviewer = row.reviewer || row.assigned_by;
4137
+ const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
4138
+ const currentStatus = String(row.status ?? "");
4139
+ if (currentStatus === "done") return;
4140
+ const existingResult = String(row.result ?? "");
4141
+ if (existingResult.includes("## Review notes")) return;
4142
+ let reviewerRole = "unknown";
4143
+ try {
4144
+ const emp = getEmployee(employees, reviewer);
4145
+ if (emp) reviewerRole = emp.role;
4146
+ } catch {
4147
+ }
4148
+ const taskTitle = String(row.title);
4149
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
4150
+ const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
4151
+ process.stderr.write(
4152
+ `[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
4153
+ `
4154
+ );
4155
+ const reviewNotes = [
4156
+ `
4157
+ ---
4158
+ ## Review notes`,
4159
+ `Review lens: ${lens}`,
4160
+ `Reviewer: **${reviewer}** (${reviewerRole})`,
4161
+ "",
4162
+ "### Checklist",
4163
+ ...checklist,
4164
+ "",
4165
+ "### Verdict",
4166
+ "- **Approved:** mark this task as done",
4167
+ "- **Needs work:** re-open with notes"
4168
+ ].join("\n");
4169
+ const originalTaskId = String(row.id);
4170
+ const updatedResult = (result ?? "No result summary provided") + reviewNotes;
4171
+ await client.execute({
4172
+ sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
4173
+ WHERE id = ?`,
4174
+ args: [updatedResult, now, originalTaskId]
4175
+ });
4176
+ orgBus.emit({
4177
+ type: "review_created",
4178
+ reviewId: originalTaskId,
4179
+ employee: agent,
4180
+ reviewer,
4181
+ timestamp: now
4182
+ });
4183
+ await writeNotification({
4184
+ agentId: agent,
4185
+ agentRole: String(row.assigned_to),
4186
+ event: "task_complete",
4187
+ project: String(row.project_name),
4188
+ summary: `completed "${taskTitle}" \u2014 ready for review`,
4189
+ taskFile
4190
+ });
4191
+ const originalPriority = String(row.priority).toLowerCase();
4192
+ const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
4193
+ if (!autoApprove) {
4194
+ try {
4195
+ const key = getSessionKey();
4196
+ const exeSession = getParentExe(key);
4197
+ if (exeSession) {
4198
+ sendIntercom(exeSession);
4199
+ }
4200
+ } catch {
4201
+ }
4202
+ }
4203
+ if (autoApprove) {
4204
+ process.stderr.write(
4205
+ `[review] Auto-approving "${taskTitle}" (P2 + tests pass)
4206
+ `
4207
+ );
4208
+ await client.execute({
4209
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
4210
+ args: [now, originalTaskId]
4211
+ });
4212
+ }
4213
+ }
4087
4214
  async function cleanupReviewFile(row, taskFile, _baseDir) {
4088
4215
  if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
4089
4216
  try {
@@ -5447,15 +5574,17 @@ function sendIntercom(targetSession) {
5447
5574
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
5448
5575
  return "queued";
5449
5576
  }
5450
- try {
5451
- const rawAgent = targetSession.split("-")[0] ?? targetSession;
5452
- const agent = baseAgentName(rawAgent);
5453
- const markerPath = path18.join(SESSION_CACHE, `current-task-${agent}.json`);
5454
- if (existsSync14(markerPath)) {
5455
- logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
5456
- return "debounced";
5577
+ if (sessionState !== "idle") {
5578
+ try {
5579
+ const rawAgent = targetSession.split("-")[0] ?? targetSession;
5580
+ const agent = baseAgentName(rawAgent);
5581
+ const markerPath = path18.join(SESSION_CACHE, `current-task-${agent}.json`);
5582
+ if (existsSync14(markerPath)) {
5583
+ logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
5584
+ return "debounced";
5585
+ }
5586
+ } catch {
5457
5587
  }
5458
- } catch {
5459
5588
  }
5460
5589
  try {
5461
5590
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
@@ -5526,6 +5655,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
5526
5655
  try {
5527
5656
  const sessions = transport.listSessions();
5528
5657
  if (!sessions.includes(coordinatorSession)) return false;
5658
+ try {
5659
+ const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
5660
+ const pending = countPendingReviews2(coordinatorSession);
5661
+ if (pending instanceof Promise) {
5662
+ pending.then((count) => {
5663
+ if (count > 0) {
5664
+ execSync7(
5665
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
5666
+ { timeout: 3e3 }
5667
+ );
5668
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
5669
+ }
5670
+ }).catch(() => {
5671
+ });
5672
+ return true;
5673
+ }
5674
+ } catch {
5675
+ }
5529
5676
  execSync7(
5530
5677
  `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
5531
5678
  { timeout: 3e3 }
@@ -8059,15 +8059,17 @@ function sendIntercom(targetSession) {
8059
8059
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
8060
8060
  return "queued";
8061
8061
  }
8062
- try {
8063
- const rawAgent = targetSession.split("-")[0] ?? targetSession;
8064
- const agent = baseAgentName(rawAgent);
8065
- const markerPath = path22.join(SESSION_CACHE, `current-task-${agent}.json`);
8066
- if (existsSync18(markerPath)) {
8067
- logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
8068
- return "debounced";
8062
+ if (sessionState !== "idle") {
8063
+ try {
8064
+ const rawAgent = targetSession.split("-")[0] ?? targetSession;
8065
+ const agent = baseAgentName(rawAgent);
8066
+ const markerPath = path22.join(SESSION_CACHE, `current-task-${agent}.json`);
8067
+ if (existsSync18(markerPath)) {
8068
+ logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
8069
+ return "debounced";
8070
+ }
8071
+ } catch {
8069
8072
  }
8070
- } catch {
8071
8073
  }
8072
8074
  try {
8073
8075
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
@@ -8138,6 +8140,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
8138
8140
  try {
8139
8141
  const sessions = transport.listSessions();
8140
8142
  if (!sessions.includes(coordinatorSession)) return false;
8143
+ try {
8144
+ const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
8145
+ const pending = countPendingReviews2(coordinatorSession);
8146
+ if (pending instanceof Promise) {
8147
+ pending.then((count) => {
8148
+ if (count > 0) {
8149
+ execSync8(
8150
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
8151
+ { timeout: 3e3 }
8152
+ );
8153
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
8154
+ }
8155
+ }).catch(() => {
8156
+ });
8157
+ return true;
8158
+ }
8159
+ } catch {
8160
+ }
8141
8161
  execSync8(
8142
8162
  `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
8143
8163
  { timeout: 3e3 }