@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.
@@ -3423,6 +3423,12 @@ async function cloudSync(config) {
3423
3423
  pulled = pullResult.records.length;
3424
3424
  }
3425
3425
  }
3426
+ if (pulled > 0) {
3427
+ try {
3428
+ await pushToPostgres(pullResult.records);
3429
+ } catch {
3430
+ }
3431
+ }
3426
3432
  if (pullResult.maxVersion > lastPullVersion) {
3427
3433
  await client.execute({
3428
3434
  sql: "INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('last_cloud_pull_version', ?)",
@@ -6940,15 +6940,17 @@ function sendIntercom(targetSession) {
6940
6940
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
6941
6941
  return "queued";
6942
6942
  }
6943
- try {
6944
- const rawAgent = targetSession.split("-")[0] ?? targetSession;
6945
- const agent = baseAgentName(rawAgent);
6946
- const markerPath = path19.join(SESSION_CACHE, `current-task-${agent}.json`);
6947
- if (existsSync16(markerPath)) {
6948
- logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
6949
- return "debounced";
6943
+ if (sessionState !== "idle") {
6944
+ try {
6945
+ const rawAgent = targetSession.split("-")[0] ?? targetSession;
6946
+ const agent = baseAgentName(rawAgent);
6947
+ const markerPath = path19.join(SESSION_CACHE, `current-task-${agent}.json`);
6948
+ if (existsSync16(markerPath)) {
6949
+ logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
6950
+ return "debounced";
6951
+ }
6952
+ } catch {
6950
6953
  }
6951
- } catch {
6952
6954
  }
6953
6955
  try {
6954
6956
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
@@ -7019,6 +7021,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName2, taskTit
7019
7021
  try {
7020
7022
  const sessions = transport.listSessions();
7021
7023
  if (!sessions.includes(coordinatorSession)) return false;
7024
+ try {
7025
+ const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
7026
+ const pending = countPendingReviews2(coordinatorSession);
7027
+ if (pending instanceof Promise) {
7028
+ pending.then((count) => {
7029
+ if (count > 0) {
7030
+ execSync6(
7031
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
7032
+ { timeout: 3e3 }
7033
+ );
7034
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName2} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
7035
+ }
7036
+ }).catch(() => {
7037
+ });
7038
+ return true;
7039
+ }
7040
+ } catch {
7041
+ }
7022
7042
  execSync6(
7023
7043
  `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
7024
7044
  { timeout: 3e3 }
@@ -796,6 +796,17 @@ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
796
796
  if (!agentName) return false;
797
797
  return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
798
798
  }
799
+ async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
800
+ if (!existsSync6(employeesPath)) {
801
+ return [];
802
+ }
803
+ const raw = await readFile2(employeesPath, "utf-8");
804
+ try {
805
+ return JSON.parse(raw);
806
+ } catch {
807
+ return [];
808
+ }
809
+ }
799
810
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
800
811
  if (!existsSync6(employeesPath)) return [];
801
812
  try {
@@ -3979,8 +3990,35 @@ var init_tasks_crud = __esm({
3979
3990
  });
3980
3991
 
3981
3992
  // src/lib/tasks-review.ts
3993
+ var tasks_review_exports = {};
3994
+ __export(tasks_review_exports, {
3995
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
3996
+ cleanupReviewFile: () => cleanupReviewFile,
3997
+ countNewPendingReviewsSince: () => countNewPendingReviewsSince,
3998
+ countPendingReviews: () => countPendingReviews,
3999
+ createReviewForCompletedTask: () => createReviewForCompletedTask,
4000
+ formatAge: () => formatAge,
4001
+ getReviewChecklist: () => getReviewChecklist,
4002
+ isStale: () => isStale,
4003
+ listPendingReviews: () => listPendingReviews
4004
+ });
3982
4005
  import path14 from "path";
3983
4006
  import { existsSync as existsSync13, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
4007
+ function formatAge(isoTimestamp) {
4008
+ if (!isoTimestamp) return "";
4009
+ const ms = Date.now() - new Date(isoTimestamp).getTime();
4010
+ if (ms < 0) return "just now";
4011
+ const minutes = Math.floor(ms / 6e4);
4012
+ if (minutes < 60) return `${minutes}m ago`;
4013
+ const hours = Math.floor(minutes / 60);
4014
+ if (hours < 24) return `${hours}h ago`;
4015
+ const days = Math.floor(hours / 24);
4016
+ return `${days}d ago`;
4017
+ }
4018
+ function isStale(isoTimestamp) {
4019
+ if (!isoTimestamp) return false;
4020
+ return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
4021
+ }
3984
4022
  async function countPendingReviews(sessionScope) {
3985
4023
  const client = getClient();
3986
4024
  const scope = strictSessionScopeFilter(
@@ -4101,6 +4139,95 @@ function getReviewChecklist(role, agent, taskSlug) {
4101
4139
  ]
4102
4140
  };
4103
4141
  }
4142
+ async function createReviewForCompletedTask(row, result, _baseDir, now) {
4143
+ const taskFile = String(row.task_file);
4144
+ const employees = await loadEmployees();
4145
+ const coordinatorName = getCoordinatorName(employees);
4146
+ if (isCoordinatorName(String(row.assigned_to), employees)) return;
4147
+ if (String(row.title).startsWith("Review:")) return;
4148
+ const fileName = taskFile.split("/").pop() ?? "";
4149
+ if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
4150
+ if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
4151
+ const client = getClient();
4152
+ const agent = String(row.assigned_to);
4153
+ const rawReviewer = row.reviewer || row.assigned_by;
4154
+ const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
4155
+ const currentStatus = String(row.status ?? "");
4156
+ if (currentStatus === "done") return;
4157
+ const existingResult = String(row.result ?? "");
4158
+ if (existingResult.includes("## Review notes")) return;
4159
+ let reviewerRole = "unknown";
4160
+ try {
4161
+ const emp = getEmployee(employees, reviewer);
4162
+ if (emp) reviewerRole = emp.role;
4163
+ } catch {
4164
+ }
4165
+ const taskTitle = String(row.title);
4166
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
4167
+ const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
4168
+ process.stderr.write(
4169
+ `[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
4170
+ `
4171
+ );
4172
+ const reviewNotes = [
4173
+ `
4174
+ ---
4175
+ ## Review notes`,
4176
+ `Review lens: ${lens}`,
4177
+ `Reviewer: **${reviewer}** (${reviewerRole})`,
4178
+ "",
4179
+ "### Checklist",
4180
+ ...checklist,
4181
+ "",
4182
+ "### Verdict",
4183
+ "- **Approved:** mark this task as done",
4184
+ "- **Needs work:** re-open with notes"
4185
+ ].join("\n");
4186
+ const originalTaskId = String(row.id);
4187
+ const updatedResult = (result ?? "No result summary provided") + reviewNotes;
4188
+ await client.execute({
4189
+ sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
4190
+ WHERE id = ?`,
4191
+ args: [updatedResult, now, originalTaskId]
4192
+ });
4193
+ orgBus.emit({
4194
+ type: "review_created",
4195
+ reviewId: originalTaskId,
4196
+ employee: agent,
4197
+ reviewer,
4198
+ timestamp: now
4199
+ });
4200
+ await writeNotification({
4201
+ agentId: agent,
4202
+ agentRole: String(row.assigned_to),
4203
+ event: "task_complete",
4204
+ project: String(row.project_name),
4205
+ summary: `completed "${taskTitle}" \u2014 ready for review`,
4206
+ taskFile
4207
+ });
4208
+ const originalPriority = String(row.priority).toLowerCase();
4209
+ const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
4210
+ if (!autoApprove) {
4211
+ try {
4212
+ const key = getSessionKey();
4213
+ const exeSession = getParentExe(key);
4214
+ if (exeSession) {
4215
+ sendIntercom(exeSession);
4216
+ }
4217
+ } catch {
4218
+ }
4219
+ }
4220
+ if (autoApprove) {
4221
+ process.stderr.write(
4222
+ `[review] Auto-approving "${taskTitle}" (P2 + tests pass)
4223
+ `
4224
+ );
4225
+ await client.execute({
4226
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
4227
+ args: [now, originalTaskId]
4228
+ });
4229
+ }
4230
+ }
4104
4231
  async function cleanupReviewFile(row, taskFile, _baseDir) {
4105
4232
  if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
4106
4233
  try {
@@ -5464,15 +5591,17 @@ function sendIntercom(targetSession) {
5464
5591
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
5465
5592
  return "queued";
5466
5593
  }
5467
- try {
5468
- const rawAgent = targetSession.split("-")[0] ?? targetSession;
5469
- const agent = baseAgentName(rawAgent);
5470
- const markerPath = path17.join(SESSION_CACHE, `current-task-${agent}.json`);
5471
- if (existsSync14(markerPath)) {
5472
- logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
5473
- return "debounced";
5594
+ if (sessionState !== "idle") {
5595
+ try {
5596
+ const rawAgent = targetSession.split("-")[0] ?? targetSession;
5597
+ const agent = baseAgentName(rawAgent);
5598
+ const markerPath = path17.join(SESSION_CACHE, `current-task-${agent}.json`);
5599
+ if (existsSync14(markerPath)) {
5600
+ logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
5601
+ return "debounced";
5602
+ }
5603
+ } catch {
5474
5604
  }
5475
- } catch {
5476
5605
  }
5477
5606
  try {
5478
5607
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
@@ -5543,6 +5672,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
5543
5672
  try {
5544
5673
  const sessions = transport.listSessions();
5545
5674
  if (!sessions.includes(coordinatorSession)) return false;
5675
+ try {
5676
+ const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
5677
+ const pending = countPendingReviews2(coordinatorSession);
5678
+ if (pending instanceof Promise) {
5679
+ pending.then((count) => {
5680
+ if (count > 0) {
5681
+ execSync6(
5682
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
5683
+ { timeout: 3e3 }
5684
+ );
5685
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
5686
+ }
5687
+ }).catch(() => {
5688
+ });
5689
+ return true;
5690
+ }
5691
+ } catch {
5692
+ }
5546
5693
  execSync6(
5547
5694
  `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
5548
5695
  { timeout: 3e3 }
@@ -7192,7 +7339,7 @@ function getRecentCommits(limit = DEFAULT_COMMIT_LIMIT) {
7192
7339
  return [];
7193
7340
  }
7194
7341
  }
7195
- function isStale(updatedAt, staleMinutes2) {
7342
+ function isStale2(updatedAt, staleMinutes2) {
7196
7343
  const updated = new Date(updatedAt).getTime();
7197
7344
  const threshold = Date.now() - staleMinutes2 * 6e4;
7198
7345
  return updated < threshold;
@@ -7257,7 +7404,7 @@ async function sweepTasks(projectName2, options = {}) {
7257
7404
  return result;
7258
7405
  }
7259
7406
  for (const task of tasks) {
7260
- if (!isStale(task.updatedAt, staleMinutes2)) {
7407
+ if (!isStale2(task.updatedAt, staleMinutes2)) {
7261
7408
  result.unchanged++;
7262
7409
  continue;
7263
7410
  }
@@ -808,6 +808,17 @@ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
808
808
  if (!agentName) return false;
809
809
  return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
810
810
  }
811
+ async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
812
+ if (!existsSync6(employeesPath)) {
813
+ return [];
814
+ }
815
+ const raw = await readFile2(employeesPath, "utf-8");
816
+ try {
817
+ return JSON.parse(raw);
818
+ } catch {
819
+ return [];
820
+ }
821
+ }
811
822
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
812
823
  if (!existsSync6(employeesPath)) return [];
813
824
  try {
@@ -3984,8 +3995,35 @@ var init_tasks_crud = __esm({
3984
3995
  });
3985
3996
 
3986
3997
  // src/lib/tasks-review.ts
3998
+ var tasks_review_exports = {};
3999
+ __export(tasks_review_exports, {
4000
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
4001
+ cleanupReviewFile: () => cleanupReviewFile,
4002
+ countNewPendingReviewsSince: () => countNewPendingReviewsSince,
4003
+ countPendingReviews: () => countPendingReviews,
4004
+ createReviewForCompletedTask: () => createReviewForCompletedTask,
4005
+ formatAge: () => formatAge,
4006
+ getReviewChecklist: () => getReviewChecklist,
4007
+ isStale: () => isStale,
4008
+ listPendingReviews: () => listPendingReviews
4009
+ });
3987
4010
  import path14 from "path";
3988
4011
  import { existsSync as existsSync13, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
4012
+ function formatAge(isoTimestamp) {
4013
+ if (!isoTimestamp) return "";
4014
+ const ms = Date.now() - new Date(isoTimestamp).getTime();
4015
+ if (ms < 0) return "just now";
4016
+ const minutes = Math.floor(ms / 6e4);
4017
+ if (minutes < 60) return `${minutes}m ago`;
4018
+ const hours = Math.floor(minutes / 60);
4019
+ if (hours < 24) return `${hours}h ago`;
4020
+ const days = Math.floor(hours / 24);
4021
+ return `${days}d ago`;
4022
+ }
4023
+ function isStale(isoTimestamp) {
4024
+ if (!isoTimestamp) return false;
4025
+ return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
4026
+ }
3989
4027
  async function countPendingReviews(sessionScope) {
3990
4028
  const client = getClient();
3991
4029
  const scope = strictSessionScopeFilter(
@@ -4106,6 +4144,95 @@ function getReviewChecklist(role, agent, taskSlug) {
4106
4144
  ]
4107
4145
  };
4108
4146
  }
4147
+ async function createReviewForCompletedTask(row, result, _baseDir, now) {
4148
+ const taskFile = String(row.task_file);
4149
+ const employees = await loadEmployees();
4150
+ const coordinatorName = getCoordinatorName(employees);
4151
+ if (isCoordinatorName(String(row.assigned_to), employees)) return;
4152
+ if (String(row.title).startsWith("Review:")) return;
4153
+ const fileName = taskFile.split("/").pop() ?? "";
4154
+ if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
4155
+ if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
4156
+ const client = getClient();
4157
+ const agent = String(row.assigned_to);
4158
+ const rawReviewer = row.reviewer || row.assigned_by;
4159
+ const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
4160
+ const currentStatus = String(row.status ?? "");
4161
+ if (currentStatus === "done") return;
4162
+ const existingResult = String(row.result ?? "");
4163
+ if (existingResult.includes("## Review notes")) return;
4164
+ let reviewerRole = "unknown";
4165
+ try {
4166
+ const emp = getEmployee(employees, reviewer);
4167
+ if (emp) reviewerRole = emp.role;
4168
+ } catch {
4169
+ }
4170
+ const taskTitle = String(row.title);
4171
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
4172
+ const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
4173
+ process.stderr.write(
4174
+ `[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
4175
+ `
4176
+ );
4177
+ const reviewNotes = [
4178
+ `
4179
+ ---
4180
+ ## Review notes`,
4181
+ `Review lens: ${lens}`,
4182
+ `Reviewer: **${reviewer}** (${reviewerRole})`,
4183
+ "",
4184
+ "### Checklist",
4185
+ ...checklist,
4186
+ "",
4187
+ "### Verdict",
4188
+ "- **Approved:** mark this task as done",
4189
+ "- **Needs work:** re-open with notes"
4190
+ ].join("\n");
4191
+ const originalTaskId = String(row.id);
4192
+ const updatedResult = (result ?? "No result summary provided") + reviewNotes;
4193
+ await client.execute({
4194
+ sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
4195
+ WHERE id = ?`,
4196
+ args: [updatedResult, now, originalTaskId]
4197
+ });
4198
+ orgBus.emit({
4199
+ type: "review_created",
4200
+ reviewId: originalTaskId,
4201
+ employee: agent,
4202
+ reviewer,
4203
+ timestamp: now
4204
+ });
4205
+ await writeNotification({
4206
+ agentId: agent,
4207
+ agentRole: String(row.assigned_to),
4208
+ event: "task_complete",
4209
+ project: String(row.project_name),
4210
+ summary: `completed "${taskTitle}" \u2014 ready for review`,
4211
+ taskFile
4212
+ });
4213
+ const originalPriority = String(row.priority).toLowerCase();
4214
+ const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
4215
+ if (!autoApprove) {
4216
+ try {
4217
+ const key = getSessionKey();
4218
+ const exeSession = getParentExe(key);
4219
+ if (exeSession) {
4220
+ sendIntercom(exeSession);
4221
+ }
4222
+ } catch {
4223
+ }
4224
+ }
4225
+ if (autoApprove) {
4226
+ process.stderr.write(
4227
+ `[review] Auto-approving "${taskTitle}" (P2 + tests pass)
4228
+ `
4229
+ );
4230
+ await client.execute({
4231
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
4232
+ args: [now, originalTaskId]
4233
+ });
4234
+ }
4235
+ }
4109
4236
  async function cleanupReviewFile(row, taskFile, _baseDir) {
4110
4237
  if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
4111
4238
  try {
@@ -5469,15 +5596,17 @@ function sendIntercom(targetSession) {
5469
5596
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
5470
5597
  return "queued";
5471
5598
  }
5472
- try {
5473
- const rawAgent = targetSession.split("-")[0] ?? targetSession;
5474
- const agent = baseAgentName(rawAgent);
5475
- const markerPath = path17.join(SESSION_CACHE, `current-task-${agent}.json`);
5476
- if (existsSync14(markerPath)) {
5477
- logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
5478
- return "debounced";
5599
+ if (sessionState !== "idle") {
5600
+ try {
5601
+ const rawAgent = targetSession.split("-")[0] ?? targetSession;
5602
+ const agent = baseAgentName(rawAgent);
5603
+ const markerPath = path17.join(SESSION_CACHE, `current-task-${agent}.json`);
5604
+ if (existsSync14(markerPath)) {
5605
+ logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
5606
+ return "debounced";
5607
+ }
5608
+ } catch {
5479
5609
  }
5480
- } catch {
5481
5610
  }
5482
5611
  try {
5483
5612
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
@@ -5548,6 +5677,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
5548
5677
  try {
5549
5678
  const sessions = transport.listSessions();
5550
5679
  if (!sessions.includes(coordinatorSession)) return false;
5680
+ try {
5681
+ const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
5682
+ const pending = countPendingReviews2(coordinatorSession);
5683
+ if (pending instanceof Promise) {
5684
+ pending.then((count) => {
5685
+ if (count > 0) {
5686
+ execSync6(
5687
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
5688
+ { timeout: 3e3 }
5689
+ );
5690
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
5691
+ }
5692
+ }).catch(() => {
5693
+ });
5694
+ return true;
5695
+ }
5696
+ } catch {
5697
+ }
5551
5698
  execSync6(
5552
5699
  `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
5553
5700
  { timeout: 3e3 }
package/dist/bin/setup.js CHANGED
@@ -4358,6 +4358,12 @@ async function cloudSync(config) {
4358
4358
  pulled = pullResult.records.length;
4359
4359
  }
4360
4360
  }
4361
+ if (pulled > 0) {
4362
+ try {
4363
+ await pushToPostgres(pullResult.records);
4364
+ } catch {
4365
+ }
4366
+ }
4361
4367
  if (pullResult.maxVersion > lastPullVersion) {
4362
4368
  await client.execute({
4363
4369
  sql: "INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('last_cloud_pull_version', ?)",
@@ -6794,8 +6794,35 @@ var init_tasks_crud = __esm({
6794
6794
  });
6795
6795
 
6796
6796
  // src/lib/tasks-review.ts
6797
+ var tasks_review_exports = {};
6798
+ __export(tasks_review_exports, {
6799
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
6800
+ cleanupReviewFile: () => cleanupReviewFile,
6801
+ countNewPendingReviewsSince: () => countNewPendingReviewsSince,
6802
+ countPendingReviews: () => countPendingReviews,
6803
+ createReviewForCompletedTask: () => createReviewForCompletedTask,
6804
+ formatAge: () => formatAge,
6805
+ getReviewChecklist: () => getReviewChecklist,
6806
+ isStale: () => isStale,
6807
+ listPendingReviews: () => listPendingReviews
6808
+ });
6797
6809
  import path17 from "path";
6798
6810
  import { existsSync as existsSync15, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
6811
+ function formatAge(isoTimestamp) {
6812
+ if (!isoTimestamp) return "";
6813
+ const ms = Date.now() - new Date(isoTimestamp).getTime();
6814
+ if (ms < 0) return "just now";
6815
+ const minutes = Math.floor(ms / 6e4);
6816
+ if (minutes < 60) return `${minutes}m ago`;
6817
+ const hours = Math.floor(minutes / 60);
6818
+ if (hours < 24) return `${hours}h ago`;
6819
+ const days = Math.floor(hours / 24);
6820
+ return `${days}d ago`;
6821
+ }
6822
+ function isStale(isoTimestamp) {
6823
+ if (!isoTimestamp) return false;
6824
+ return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
6825
+ }
6799
6826
  async function countPendingReviews(sessionScope) {
6800
6827
  const client = getClient();
6801
6828
  const scope = strictSessionScopeFilter(
@@ -6916,6 +6943,95 @@ function getReviewChecklist(role, agent, taskSlug) {
6916
6943
  ]
6917
6944
  };
6918
6945
  }
6946
+ async function createReviewForCompletedTask(row, result, _baseDir, now) {
6947
+ const taskFile = String(row.task_file);
6948
+ const employees = await loadEmployees();
6949
+ const coordinatorName = getCoordinatorName(employees);
6950
+ if (isCoordinatorName(String(row.assigned_to), employees)) return;
6951
+ if (String(row.title).startsWith("Review:")) return;
6952
+ const fileName = taskFile.split("/").pop() ?? "";
6953
+ if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
6954
+ if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
6955
+ const client = getClient();
6956
+ const agent = String(row.assigned_to);
6957
+ const rawReviewer = row.reviewer || row.assigned_by;
6958
+ const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
6959
+ const currentStatus = String(row.status ?? "");
6960
+ if (currentStatus === "done") return;
6961
+ const existingResult = String(row.result ?? "");
6962
+ if (existingResult.includes("## Review notes")) return;
6963
+ let reviewerRole = "unknown";
6964
+ try {
6965
+ const emp = getEmployee(employees, reviewer);
6966
+ if (emp) reviewerRole = emp.role;
6967
+ } catch {
6968
+ }
6969
+ const taskTitle = String(row.title);
6970
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
6971
+ const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
6972
+ process.stderr.write(
6973
+ `[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
6974
+ `
6975
+ );
6976
+ const reviewNotes = [
6977
+ `
6978
+ ---
6979
+ ## Review notes`,
6980
+ `Review lens: ${lens}`,
6981
+ `Reviewer: **${reviewer}** (${reviewerRole})`,
6982
+ "",
6983
+ "### Checklist",
6984
+ ...checklist,
6985
+ "",
6986
+ "### Verdict",
6987
+ "- **Approved:** mark this task as done",
6988
+ "- **Needs work:** re-open with notes"
6989
+ ].join("\n");
6990
+ const originalTaskId = String(row.id);
6991
+ const updatedResult = (result ?? "No result summary provided") + reviewNotes;
6992
+ await client.execute({
6993
+ sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
6994
+ WHERE id = ?`,
6995
+ args: [updatedResult, now, originalTaskId]
6996
+ });
6997
+ orgBus.emit({
6998
+ type: "review_created",
6999
+ reviewId: originalTaskId,
7000
+ employee: agent,
7001
+ reviewer,
7002
+ timestamp: now
7003
+ });
7004
+ await writeNotification({
7005
+ agentId: agent,
7006
+ agentRole: String(row.assigned_to),
7007
+ event: "task_complete",
7008
+ project: String(row.project_name),
7009
+ summary: `completed "${taskTitle}" \u2014 ready for review`,
7010
+ taskFile
7011
+ });
7012
+ const originalPriority = String(row.priority).toLowerCase();
7013
+ const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
7014
+ if (!autoApprove) {
7015
+ try {
7016
+ const key = getSessionKey();
7017
+ const exeSession = getParentExe(key);
7018
+ if (exeSession) {
7019
+ sendIntercom(exeSession);
7020
+ }
7021
+ } catch {
7022
+ }
7023
+ }
7024
+ if (autoApprove) {
7025
+ process.stderr.write(
7026
+ `[review] Auto-approving "${taskTitle}" (P2 + tests pass)
7027
+ `
7028
+ );
7029
+ await client.execute({
7030
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
7031
+ args: [now, originalTaskId]
7032
+ });
7033
+ }
7034
+ }
6919
7035
  async function cleanupReviewFile(row, taskFile, _baseDir) {
6920
7036
  if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
6921
7037
  try {
@@ -8279,15 +8395,17 @@ function sendIntercom(targetSession) {
8279
8395
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
8280
8396
  return "queued";
8281
8397
  }
8282
- try {
8283
- const rawAgent = targetSession.split("-")[0] ?? targetSession;
8284
- const agent = baseAgentName(rawAgent);
8285
- const markerPath = path20.join(SESSION_CACHE, `current-task-${agent}.json`);
8286
- if (existsSync16(markerPath)) {
8287
- logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
8288
- return "debounced";
8398
+ if (sessionState !== "idle") {
8399
+ try {
8400
+ const rawAgent = targetSession.split("-")[0] ?? targetSession;
8401
+ const agent = baseAgentName(rawAgent);
8402
+ const markerPath = path20.join(SESSION_CACHE, `current-task-${agent}.json`);
8403
+ if (existsSync16(markerPath)) {
8404
+ logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
8405
+ return "debounced";
8406
+ }
8407
+ } catch {
8289
8408
  }
8290
- } catch {
8291
8409
  }
8292
8410
  try {
8293
8411
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
@@ -8358,6 +8476,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
8358
8476
  try {
8359
8477
  const sessions = transport.listSessions();
8360
8478
  if (!sessions.includes(coordinatorSession)) return false;
8479
+ try {
8480
+ const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
8481
+ const pending = countPendingReviews2(coordinatorSession);
8482
+ if (pending instanceof Promise) {
8483
+ pending.then((count) => {
8484
+ if (count > 0) {
8485
+ execSync6(
8486
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
8487
+ { timeout: 3e3 }
8488
+ );
8489
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
8490
+ }
8491
+ }).catch(() => {
8492
+ });
8493
+ return true;
8494
+ }
8495
+ } catch {
8496
+ }
8361
8497
  execSync6(
8362
8498
  `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
8363
8499
  { timeout: 3e3 }